// Package rootaws keeps root's AWS clients away from executable shared profiles. package rootaws import ( "context" "errors" "fmt" "log" "os" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" ) const credentialsPath = "/etc/carlos/credentials" // OffcloudCredentials returns the validated root file credentials only when // complete environment keys do not take precedence. It never contacts IMDS. func OffcloudCredentials() (aws.Credentials, error) { return offcloudCredentials(credentialsPath) } func offcloudCredentials(path string) (aws.Credentials, error) { if envAccessKey() != "" && envSecretKey() != "" { return aws.Credentials{}, nil } creds, err := readStaticCredentials(path) if errors.Is(err, os.ErrNotExist) { return aws.Credentials{}, nil } return creds, err } // LoadDefaultConfig preserves the SDK chain for non-root callers. Root disables // both shared-file lists, including paths selected through HOME or AWS_* vars. func LoadDefaultConfig(ctx context.Context, opts ...func(*awsconfig.LoadOptions) error) (aws.Config, error) { if os.Geteuid() != 0 { return awsconfig.LoadDefaultConfig(ctx, opts...) } return loadRootConfig(ctx, credentialsPath, opts...) } func loadRootConfig(ctx context.Context, path string, opts ...func(*awsconfig.LoadOptions) error) (aws.Config, error) { // The SDK cannot ignore an environment profile per call. Refuse it rather // than mutate process-wide environment while other goroutines may read it. if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" { err := fmt.Errorf("root AWS clients do not support AWS_PROFILE or AWS_DEFAULT_PROFILE") log.Printf("ALERT: %v", err) return aws.Config{}, err } // Complete environment keys are used as given and the fallback file is not // opened, so a file nobody uses cannot make loading fail. access, secret := envAccessKey(), envSecretKey() envKeys := access != "" && secret != "" var static aws.Credentials var refused error if !envKeys { var err error static, err = readStaticCredentials(path) if err != nil && !errors.Is(err, os.ErrNotExist) { log.Printf("ALERT: refusing unsafe offcloud credentials %s: %v", path, err) refused = fmt.Errorf("root AWS credentials: %w", err) } if err != nil { static = aws.Credentials{} } } // Append last so callers cannot restore file loading or select a profile. opts = append(opts, awsconfig.WithSharedConfigFiles([]string{}), awsconfig.WithSharedCredentialsFiles([]string{}), awsconfig.WithSharedConfigProfile("")) switch { case refused != nil: // Opening a store must not abort adoption before it uses AWS. Refuse // retrieval instead, without falling through to another identity. opts = append(opts, awsconfig.WithCredentialsProvider(aws.CredentialsProviderFunc( func(context.Context) (aws.Credentials, error) { return aws.Credentials{}, refused }))) case envKeys: opts = append(opts, awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( access, secret, os.Getenv("AWS_SESSION_TOKEN")))) case static.AccessKeyID != "": opts = append(opts, awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( static.AccessKeyID, static.SecretAccessKey, static.SessionToken))) } return awsconfig.LoadDefaultConfig(ctx, opts...) } func envAccessKey() string { if v := os.Getenv("AWS_ACCESS_KEY_ID"); v != "" { return v } return os.Getenv("AWS_ACCESS_KEY") } func envSecretKey() string { if v := os.Getenv("AWS_SECRET_ACCESS_KEY"); v != "" { return v } return os.Getenv("AWS_SECRET_KEY") }