If you manage several Kubernetes clusters across different AWS accounts, you know it can be challenging to manage Kubernetes secrets across all of those clusters. This article describes a method to store secrets values in AWS Secrets Manager in a single account and use those secrets in Kubernetes clusters running in different AWS accounts.
Furthermore, if you have secret values that are common to all Kubernetes clusters, such as license keys or API tokens, storing these values in a single account makes it easier to rotate the secrets and to control access to them.
The approach uses the following concepts:
- AWS Secrets Manager
- AWS IAM Role in the account storing the secret
- AWS IAM Role associated with the ESO SecretStore / ClusterSecretStore
- ESO [Cluster]SecretStore and [Cluster]ExternalSecret
If you are not familiar with the External Secrets Operator (ESO), I’ve written an introductory article. To keep this article short, I won’t go into too many details of how ESO works.
Note
This document describes a method in which the ESO Secret Store is configured to assume an IAM role in the AWS account where the secrets exist. An alternative method (not described here) is to configure the IAM role used by ESO to fetch the secret directly in a cross-account way.
Article Overview
AWS Secrets Manager
As mentioned, we will use AWS Secrets Manager as our secret store. This strategy can also be used for secrets stored in AWS SSM Parameter Store.
Choose an AWS account that will act as the “central” secret store and create the secret. A single AWS Secrets Manager secret can also be used to store multiple secrets in key/value form. If you have multiple secret values you want to share, using the key/value structured data option could reduce coding and configuration overhead.
Here is a terraform code snippet
resource "aws_secretsmanager_secret" "eso_shared_secret" {
name = "my-shared-secret"
description = "Shared secret"
}
resource "aws_secretsmanager_secret_policy" "eso_shared_secret" {
secret_arn = aws_secretsmanager_secret.eso_shared_secret.arn
policy = data.aws_iam_policy_document.eso.json
}
data "aws_iam_policy_document" "eso" {
statement {
effect = "Allow"
principals {
type = "AWS"
identifiers = [module.eso_shared_secret_role.iam_role_arn]
}
actions = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
]
resources = ["*"]
}
}
Notice the aws_iam_policy_document
attached to the secret contains an IAM role identifier. This role is the one that will be assumed by the ESO ClusterSecretStore
and we will discuss this in the next section. The policy ensures that the role is only able to read the secret.
AWS IAM Role to be Assumed
In the same AWS account where the secret was created, we will need a role that can access that secret in a read-only way. This will be configured later as the role to assume by ClusterSecretStore
for retrieving the secret.
In this terraform snippet, the community iam-assumable-role
module is used to create the role.
module "eso_shared_secret_role" {
source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role"
trusted_role_arns = [<list of cross-account IAM roles that can assume this role>]
create_role = true
role_name = "eso-shared-secret-assumable-role"
custom_role_policy_arns = [
aws_iam_policy.eso_shared.arn
]
number_of_custom_role_policy_arns = 1
role_requires_mfa = false
}
resource "aws_iam_policy" "eso_shared" {
name = "eso-shared-secret-policy"
description = "ESO ClusterSecretStore policy"
policy = data.aws_iam_policy_document.eso_shared_secret.json
}
data "aws_iam_policy_document" "eso_shared_secret" {
statement {
actions = [
"secretsmanager:GetResourcePolicy",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecrets",
"secretsmanager:ListSecretVersionIds",
]
resources = [aws_secretsmanager_secret.eso_shared_secret.arn]
}
Note that later we will need to pass to trusted_role_arns
a list of IAM role arns from other accounts that are allowed to assume this role. Also note that we have an identity-based policy on the role to restrict what actions can be performed by the role. This is important since this role will be assumed by other accounts, and we want to strictly limit the actions that role is allowed to perform.
IAM “IRSA” Role Used By ESO
With the ESO Objects ClusterSecretStore and SecretStore, it is possible to pass an IAM role by using a serviceaccount annotated with an IAM role that is configured as an “IRSA” role. We won’t cover the details of creating an IRSA role, however the terraform community module iam-role-for-service-accounts-eks
can help create such a role.
Once you create the IRSA role, you will need to attach an IAM policy to allow the role to assume the IAM role in our “central” secrets account. How this is done will depend on how the role was created:
data "aws_iam_policy_document" "eso_external_secrets_assume_role" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
resources = ["arn:aws:iam::<aws-secrets-account-id>:role/<secrets-account-role-name>"]
}
}
Next, we associate the IRSA role with the serviceaccount used by the ClusterSecretStore
(or SecretStore
) using an annotation:
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<aws-account-id>:role/<my-role-name>
name: eso-default
namespace: external-secrets
Then we specify this role in the serviceAccountRef
of the ClusterSecretStore
(or SecretStore
):
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
spec:
...
provider:
aws:
auth:
jwt:
serviceAccountRef:
name: eso-default # serviceaccount with the IRSA annotation
namespace: external-secrets
...
ClusterSecretStore / SecretStore Assume Role
Returning back to our example ClusterSecretStore
definition, we need to pass the IAM role we want to be assumed (the role in the “central” secrets account):
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: shared-secrets
spec:
conditions:
- namespaces:
- myapp
- kube-system
provider:
aws:
auth:
jwt:
serviceAccountRef:
name: eso-default
namespace: external-secrets
region: eu-west-1
role: "arn:aws:iam::<aws-secrets-account-id>:role/<secrets-account-role-name>"
service: SecretsManager
I know there is a lot going on here, so let’s summarize before we continue:
- We created an AWS Secrets Manager secret in account “A”
- We created an IAM role that can read this secret, also in account “A”
- For our Kubernetes cluster running in account “B”, we created an IRSA enabled IAM role allowed to assume the role in account “A”
- We associated the IRSA role with the serviceaccount used by the ClusterSecretStore in account “B”
- We specified that the ClusterSecretStore should use the role in account “A” to retrieve the secret
ClusterExternalSecret
This is the final step, which will create the actual Kubernetes secret that can be used by apps. In this example, I am using a ClusterExternalSecret
in order to have the secret created in multiple namespaces, but an ExternalSecret
can also be used for a single namespace.
The syntax is a bit tricky, so please refer to the ClusterExternalSecret documentation for an explanation of each field.
apiVersion: external-secrets.io/v1beta1
kind: ClusterExternalSecret
metadata:
labels:
name: my-shared-secret
spec:
externalSecretName: my-shared-secret
externalSecretSpec:
data:
- remoteRef:
conversionStrategy: Default
decodingStrategy: None
key: <secrets-manager-secret-name> # the name of the AWS Secrets Manager secret
property: MY_SECRET
secretKey: MY_SECRET
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: shared-secrets # use to the ClusterSecretStore we created
target:
creationPolicy: Owner
deletionPolicy: Retain
name: shared-secrets # the kubernetes secret name
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: In
values:
- some-other-namespace # list of namespaces to create the secret
- kube-system
refreshTime: 10m
If everything goes well, after a short while you should see the Kubernetes secret in the namespace(s):
❯ kubectl get secret
NAME TYPE DATA AGE
shared-secrets Opaque 1 14d
❯ kubectl get secret shared-secrets -o yaml
apiVersion: v1
data:
MY_SECRET: <base64 hash encoded secret>
kind: Secret
metadata:
name: shared-secrets
namespace: kube-system
type: Opaque
Summary
This took a few steps to get running, but once you create the AWS Secrets Manager secret, the IAM roles and the ClusterSecretStore, you can reuse those components to create other secrets.
With this scalable approach we are able to centrally manage secret values (e.g., rotate the secret) across many different Kubernetes clusters in different AWS accounts.
Of course, please evaluate whether this approach meets the security standards for your organization, and be sure to control the IAM policies for identities and resources.