carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Package rootaws keeps root's AWS clients away from executable shared profiles.
2package rootaws
3
4import (
5 "context"
6 "errors"
7 "fmt"
8 "log"
9 "os"
10
11 "github.com/aws/aws-sdk-go-v2/aws"
12 awsconfig "github.com/aws/aws-sdk-go-v2/config"
13 "github.com/aws/aws-sdk-go-v2/credentials"
14)
15
16const credentialsPath = "/etc/carlos/credentials"
17
18// OffcloudCredentials returns the validated root file credentials only when
19// complete environment keys do not take precedence. It never contacts IMDS.
20func OffcloudCredentials() (aws.Credentials, error) {
21 return offcloudCredentials(credentialsPath)
22}
23
24func offcloudCredentials(path string) (aws.Credentials, error) {
25 if envAccessKey() != "" && envSecretKey() != "" {
26 return aws.Credentials{}, nil
27 }
28 creds, err := readStaticCredentials(path)
29 if errors.Is(err, os.ErrNotExist) {
30 return aws.Credentials{}, nil
31 }
32 return creds, err
33}
34
35// LoadDefaultConfig preserves the SDK chain for non-root callers. Root disables
36// both shared-file lists, including paths selected through HOME or AWS_* vars.
37func LoadDefaultConfig(ctx context.Context, opts ...func(*awsconfig.LoadOptions) error) (aws.Config, error) {
38 if os.Geteuid() != 0 {
39 return awsconfig.LoadDefaultConfig(ctx, opts...)
40 }
41 return loadRootConfig(ctx, credentialsPath, opts...)
42}
43
44func loadRootConfig(ctx context.Context, path string, opts ...func(*awsconfig.LoadOptions) error) (aws.Config, error) {
45 // The SDK cannot ignore an environment profile per call. Refuse it rather
46 // than mutate process-wide environment while other goroutines may read it.
47 if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
48 err := fmt.Errorf("root AWS clients do not support AWS_PROFILE or AWS_DEFAULT_PROFILE")
49 log.Printf("ALERT: %v", err)
50 return aws.Config{}, err
51 }
52 // Complete environment keys are used as given and the fallback file is not
53 // opened, so a file nobody uses cannot make loading fail.
54 access, secret := envAccessKey(), envSecretKey()
55 envKeys := access != "" && secret != ""
56 var static aws.Credentials
57 var refused error
58 if !envKeys {
59 var err error
60 static, err = readStaticCredentials(path)
61 if err != nil && !errors.Is(err, os.ErrNotExist) {
62 log.Printf("ALERT: refusing unsafe offcloud credentials %s: %v", path, err)
63 refused = fmt.Errorf("root AWS credentials: %w", err)
64 }
65 if err != nil {
66 static = aws.Credentials{}
67 }
68 }
69 // Append last so callers cannot restore file loading or select a profile.
70 opts = append(opts, awsconfig.WithSharedConfigFiles([]string{}),
71 awsconfig.WithSharedCredentialsFiles([]string{}), awsconfig.WithSharedConfigProfile(""))
72 switch {
73 case refused != nil:
74 // Opening a store must not abort adoption before it uses AWS. Refuse
75 // retrieval instead, without falling through to another identity.
76 opts = append(opts, awsconfig.WithCredentialsProvider(aws.CredentialsProviderFunc(
77 func(context.Context) (aws.Credentials, error) { return aws.Credentials{}, refused })))
78 case envKeys:
79 opts = append(opts, awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
80 access, secret, os.Getenv("AWS_SESSION_TOKEN"))))
81 case static.AccessKeyID != "":
82 opts = append(opts, awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
83 static.AccessKeyID, static.SecretAccessKey, static.SessionToken)))
84 }
85 return awsconfig.LoadDefaultConfig(ctx, opts...)
86}
87
88func envAccessKey() string {
89 if v := os.Getenv("AWS_ACCESS_KEY_ID"); v != "" {
90 return v
91 }
92 return os.Getenv("AWS_ACCESS_KEY")
93}
94
95func envSecretKey() string {
96 if v := os.Getenv("AWS_SECRET_ACCESS_KEY"); v != "" {
97 return v
98 }
99 return os.Getenv("AWS_SECRET_KEY")
100}
101