From 07c8074ce58c7134d8612118b3b54bad4a149a9b Mon Sep 17 00:00:00 2001 From: Jack Kleeman Date: Fri, 23 Feb 2024 23:03:46 +0000 Subject: [PATCH] Support EKS Pod Identity credentials (#3416) ## Motivation and Context I would like to support EKS Pod Identity credentials in the Rust SDKs ## Description This brings the ECS provider in line with other sdks (eg, Go) by supporting AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE as well as permitting http IPs to be non-loopback if they are the EKS pod identity IPs. ## Testing I have added various new unit tests, and I have updated the existing integration test to also create pods with eks pod identity creds, which I have used to test in a real EKS cluster as well. ## Checklist - [x] I have updated `CHANGELOG.next.toml` if I made changes to the smithy-rs codegen or runtime crates ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._ --------- Signed-off-by: Jack Kleeman Co-authored-by: John DiSanti Co-authored-by: John DiSanti --- .pre-commit-config.yaml | 2 +- CHANGELOG.next.toml | 8 +- .../eks-credentials/README.md | 10 +- .../lib/eks-credentials-stack.ts | 80 +++- .../eks-credentials/package-lock.json | 424 ++++++++++++++++-- .../eks-credentials/package.json | 3 +- .../integration-tests/eks-credentials/test.rs | 4 +- .../src/default_provider/credentials.rs | 3 + aws/rust-runtime/aws-config/src/ecs.rs | 301 +++++++++++-- .../eks_pod_identity_credentials/env.json | 6 + .../eks_pod_identity_credentials/fs/token.jwt | 1 + .../http-traffic.json | 87 ++++ .../test-case.json | 12 + .../eks_pod_identity_no_token_file/env.json | 6 + .../http-traffic.json | 5 + .../test-case.json | 7 + .../aws-config/test-data/ecs-tests.json | 27 ++ 17 files changed, 886 insertions(+), 100 deletions(-) create mode 100644 aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/env.json create mode 100644 aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/fs/token.jwt create mode 100644 aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/http-traffic.json create mode 100644 aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/test-case.json create mode 100644 aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/env.json create mode 100644 aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/http-traffic.json create mode 100644 aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/test-case.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b29005e206..7a45a31a88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: check-yaml - id: end-of-file-fixer - exclude: ^aws/rust-runtime/aws-sigv4/aws-sig-v4-test-suite/ + exclude: ^aws/rust-runtime/(aws-sigv4/aws-sig-v4-test-suite/|aws-config/test-data/default-provider-chain/eks_pod_identity_credentials/fs/token.jwt$) - id: trailing-whitespace - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.11.0 diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index fc4c4c2578..097e51e636 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -9,4 +9,10 @@ # message = "Fix typos in module documentation for generated crates" # references = ["smithy-rs#920"] # meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"} -# author = "rcoh" \ No newline at end of file +# author = "rcoh" + +[[aws-sdk-rust]] +message = "EKS Pod Identity is now supported as part of the default ECS credential provider." +references = ["smithy-rs#3416"] +meta = { "breaking" = false, "bug" = false, "tada" = true } +author = "jackkleeman" diff --git a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/README.md b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/README.md index 77b5a4e2aa..a37f902e3c 100644 --- a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/README.md +++ b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/README.md @@ -1,6 +1,8 @@ # CDK Stack for EKS credentials provider testing -This project defines a CDK stack that launches an EKS cluster, creates a DynamoDB table, and sets up a service account role so that the DynamoDB pod can access the table. +This project defines a CDK stack that launches an EKS cluster, creates a DynamoDB table, and sets up: +1. A service account role that allows a pod to access the table via IRSA. +2. A service account and pod identity association that allows a pod to access the table via EKS Pod Identity. `test.rs` is provided as an example script to run. @@ -9,6 +11,8 @@ This project defines a CDK stack that launches an EKS cluster, creates a DynamoD cdk bootstrap aws://accountid/region cdk deploy # make lunch, go for a bike ride, etc. ~1h. -kubectl exec rust-sdk-test -it bash -# write some rust code, e.g. test.rs, run it. +kubectl exec rust-sdk-test-irsa -it bash +# write some rust code, e.g. test.rs, run it. will have irsa identity +kubectl exec rust-sdk-test-pod-identity -it bash +# run more rust code. will have eks pod identity ``` diff --git a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/lib/eks-credentials-stack.ts b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/lib/eks-credentials-stack.ts index 644402f019..33966d8e67 100644 --- a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/lib/eks-credentials-stack.ts +++ b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/lib/eks-credentials-stack.ts @@ -5,8 +5,10 @@ import { Stack, StackProps } from "aws-cdk-lib"; import * as eks from "aws-cdk-lib/aws-eks"; +import * as iam from "aws-cdk-lib/aws-iam"; import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; import { Construct } from "constructs"; +import {KubectlV28Layer} from "@aws-cdk/lambda-layer-kubectl-v28"; export class EksCredentialsStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { @@ -15,21 +17,64 @@ export class EksCredentialsStack extends Stack { // create a cluster const cluster = new eks.Cluster(this, 'hello-eks', { - version: eks.KubernetesVersion.V1_21, + version: eks.KubernetesVersion.V1_28, + kubectlLayer: new KubectlV28Layer(this, 'hello-eks-kubectl'), }); - const serviceAccount = cluster.addServiceAccount("eks-service-account"); + + const podIdentityAddon = new eks.CfnAddon(this, 'eks-pod-identity-addon', { + addonName: 'eks-pod-identity-agent', + clusterName: cluster.clusterName, + addonVersion: 'v1.1.0-eksbuild.1', + }); + + const serviceAccountIRSA = cluster.addServiceAccount("eks-service-account-irsa"); + const serviceAccountPodIdentity = cluster.addManifest("eks-service-account-pod-identity", { + apiVersion: "v1", + kind: "ServiceAccount", + metadata: { + name: "eks-service-account-pod-identity", + namespace: "default", + labels: {"app.kubernetes.io/name": "eks-service-account-pod-identity"}, + } + }) + const table = new dynamodb.Table(this, 'Table', { partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } }); - table.grantReadWriteData(serviceAccount); + table.grantReadWriteData(serviceAccountIRSA); + + let podIdentityPrincipal = new iam.ServicePrincipal("pods.eks.amazonaws.com") + + let podIdentityRole = new iam.Role(cluster, "eks-role-pod-identity", { + assumedBy: podIdentityPrincipal, + }) + podIdentityRole.assumeRolePolicy?.addStatements(iam.PolicyStatement.fromJson({ + "Sid": "AllowEksAuthToAssumeRoleForPodIdentity", + "Effect": "Allow", + "Principal": { + "Service": "pods.eks.amazonaws.com" + }, + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ] + })) + table.grantReadWriteData(podIdentityRole); + + let podIdentityAssociation = new eks.CfnPodIdentityAssociation(cluster, "eks-pod-identity-associations", { + roleArn: podIdentityRole.roleArn, + clusterName: cluster.clusterName, + namespace: "default", + serviceAccount: "eks-service-account-pod-identity", + }) // apply a kubernetes manifest to the cluster - const pod = cluster.addManifest('rust-sdk-test', { + const podIRSA = cluster.addManifest('rust-sdk-test-irsa', { apiVersion: 'v1', kind: 'Pod', - metadata: { name: 'rust-sdk-test' }, + metadata: { name: 'rust-sdk-test-irsa' }, spec: { - serviceAccountName: serviceAccount.serviceAccountName, + serviceAccountName: serviceAccountIRSA.serviceAccountName, containers: [ { name: 'hello', @@ -41,6 +86,27 @@ export class EksCredentialsStack extends Stack { ] } }); - pod.node.addDependency(serviceAccount); + podIRSA.node.addDependency(serviceAccountIRSA); + + const podPodIdentity = cluster.addManifest('rust-sdk-test-pod-identity', { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'rust-sdk-test-pod-identity' }, + spec: { + serviceAccountName: "eks-service-account-pod-identity", + containers: [ + { + name: 'hello', + image: 'rust:buster', + ports: [{ containerPort: 8080 }], + command: ['sh', '-c', 'sleep infinity'], + env: [{name: 'DYNAMO_TABLE', value: table.tableName}] + } + ] + } + }); + podPodIdentity.node.addDependency(serviceAccountPodIdentity); + podPodIdentity.node.addDependency(podIdentityAssociation); + podPodIdentity.node.addDependency(podIdentityAddon) } } diff --git a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/package-lock.json b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/package-lock.json index 3c54073866..f9927e3212 100644 --- a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/package-lock.json +++ b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/package-lock.json @@ -12,14 +12,43 @@ "eks-credentials": "bin/eks-credentials.js" }, "devDependencies": { + "@aws-cdk/lambda-layer-kubectl-v28": "^2.2.0", "@types/node": "10.17.27", "aws-cdk": "^2.0.0", - "aws-cdk-lib": "^2.0.0", + "aws-cdk-lib": "^2.124.0", "constructs": "^10.0.0", "ts-node": "^9.0.0", "typescript": "~4.5.5" } }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.202", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", + "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==", + "dev": true + }, + "node_modules/@aws-cdk/asset-kubectl-v20": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", + "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==", + "dev": true + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", + "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==", + "dev": true + }, + "node_modules/@aws-cdk/lambda-layer-kubectl-v28": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/lambda-layer-kubectl-v28/-/lambda-layer-kubectl-v28-2.2.0.tgz", + "integrity": "sha512-m7nMDn/Ff9S+gJ5Sok5NuYHBzgsj3Xz3dOo0BxXYJJNPl9UtD1HnPcKV56lHn9+BACJff/h8aPUMln0xCUPuIw==", + "dev": true, + "peerDependencies": { + "aws-cdk-lib": "^2.28.0", + "constructs": "^10.0.5" + } + }, "node_modules/@types/node": { "version": "10.17.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.27.tgz", @@ -48,9 +77,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.17.0.tgz", - "integrity": "sha512-bga2HptbGx3rMdSkIKxBS13miogj/DHB2VPfQZAoKoCOAanOot+M3mHhYqe5aNdxhrppaRjG2eid2p1/MvRnvg==", + "version": "2.128.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.128.0.tgz", + "integrity": "sha512-cAU1L4jtPXPQXpa9kS2/HhHdkg3xGc5GCqwRgivdoj/iLQF3dDwIouOkwDBY/S5pXMqOUC7IoVdIPPbIgfGlsQ==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -60,18 +89,23 @@ "minimatch", "punycode", "semver", + "table", "yaml" ], "dev": true, "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.202", + "@aws-cdk/asset-kubectl-v20": "^2.1.2", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^9.1.0", - "ignore": "^5.2.0", - "jsonschema": "^1.4.0", + "fs-extra": "^11.2.0", + "ignore": "^5.3.1", + "jsonschema": "^1.4.1", "minimatch": "^3.1.2", - "punycode": "^2.1.1", - "semver": "^7.3.5", + "punycode": "^2.3.1", + "semver": "^7.5.4", + "table": "^6.8.1", "yaml": "1.10.2" }, "engines": { @@ -87,13 +121,53 @@ "inBundle": true, "license": "Apache-2.0" }, - "node_modules/aws-cdk-lib/node_modules/at-least-node": { - "version": "1.0.0", + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.12.0", "dev": true, "inBundle": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", "engines": { - "node": ">= 4.0.0" + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/aws-cdk-lib/node_modules/balanced-match": { @@ -121,35 +195,64 @@ "node": ">= 0.8.0" } }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, "node_modules/aws-cdk-lib/node_modules/concat-map": { "version": "0.0.1", "dev": true, "inBundle": true, "license": "MIT" }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "9.1.0", + "version": "11.2.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=14.14" } }, "node_modules/aws-cdk-lib/node_modules/graceful-fs": { - "version": "4.2.9", + "version": "4.2.11", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.2.0", + "version": "5.3.1", "dev": true, "inBundle": true, "license": "MIT", @@ -157,6 +260,21 @@ "node": ">= 4" } }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, "node_modules/aws-cdk-lib/node_modules/jsonfile": { "version": "6.1.0", "dev": true, @@ -170,7 +288,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/jsonschema": { - "version": "1.4.0", + "version": "1.4.1", "dev": true, "inBundle": true, "license": "MIT", @@ -178,6 +296,12 @@ "node": "*" } }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, "node_modules/aws-cdk-lib/node_modules/lru-cache": { "version": "6.0.0", "dev": true, @@ -203,7 +327,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.1.1", + "version": "2.3.1", "dev": true, "inBundle": true, "license": "MIT", @@ -211,8 +335,17 @@ "node": ">=6" } }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.3.5", + "version": "7.5.4", "dev": true, "inBundle": true, "license": "ISC", @@ -226,8 +359,67 @@ "node": ">=10" } }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.8.1", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", "dev": true, "inBundle": true, "license": "MIT", @@ -235,6 +427,15 @@ "node": ">= 10.0.0" } }, + "node_modules/aws-cdk-lib/node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/aws-cdk-lib/node_modules/yallist": { "version": "4.0.0", "dev": true, @@ -369,6 +570,31 @@ } }, "dependencies": { + "@aws-cdk/asset-awscli-v1": { + "version": "2.2.202", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", + "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==", + "dev": true + }, + "@aws-cdk/asset-kubectl-v20": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", + "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==", + "dev": true + }, + "@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", + "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==", + "dev": true + }, + "@aws-cdk/lambda-layer-kubectl-v28": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/lambda-layer-kubectl-v28/-/lambda-layer-kubectl-v28-2.2.0.tgz", + "integrity": "sha512-m7nMDn/Ff9S+gJ5Sok5NuYHBzgsj3Xz3dOo0BxXYJJNPl9UtD1HnPcKV56lHn9+BACJff/h8aPUMln0xCUPuIw==", + "dev": true, + "requires": {} + }, "@types/node": { "version": "10.17.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.27.tgz", @@ -391,19 +617,23 @@ } }, "aws-cdk-lib": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.17.0.tgz", - "integrity": "sha512-bga2HptbGx3rMdSkIKxBS13miogj/DHB2VPfQZAoKoCOAanOot+M3mHhYqe5aNdxhrppaRjG2eid2p1/MvRnvg==", + "version": "2.128.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.128.0.tgz", + "integrity": "sha512-cAU1L4jtPXPQXpa9kS2/HhHdkg3xGc5GCqwRgivdoj/iLQF3dDwIouOkwDBY/S5pXMqOUC7IoVdIPPbIgfGlsQ==", "dev": true, "requires": { + "@aws-cdk/asset-awscli-v1": "^2.2.202", + "@aws-cdk/asset-kubectl-v20": "^2.1.2", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^9.1.0", - "ignore": "^5.2.0", - "jsonschema": "^1.4.0", + "fs-extra": "^11.2.0", + "ignore": "^5.3.1", + "jsonschema": "^1.4.1", "minimatch": "^3.1.2", - "punycode": "^2.1.1", - "semver": "^7.3.5", + "punycode": "^2.3.1", + "semver": "^7.5.4", + "table": "^6.8.1", "yaml": "1.10.2" }, "dependencies": { @@ -412,8 +642,32 @@ "bundled": true, "dev": true }, - "at-least-node": { - "version": "1.0.0", + "ajv": { + "version": "8.12.0", + "bundled": true, + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "bundled": true, + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", "bundled": true, "dev": true }, @@ -436,29 +690,61 @@ "bundled": true, "dev": true }, + "color-convert": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "bundled": true, + "dev": true + }, "concat-map": { "version": "0.0.1", "bundled": true, "dev": true }, + "emoji-regex": { + "version": "8.0.0", + "bundled": true, + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "bundled": true, + "dev": true + }, "fs-extra": { - "version": "9.1.0", + "version": "11.2.0", "bundled": true, "dev": true, "requires": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "graceful-fs": { - "version": "4.2.9", + "version": "4.2.11", "bundled": true, "dev": true }, "ignore": { - "version": "5.2.0", + "version": "5.3.1", + "bundled": true, + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", "bundled": true, "dev": true }, @@ -472,7 +758,12 @@ } }, "jsonschema": { - "version": "1.4.0", + "version": "1.4.1", + "bundled": true, + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", "bundled": true, "dev": true }, @@ -493,23 +784,76 @@ } }, "punycode": { - "version": "2.1.1", + "version": "2.3.1", + "bundled": true, + "dev": true + }, + "require-from-string": { + "version": "2.0.2", "bundled": true, "dev": true }, "semver": { - "version": "7.3.5", + "version": "7.5.4", "bundled": true, "dev": true, "requires": { "lru-cache": "^6.0.0" } }, + "slice-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "bundled": true, + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "table": { + "version": "6.8.1", + "bundled": true, + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + } + }, "universalify": { - "version": "2.0.0", + "version": "2.0.1", "bundled": true, "dev": true }, + "uri-js": { + "version": "4.4.1", + "bundled": true, + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "yallist": { "version": "4.0.0", "bundled": true, diff --git a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/package.json b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/package.json index 3e83cbc8b3..aff04c051b 100644 --- a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/package.json +++ b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/node": "10.17.27", "aws-cdk": "^2.0.0", - "aws-cdk-lib": "^2.0.0", + "aws-cdk-lib": "^2.124.0", + "@aws-cdk/lambda-layer-kubectl-v28": "^2.2.0", "constructs": "^10.0.0", "ts-node": "^9.0.0", "typescript": "~4.5.5" diff --git a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/test.rs b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/test.rs index 130476ef55..a3ef1c4d05 100644 --- a/aws/rust-runtime/aws-config/integration-tests/eks-credentials/test.rs +++ b/aws/rust-runtime/aws-config/integration-tests/eks-credentials/test.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_sdk_dynamodb::model::AttributeValue; +use aws_sdk_dynamodb::types::AttributeValue; #[tokio::main] async fn main() { let conf = aws_config::load_from_env().await; @@ -13,7 +13,7 @@ async fn main() { dynamo .get_item() .key("id", AttributeValue::S("foo".into())) - .table_name("EksCredentialsStack-TableCD117FA1-18ZPICQWXOPW") + .table_name(std::env::var("DYNAMO_TABLE").unwrap()) .send() .await ); diff --git a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs index fcb9b19812..20fcc1c28e 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs @@ -297,6 +297,9 @@ mod test { make_test!(ecs_credentials); make_test!(ecs_credentials_invalid_profile); + make_test!(eks_pod_identity_credentials); + make_test!(eks_pod_identity_no_token_file); + #[cfg(not(feature = "sso"))] make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]); #[cfg(not(feature = "sso"))] diff --git a/aws/rust-runtime/aws-config/src/ecs.rs b/aws/rust-runtime/aws-config/src/ecs.rs index 0e11dc8802..fc7e8ad290 100644 --- a/aws/rust-runtime/aws-config/src/ecs.rs +++ b/aws/rust-runtime/aws-config/src/ecs.rs @@ -17,12 +17,21 @@ //! to construct a URI rooted at `http://169.254.170.2`. For example, if the value of the environment //! variable was `/credentials`, the SDK would look for credentials at `http://169.254.170.2/credentials`. //! -//! **Next**: It wil check the value of `$AWS_CONTAINER_CREDENTIALS_FULL_URI`. This specifies the full -//! URL to load credentials. The URL MUST satisfy one of the following two properties: +//! **Next**: It will check the value of `$AWS_CONTAINER_CREDENTIALS_FULL_URI`. This specifies the full +//! URL to load credentials. The URL MUST satisfy one of the following three properties: //! 1. The URL begins with `https` -//! 2. The URL refers to a loopback device. If a URL contains a domain name instead of an IP address, -//! a DNS lookup will be performed. ALL resolved IP addresses MUST refer to a loopback interface, or -//! the credentials provider will return `CredentialsError::InvalidConfiguration` +//! 2. The URL refers to an allowed IP address. If a URL contains a domain name instead of an IP address, +//! a DNS lookup will be performed. ALL resolved IP addresses MUST refer to an allowed IP address, or +//! the credentials provider will return `CredentialsError::InvalidConfiguration`. Valid IP addresses are: +//! a) Loopback interfaces +//! b) The [ECS Task Metadata V2](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v2.html) +//! address ie 169.254.170.2. +//! c) [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) addresses +//! ie 169.254.170.23 or fd00:ec2::23 +//! +//! **Next**: It will check the value of `$AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE`. If this is set, +//! the filename specified will be read, and the value passed in the `Authorization` header. If the file +//! cannot be read, an error is returned. //! //! **Finally**: It will check the value of `$AWS_CONTAINER_AUTHORIZATION_TOKEN`. If this is set, the //! value will be passed in the `Authorization` header. @@ -54,13 +63,13 @@ use aws_smithy_runtime_api::client::dns::{ResolveDns, ResolveDnsError, SharedDns use aws_smithy_runtime_api::client::http::HttpConnectorSettings; use aws_smithy_runtime_api::shared::IntoShared; use aws_smithy_types::error::display::DisplayErrorContext; -use aws_types::os_shim_internal::Env; +use aws_types::os_shim_internal::{Env, Fs}; use http::header::InvalidHeaderValue; use http::uri::{InvalidUri, PathAndQuery, Scheme}; use http::{HeaderValue, Uri}; use std::error::Error; use std::fmt::{Display, Formatter}; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::time::Duration; use tokio::sync::OnceCell; @@ -71,7 +80,8 @@ const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(2); const BASE_HOST: &str = "http://169.254.170.2"; const ENV_RELATIVE_URI: &str = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; const ENV_FULL_URI: &str = "AWS_CONTAINER_CREDENTIALS_FULL_URI"; -const ENV_AUTHORIZATION: &str = "AWS_CONTAINER_AUTHORIZATION_TOKEN"; +const ENV_AUTHORIZATION_TOKEN: &str = "AWS_CONTAINER_AUTHORIZATION_TOKEN"; +const ENV_AUTHORIZATION_TOKEN_FILE: &str = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"; /// Credential provider for ECS and generalized HTTP credentials /// @@ -82,6 +92,7 @@ const ENV_AUTHORIZATION: &str = "AWS_CONTAINER_AUTHORIZATION_TOKEN"; pub struct EcsCredentialsProvider { inner: OnceCell, env: Env, + fs: Fs, builder: Builder, } @@ -93,15 +104,32 @@ impl EcsCredentialsProvider { /// Load credentials from this credentials provider pub async fn credentials(&self) -> provider::Result { - let auth = match self.env.get(ENV_AUTHORIZATION).ok() { - Some(auth) => Some(HeaderValue::from_str(&auth).map_err(|err| { - tracing::warn!(token = %auth, "invalid auth token"); + let env_token_file = self.env.get(ENV_AUTHORIZATION_TOKEN_FILE).ok(); + let env_token = self.env.get(ENV_AUTHORIZATION_TOKEN).ok(); + let auth = if let Some(auth_token_file) = env_token_file { + let auth = self + .fs + .read_to_end(auth_token_file) + .await + .map_err(CredentialsError::provider_error)?; + Some(HeaderValue::from_bytes(auth.as_slice()).map_err(|err| { + let auth_token = String::from_utf8_lossy(auth.as_slice()).to_string(); + tracing::warn!(token = %auth_token, "invalid auth token"); CredentialsError::invalid_configuration(EcsConfigurationError::InvalidAuthToken { err, - value: auth, + value: auth_token, }) - })?), - None => None, + })?) + } else if let Some(auth_token) = env_token { + Some(HeaderValue::from_str(&auth_token).map_err(|err| { + tracing::warn!(token = %auth_token, "invalid auth token"); + CredentialsError::invalid_configuration(EcsConfigurationError::InvalidAuthToken { + err, + value: auth_token, + }) + })?) + } else { + None }; match self.provider().await { Provider::NotConfigured => { @@ -272,7 +300,7 @@ impl Builder { /// Override the DNS resolver used to validate URIs /// - /// URIs must refer to loopback addresses. The [`ResolveDns`] + /// URIs must refer to valid IP addresses as defined in the module documentation. The [`ResolveDns`] /// implementation is used to retrieve IP addresses for a given domain. pub fn dns(mut self, dns: impl ResolveDns + 'static) -> Self { self.dns = Some(dns.into_shared()); @@ -302,9 +330,15 @@ impl Builder { .as_ref() .map(|config| config.env()) .unwrap_or_default(); + let fs = self + .provider_config + .as_ref() + .map(|config| config.fs()) + .unwrap_or_default(); EcsCredentialsProvider { inner: OnceCell::new(), env, + fs, builder: self, } } @@ -324,9 +358,9 @@ enum InvalidFullUriErrorKind { #[non_exhaustive] MissingHost, - /// The URI did not refer to the loopback interface + /// The URI did not refer to an allowed IP address #[non_exhaustive] - NotLoopback, + DisallowedIP, /// DNS lookup failed when attempting to resolve the host to an IP Address for validation. DnsLookupFailed(ResolveDnsError), @@ -334,7 +368,8 @@ enum InvalidFullUriErrorKind { /// Invalid Full URI /// -/// When the full URI setting is used, the URI must either be HTTPS or point to a loopback interface. +/// When the full URI setting is used, the URI must either be HTTPS, point to a loopback interface, +/// or point to known ECS/EKS container IPs. #[derive(Debug)] pub struct InvalidFullUriError { kind: InvalidFullUriErrorKind, @@ -346,8 +381,8 @@ impl Display for InvalidFullUriError { match self.kind { InvalidUri(_) => write!(f, "URI was invalid"), MissingHost => write!(f, "URI did not specify a host"), - NotLoopback => { - write!(f, "URI did not refer to the loopback interface") + DisallowedIP => { + write!(f, "URI did not refer to an allowed IP address") } DnsLookupFailed(_) => { write!( @@ -380,9 +415,10 @@ impl From for InvalidFullUriError { /// Validate that `uri` is valid to be used as a full provider URI /// Either: /// 1. The URL is uses `https` -/// 2. The URL refers to a loopback device. If a URL contains a domain name instead of an IP address, -/// a DNS lookup will be performed. ALL resolved IP addresses MUST refer to a loopback interface, or -/// the credentials provider will return `CredentialsError::InvalidConfiguration` +/// 2. The URL refers to an allowed IP. If a URL contains a domain name instead of an IP address, +/// a DNS lookup will be performed. ALL resolved IP addresses MUST refer to an allowed IP, or +/// the credentials provider will return `CredentialsError::InvalidConfiguration`. Allowed IPs +/// are the loopback interfaces, and the known ECS/EKS container IPs. async fn validate_full_uri( uri: &str, dns: Option, @@ -393,10 +429,15 @@ async fn validate_full_uri( if uri.scheme() == Some(&Scheme::HTTPS) { return Ok(uri); } - // For HTTP URIs, we need to validate that it points to a loopback address + // For HTTP URIs, we need to validate that it points to a valid IP let host = uri.host().ok_or(InvalidFullUriErrorKind::MissingHost)?; - let is_loopback = match host.parse::() { - Ok(addr) => addr.is_loopback(), + let maybe_ip = if host.starts_with('[') && host.ends_with(']') { + host[1..host.len() - 1].parse::() + } else { + host.parse::() + }; + let is_allowed = match maybe_ip { + Ok(addr) => is_full_uri_ip_allowed(&addr), Err(_domain_name) => { let dns = dns.ok_or(InvalidFullUriErrorKind::NoDnsResolver)?; dns.resolve_dns(host) @@ -404,25 +445,40 @@ async fn validate_full_uri( .map_err(|err| InvalidFullUriErrorKind::DnsLookupFailed(ResolveDnsError::new(err)))? .iter() .all(|addr| { - if !addr.is_loopback() { + if !is_full_uri_ip_allowed(addr) { tracing::warn!( addr = ?addr, - "HTTP credential provider cannot be used: Address does not resolve to the loopback interface." + "HTTP credential provider cannot be used: Address does not resolve to an allowed IP." ) }; - addr.is_loopback() + is_full_uri_ip_allowed(addr) }) } }; - match is_loopback { + match is_allowed { true => Ok(uri), - false => Err(InvalidFullUriErrorKind::NotLoopback.into()), + false => Err(InvalidFullUriErrorKind::DisallowedIP.into()), } } +// "169.254.170.2" +const ECS_CONTAINER_IPV4: IpAddr = IpAddr::V4(Ipv4Addr::new(169, 254, 170, 2)); + +// "169.254.170.23" +const EKS_CONTAINER_IPV4: IpAddr = IpAddr::V4(Ipv4Addr::new(169, 254, 170, 23)); + +// "fd00:ec2::23" +const EKS_CONTAINER_IPV6: IpAddr = IpAddr::V6(Ipv6Addr::new(0xFD00, 0x0EC2, 0, 0, 0, 0, 0, 0x23)); +fn is_full_uri_ip_allowed(ip: &IpAddr) -> bool { + ip.is_loopback() + || ip.eq(&ECS_CONTAINER_IPV4) + || ip.eq(&EKS_CONTAINER_IPV4) + || ip.eq(&EKS_CONTAINER_IPV6) +} + /// Default DNS resolver impl /// -/// DNS resolution is required to validate that provided URIs point to the loopback interface +/// DNS resolution is required to validate that provided URIs point to a valid IP address #[cfg(any(not(feature = "rt-tokio"), target_family = "wasm"))] fn default_dns() -> Option { None @@ -437,7 +493,7 @@ fn default_dns() -> Option { mod test { use super::*; use crate::provider_config::ProviderConfig; - use crate::test_case::GenericTestResult; + use crate::test_case::{no_traffic_client, GenericTestResult}; use aws_credential_types::provider::ProvideCredentials; use aws_credential_types::Credentials; use aws_smithy_async::future::never::Never; @@ -454,13 +510,19 @@ mod test { use serde::Deserialize; use std::collections::HashMap; use std::error::Error; + use std::ffi::OsString; use std::net::IpAddr; use std::time::{Duration, UNIX_EPOCH}; use tracing_test::traced_test; - fn provider(env: Env, http_client: impl HttpClient + 'static) -> EcsCredentialsProvider { + fn provider( + env: Env, + fs: Fs, + http_client: impl HttpClient + 'static, + ) -> EcsCredentialsProvider { let provider_config = ProviderConfig::empty() .with_env(env) + .with_fs(fs) .with_http_client(http_client) .with_sleep_impl(TokioSleep::new()); Builder::default().configure(&provider_config).build() @@ -550,7 +612,54 @@ mod test { assert!(matches!( err, InvalidFullUriError { - kind: InvalidFullUriErrorKind::NotLoopback + kind: InvalidFullUriErrorKind::DisallowedIP + } + )); + } + + #[test] + fn valid_uri_ecs_eks() { + assert_eq!( + validate_full_uri("http://169.254.170.2:8080/get-credentials", None) + .now_or_never() + .unwrap() + .expect("valid uri"), + Uri::from_static("http://169.254.170.2:8080/get-credentials") + ); + assert_eq!( + validate_full_uri("http://169.254.170.23:8080/get-credentials", None) + .now_or_never() + .unwrap() + .expect("valid uri"), + Uri::from_static("http://169.254.170.23:8080/get-credentials") + ); + assert_eq!( + validate_full_uri("http://[fd00:ec2::23]:8080/get-credentials", None) + .now_or_never() + .unwrap() + .expect("valid uri"), + Uri::from_static("http://[fd00:ec2::23]:8080/get-credentials") + ); + + let err = validate_full_uri("http://169.254.171.23/creds", None) + .now_or_never() + .unwrap() + .expect_err("not an ecs/eks container address"); + assert!(matches!( + err, + InvalidFullUriError { + kind: InvalidFullUriErrorKind::DisallowedIP + } + )); + + let err = validate_full_uri("http://[fd00:ec2::2]/creds", None) + .now_or_never() + .unwrap() + .expect_err("not an ecs/eks container address"); + assert!(matches!( + err, + InvalidFullUriError { + kind: InvalidFullUriErrorKind::DisallowedIP } )); } @@ -561,6 +670,8 @@ mod test { TestDns::with_fallback(vec![ "127.0.0.1".parse().unwrap(), "127.0.0.2".parse().unwrap(), + "169.254.170.23".parse().unwrap(), + "fd00:ec2::23".parse().unwrap(), ]) .into_shared(), ); @@ -586,7 +697,7 @@ mod test { matches!( resp, Err(InvalidFullUriError { - kind: InvalidFullUriErrorKind::NotLoopback + kind: InvalidFullUriErrorKind::DisallowedIP }) ), "Should be invalid: {:?}", @@ -637,7 +748,7 @@ mod test { creds_request("http://169.254.170.2/credentials", Some("Basic password")), ok_creds_response(), )]); - let provider = provider(env, http_client.clone()); + let provider = provider(env, Fs::default(), http_client.clone()); let creds = provider .provide_credentials() .await @@ -646,6 +757,99 @@ mod test { http_client.assert_requests_match(&[]); } + #[tokio::test] + async fn load_valid_creds_auth_file() { + let env = Env::from_slice(&[ + ( + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "http://169.254.170.23/v1/credentials", + ), + ( + "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", + "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token", + ), + ]); + let fs = Fs::from_raw_map(HashMap::from([( + OsString::from( + "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token", + ), + "Basic password".into(), + )])); + + let http_client = StaticReplayClient::new(vec![ReplayEvent::new( + creds_request( + "http://169.254.170.23/v1/credentials", + Some("Basic password"), + ), + ok_creds_response(), + )]); + let provider = provider(env, fs, http_client.clone()); + let creds = provider + .provide_credentials() + .await + .expect("valid credentials"); + assert_correct(creds); + http_client.assert_requests_match(&[]); + } + + #[tokio::test] + async fn auth_file_precedence_over_env() { + let env = Env::from_slice(&[ + ( + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "http://169.254.170.23/v1/credentials", + ), + ( + "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", + "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token", + ), + ("AWS_CONTAINER_AUTHORIZATION_TOKEN", "unused"), + ]); + let fs = Fs::from_raw_map(HashMap::from([( + OsString::from( + "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token", + ), + "Basic password".into(), + )])); + + let http_client = StaticReplayClient::new(vec![ReplayEvent::new( + creds_request( + "http://169.254.170.23/v1/credentials", + Some("Basic password"), + ), + ok_creds_response(), + )]); + let provider = provider(env, fs, http_client.clone()); + let creds = provider + .provide_credentials() + .await + .expect("valid credentials"); + assert_correct(creds); + http_client.assert_requests_match(&[]); + } + + #[tokio::test] + async fn fs_missing_file() { + let env = Env::from_slice(&[ + ( + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "http://169.254.170.23/v1/credentials", + ), + ( + "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", + "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token", + ), + ]); + let fs = Fs::from_raw_map(HashMap::new()); + + let provider = provider(env, fs, no_traffic_client()); + let err = provider.credentials().await.expect_err("no JWT token file"); + match err { + CredentialsError::ProviderError { .. } => { /* ok */ } + _ => panic!("incorrect error variant"), + } + } + #[tokio::test] async fn retry_5xx() { let env = Env::from_slice(&[("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/credentials")]); @@ -663,7 +867,7 @@ mod test { ), ]); tokio::time::pause(); - let provider = provider(env, http_client.clone()); + let provider = provider(env, Fs::default(), http_client.clone()); let creds = provider .provide_credentials() .await @@ -678,7 +882,7 @@ mod test { creds_request("http://169.254.170.2/credentials", None), ok_creds_response(), )]); - let provider = provider(env, http_client.clone()); + let provider = provider(env, Fs::default(), http_client.clone()); let creds = provider .provide_credentials() .await @@ -700,23 +904,30 @@ mod test { ); let err = validate_full_uri("http://www.amazon.com/creds", dns.clone()) .await - .expect_err("not a loopback"); + .expect_err("not a valid IP"); assert!( matches!( err, InvalidFullUriError { - kind: InvalidFullUriErrorKind::NotLoopback + kind: InvalidFullUriErrorKind::DisallowedIP } ), "{:?}", err ); - assert!(logs_contain( - "Address does not resolve to the loopback interface" - )); - validate_full_uri("http://localhost:8888/creds", dns) + assert!(logs_contain("Address does not resolve to an allowed IP")); + validate_full_uri("http://localhost:8888/creds", dns.clone()) .await .expect("localhost is the loopback interface"); + validate_full_uri("http://169.254.170.2.backname.io:8888/creds", dns.clone()) + .await + .expect("169.254.170.2.backname.io is the ecs container address"); + validate_full_uri("http://169.254.170.23.backname.io:8888/creds", dns.clone()) + .await + .expect("169.254.170.23.backname.io is the eks pod identity address"); + validate_full_uri("http://fd00-ec2--23.backname.io:8888/creds", dns) + .await + .expect("fd00-ec2--23.backname.io is the eks pod identity address"); } /// Always returns the same IP addresses diff --git a/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/env.json b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/env.json new file mode 100644 index 0000000000..45324e91b5 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/env.json @@ -0,0 +1,6 @@ +{ + "HOME": "/home", + "AWS_REGION": "us-east-1", + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://169.254.170.23/v1/credentials", + "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE": "/token.jwt" +} diff --git a/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/fs/token.jwt b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/fs/token.jwt new file mode 100644 index 0000000000..661b5e547a --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/fs/token.jwt @@ -0,0 +1 @@ +sometoken \ No newline at end of file diff --git a/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/http-traffic.json new file mode 100644 index 0000000000..d8282ff57e --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/http-traffic.json @@ -0,0 +1,87 @@ +{ + "events": [ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "http://169.254.170.23/v1/credentials", + "headers": { + "accept": [ + "application/json" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "content-type": [ + "application/json" + ], + "x-rate-limit-duration": [ + "1" + ], + "x-rate-limit-limit": [ + "40" + ], + "x-rate-limit-request-forwarded-for": [ + "" + ], + "x-rate-limit-request-remote-addr": [ + "169.254.172.2:35484" + ], + "date": [ + "Fri, 15 Oct 2021 15:23:49 GMT" + ], + "content-length": [ + "1231" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "{\"RoleArn\":\"arn:aws:iam::123456789:role/eks-pod-identity-role\",\"AccessKeyId\":\"ASIARCORRECT\",\"SecretAccessKey\":\"secretkeycorrect\",\"Token\":\"tokencorrect\",\"Expiration\" : \"2009-02-13T23:31:30Z\"}" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } + ], + "docs": "Load EKS Pod Identity credentials", + "version": "V0" +} diff --git a/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/test-case.json b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/test-case.json new file mode 100644 index 0000000000..3bf1a5338a --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_credentials/test-case.json @@ -0,0 +1,12 @@ +{ + "name": "ecs-pod-identity-credentials", + "docs": "load credentials directly from EKS Pod Identity", + "result": { + "Ok": { + "access_key_id": "ASIARCORRECT", + "secret_access_key": "secretkeycorrect", + "session_token": "tokencorrect", + "expiry": 1234567890 + } + } +} diff --git a/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/env.json b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/env.json new file mode 100644 index 0000000000..45324e91b5 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/env.json @@ -0,0 +1,6 @@ +{ + "HOME": "/home", + "AWS_REGION": "us-east-1", + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://169.254.170.23/v1/credentials", + "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE": "/token.jwt" +} diff --git a/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/http-traffic.json new file mode 100644 index 0000000000..a0965529c7 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/http-traffic.json @@ -0,0 +1,5 @@ +{ + "events": [], + "docs": "missing token file, no traffic", + "version": "V0" +} diff --git a/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/test-case.json b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/test-case.json new file mode 100644 index 0000000000..6e2b5c09f5 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/default-credential-provider-chain/eks_pod_identity_no_token_file/test-case.json @@ -0,0 +1,7 @@ +{ + "name": "ecs-pod-identity-no-token-file", + "docs": "fail to load credentials from EKS Pod Identity without a token file", + "result": { + "ErrorContains": "No such file or directory (os error 2)" + } +} diff --git a/aws/rust-runtime/aws-config/test-data/ecs-tests.json b/aws/rust-runtime/aws-config/test-data/ecs-tests.json index 0c0e36a21f..10a040bf4a 100644 --- a/aws/rust-runtime/aws-config/test-data/ecs-tests.json +++ b/aws/rust-runtime/aws-config/test-data/ecs-tests.json @@ -52,6 +52,33 @@ "Ok": "http://localhost:8080/credentials" } }, + { + "docs": "ecs task role", + "env": { + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://169.254.170.2/credentials" + }, + "result": { + "Ok": "http://169.254.170.2/credentials" + } + }, + { + "docs": "eks pod identity ipv4", + "env": { + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://169.254.170.23/v1/credentials" + }, + "result": { + "Ok": "http://169.254.170.23/v1/credentials" + } + }, + { + "docs": "eks pod identity ipv6", + "env": { + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://[fd00:ec2::23]/v1/credentials" + }, + "result": { + "Ok": "http://[fd00:ec2::23]/v1/credentials" + } + }, { "docs": "relative takes precedence over full", "env": {