common/identity/credentialproviders/ec2roleprovider/ec2_role_provider.go (192 lines of code) (raw):
// Copyright 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.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 ec2roleprovider
import (
"context"
"fmt"
"runtime"
"sync"
"time"
"github.com/aws/amazon-ssm-agent/agent/appconfig"
"github.com/aws/amazon-ssm-agent/agent/log"
"github.com/aws/amazon-ssm-agent/agent/sdkutil"
"github.com/aws/amazon-ssm-agent/agent/version"
"github.com/aws/amazon-ssm-agent/common/identity/credentialproviders/ssmec2roleprovider"
"github.com/aws/amazon-ssm-agent/common/runtimeconfig"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/service/ssm"
)
// EC2RoleProvider provides credentials for the agent when on an EC2 instance
type EC2RoleProvider struct {
credentials.Expiry
InnerProviders *EC2InnerProviders
Log log.T
InstanceInfo *ssmec2roleprovider.InstanceInfo
expirationUpdateLock *sync.Mutex
credentialSource string
SsmEndpoint string
shouldShareCredentials bool
RuntimeConfigClient runtimeconfig.IIdentityRuntimeConfigClient
}
// NewEC2RoleProvider initializes a new EC2RoleProvider using runtime config values
func NewEC2RoleProvider(log log.T, innerProviders *EC2InnerProviders, instanceInfo *ssmec2roleprovider.InstanceInfo, ssmEndpoint string, runtimeConfigClient runtimeconfig.IIdentityRuntimeConfigClient) *EC2RoleProvider {
runtimeConfig, err := runtimeConfigClient.GetConfigWithRetry()
if err != nil {
log.Warnf("Failed to get credential source from runtime config. Err: %v", err)
}
var credentialSource string
if runtimeConfig.CredentialSource == CredentialSourceEC2 && runtimeConfig.IdentityType == IdentityTypeEC2 {
credentialSource = CredentialSourceEC2
} else if runtimeConfig.CredentialSource == CredentialSourceSSM && runtimeConfig.IdentityType == IdentityTypeEC2 {
credentialSource = CredentialSourceSSM
} else {
credentialSource = CredentialSourceNone
}
return &EC2RoleProvider{
InnerProviders: innerProviders,
Log: log.WithContext(ec2rolecreds.ProviderName),
InstanceInfo: instanceInfo,
SsmEndpoint: ssmEndpoint,
RuntimeConfigClient: runtimeConfigClient,
credentialSource: credentialSource,
shouldShareCredentials: true,
expirationUpdateLock: &sync.Mutex{},
}
}
// GetInnerProvider gets the remote role provider that is currently being used for credentials
func (p *EC2RoleProvider) GetInnerProvider() IInnerProvider {
if p.credentialSource == CredentialSourceSSM {
return p.InnerProviders.SsmEc2Provider
}
return p.InnerProviders.IPRProvider
}
// GetInstanceRegion gets the region of the instance for this provider.
func (p *EC2RoleProvider) GetInstanceRegion() string {
if p.InstanceInfo == nil {
return "" // Should never happen with proper initialization
}
return p.InstanceInfo.Region
}
// RetrieveWithContext returns shared credentials if specified in runtime config
// and returns instance profile role credentials otherwise.
// If neither can be retrieved then empty credentials are returned
// This function is intended for use by agent workers that require credentials
func (p *EC2RoleProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) {
if runtimeConfig, err := p.RuntimeConfigClient.GetConfigWithRetry(); err != nil {
p.Log.Errorf("Failed to read runtime config for ShareFile information. Err: %v", err)
} else if runtimeConfig.ShareFile != "" {
sharedCreds, err := p.InnerProviders.SharedCredentialsProvider.RetrieveWithContext(ctx)
if err != nil {
err = fmt.Errorf("unable to load shared credentials. Err: %w", err)
p.Log.Error(err)
return iprEmptyCredential, err
}
p.credentialSource = CredentialSourceSSM
return sharedCreds, nil
}
iprCredentials, err := p.InnerProviders.IPRProvider.RetrieveWithContext(ctx)
if err != nil {
if awsErr := sdkutil.GetAwsError(err); awsErr != nil {
err = awserr.New(awsErr.Code(), awsErr.Message(), nil)
}
err = fmt.Errorf("failed to retrieve instance profile role credentials. Err: %w", err)
p.Log.Error(err)
return iprEmptyCredential, err
}
// set expiration to 30 minutes
p.InnerProviders.IPRProvider.SetExpiration(timeNowFunc().Add(30*time.Minute), 0)
p.credentialSource = CredentialSourceEC2
return iprCredentials, nil
}
// RemoteRetrieve uses network calls to retrieve credentials for EC2 instances
// This function is intended for use by the core module's credential refresher routine
// When an error is returned, credential source is updated to CredentialSourceNone
func (p *EC2RoleProvider) RemoteRetrieve(ctx context.Context) (credentials.Value, error) {
p.Log.Debug("Attempting to retrieve instance profile role")
if iprCredentials, err := p.iprCredentials(ctx, p.SsmEndpoint); err != nil {
errCode := sdkutil.GetAwsErrorCode(err)
if _, ok := exceptionsForDefaultHostMgmt[errCode]; ok {
p.Log.Warnf("Failed to connect to Systems Manager with instance profile role credentials. Err: %v", err)
} else {
p.credentialSource = CredentialSourceNone
return iprEmptyCredential, fmt.Errorf("unexpected error getting instance profile role credentials or calling UpdateInstanceInformation. Skipping default host management fallback: %w", err)
}
} else {
p.Log.Info("Successfully connected with instance profile role credentials")
p.credentialSource = CredentialSourceEC2
return iprCredentials.Get()
}
p.Log.Debug("Attempting to retrieve credentials from Systems Manager")
if ssmCredentials, err := p.InnerProviders.SsmEc2Provider.RetrieveWithContext(ctx); err != nil {
p.Log.Errorf("Failed to connect to Systems Manager with SSM role credentials. %v", err)
p.credentialSource = CredentialSourceNone
return iprEmptyCredential, fmt.Errorf("no valid credentials could be retrieved for ec2 identity. Default Host Management Err: %w", err)
} else {
p.Log.Info("Successfully connected with Systems Manager role credentials")
p.credentialSource = CredentialSourceSSM
return ssmCredentials, nil
}
}
// Retrieve returns instance profile role credentials if it has sufficient systems manager permissions and
// returns ssm provided credentials otherwise. If neither can be retrieved then empty credentials are returned
// This function is intended for use by agent workers that require credentials
func (p *EC2RoleProvider) Retrieve() (credentials.Value, error) {
return p.RetrieveWithContext(context.Background())
}
// iprCredentials retrieves instance profile role credentials and returns an error if the returned credentials cannot
// connect to Systems Manager
func (p *EC2RoleProvider) iprCredentials(ctx context.Context, ssmEndpoint string) (*credentials.Credentials, error) {
// Setup SSM client with instance profile role credentials
iprCredentials := newCredentials(p.InnerProviders.IPRProvider)
err := p.updateEmptyInstanceInformation(ctx, ssmEndpoint, iprCredentials)
if err != nil {
if awsErr, ok := err.(awserr.RequestFailure); ok {
err = fmt.Errorf("retrieved credentials failed to report to ssm. RequestId: %s Error: %w", awsErr.RequestID(), awsErr)
} else {
err = fmt.Errorf("retrieved credentials failed to report to ssm. Error: %w", err)
}
return nil, err
}
// Trusting instance profile role credentials are valid for at least 1 hour when retrieved
p.InnerProviders.IPRProvider.SetExpiration(timeNowFunc().Add(1*time.Hour), 0)
return iprCredentials, nil
}
// updateEmptyInstanceInformation calls UpdateInstanceInformation with minimal parameters
func (p *EC2RoleProvider) updateEmptyInstanceInformation(ctx context.Context, ssmEndpoint string, roleCredentials *credentials.Credentials) error {
ssmClient := newV4ServiceWithCreds(p.Log.WithContext("SSMService"), roleCredentials, p.GetInstanceRegion(), ssmEndpoint)
p.Log.Debugf("Calling UpdateInstanceInformation")
// Call update instance information with instance profile role
input := &ssm.UpdateInstanceInformationInput{
AgentName: aws.String(agentName),
AgentVersion: aws.String(version.Version),
InstanceId: aws.String(p.InstanceInfo.InstanceId),
}
goOS := runtime.GOOS
switch goOS {
case "windows":
input.PlatformType = aws.String(ssm.PlatformTypeWindows)
case "linux", "freebsd":
input.PlatformType = aws.String(ssm.PlatformTypeLinux)
case "darwin":
input.PlatformType = aws.String(ssm.PlatformTypeMacOs)
}
_, err := ssmClient.UpdateInstanceInformationWithContext(ctx, input)
if err != nil {
if awsErr := sdkutil.GetAwsError(err); awsErr != nil {
err = awserr.New(awsErr.Code(), awsErr.Message(), nil)
}
}
return err
}
// ShareFile is the credentials file where the agent should write shared credentials
// Only default host management role credentials are shared across workers
func (p *EC2RoleProvider) ShareFile() string {
switch p.credentialSource {
case CredentialSourceSSM:
return appconfig.DefaultEC2SharedCredentialsFilePath
default:
return ""
}
}
// ShareProfile is the profile where the agent should write shared credentials
func (p *EC2RoleProvider) ShareProfile() string {
return ""
}
// SharesCredentials returns true if credentials may be saved to disk
func (p *EC2RoleProvider) SharesCredentials() bool {
return true
}
// IsExpired wraps the IsExpired method of the current provider
func (p *EC2RoleProvider) IsExpired() bool {
if p.credentialSource == CredentialSourceSSM {
return p.InnerProviders.SharedCredentialsProvider.IsExpired()
}
return p.InnerProviders.IPRProvider.IsExpired()
}
// ExpiresAt returns the expiry of shared credentials using shared credentials
// and returns instance profile role provider expiry otherwise
func (p *EC2RoleProvider) ExpiresAt() time.Time {
if p.credentialSource == CredentialSourceSSM {
return p.InnerProviders.SharedCredentialsProvider.ExpiresAt()
}
return p.InnerProviders.IPRProvider.ExpiresAt()
}
// RemoteExpiresAt returns the expiry of the remote inner provider currently in use
// This function is intended for use by the core module's credential refresher routine
func (p *EC2RoleProvider) RemoteExpiresAt() time.Time {
return p.GetInnerProvider().ExpiresAt()
}
// CredentialSource returns the name of the current provider being used
func (p *EC2RoleProvider) CredentialSource() string {
return p.credentialSource
}