Skip to main content

SecretStore

Lo primero que necesitamos hacer al usar el External Secrets Operator (ESO) es elegir el provider que va a almacenar los secretos — como AWS Secrets Manager, SSM Parameter Store, GCP Secrets, entre otros.

No tiene sentido estudiar todos los providers en profundidad: cada uno tiene detalles diferentes, y la documentación lo cubre muy bien. Lo que todos tienen en común es que necesitan ser configurados mediante un SecretStore.

Ejemplo con AWS Secrets Manager

No vamos a detallar en profundidad sobre IAM en AWS, solo el concepto necesario que necesitas saber.

Uno de los providers más utilizados es el AWS Secrets Manager. Para usarlo, necesitamos:

  1. Crear el recurso SecretStore
  2. Crear un secret con las credenciales de AWS en el cluster (en caso de no usar IRSA). Veremos este enfoque más adelante.
  3. Garantizar que el IAM Role tenga permisos correctos

Antes de comenzar, es necesario tener las claves del IAM en AWS y saber qué role vamos a usar.

Conceptos Rápidos Importantes sobre IAM

IAM User >>> Asume un Role >>> Y el Role posee permisos.

Un usuario generalmente posee las siguientes claves que se usan para identificarlo:

  • access-key-id
  • secret-access-key

Un segundo enfoque del que vamos a hablar aquí es que una service account del cluster Kubernetes asuma un role de AWS directamente, haciendo el papel de IAM User.

Service Account (RBAC Kubernetes) >>> Asume un Role AWS >>> Y el Role posee permisos.

Si observas bien, ambos casos necesitan del role que será asumido. Es en él que colocamos los permisos necesarios.

Ejemplo con IAM User

Crea en AWS un IAM user llamado external-secrets-iam (o cualquier otro nombre) y genera las credenciales de acceso. Varios tutoriales muestran cómo hacer esto. Necesitamos esas credenciales como secret dentro de Kubernetes para referenciarlas.

# Creando un secret aws-external-secrets-credentials en el namespace external-secrets
kubectl create secret generic aws-external-secrets-credentials --from-literal=access-key=xxxxxxxxxxxx --from-literal=secret-access-key=xxxxxxxxxxxxxxxxxxxx -n external-secrets

Aquí tenemos el ejemplo del SecretStore usando esas credenciales. Aún no creamos el role. Un IAM User puede asumir varios roles, por eso necesitamos referenciar cuál será usado, pero aún no lo creamos.

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
role: arn:aws:iam::123456789012:role/external-secrets # Role que vamos a usar (crearemos más adelante)
region: us-east-2
auth:
secretRef: # Referencia al secret creado anteriormente
accessKeyIDSecretRef:
name: aws-external-secrets-credentials
key: access-key
secretAccessKeySecretRef:
name: aws-external-secrets-credentials
key: secret-access-key

Este role necesita permisos de lectura en Secrets Manager para que el ESO pueda sincronizar los secrets que vamos a mapear en el futuro.

El Parameter Store también es soportado, pero debe ser creado un nuevo SecretStore apuntando al servicio correcto.

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-parameter-store
spec:
provider:
aws:
service: ParameterStore
role: arn:aws:iam::ID_DE_TU_CUENTA:role/external-secrets
region: us-east-2
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-external-secrets-credentials
key: access-key
secretAccessKeySecretRef:
name: aws-external-secrets-credentials
key: secret-access-key

El mismo IAM Role puede ser usado, siempre que tenga permisos para los dos servicios — que es lo que haremos aquí.

Policy (Permiso en el Role)

Crea un role en AWS llamado external-secrets, pues fue ese nombre usado en los ejemplos arriba, y añade permisos para los servicios principales del ESO.

Es una buena práctica limitar el acceso del role solo a los secrets/parameters que debe acceder. Podemos hacer esto con Resource Tags. En caso de que no sea necesario, solo hay que remover la condición.

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SecretsManagerAccess",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:GetResourcePolicy",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecrets",
"secretsmanager:ListSecretVersionIds",
"secretsmanager:BatchGetSecretValue"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"secretsmanager:ResourceTag/external-secrets": "true"
}
}
},
{
"Sid": "SSMParameterStoreAccess",
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
"ssm:DescribeParameters"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"ssm:ResourceTag/external-secrets": "true"
}
}
}
]
}

Push Secret

El ESO también soporta el recurso PushSecret, que permite crear y actualizar secrets en el provider (como el Secrets Manager). Para esto, necesitamos añadir permisos extras. Generalmente me gusta hacer esto en una policy separada.

OBS: Usa tu región e ID de la cuenta.

{
"Effect": "Allow",
"Action": [
"secretsmanager:CreateSecret",
"secretsmanager:PutSecretValue",
"secretsmanager:TagResource",
"secretsmanager:DeleteSecret"
],
"Resource": [
"arn:aws:secretsmanager:us-east-2:ID_DE_TU_CUENTA:secret:dev-*"
]
}

Lo ideal es ser más restrictivo, por eso prefiero usar un control por tag en el delete:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:CreateSecret",
"secretsmanager:PutSecretValue",
"secretsmanager:TagResource"
],
"Resource": [
"arn:aws:secretsmanager:us-east-2:ID_DE_TU_CUENTA:secret:dev-*"
]
},
{
"Effect": "Allow",
"Action": [
"secretsmanager:DeleteSecret"
],
"Resource": [
"arn:aws:secretsmanager:us-east-2:ID_DE_TU_CUENTA:secret:dev-*"
],
"Condition": {
"StringEquals": {
"secretsmanager:ResourceTag/managed-by": "external-secrets"
}
}
}
]
}

