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: hooks:
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - 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 - id: trailing-whitespace
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.11.0 rev: v2.11.0

View File

@ -10,3 +10,9 @@
# references = ["smithy-rs#920"] # references = ["smithy-rs#920"]
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"} # 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 # 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. `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 bootstrap aws://accountid/region
cdk deploy cdk deploy
# make lunch, go for a bike ride, etc. ~1h. # make lunch, go for a bike ride, etc. ~1h.
kubectl exec rust-sdk-test -it bash kubectl exec rust-sdk-test-irsa -it bash
# write some rust code, e.g. test.rs, run it. # 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 { Stack, StackProps } from "aws-cdk-lib";
import * as eks from "aws-cdk-lib/aws-eks"; 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 * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import { Construct } from "constructs"; import { Construct } from "constructs";
import {KubectlV28Layer} from "@aws-cdk/lambda-layer-kubectl-v28";
export class EksCredentialsStack extends Stack { export class EksCredentialsStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) { constructor(scope: Construct, id: string, props?: StackProps) {
@ -15,21 +17,64 @@ export class EksCredentialsStack extends Stack {
// create a cluster // create a cluster
const cluster = new eks.Cluster(this, 'hello-eks', { 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', { const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } 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 // apply a kubernetes manifest to the cluster
const pod = cluster.addManifest('rust-sdk-test', { const podIRSA = cluster.addManifest('rust-sdk-test-irsa', {
apiVersion: 'v1', apiVersion: 'v1',
kind: 'Pod', kind: 'Pod',
metadata: { name: 'rust-sdk-test' }, metadata: { name: 'rust-sdk-test-irsa' },
spec: { spec: {
serviceAccountName: serviceAccount.serviceAccountName, serviceAccountName: serviceAccountIRSA.serviceAccountName,
containers: [ containers: [
{ {
name: 'hello', 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" "eks-credentials": "bin/eks-credentials.js"
}, },
"devDependencies": { "devDependencies": {
"@aws-cdk/lambda-layer-kubectl-v28": "^2.2.0",
"@types/node": "10.17.27", "@types/node": "10.17.27",
"aws-cdk": "^2.0.0", "aws-cdk": "^2.0.0",
"aws-cdk-lib": "^2.0.0", "aws-cdk-lib": "^2.124.0",
"constructs": "^10.0.0", "constructs": "^10.0.0",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "~4.5.5" "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": { "node_modules/@types/node": {
"version": "10.17.27", "version": "10.17.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.27.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.27.tgz",
@ -48,9 +77,9 @@
} }
}, },
"node_modules/aws-cdk-lib": { "node_modules/aws-cdk-lib": {
"version": "2.17.0", "version": "2.128.0",
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.17.0.tgz", "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.128.0.tgz",
"integrity": "sha512-bga2HptbGx3rMdSkIKxBS13miogj/DHB2VPfQZAoKoCOAanOot+M3mHhYqe5aNdxhrppaRjG2eid2p1/MvRnvg==", "integrity": "sha512-cAU1L4jtPXPQXpa9kS2/HhHdkg3xGc5GCqwRgivdoj/iLQF3dDwIouOkwDBY/S5pXMqOUC7IoVdIPPbIgfGlsQ==",
"bundleDependencies": [ "bundleDependencies": [
"@balena/dockerignore", "@balena/dockerignore",
"case", "case",
@ -60,18 +89,23 @@
"minimatch", "minimatch",
"punycode", "punycode",
"semver", "semver",
"table",
"yaml" "yaml"
], ],
"dev": true, "dev": true,
"dependencies": { "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", "@balena/dockerignore": "^1.0.2",
"case": "1.6.3", "case": "1.6.3",
"fs-extra": "^9.1.0", "fs-extra": "^11.2.0",
"ignore": "^5.2.0", "ignore": "^5.3.1",
"jsonschema": "^1.4.0", "jsonschema": "^1.4.1",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"punycode": "^2.1.1", "punycode": "^2.3.1",
"semver": "^7.3.5", "semver": "^7.5.4",
"table": "^6.8.1",
"yaml": "1.10.2" "yaml": "1.10.2"
}, },
"engines": { "engines": {
@ -87,13 +121,53 @@
"inBundle": true, "inBundle": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/aws-cdk-lib/node_modules/at-least-node": { "node_modules/aws-cdk-lib/node_modules/ajv": {
"version": "1.0.0", "version": "8.12.0",
"dev": true, "dev": true,
"inBundle": 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": { "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": { "node_modules/aws-cdk-lib/node_modules/balanced-match": {
@ -121,35 +195,64 @@
"node": ">= 0.8.0" "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": { "node_modules/aws-cdk-lib/node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT" "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": { "node_modules/aws-cdk-lib/node_modules/fs-extra": {
"version": "9.1.0", "version": "11.2.0",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=14.14"
} }
}, },
"node_modules/aws-cdk-lib/node_modules/graceful-fs": { "node_modules/aws-cdk-lib/node_modules/graceful-fs": {
"version": "4.2.9", "version": "4.2.11",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/aws-cdk-lib/node_modules/ignore": { "node_modules/aws-cdk-lib/node_modules/ignore": {
"version": "5.2.0", "version": "5.3.1",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@ -157,6 +260,21 @@
"node": ">= 4" "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": { "node_modules/aws-cdk-lib/node_modules/jsonfile": {
"version": "6.1.0", "version": "6.1.0",
"dev": true, "dev": true,
@ -170,7 +288,7 @@
} }
}, },
"node_modules/aws-cdk-lib/node_modules/jsonschema": { "node_modules/aws-cdk-lib/node_modules/jsonschema": {
"version": "1.4.0", "version": "1.4.1",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@ -178,6 +296,12 @@
"node": "*" "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": { "node_modules/aws-cdk-lib/node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"dev": true, "dev": true,
@ -203,7 +327,7 @@
} }
}, },
"node_modules/aws-cdk-lib/node_modules/punycode": { "node_modules/aws-cdk-lib/node_modules/punycode": {
"version": "2.1.1", "version": "2.3.1",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@ -211,8 +335,17 @@
"node": ">=6" "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": { "node_modules/aws-cdk-lib/node_modules/semver": {
"version": "7.3.5", "version": "7.5.4",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
@ -226,8 +359,67 @@
"node": ">=10" "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": { "node_modules/aws-cdk-lib/node_modules/universalify": {
"version": "2.0.0", "version": "2.0.1",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@ -235,6 +427,15 @@
"node": ">= 10.0.0" "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": { "node_modules/aws-cdk-lib/node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"dev": true, "dev": true,
@ -369,6 +570,31 @@
} }
}, },
"dependencies": { "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": { "@types/node": {
"version": "10.17.27", "version": "10.17.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.27.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.27.tgz",
@ -391,19 +617,23 @@
} }
}, },
"aws-cdk-lib": { "aws-cdk-lib": {
"version": "2.17.0", "version": "2.128.0",
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.17.0.tgz", "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.128.0.tgz",
"integrity": "sha512-bga2HptbGx3rMdSkIKxBS13miogj/DHB2VPfQZAoKoCOAanOot+M3mHhYqe5aNdxhrppaRjG2eid2p1/MvRnvg==", "integrity": "sha512-cAU1L4jtPXPQXpa9kS2/HhHdkg3xGc5GCqwRgivdoj/iLQF3dDwIouOkwDBY/S5pXMqOUC7IoVdIPPbIgfGlsQ==",
"dev": true, "dev": true,
"requires": { "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", "@balena/dockerignore": "^1.0.2",
"case": "1.6.3", "case": "1.6.3",
"fs-extra": "^9.1.0", "fs-extra": "^11.2.0",
"ignore": "^5.2.0", "ignore": "^5.3.1",
"jsonschema": "^1.4.0", "jsonschema": "^1.4.1",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"punycode": "^2.1.1", "punycode": "^2.3.1",
"semver": "^7.3.5", "semver": "^7.5.4",
"table": "^6.8.1",
"yaml": "1.10.2" "yaml": "1.10.2"
}, },
"dependencies": { "dependencies": {
@ -412,8 +642,32 @@
"bundled": true, "bundled": true,
"dev": true "dev": true
}, },
"at-least-node": { "ajv": {
"version": "1.0.0", "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, "bundled": true,
"dev": true "dev": true
}, },
@ -436,29 +690,61 @@
"bundled": true, "bundled": true,
"dev": 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": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": 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": { "fs-extra": {
"version": "9.1.0", "version": "11.2.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"requires": { "requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
"universalify": "^2.0.0" "universalify": "^2.0.0"
} }
}, },
"graceful-fs": { "graceful-fs": {
"version": "4.2.9", "version": "4.2.11",
"bundled": true, "bundled": true,
"dev": true "dev": true
}, },
"ignore": { "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, "bundled": true,
"dev": true "dev": true
}, },
@ -472,7 +758,12 @@
} }
}, },
"jsonschema": { "jsonschema": {
"version": "1.4.0", "version": "1.4.1",
"bundled": true,
"dev": true
},
"lodash.truncate": {
"version": "4.4.2",
"bundled": true, "bundled": true,
"dev": true "dev": true
}, },
@ -493,23 +784,76 @@
} }
}, },
"punycode": { "punycode": {
"version": "2.1.1", "version": "2.3.1",
"bundled": true,
"dev": true
},
"require-from-string": {
"version": "2.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true
}, },
"semver": { "semver": {
"version": "7.3.5", "version": "7.5.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "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": { "universalify": {
"version": "2.0.0", "version": "2.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true
}, },
"uri-js": {
"version": "4.4.1",
"bundled": true,
"dev": true,
"requires": {
"punycode": "^2.1.0"
}
},
"yallist": { "yallist": {
"version": "4.0.0", "version": "4.0.0",
"bundled": true, "bundled": true,

View File

@ -14,7 +14,8 @@
"devDependencies": { "devDependencies": {
"@types/node": "10.17.27", "@types/node": "10.17.27",
"aws-cdk": "^2.0.0", "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", "constructs": "^10.0.0",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "~4.5.5" "typescript": "~4.5.5"

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
use aws_sdk_dynamodb::model::AttributeValue; use aws_sdk_dynamodb::types::AttributeValue;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let conf = aws_config::load_from_env().await; let conf = aws_config::load_from_env().await;
@ -13,7 +13,7 @@ async fn main() {
dynamo dynamo
.get_item() .get_item()
.key("id", AttributeValue::S("foo".into())) .key("id", AttributeValue::S("foo".into()))
.table_name("EksCredentialsStack-TableCD117FA1-18ZPICQWXOPW") .table_name(std::env::var("DYNAMO_TABLE").unwrap())
.send() .send()
.await .await
); );

View File

@ -297,6 +297,9 @@ mod test {
make_test!(ecs_credentials); make_test!(ecs_credentials);
make_test!(ecs_credentials_invalid_profile); make_test!(ecs_credentials_invalid_profile);
make_test!(eks_pod_identity_credentials);
make_test!(eks_pod_identity_no_token_file);
#[cfg(not(feature = "sso"))] #[cfg(not(feature = "sso"))]
make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]); make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]);
#[cfg(not(feature = "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 //! 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`. //! 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 //! **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 two properties: //! URL to load credentials. The URL MUST satisfy one of the following three properties:
//! 1. The URL begins with `https` //! 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, //! 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 a loopback interface, or //! 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` //! 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 //! **Finally**: It will check the value of `$AWS_CONTAINER_AUTHORIZATION_TOKEN`. If this is set, the
//! value will be passed in the `Authorization` header. //! 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::client::http::HttpConnectorSettings;
use aws_smithy_runtime_api::shared::IntoShared; use aws_smithy_runtime_api::shared::IntoShared;
use aws_smithy_types::error::display::DisplayErrorContext; 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::header::InvalidHeaderValue;
use http::uri::{InvalidUri, PathAndQuery, Scheme}; use http::uri::{InvalidUri, PathAndQuery, Scheme};
use http::{HeaderValue, Uri}; use http::{HeaderValue, Uri};
use std::error::Error; use std::error::Error;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::net::IpAddr; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::Duration; use std::time::Duration;
use tokio::sync::OnceCell; 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 BASE_HOST: &str = "http://169.254.170.2";
const ENV_RELATIVE_URI: &str = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; const ENV_RELATIVE_URI: &str = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
const ENV_FULL_URI: &str = "AWS_CONTAINER_CREDENTIALS_FULL_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 /// Credential provider for ECS and generalized HTTP credentials
/// ///
@ -82,6 +92,7 @@ const ENV_AUTHORIZATION: &str = "AWS_CONTAINER_AUTHORIZATION_TOKEN";
pub struct EcsCredentialsProvider { pub struct EcsCredentialsProvider {
inner: OnceCell<Provider>, inner: OnceCell<Provider>,
env: Env, env: Env,
fs: Fs,
builder: Builder, builder: Builder,
} }
@ -93,15 +104,32 @@ impl EcsCredentialsProvider {
/// Load credentials from this credentials provider /// Load credentials from this credentials provider
pub async fn credentials(&self) -> provider::Result { pub async fn credentials(&self) -> provider::Result {
let auth = match self.env.get(ENV_AUTHORIZATION).ok() { let env_token_file = self.env.get(ENV_AUTHORIZATION_TOKEN_FILE).ok();
Some(auth) => Some(HeaderValue::from_str(&auth).map_err(|err| { let env_token = self.env.get(ENV_AUTHORIZATION_TOKEN).ok();
tracing::warn!(token = %auth, "invalid auth token"); 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 { CredentialsError::invalid_configuration(EcsConfigurationError::InvalidAuthToken {
err, 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 { match self.provider().await {
Provider::NotConfigured => { Provider::NotConfigured => {
@ -272,7 +300,7 @@ impl Builder {
/// Override the DNS resolver used to validate URIs /// 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. /// implementation is used to retrieve IP addresses for a given domain.
pub fn dns(mut self, dns: impl ResolveDns + 'static) -> Self { pub fn dns(mut self, dns: impl ResolveDns + 'static) -> Self {
self.dns = Some(dns.into_shared()); self.dns = Some(dns.into_shared());
@ -302,9 +330,15 @@ impl Builder {
.as_ref() .as_ref()
.map(|config| config.env()) .map(|config| config.env())
.unwrap_or_default(); .unwrap_or_default();
let fs = self
.provider_config
.as_ref()
.map(|config| config.fs())
.unwrap_or_default();
EcsCredentialsProvider { EcsCredentialsProvider {
inner: OnceCell::new(), inner: OnceCell::new(),
env, env,
fs,
builder: self, builder: self,
} }
} }
@ -324,9 +358,9 @@ enum InvalidFullUriErrorKind {
#[non_exhaustive] #[non_exhaustive]
MissingHost, MissingHost,
/// The URI did not refer to the loopback interface /// The URI did not refer to an allowed IP address
#[non_exhaustive] #[non_exhaustive]
NotLoopback, DisallowedIP,
/// DNS lookup failed when attempting to resolve the host to an IP Address for validation. /// DNS lookup failed when attempting to resolve the host to an IP Address for validation.
DnsLookupFailed(ResolveDnsError), DnsLookupFailed(ResolveDnsError),
@ -334,7 +368,8 @@ enum InvalidFullUriErrorKind {
/// Invalid Full URI /// 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)] #[derive(Debug)]
pub struct InvalidFullUriError { pub struct InvalidFullUriError {
kind: InvalidFullUriErrorKind, kind: InvalidFullUriErrorKind,
@ -346,8 +381,8 @@ impl Display for InvalidFullUriError {
match self.kind { match self.kind {
InvalidUri(_) => write!(f, "URI was invalid"), InvalidUri(_) => write!(f, "URI was invalid"),
MissingHost => write!(f, "URI did not specify a host"), MissingHost => write!(f, "URI did not specify a host"),
NotLoopback => { DisallowedIP => {
write!(f, "URI did not refer to the loopback interface") write!(f, "URI did not refer to an allowed IP address")
} }
DnsLookupFailed(_) => { DnsLookupFailed(_) => {
write!( write!(
@ -380,9 +415,10 @@ impl From<InvalidFullUriErrorKind> for InvalidFullUriError {
/// Validate that `uri` is valid to be used as a full provider URI /// Validate that `uri` is valid to be used as a full provider URI
/// Either: /// Either:
/// 1. The URL is uses `https` /// 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, /// 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 a loopback interface, or /// a DNS lookup will be performed. ALL resolved IP addresses MUST refer to an allowed IP, or
/// the credentials provider will return `CredentialsError::InvalidConfiguration` /// 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( async fn validate_full_uri(
uri: &str, uri: &str,
dns: Option<SharedDnsResolver>, dns: Option<SharedDnsResolver>,
@ -393,10 +429,15 @@ async fn validate_full_uri(
if uri.scheme() == Some(&Scheme::HTTPS) { if uri.scheme() == Some(&Scheme::HTTPS) {
return Ok(uri); 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 host = uri.host().ok_or(InvalidFullUriErrorKind::MissingHost)?;
let is_loopback = match host.parse::<IpAddr>() { let maybe_ip = if host.starts_with('[') && host.ends_with(']') {
Ok(addr) => addr.is_loopback(), 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) => { Err(_domain_name) => {
let dns = dns.ok_or(InvalidFullUriErrorKind::NoDnsResolver)?; let dns = dns.ok_or(InvalidFullUriErrorKind::NoDnsResolver)?;
dns.resolve_dns(host) dns.resolve_dns(host)
@ -404,25 +445,40 @@ async fn validate_full_uri(
.map_err(|err| InvalidFullUriErrorKind::DnsLookupFailed(ResolveDnsError::new(err)))? .map_err(|err| InvalidFullUriErrorKind::DnsLookupFailed(ResolveDnsError::new(err)))?
.iter() .iter()
.all(|addr| { .all(|addr| {
if !addr.is_loopback() { if !is_full_uri_ip_allowed(addr) {
tracing::warn!( tracing::warn!(
addr = ?addr, 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), 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 /// 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"))] #[cfg(any(not(feature = "rt-tokio"), target_family = "wasm"))]
fn default_dns() -> Option<SharedDnsResolver> { fn default_dns() -> Option<SharedDnsResolver> {
None None
@ -437,7 +493,7 @@ fn default_dns() -> Option<SharedDnsResolver> {
mod test { mod test {
use super::*; use super::*;
use crate::provider_config::ProviderConfig; 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::provider::ProvideCredentials;
use aws_credential_types::Credentials; use aws_credential_types::Credentials;
use aws_smithy_async::future::never::Never; use aws_smithy_async::future::never::Never;
@ -454,13 +510,19 @@ mod test {
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::ffi::OsString;
use std::net::IpAddr; use std::net::IpAddr;
use std::time::{Duration, UNIX_EPOCH}; use std::time::{Duration, UNIX_EPOCH};
use tracing_test::traced_test; 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() let provider_config = ProviderConfig::empty()
.with_env(env) .with_env(env)
.with_fs(fs)
.with_http_client(http_client) .with_http_client(http_client)
.with_sleep_impl(TokioSleep::new()); .with_sleep_impl(TokioSleep::new());
Builder::default().configure(&provider_config).build() Builder::default().configure(&provider_config).build()
@ -550,7 +612,54 @@ mod test {
assert!(matches!( assert!(matches!(
err, err,
InvalidFullUriError { 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![ TestDns::with_fallback(vec![
"127.0.0.1".parse().unwrap(), "127.0.0.1".parse().unwrap(),
"127.0.0.2".parse().unwrap(), "127.0.0.2".parse().unwrap(),
"169.254.170.23".parse().unwrap(),
"fd00:ec2::23".parse().unwrap(),
]) ])
.into_shared(), .into_shared(),
); );
@ -586,7 +697,7 @@ mod test {
matches!( matches!(
resp, resp,
Err(InvalidFullUriError { Err(InvalidFullUriError {
kind: InvalidFullUriErrorKind::NotLoopback kind: InvalidFullUriErrorKind::DisallowedIP
}) })
), ),
"Should be invalid: {:?}", "Should be invalid: {:?}",
@ -637,7 +748,7 @@ mod test {
creds_request("http://169.254.170.2/credentials", Some("Basic password")), creds_request("http://169.254.170.2/credentials", Some("Basic password")),
ok_creds_response(), ok_creds_response(),
)]); )]);
let provider = provider(env, http_client.clone()); let provider = provider(env, Fs::default(), http_client.clone());
let creds = provider let creds = provider
.provide_credentials() .provide_credentials()
.await .await
@ -646,6 +757,99 @@ mod test {
http_client.assert_requests_match(&[]); 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] #[tokio::test]
async fn retry_5xx() { async fn retry_5xx() {
let env = Env::from_slice(&[("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/credentials")]); let env = Env::from_slice(&[("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/credentials")]);
@ -663,7 +867,7 @@ mod test {
), ),
]); ]);
tokio::time::pause(); tokio::time::pause();
let provider = provider(env, http_client.clone()); let provider = provider(env, Fs::default(), http_client.clone());
let creds = provider let creds = provider
.provide_credentials() .provide_credentials()
.await .await
@ -678,7 +882,7 @@ mod test {
creds_request("http://169.254.170.2/credentials", None), creds_request("http://169.254.170.2/credentials", None),
ok_creds_response(), ok_creds_response(),
)]); )]);
let provider = provider(env, http_client.clone()); let provider = provider(env, Fs::default(), http_client.clone());
let creds = provider let creds = provider
.provide_credentials() .provide_credentials()
.await .await
@ -700,23 +904,30 @@ mod test {
); );
let err = validate_full_uri("http://www.amazon.com/creds", dns.clone()) let err = validate_full_uri("http://www.amazon.com/creds", dns.clone())
.await .await
.expect_err("not a loopback"); .expect_err("not a valid IP");
assert!( assert!(
matches!( matches!(
err, err,
InvalidFullUriError { InvalidFullUriError {
kind: InvalidFullUriErrorKind::NotLoopback kind: InvalidFullUriErrorKind::DisallowedIP
} }
), ),
"{:?}", "{:?}",
err err
); );
assert!(logs_contain( assert!(logs_contain("Address does not resolve to an allowed IP"));
"Address does not resolve to the loopback interface" validate_full_uri("http://localhost:8888/creds", dns.clone())
));
validate_full_uri("http://localhost:8888/creds", dns)
.await .await
.expect("localhost is the loopback interface"); .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 /// 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" "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", "docs": "relative takes precedence over full",
"env": { "env": {