mirror of https://github.com/smithy-lang/smithy-rs
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:
parent
607c89184f
commit
07c8074ce5
|
@ -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
|
||||
|
|
|
@ -10,3 +10,9 @@
|
|||
# references = ["smithy-rs#920"]
|
||||
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
|
||||
# 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"
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
sometoken
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"events": [],
|
||||
"docs": "missing token file, no traffic",
|
||||
"version": "V0"
|
||||
}
|
|
@ -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)"
|
||||
}
|
||||
}
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in New Issue