2021-07-19 00:00:00
In companies, particularly big corporates, where security is paramount and best practice must be adhered to, an increasingly common requirement is for applications to access secrets in other AWS accounts — database passwords, api keys, etc.
Doing this in kubernetes has, in the past, been difficult. Thankfully, the addition of fine grained IAM roles for service accounts in the last few years has brought a much more structured and automatable approach to configuring this access securely.
This post looks at how we can utilise AWS’s IAM roles for service accounts (IRSA) for maximum security when accessing secrets in different AWS accounts, from pods running on EKS. Although quite a specific use case, it’s possibly the most complex, given that the KMS key used to encrypt a secret may well exist in another AWS account, which means we must consider access to resources across three different accounts. This complexity makes this scenario arguably the best one to consider — hopefully, if we can make the most complicated scenerio clear, it should enable us to understand other scenarios more easily.
So what do we need to do?
Let’s say we have a pod on an EKS cluster, running an instance of our application. And the application needs to access a database on an RDS instance in another AWS account. To do this, our app must retrieve the database password, which is stored in a secret within the other account. Additionally, the secret is encrypted using a customer managed key (CMK) stored in KMS in the account of our cyber security department. To retrieve the secret value and decrypt it, our application needs access not only to the secret itself, but also the key.
Let’s break down exactly what is required:
(NOTE: For the obvious purpose of not exposing details of our infrastructure, names for all the following resources have been replaced with dummy names. The resources are also edited for readability, showing only the key details.)
Our service account is configured as follows:
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam:accountid1:role/pod-role
name: testapp
namespace: testnamespace
And our pod:
apiVersion: v1
kind: Pod
metadata:
name: testapp
namespace: testnamespace
spec:
containers:
- env:
- name: AWS_ROLE_ARN
value: arn:aws:iam:accountid1:role/pod-role
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
image: accountid1.dkr.ecr.eu-west-1.amazonaws.com/testapp:1.0.1
name: testapp
...
volumeMounts:
- mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
name: aws-iam-token
readOnly: true
...
serviceAccount: testapp
serviceAccountName: testapp
...
volumes:
- name: aws-iam-token
projected:
defaultMode: 420
sources:
- serviceAccountToken:
audience: sts.amazonaws.com
expirationSeconds: 86400
path: token
...
Importantly, the environment variables AWS_ROLE_ARN
and AWS_WEB_IDENTITY_TOKEN_FILE
, along with the volume aws-iam-token
and corresponding volume-mount, did not need to be included in the original pod config. They were autogenerated by the mutating admission webhook, which has been developed by AWS and is deployed on the EKS api server. This occurs automatically when the service account is annotated with the IAM role.
The IAM role pod-role
has a policy attached as follows;
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "testappSecretAccess",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:eu-west-1:accountid2:secret:testsecret"
},
{
"Sid": "testappKmsAccess",
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:eu-west-1:accountid3:key/test-key-id"
}
]
}
and a trust policy …
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam:accountid1:oidc-provider/oidc.eks.eu-west-1.amazonaws.com/id/CLUSTERNODETESTID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"oidc.eks.eu-west-1.amazonaws.com/id/CLUSTERNODETESTID:sub": "system:serviceaccount:testnamespace:testapp"
}
}
}
]
}
The trust policy must be configured this way to allow the service account to assume the role. As can be seen, this access is also limited to our testapp
service account running pods in our testnamespace
.
In our cyber security account, accountid3
, we have a symmetric CMK created with arn arn:aws:kms:eu-west-1:accountid3:key/test-key-id
, and a key policy as follows;
{
"Version": "2012-10-17",
"Id": "project_account_accountid1",
"Statement": [
{
"Sid": "testappKmsAccess",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam:accountid1:role/pod-role"
]
},
"Action": "kms:Decrypt",
"Resource": "*"
}
]
}
Our secret is created in accountid2
with name testsecret
, and using the KMS key arn:aws:kms:eu-west-1:accountid3:key/test-key-id
to encrypt it on creation. It has the following resource permissions policy set;
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam:accountid1:role/pod-role"
},
"Action": "secretsmanager:GetSecretValue",
"Resource": "*"
}
]
}
So first, I log into the pod, and then assume the role. Ordinarily an application would do this programmatically, using the injected environment variables. My test container image is simply one with the awscli tools installed, so for the purposes of this example, I’ll do this on the command line.
> kubectl -n testnamespace exec -it testapp /bin/bash
testapp>
testapp> aws sts assume-role-with-web-identity --role-arn $AWS_ROLE_ARN --role-session-name testsession --web-identity-token-file file://$AWS_WEB_IDENTITY_TOKEN_FILE --duration-seconds 1000 > /tmp/tempcreds.txt
This has generated the temporary credentials AccessKeyId
, SecretAccessKey
and SessionToken
in the file /tmp/tempcreds.txt
, and I can now use the values for them, when finally retrieving our secret (edited here for readability).
testapp> export AWS_ACCESS_KEY_ID=
testapp> export AWS_SECRET_ACCESS_KEY=
testapp> export AWS_SESSION_TOKEN=
testapp>
testapp> aws secretsmanager get-secret-value --secret-id arn:aws:secretsmanager:eu-west-1:accountid2:secret:testsecret
{
"ARN": "arn:aws:secretsmanager:eu-west-1:accountid2:secret:testsecret",
"Name" "testsecret",
"VersionId": "cdd0c402-33b8-4d18-9365-b4ed0dd3e33f",
"SecretString": "testpassword",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": 1620897362.815
}
So there we have it. In this case our application is able to programmatically and dynamically retrieve the necessary secret value (testpassword
in this case), and use it as needed.
eu-west-1
. If you are unable to retrieve your secret, you may just need to specify --region eu-west-1
in your get-secret-value
command.--version-stage AWSCURRENT
to retrieve a value.Fine grained IAM roles are extremely useful in enabling pods in EKS to access resources outside of clusters, and the fact that they are so granular means we can give permissions for the pod to access only what is needed, which is very important.
The fact that they are so granular, however, also means there is a lot of complexity in configuring them. Everything has to be set, and everything must be correct, which is why it’s useful to lay it all out in blog posts like this one, so it’s clear how we at Airwalk Reply have been able to achieve this for our clients, and how you can too.