carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package rootaws
2
3import (
4 "log"
5 "os"
6 "strings"
7)
8
9// A child of a device cannot exist, even if a service user creates directories.
10const disabledSharedFile = "/dev/null/carlos-root-aws"
11
12// rootScrubbedEnvironment names every variable that selects a file, a profile,
13// an endpoint or a trust store the SDK would use on root's behalf. Root's own
14// credentials come from the environment keys, the instance profile, or the
15// validated off-cloud file, never from these.
16var rootScrubbedEnvironment = []string{
17 "AWS_CONFIG_FILE",
18 "AWS_SHARED_CREDENTIALS_FILE",
19 "AWS_PROFILE",
20 "AWS_DEFAULT_PROFILE",
21 "AWS_SDK_LOAD_CONFIG",
22 "AWS_CA_BUNDLE",
23 "AWS_WEB_IDENTITY_TOKEN_FILE",
24 "AWS_ROLE_ARN",
25 "AWS_ROLE_SESSION_NAME",
26 "AWS_CONTAINER_CREDENTIALS_FULL_URI",
27 "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
28 "AWS_CONTAINER_AUTHORIZATION_TOKEN",
29 "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
30 "AWS_ENDPOINT_URL",
31 "AWS_EC2_METADATA_SERVICE_ENDPOINT",
32 "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE",
33 // Go's TLS trust store reads these unvalidated; AWS transports use it.
34 "SSL_CERT_FILE",
35 "SSL_CERT_DIR",
36}
37
38// Service-specific endpoint overrides (AWS_ENDPOINT_URL_S3 and the rest)
39// are matched by prefix.
40const rootScrubbedEndpointPrefix = "AWS_ENDPOINT_URL_"
41
42// ScrubEnvironment must run before dispatch or goroutines in each executable.
43// It also protects SDK loads in dependencies that do not call this package.
44func ScrubEnvironment() {
45 scrubEnvironment(os.Geteuid())
46}
47
48func scrubEnvironment(euid int) {
49 if euid != 0 {
50 return
51 }
52 changed := false
53 keys := append([]string(nil), rootScrubbedEnvironment...)
54 for _, kv := range os.Environ() {
55 if key, _, _ := strings.Cut(kv, "="); strings.HasPrefix(key, rootScrubbedEndpointPrefix) {
56 keys = append(keys, key)
57 }
58 }
59 for _, key := range keys {
60 if value, set := os.LookupEnv(key); set && !inheritedSentinel(key, value) {
61 changed = true
62 }
63 _ = os.Unsetenv(key)
64 }
65 // A proxy may be needed off-cloud, so it stays, but it never answers for
66 // the instance metadata service: that is where root's EC2 credentials
67 // come from.
68 for _, key := range []string{"NO_PROXY", "no_proxy"} {
69 _ = os.Setenv(key, withIMDSBypass(os.Getenv(key)))
70 }
71 // Root never reads shared config, not even under its own HOME: a
72 // root-owned HOME can still hold a .aws the service user can write.
73 _ = os.Setenv("AWS_CONFIG_FILE", disabledSharedFile)
74 _ = os.Setenv("AWS_SHARED_CREDENTIALS_FILE", disabledSharedFile)
75 if changed {
76 log.Print("ALERT: root AWS file, profile, endpoint and trust-store environment overrides ignored")
77 }
78}
79
80// inheritedSentinel reports a shared-file key that already holds the value
81// this scrub sets: a root child of a scrubbed root parent inherits it, and
82// that is not an override. Alerting on it would put the alert on the child's
83// stderr, where a guarded updater reading `version` with combined output
84// refuses the candidate.
85func inheritedSentinel(key, value string) bool {
86 return (key == "AWS_CONFIG_FILE" || key == "AWS_SHARED_CREDENTIALS_FILE") && value == disabledSharedFile
87}
88
89// imdsHosts are the IPv4 and IPv6 instance metadata addresses.
90var imdsHosts = []string{"169.254.169.254", "fd00:ec2::254"}
91
92func withIMDSBypass(noProxy string) string {
93 entries := strings.FieldsFunc(noProxy, func(r rune) bool { return r == ',' })
94 for _, host := range imdsHosts {
95 found := false
96 for _, entry := range entries {
97 if strings.TrimSpace(entry) == host {
98 found = true
99 }
100 }
101 if !found {
102 entries = append(entries, host)
103 }
104 }
105 return strings.Join(entries, ",")
106}
107