package rootaws import ( "log" "os" "strings" ) // A child of a device cannot exist, even if a service user creates directories. const disabledSharedFile = "/dev/null/carlos-root-aws" // rootScrubbedEnvironment names every variable that selects a file, a profile, // an endpoint or a trust store the SDK would use on root's behalf. Root's own // credentials come from the environment keys, the instance profile, or the // validated off-cloud file, never from these. var rootScrubbedEnvironment = []string{ "AWS_CONFIG_FILE", "AWS_SHARED_CREDENTIALS_FILE", "AWS_PROFILE", "AWS_DEFAULT_PROFILE", "AWS_SDK_LOAD_CONFIG", "AWS_CA_BUNDLE", "AWS_WEB_IDENTITY_TOKEN_FILE", "AWS_ROLE_ARN", "AWS_ROLE_SESSION_NAME", "AWS_CONTAINER_CREDENTIALS_FULL_URI", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "AWS_CONTAINER_AUTHORIZATION_TOKEN", "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", "AWS_ENDPOINT_URL", "AWS_EC2_METADATA_SERVICE_ENDPOINT", "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE", // Go's TLS trust store reads these unvalidated; AWS transports use it. "SSL_CERT_FILE", "SSL_CERT_DIR", } // Service-specific endpoint overrides (AWS_ENDPOINT_URL_S3 and the rest) // are matched by prefix. const rootScrubbedEndpointPrefix = "AWS_ENDPOINT_URL_" // ScrubEnvironment must run before dispatch or goroutines in each executable. // It also protects SDK loads in dependencies that do not call this package. func ScrubEnvironment() { scrubEnvironment(os.Geteuid()) } func scrubEnvironment(euid int) { if euid != 0 { return } changed := false keys := append([]string(nil), rootScrubbedEnvironment...) for _, kv := range os.Environ() { if key, _, _ := strings.Cut(kv, "="); strings.HasPrefix(key, rootScrubbedEndpointPrefix) { keys = append(keys, key) } } for _, key := range keys { if value, set := os.LookupEnv(key); set && !inheritedSentinel(key, value) { changed = true } _ = os.Unsetenv(key) } // A proxy may be needed off-cloud, so it stays, but it never answers for // the instance metadata service: that is where root's EC2 credentials // come from. for _, key := range []string{"NO_PROXY", "no_proxy"} { _ = os.Setenv(key, withIMDSBypass(os.Getenv(key))) } // Root never reads shared config, not even under its own HOME: a // root-owned HOME can still hold a .aws the service user can write. _ = os.Setenv("AWS_CONFIG_FILE", disabledSharedFile) _ = os.Setenv("AWS_SHARED_CREDENTIALS_FILE", disabledSharedFile) if changed { log.Print("ALERT: root AWS file, profile, endpoint and trust-store environment overrides ignored") } } // inheritedSentinel reports a shared-file key that already holds the value // this scrub sets: a root child of a scrubbed root parent inherits it, and // that is not an override. Alerting on it would put the alert on the child's // stderr, where a guarded updater reading `version` with combined output // refuses the candidate. func inheritedSentinel(key, value string) bool { return (key == "AWS_CONFIG_FILE" || key == "AWS_SHARED_CREDENTIALS_FILE") && value == disabledSharedFile } // imdsHosts are the IPv4 and IPv6 instance metadata addresses. var imdsHosts = []string{"169.254.169.254", "fd00:ec2::254"} func withIMDSBypass(noProxy string) string { entries := strings.FieldsFunc(noProxy, func(r rune) bool { return r == ',' }) for _, host := range imdsHosts { found := false for _, entry := range entries { if strings.TrimSpace(entry) == host { found = true } } if !found { entries = append(entries, host) } } return strings.Join(entries, ",") }