Este prefijo dev-* puede seguir cualquier patrón: nombre del app, namespace, cluster, etc.

Para Parameter Store (Push), la idea es similar:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter*",
"ssm:PutParameter*",
"ssm:AddTagsToResource",
"ssm:ListTagsForResource"
],
"Resource": [
"arn:aws:ssm:us-east-2:ID_DE_TU_CUENTA:secret-manager/dev-*"
]
},
{
"Effect": "Allow",
"Action": [
"ssm:DeleteParameter*"
],
"Resource": [
"arn:aws:ssm:us-east-2:ID_DE_TU_CUENTA:secret-manager/dev-*"
],
"Condition": {
"StringEquals": {
"ssm:ResourceTag/managed-by": "external-secrets"
}
}
}
]
}

Al final, configura para que el IAM User external-secrets-iam pueda asumir el role external-secrets. Esto se hace en el role, no en el user:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ID_DE_TU_CUENTA:user/external-secrets-iam"
},
"Action": "sts:AssumeRole"
}
]
}

IAM Role con IRSA

La configuración con claves estáticas (accessKey/secretAccessKey) en un Secret de Kubernetes tiene algunos riesgos:

  • Seguridad: Son credenciales de larga duración. Si se filtran, pueden ser usadas desde cualquier lugar.
  • Gestión: La rotación manual es trabajosa y muchas veces olvidada.

IRSA (IAM Roles for Service Accounts) es la forma nativa y segura de dar permisos AWS a pods en EKS sin usar AWS credentials en el pod. Un pod utiliza una service account y si esta service account tiene permiso para asumir el role en AWS no necesitamos pasar las credenciales vía secretRef.

  • Seguridad Superior: Sin claves largas almacenadas. El pod usa tokens temporales generados y validados por el STS.
  • Gestión Simplificada: Sin crear, rotar o distribuir claves manualmente.

Al añadir una relación de confianza en el role, podemos decir que la service account (external-secrets) creada por external-secrets en el namespace (external-secrets) puede asumir el role external-secrets en AWS. Una vez más recordando que esta es una configuración del role external-secrets.

Necesitas configurar un OIDC Provider usado por el cluster para la federación pues él es tu IDP. Probablemente ya debes tener el OIDC Provider configurado. En caso de que no lo tengas busca mejores informaciones sobre esto sino nos alejaremos mucho del asunto.

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeRulesExternalSecrets",
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ID_DE_TU_CUENTA:oidc-provider/oidc.eks.us-east-2.amazonaws.com/id/XXXXXXXXXXXXXXXXXXXXX"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-2.amazonaws.com/id/XXXXXXXXXXXXXXXXXXXXX:sub": "system:serviceaccount:external-secrets:external-secrets"
}
}
}
]
}

Así, puedes remover los secretRef en el SecretStore y usar JWT.

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore # Ya vamos a explicar por qué cambiamos a ClusterSecretStore en vez de SecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: "us-east-2"
auth:
jwt:
serviceAccountRef: # Decimos cuál service account será usada y en qué namespace estará.
name: external-secrets
namespace: external-secrets

Pero la service account external-secrets necesita tener una annotation en ella diciendo qué role puede asumir. Esto puede ser definido en el values.yaml durante la instalación del helm chart.

#values.yaml
###...
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::1234567889911:role/external-secrets
###...

Que se reflejaría en el service account external-secrets creado por el chart.

❯ kubectl describe sa -n external-secrets external-secrets

Name: external-secrets
Namespace: external-secrets
Labels: app.kubernetes.io/instance=external-secrets
app.kubernetes.io/managed-by=Helm
app.kubernetes.io/name=external-secrets
app.kubernetes.io/version=v0.18.1
helm.sh/chart=external-secrets-0.18.1
Annotations: eks.amazonaws.com/role-arn: arn:aws:iam::1234567889911:role/external-secrets ### Aquí...
meta.helm.sh/release-name: external-secrets
meta.helm.sh/release-namespace: external-secrets
Image pull secrets: <none>
Mountable secrets: <none>
Tokens: <none>
Events: <none>

Aplica el SecretStore de la mejor manera de acuerdo con la solución que elegiste.

kubectl apply -f clustersecretstore.yaml

ClusterSecretStore vs SecretStore

Ambos son recursos del External Secrets Operator (ESO) usados para definir cómo y con quién el ESO se conecta a un provider (tipo AWS Secrets Manager, SSM, etc). La diferencia entre ellos es básicamente el ámbito de donde pueden ser usados.

SecretStore

Ámbito: Namespace

Uso: Solo puede ser referenciado por ExternalSecret dentro del mismo namespace

Ideal cuando: Quieres separar secretos por equipos, ambientes o apps con políticas aisladas

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-store
namespace: app-dev
...

Un ExternalSecret en el namespace app-prod no puede usar ese SecretStore arriba.

¿Por qué haríamos esto? Podemos tener roles diferentes siendo que el permiso dentro de cada uno de ellos es diferente para las tags. Por ejemplo un role que solo permite leer los secretos con la tag team=frontend y otro role team=backend.

ClusterSecretStore

Ámbito: Cluster-wide (global)

Uso: Puede ser referenciado por ExternalSecret en cualquier namespace

Ideal cuando: Quieres una única configuración para varios namespaces, centralizar el acceso al provider, o estandarizar uso del role.

Perfecto para clusters con múltiples apps/namespaces usando el mismo proveedor y role.

Si vas a definir solo uno para el cluster, es el ClusterSecretStore el que estás buscando.