// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not // use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing // permissions and limitations under the License. package onpremprovider import ( "context" "fmt" "time" "github.com/aws/amazon-ssm-agent/agent/appconfig" "github.com/aws/amazon-ssm-agent/agent/backoffconfig" "github.com/aws/amazon-ssm-agent/agent/log" "github.com/aws/amazon-ssm-agent/agent/managedInstances/registration" "github.com/aws/amazon-ssm-agent/agent/managedInstances/sharedCredentials" "github.com/aws/amazon-ssm-agent/agent/ssm/authtokenrequest" "github.com/aws/amazon-ssm-agent/agent/ssm/rsaauth" "github.com/aws/amazon-ssm-agent/common/identity/credentialproviders" "github.com/aws/amazon-ssm-agent/common/identity/endpoint" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/service/ssm" "github.com/cenkalti/backoff/v4" ) const ( // ProviderName provides a name of managed instance Role provider ProviderName = "OnPremRoleProvider" CredentialSourceOnPrem = "SSM" ) // NewCredentialsProvider initializes a new credential provider that retrieves OnPrem credentials from Systems Manager func NewCredentialsProvider(log log.T, config *appconfig.SsmagentConfig, info registration.IOnpremRegistrationInfo, sharingCreds bool) credentialproviders.IRemoteProvider { log = log.WithContext("[OnPremCredsProvider]") provider := &onpremCredentialsProvider{ log: log, config: config, registrationInfo: info, isSharingCreds: sharingCreds, endpointHelper: endpoint.NewEndpointHelper(log, *config), } // If credentials are not being shared, the ssm-agent-worker should be in charge of rotating private key // because as of now the amazon-ssm-agent does not use the aws sdk and therefore the retrieve function is never called. // If credentials are being shared, the amazon-ssm-agent is the only executable that calls retrieve using the onpremcreds provider, // all other workers will be using the sharedprovider. // TODO: When amazon-ssm-agent starts using the aws-sdk, make the executableToRotateKey always be amazon-ssm-agent provider.executableToRotateKey = "ssm-agent-worker" if sharingCreds { provider.executableToRotateKey = "amazon-ssm-agent" shareFile, err := sharedCredentials.GetSharedCredsFilePath("") if err != nil { log.Errorf("failed to get path to shared credentials file, not sharing credentials: %v", err) provider.isSharingCreds = false } else { provider.shareFile = shareFile } } provider.initializeClient(info.PrivateKey(log, "", registration.RegVaultKey)) return provider } var emptyCredential = credentials.Value{ProviderName: ProviderName} func shouldRetryAwsRequest(err error) bool { // Don't retry if no error if err == nil { return false } if _, ok := err.(awserr.Error); ok { // No aws sdk errors for RequestManagedInstanceRoleToken nor UpdateManagedInstancePublicKey are retryable return false } // Retry for any non-aws errors return true } // Retrieve retrieves OnPrem credentials from the SSM Auth service. // Error will be returned if the request fails, or unable to extract // the desired credentials. // This function is intended for use by agent workers that require credentials func (m *onpremCredentialsProvider) Retrieve() (credentials.Value, error) { return m.RemoteRetrieve(context.Background()) } // RemoteRetrieve retrieves OnPrem credentials from the SSM Auth service. // Error will be returned if the request fails, or unable to extract // the desired credentials. // This function is intended for use by the core module's credential refresher routine func (m *onpremCredentialsProvider) RemoteRetrieve(ctx context.Context) (credentials.Value, error) { var err error var roleCreds *ssm.RequestManagedInstanceRoleTokenOutput fingerprint, err := m.registrationInfo.Fingerprint(m.log) if err != nil { m.log.Warnf("Failed to get machine fingerprint: %v", err) return emptyCredential, err } exponentialBackoff, err := backoffconfig.GetDefaultExponentialBackoff() if err != nil { m.log.Warnf("Failed to create backoff config with error: %v", err) return emptyCredential, err } // Get role token err = backoffRetry(func() error { roleCreds, err = m.client.RequestManagedInstanceRoleTokenWithContext(ctx, fingerprint) if err == nil { return nil } if shouldRetryAwsRequest(err) { return err } return backoff.Permanent(err) }, exponentialBackoff) // Failed to get role token if err != nil { return emptyCredential, err } shouldRotate, err := m.registrationInfo.ShouldRotatePrivateKey(m.log, m.executableToRotateKey, m.config.Profile.KeyAutoRotateDays, *roleCreds.UpdateKeyPair, "", registration.RegVaultKey) if err != nil { m.log.Warnf("Failed to check if private key should be rotated: %v", err) } else if shouldRotate { rotateKeyErr := m.rotatePrivateKey(fingerprint, exponentialBackoff) if rotateKeyErr != nil { m.log.Error("Failed to rotate private key with error: ", rotateKeyErr) } } expiryWindow := time.Duration(0) // If isSharingCreds is false, the credentials are not being shared and the expiration/refresh is handled by the aws sdk // if isSharingCreds is true, the credentialsRefresher will be the one to refresh the credentials and make sure credentials are refreshed at the required time if !m.isSharingCreds { // Set the expiration window to be half of the token's lifetime. This allows credential refreshes to survive transient // network issues more easily. Expiring at half the lifetime also follows the behavior of other protocols such as DHCP // https://tools.ietf.org/html/rfc2131#section-4.4.5. Note that not all of the behavior specified in that RFC is // implemented, just the suggestion to start renewals at 50% of token validity. expiryWindow = time.Until(*roleCreds.TokenExpirationDate) / 2 } // Set the expiration of our credentials m.SetExpiration(*roleCreds.TokenExpirationDate, expiryWindow) return credentials.Value{ AccessKeyID: *roleCreds.AccessKeyId, SecretAccessKey: *roleCreds.SecretAccessKey, SessionToken: *roleCreds.SessionToken, ProviderName: ProviderName, }, nil } // RemoteExpiresAt is used by the core module's credential refresher to check credential expiry func (m *onpremCredentialsProvider) RemoteExpiresAt() time.Time { return m.ExpiresAt() } // rotatePrivateKey attempts to rotate the instance private key func (m *onpremCredentialsProvider) rotatePrivateKey(fingerprint string, exponentialBackoff *backoff.ExponentialBackOff) error { m.log.Infof("Attempting to rotate private key") oldPrivateKey := m.registrationInfo.PrivateKey(m.log, "", registration.RegVaultKey) oldKeyType := m.registrationInfo.PrivateKeyType(m.log, "", registration.RegVaultKey) oldPublicKey, err := m.registrationInfo.GeneratePublicKey(oldPrivateKey) if err != nil { m.log.Warnf("Failed to generate old public key: %v", err) return err } newPublicKey, newPrivateKey, newKeyType, err := m.registrationInfo.GenerateKeyPair() if err != nil { m.log.Warnf("Failed to generate new key pair: %v", err) return err } // Update remote public key err = backoffRetry(func() error { _, err = m.client.UpdateManagedInstancePublicKey(newPublicKey, newKeyType) if shouldRetryAwsRequest(err) { return err } return nil }, exponentialBackoff) if err != nil { m.log.Warnf("Failed to update public key, trying to recover: %s", err) // Updating public key failed, test if old public key is stored in SSM _ = backoffRetry(func() error { _, err = m.client.RequestManagedInstanceRoleToken(fingerprint) if shouldRetryAwsRequest(err) { return err } return nil }, exponentialBackoff) if err == nil { return fmt.Errorf("Failed to update remote public key, old key still works") } // Test if new key works m.initializeClient(newPrivateKey) _ = backoffRetry(func() error { _, err = m.client.RequestManagedInstanceRoleToken(fingerprint) if shouldRetryAwsRequest(err) { return err } return nil }, exponentialBackoff) if err != nil { m.log.Warnf("Unable to verify neither new nor old key, rolling back private key change") m.initializeClient(m.registrationInfo.PrivateKey(m.log, "", registration.RegVaultKey)) return err } m.log.Infof("Successfully verified new key is upstream, updating local key") } // New key has been updated remotely, update client to use new private key m.initializeClient(newPrivateKey) // New key was successfully updated in service, trying to save new key to disk _ = backoffRetry(func() error { err = m.registrationInfo.UpdatePrivateKey(m.log, newPrivateKey, newKeyType, "", registration.RegVaultKey) return err }, exponentialBackoff) if err != nil { m.log.Warn("Failed to save private key locally, attempting to update remote key back to old public key: ", err) // Attempt to update the remote public key to the old public key err = backoffRetry(func() error { _, err = m.client.UpdateManagedInstancePublicKey(oldPublicKey, oldKeyType) if shouldRetryAwsRequest(err) { return err } return nil }, exponentialBackoff) if err != nil { m.log.Error("Failed to roll-back remove public key change, instance most likely needs to be re-registered: %v", err) return fmt.Errorf("Failed to update remote public key after saving locally failed: %v", err) } m.log.Warn("Successfully rolled back remote key, and recovered registration") m.initializeClient(oldPrivateKey) return fmt.Errorf("failed to save new private key to disk") } m.log.Info("Successfully rotated private key") return nil } func (m *onpremCredentialsProvider) initializeClient(newPrivateKey string) { m.client = createNewClient(m, newPrivateKey) } // ShareProfile is the aws profile to which OnPrem credentials should be saved func (m *onpremCredentialsProvider) ShareProfile() string { return m.config.Profile.ShareProfile } // ShareFile is the aws credentials file location to which OnPrem credentials should be saved func (m *onpremCredentialsProvider) ShareFile() string { return m.shareFile } // SharesCredentials returns true if the role provider requires credentials to be saved to disk func (m *onpremCredentialsProvider) SharesCredentials() bool { return m.isSharingCreds } // CredentialSource returns the name of the current provider being used func (m *onpremCredentialsProvider) CredentialSource() string { return CredentialSourceOnPrem } // Assigning function to variable to be able to mock out during tests var createNewClient = func(m *onpremCredentialsProvider, privateKey string) authtokenrequest.IClient { instanceID := m.registrationInfo.InstanceID(m.log, "", registration.RegVaultKey) region := m.registrationInfo.Region(m.log, "", registration.RegVaultKey) return rsaauth.NewRsaClient(m.log, m.config, instanceID, region, privateKey) }