diff --git a/common/etc/nginx/include/awscredentials.js b/common/etc/nginx/include/awscredentials.js index b64d03f8..59cc832a 100644 --- a/common/etc/nginx/include/awscredentials.js +++ b/common/etc/nginx/include/awscredentials.js @@ -58,6 +58,12 @@ const EC2_IMDS_TOKEN_ENDPOINT = 'http://169.254.169.254/latest/api/token'; */ const EC2_IMDS_SECURITY_CREDENTIALS_ENDPOINT = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/'; +/** + * URL to EKS Pod Identity Agent credentials endpoint + * @type {string} + */ +const EKS_POD_IDENTITY_AGENT_CREDENTIALS_ENDPOINT = 'http://169.254.170.23/v1/credentials' + /** * Offset to the expiration of credentials, when they should be considered expired and refreshed. The maximum * time here can be 5 minutes, the IMDS and ECS credentials endpoint will make sure that each returned set of credentials @@ -293,6 +299,15 @@ async function fetchCredentials(r) { r.return(500); return; } + } + else if (utils.areAllEnvVarsSet('AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE')) { + try { + credentials = await _fetchEKSPodIdentityCredentials(r) + } catch (e) { + utils.debug_log(r, 'Could not assume role using EKS pod identity: ' + JSON.stringify(e)); + r.return(500); + return; + } } else { try { credentials = await _fetchEC2RoleCredentials(); @@ -378,6 +393,29 @@ async function _fetchEC2RoleCredentials() { }; } +/** + * Get the credentials needed to generate AWS signatures from the EKS Pod Identity Agent + * endpoint. + * + * @returns {Promise} + * @private + */ +async function _fetchEKSPodIdentityCredentials() { + const token = fs.readFileSync(process.env['AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE']); + let resp = await ngx.fetch(EKS_POD_IDENTITY_AGENT_CREDENTIALS_ENDPOINT, { + headers: { + 'Authorization': token, + }, + }); + const creds = await resp.json(); + + return { + accessKeyId: creds.AccessKeyId, + secretAccessKey: creds.SecretAccessKey, + sessionToken: creds.Token, + expiration: creds.Expiration, + }; +} /** * Get the credentials by assuming calling AssumeRoleWithWebIdentity with the environment variable * values ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE and AWS_ROLE_SESSION_NAME diff --git a/docs/getting_started.md b/docs/getting_started.md index b30dd0a0..e44af845 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -6,6 +6,8 @@ [Running as a Systemd Service](#running-as-a-systemd-service) [Running in Containers](#running-in-containers) [Running Using AWS Instance Profile Credentials](#running-using-aws-instance-profile-credentials) +[Running on EKS with IAM roles for service accounts](#running-on-eks-with-iam-roles-for-service-accounts) +[Running on EKS with EKS Pod Identities](#running-on-eks-with-eks-pod-identities) [Troubleshooting](#troubleshooting) ## Configuration @@ -470,6 +472,23 @@ spec: path: /health port: http ``` +## Running on EKS with EKS Pod Identities + +An alternative way to use the container image on an EKS cluster is to use a service account which can assume a role using [Pod Identities](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html). +- Installing the [Amazon EKS Pod Identity Agent](https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html) on the cluster +- Configuring a [Kubernetes service account to assume an IAM role with EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-id-association.html) +- [Configure your pods, Deployments, etc to use the Service Account](https://docs.aws.amazon.com/eks/latest/userguide/pod-configuration.html) +- As soon as the pods/deployments are updated, you will see the couple of Env Variables listed below in the pods. + - `AWS_CONTAINER_CREDENTIALS_FULL_URI` - Contains the Uri of the EKS Pod Identity Agent that will provide the credentials + - `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` - Contains the token which will be used to create temporary credentials using the EKS Pod Identity Agent. + +The minimal set of resources to deploy is the same than for [Running on EKS with IAM roles for service accounts](#running-on-eks-with-iam-roles-for-service-accounts), except there is no need to annotate the service account: +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nginx-s3-gateway +``` ## Troubleshooting diff --git a/test/unit/awscredentials_test.js b/test/unit/awscredentials_test.js index 11eee36f..4c48366d 100644 --- a/test/unit/awscredentials_test.js +++ b/test/unit/awscredentials_test.js @@ -234,7 +234,7 @@ async function testEc2CredentialRetrieval() { delete process.env['AWS_ACCESS_KEY_ID']; } if ('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' in process.env) { - delete process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']; + delete process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']; } globalThis.ngx.fetch = function (url, options) { if (url === 'http://169.254.169.254/latest/api/token' && options && options.method === 'PUT') { @@ -300,13 +300,82 @@ async function testEc2CredentialRetrieval() { await awscred.fetchCredentials(r); if (!globalThis.credentialsIssued) { - throw 'Did not reach the point where EC2 credentials were issues.'; + throw 'Did not reach the point where EC2 credentials were issued.'; + } +} + +async function testEKSPodIdentityCredentialRetrieval() { + printHeader('testEKSPodIdentityCredentialRetrieval'); + if ('AWS_ACCESS_KEY_ID' in process.env) { + delete process.env['AWS_ACCESS_KEY_ID']; + } + if ('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' in process.env) { + delete process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']; + } + if ('AWS_WEB_IDENTITY_TOKEN_FILE' in process.env) { + delete process.env['AWS_WEB_IDENTITY_TOKEN_FILE']; + } + var tempDir = (process.env['TMPDIR'] ? process.env['TMPDIR'] : '/tmp'); + var uniqId = `${new Date().getTime()}-${Math.floor(Math.random()*101)}`; + var tempFile = `${tempDir}/credentials-unit-test-${uniqId}.json`; + var testToken = 'A_TOKEN'; + fs.writeFileSync(tempFile, testToken); + process.env['AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE'] = tempFile; + globalThis.ngx.fetch = function(url, options) { + console.log(' fetching eks pod identity mock credentials'); + if (url === 'http://169.254.170.23/v1/credentials') { + if (options && options.headers && options.headers['Authorization'].toString() === testToken) { + return Promise.resolve({ + ok: true, + json: function() { + globalThis.credentialsIssued = true; + return Promise.resolve({ + AccessKeyId: 'AN_ACCESS_KEY_ID', + Expiration: '2017-05-17T15:09:54Z', + AccountId: 'AN_ACCOUNT_ID', + SecretAccessKey: 'A_SECRET_ACCESS_KEY', + Token: 'A_SECURITY_TOKEN', + }); + }, + }); + } else { + throw 'Invalid token passed: ' + options.headers['Authorization']; + } + } else { + throw 'Invalid request URL: ' + url; + } + }; + var r = { + "headersOut": { + "Accept-Ranges": "bytes", + "Content-Length": 42, + "Content-Security-Policy": "block-all-mixed-content", + "Content-Type": "text/plain", + "X-Amz-Bucket-Region": "us-east-1", + "X-Amz-Request-Id": "166539E18A46500A", + "X-Xss-Protection": "1; mode=block" + }, + log: function(msg) { + console.log(msg); + }, + return: function(code) { + if (code !== 200) { + throw 'Expected 200 status code, got: ' + code; + } + }, + }; + + await awscred.fetchCredentials(r); + + if (!globalThis.credentialsIssued) { + throw 'Did not reach the point where EKS Pod Identity credentials were issued.'; } } async function test() { await testEc2CredentialRetrieval(); await testEcsCredentialRetrieval(); + await testEKSPodIdentityCredentialRetrieval(); testReadCredentialsWithAccessSecretKeyAndSessionTokenSet(); testReadCredentialsFromFilePath(); testReadCredentialsFromNonexistentPath();