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
<!--- If a checkbox below is not applicable, then please DELETE it
rather than leaving it unchecked -->
- [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 <jackkleeman@gmail.com>
Co-authored-by: John DiSanti <john@vinylsquid.com>
Co-authored-by: John DiSanti <jdisanti@amazon.com>
This commit is contained in:
Jack Kleeman 2024-02-23 23:03:46 +00:00 committed by GitHub
parent 607c89184f
commit 07c8074ce5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 886 additions and 100 deletions

View File

@ -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

View File

@ -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"
# 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"

View File

@ -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
```

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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"

View File

@ -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
);

View File

@ -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"))]

View File

@ -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<Provider>,
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<InvalidFullUriErrorKind> 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<SharedDnsResolver>,
@ -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::<IpAddr>() {
Ok(addr) => addr.is_loopback(),
let maybe_ip = if host.starts_with('[') && host.ends_with(']') {
host[1..host.len() - 1].parse::<IpAddr>()
} else {
host.parse::<IpAddr>()
};
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<SharedDnsResolver> {
None
@ -437,7 +493,7 @@ fn default_dns() -> Option<SharedDnsResolver> {
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

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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
}
}
}

View File

@ -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"
}

View File

@ -0,0 +1,5 @@
{
"events": [],
"docs": "missing token file, no traffic",
"version": "V0"
}

View File

@ -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)"
}
}

View File

@ -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": {