Skip to content

Commit

Permalink
feat: add EKS pod identity credentials support (#252)
Browse files Browse the repository at this point in the history
Uses the `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` environment variable to provide support for EKS pod identity credentials. Ref: https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html
---------

Co-authored-by: Javier Evans <j.evans@f5.com>
  • Loading branch information
tieum and 4141done authored May 21, 2024
1 parent a2f1540 commit 1e0e5e7
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 2 deletions.
38 changes: 38 additions & 0 deletions common/etc/nginx/include/awscredentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -378,6 +393,29 @@ async function _fetchEC2RoleCredentials() {
};
}

/**
* Get the credentials needed to generate AWS signatures from the EKS Pod Identity Agent
* endpoint.
*
* @returns {Promise<Credentials>}
* @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
Expand Down
19 changes: 19 additions & 0 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
73 changes: 71 additions & 2 deletions test/unit/awscredentials_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 1e0e5e7

Please sign in to comment.