AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Автор |  21/08/2024
 

Маємо на проекті новий EKS кластер 1.30, на якому хочемо повністю відмовитись від старого IRSA з OIDC і почати користуватись EKS Pod Identities – див. AWS: EKS Pod Identities – заміна IRSA? Спрощуємо менеджмент IAM доступів.

І все наче працює чудово, але коли почав деплоїти наш Backend API – поди не стартують, і висять в статусі ContainerCreating.

Проблема: Kubernetes Secrets Store CSI Driver та “An IAM role must be associated with service account”

Сікрети для бекенду зберігаються в AWS Secrets Manager, звідки за допомогою Kubernetes Secrets Store CSI Driver синхронізуються в Kubernetes Secrets, з яких поди Бекенду створюють свої змінні оточення – див. AWS: Kubernetes – інтеграція AWS Secrets Manager та Parameter Store.

Перевіряємо статус поду – і бачимо чудове повідомлення “An IAM role must be associated with service account“:

Warning  FailedMount  43s (x2 over 2m45s)  kubelet  MountVolume.SetUp failed for volume "backend-api-secret-class-volume" : rpc error: code = Unknown desc = failed to mount secrets store objects for pod dev-backend-api-ns/backend-api-deployment-65c559d47-bb4dz, err: rpc error: code = Unknown desc = us-east-1: An IAM role must be associated with service account backend-api-sa (namespace: dev-backend-api-ns)

Google приводить нас до GitHub Issue Pod Identity Association not recognised by secrets store CSI driver, яка відкрита ще в грудні 2023.

В ній жеж є і пул-реквест Add support for Pod Identity Association, який наче має пофіксити цю проблему – але і він досі в Open, хоча декілька днів тому додали комент, що “We are conducting initial investigation on this feature request and will share updates soon“.

І драйвер зараз останньої версії – 0.3.9:

$ helm list -n kube-system | grep secret
secrets-store-csi-driver                kube-system     1               2024-07-18 12:46:20.945937022 +0300 EEST        deployed        secrets-store-csi-driver-1.4.4                  1.4.4      
secrets-store-csi-driver-provider-aws   kube-system     1               2024-07-18 12:46:23.287242734 +0300 EEST        deployed        secrets-store-csi-driver-provider-aws-0.3.9                

То що робити?

Варіант перший – це знов додавати OIDC і стару схему. Але цього прям зовсім не хочеться, бо на новому Kubernetes-кластері хотілося б вже і повністю нову аутентифікацію, а не ліпити костилі, які потім треба буде випилювати.

Варіант другий – спробувати перейти з  Kubernetes Secrets Store CSI Driver на External Secrets Operator, який вміє працювати з купою різних провайдерів – AWS Secrets Manager, Hashicorp Vault, Google Secrets Manager тощо.

Крім того, мені не дуже подобається те, що для створення Kubernetes Secret за допомогою Kubernetes Secrets Store CSI Driver, в його SecretProviderClass треба окремо описувати objects, а потім їх фактично дублювати в secretObjects.

External Secrets Operator: знайомство

Отже, External Secrets Operator (ESO) вміє отримувати сікрети із зовнішніх ресурсів і створювати звичайні Kubernetes Secrets.

Для доступу в AWS він використовує стандартну схему з ServiceAccount, а значить ми можемо створити EKS Pod Identity Association на AWS IAM Role, яка буде давати доступ до AWS Secrets Manager.

External Secrets Operator використовує два основні ресурси:

  • SecretStore: описує як саме отримати доступ до секретів – який провайдер (AWS, Google, Vault, etc) та аутентифікація, і створюється на рівні окремого Kubernetes Namespace для розподілення доступів
    • також є ClusterSecretStore, який можна створити глобально і доступний з будь-якого Namespace
  • ExternalSecret: описує які дані отримати від провайдера, і при потребі – які зміни зробити

External Secrets Operator має цілу купу зовнішніх провайдерів – див. Provider.

В AWS вміє працювати як з самим SecretsManager, та і ParameterStore.

Крім того, External Secrets Operator може навіть вносити зміни в AWS Secrets Manager – але ми будемо його використовувати тільки для створення Kubernetes Secrets, бо самі секрети в AWS Secrets Manager створюються з Terraform кожного проекту.

Отже, наша задача:

  • встановити External Secrets Operator
  • налаштувати йому доступ до AWS з AWS IAM Role та Kubernetes ServiceAccount використовуючи EKS Pod Identities
  • і створити Kubernetes Secret, який ми зможемо підключити в Kubernetes Pod, аби задати потрібні environment variables для роботи нашого сервісу

Запуск External Secrets Operator з Helm

Додаємо репозиторій:

$ helm repo add external-secrets https://charts.external-secrets.io
"external-secrets" has been added to your repositories

Сам Helm-чарт і доступні values – external-secrets.

Встановлюємо чарт з оператором в ops-external-secrets-ns Namespace:

$ helm install -n ops-external-secrets-ns --create-namespace external-secrets external-secrets/external-secrets

Перевіряємо поди:

$ kk -n ops-external-secrets-ns get pod
NAME                                                READY   STATUS    RESTARTS   AGE
external-secrets-5859d8dc69-vxhjb                   1/1     Running   0          33s
external-secrets-cert-controller-5bbb8c4bb8-nmjn9   1/1     Running   0          33s
external-secrets-webhook-564cd5b69-r5mmb            1/1     Running   0          33s

Та ServiceAccounts:

$ kk -n ops-external-secrets-ns get sa 
NAME                               SECRETS   AGE
default                            0         10m
external-secrets                   0         9m59s
external-secrets-cert-controller   0         9m59s
external-secrets-webhook           0         9m59s

Нас тут цікавить ServiceAccount external-secrets – оператор буде використовувати його для доступу до Secrets Manager та Parameter Store, і його ми будемо підключати до EKS з Pod Indentity.

Аутентифікація з AWS IAM

Що нам потрібно:

  • IAM Policy, яка надає доступ до Secrets Manager та Parameter Store
  • IAM Role з Trust Policy для EKS Pod Indentity
    • і до цієї ролі підключимо IAM Policy

Тоді под з External Secrets Operator через Kubernetes ServiceAccount буде виконувати Assume цієї ролі, і отримувати доступ до секретів.

Поки зробимо найпростішою схемою, а далі подивимось, як додатково можна розділяти доступи через окремі IAM Roles для кожного SecretStore в різних неймспейсах.

Створення IAM Policy

Переходимо в IAM, створюємо нову IAM Policy, дозволяємо тільки read-операції:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAccessToSecretsManager",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:ListSecrets",
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret",
                "secretsmanager:ListSecretVersionIds"
            ],
            "Resource": [
                "arn:aws:secretsmanager:<AWS_REGION>:<AWS_ACCOUNT_ID>:secret:*"
            ]
        },
        {
            "Sid": "AllowAccessToParameterStore",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters",
                "ssm:GetParameter",
                "ssm:GetParametersByPath"
            ],
            "Resource": [
                "arn:aws:ssm:<AWS_REGION>:<AWS_ACCOUNT_ID>:parameter/*"
            ]
        }
    ]
}

Зберігаємо з ім’ям external-secrets-operator-test-policy:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Створення IAM Role для EKS Pod Identity

Переходимо в IAM Role, створюємо нову роль, в Use case вибираємо EKS – Pod Identity:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Підключаємо створену вище IAM Policy external-secrets-operator-test-policy:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Зберігаємо нову роль з ім’ям external-secrets-operator-test-role, в Trust policy маємо "Service": "pods.eks.amazonaws.com":

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Створення EKS Pod Identity Association

Тепер підключаємо цю роль до нашого EKS-кластеру atlas-eks-ops-1-30-cluster в неймспейс ops-external-secrets-ns до Kubernetes ServiceAccount з ім’ям external-secrets:

$ aws --profile work eks create-pod-identity-association --cluster-name atlas-eks-ops-1-30-cluster \
> --role-arn arn:aws:iam::492***148:role/external-secrets-operator-test-role \
> --namespace ops-external-secrets-ns \
> --service-account external-secrets

Перевіряємо в EKS > Access:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Окей – тут все готово. Тепер ESO має отримати доступ до секретів та параметрів.

Далі нам потрібен SecretStore, який буде описувати як отримати доступ до AWS Secrets Manager або Parameter Store, та ExternalSecret, який власне буде відповідати за Kubernetes Secrets.

Створення Kubernetes Secrets з AWS Secrets Manager

Документація по всім значенням для ресурсів Operator – в API specification.

Створення SecretStore

Тестити будемо в окремому Namespace ops-test-ns.

Пишемо маніфест для SecretStore:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: test-secret-store
  namespace: ops-test-ns
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1

Створюємо ресурс:

$ kk apply -f test-secretstore.yml 
secretstore.external-secrets.io/test-secret-store created

Перевіряємо:

$ kk get secretstore
NAME                AGE   STATUS   CAPABILITIES   READY
test-secret-store   29s   Valid    ReadWrite      True

Створення ExternalSecret

Тепер нам потрібен ExternalSecret, який буде використовувати створений вище SecretStore.

Створимо тестовий секрет в AWS Secrets Manager:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Зберігаємо його з ім’ям test-aws-secret:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

І маємо секрет зі значенням {"secret_key_1":"secret_value_1","secret_key_2":"secret_value_2"}:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Спочатку отримаємо всю строку, а потім подивимось, як її можна додавати в Kubernetes Secret.

Описуємо маніфест для ExternalSecret:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: test-external-secret
  namespace: ops-test-ns
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: test-secret-store
    kind: SecretStore
  target:
    name: test-kubernetes-secret
    creationPolicy: Owner
    deletionPolicy: Delete
    template:
      metadata:
        labels:
          app: test
  data:
    - secretKey: key_in_the_kubernetes_secret
      remoteRef:
        # AWS Secrets Manager secret's name
        key: test-aws-secret

Тут в spec:

  • refreshInterval: як часто перевіряти зміни в AWS Secrets Manager
  • secretStoreRef: який SecretStore використовувати для доступу в AWS Secrets Manager (можна вказати окремо в data.sourceRef.storeRef)
  • target:
    • name: ім’я Kubernetes Secret, який буде створено
    • creationPolicy: “власник” Kubernetes Secret:
      • Owner: видаляє Kubernetes Secret, якщо видаляється відповідний ExternalSecret
      • Merge: не створює Kubernetes Secret, а міняє записи в існуючому
      • Orphan: залишає Kubernetes Secret, якщо видаляється відповідний ExternalSecret
    • deletionPolicy:
      • Retain: залишає Kubernetes Secret, якщо у відповідному AWS Secrets Manager Secret видалені всі поля
      • Delete: видаляє Kubernetes Secret, якщо у відповідному AWS Secrets Manager Secret видалені всі поля
      • Merge: видаляє всі записи в Kubernetes Secret, якщо у відповідному AWS Secrets Manager Secret видалені всі поля, але залишає сам Kubernetes Secret
    • template: описує структуру Kubernetes Secret, який буде створено – type, labels, annnotations, etc
  • data: описує зв’язок між секретом в AWS Secrets Manager та Kubernetes Secret:
    • secretKey: ім’я key в Kubernetes Secret
    • remoteRef:
      • key: ім’я секрету в AWS Secrets Manager

Створюємо ресурс:

$ kk apply -f test-externalsecret.yml 
externalsecret.external-secrets.io/test-external-secret created

Перевіряємо його статус:

$ kk get externalsecret
NAME                   STORE               REFRESH INTERVAL   STATUS         READY
test-external-secret   test-secret-store   1h                 SecretSynced   True

STATUS == SecretSynced – тобто, External Secrets Operator зміг отримати значення з AWS Secrets Manager та створити Kubernetes Secret.

При проблемах можна глянути логи ESO:

$ ktail -n ops-external-secrets-ns -l app.kubernetes.io/instance=external-secrets

Перевіряємо сам Kubernetes Secret:

$ kk get secret test-kubernetes-secret -o yaml
apiVersion: v1
data:
  key_in_the_kubernetes_secret: eyJzZWNyZXRfa2V5XzEiOiJzZWNyZXRfdmFsdWVfMSIsInNlY3JldF9rZXlfMiI6InNlY3JldF92YWx1ZV8yIn0=
...

І строка “eyJzZWNyZXRfa2V5XzEiOiJzZWNyZXRfdmFsdWVfMSIsInNlY3JldF9rZXlfMiI6InNlY3JldF92YWx1ZV8yIn0=” дає нам весь секрет з AWS Secrets Manager:

$ echo eyJzZWNyZXRfa2V5XzEiOiJzZWNyZXRfdmFsdWVfMSIsInNlY3JldF9rZXlfMiI6InNlY3JldF92YWx1ZV8yIn0= | base64 -d
{"secret_key_1":"secret_value_1","secret_key_2":"secret_value_2"}

Але в такому вигляді воно нам не дуже корисно, тому можемо зробити інакше – додати в data параметр property, в якому вказати конкретний ключ з секрету:

data:
  - secretKey: secret_key_1_value
    remoteRef:
      # AWS Secrets Manager secret's name
      key: test-aws-secret
      property: secret_key_1

Тоді наш Kubernetes Secret вже буде виглядати так:

$ kk get secret test-kubernetes-secret -o yaml
apiVersion: v1
data:
  secret_key_1_value: c2VjcmV0X3ZhbHVlXzE=
...

Де “c2VjcmV0X3ZhbHVlXzE=” – це значення “secret_value_1“.

Або замість того, щоб описувати кожен ключ з AWS Secrets Manager – в нашому ExternalSercret замість spec.data можемо використати spec.dataFrom:

...
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: test-secret-store
    kind: SecretStore
  target:
    name: test-kubernetes-secret
    creationPolicy: Owner
  ...     
  dataFrom:
  - extract:
      key: test-aws-secret
...

І тоді наш Kubernetes Secret буде виглядати так:

$ kk get secret test-kubernetes-secret -o yaml
apiVersion: v1
data:
  secret_key_1: c2VjcmV0X3ZhbHVlXzE=
  secret_key_2: c2VjcmV0X3ZhbHVlXzI=
...

Де “c2VjcmV0X3ZhbHVlXzE=” – це значення “secret_value_1“, а “c2VjcmV0X3ZhbHVlXzI=” – “secret_value_2“.

Або можемо переіменувати ключі, наприклад:

...
  dataFrom:
  - extract:
      key: test-aws-secret
    rewrite:
    - regexp:
        source: "secret_key_([0-9])"
        target: "SECRET_KEY_${1}"

І результат:

$ kk get secret test-kubernetes-secret -o yaml
apiVersion: v1
data:
  SECRET_KEY_1: c2VjcmV0X3ZhbHVlXzE=
  SECRET_KEY_2: c2VjcmV0X3ZhbHVlXzI=
...

Advanced IAM Permissions per SecretStore

Тепер давайте глянемо, як ми можемо використовувати IAM Role з окремою IAM Policy на рівні SecretStore.

Тобто замість того, аби мати одну IAM Role з IAM Policy, яка надає доступ до всіх секретів в AWS Secrets Manager, і яку використовує наш External Secrets Operator – ми можемо створити окрему IAM Role, її підключити до конкретного SecretStore в конкретному Kubernetes Namespace, і тоді цей SecretStore буде мати доступ тільки до тих секретів, які описані у відповідній IAM Policy.

Схематично це можна відобразити так:

AWS: Kubernetes та External Secrets Operator для AWS Secrets ManagerОтже, що зробимо:

  • в ролі external-secrets-operator-test-role, яка через EKS Pod Identity підключена до ServiceAccount external-secrets відключимо IAM Policy, яка дає доступ до всіх AWS Secrets
  • створимо нову IAM Policy external-secrets-operator-test-application-policy, яка буде надавати дозвіл тільки до конкретного секрету
  • створимо нову IAM Role external-secrets-operator-test-application-role:
    • їй в Trust Policy дозволимо виконувати Assume від імені ролі external-secrets-operator-test-role
    • і до цієї ролі підключимо IAM Policy external-secrets-operator-test-application-policy
  • а до SecretStore додамо параметр з role: external-secrets-operator-test-application-role

Тоді External Secrets буде працювати так:

  • Kubernets Pod з External Secrets через EKS Pod Identity виконує AssumeRole external-secrets-operator-test-role
  • при створенні ExternalSecret він використає SecretStore, в якому задана external-secrets-operator-test-application-role
    • External Secrets з external-secrets-operator-test-role виконає другий AssumeRole – “візьме” роль external-secrets-operator-test-application-role з її IAM Policy external-secrets-operator-test-application-policy
    • і вже з цієї роллю отримає доступ до секрету в AWS Secrets Manager

Поїхали.

В IAM Role external-secrets-operator-test-role видаляємо підключену IAM Policy, яка давала повний доступ до AWS Secrets Manager:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Створимо нову IAM Policy external-secrets-operator-test-application-policy з доступом до одного конкретного секрету test-aws-secret:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAccessToSecretsManager",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:ListSecrets",
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret",
                "secretsmanager:ListSecretVersionIds"
            ],
            "Resource": [
                "arn:aws:secretsmanager:us-east-1:492***148:secret:test-aws-secret*"
            ]
        }
    ]
}

Зберігаємо:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

Створюємо нову IAM Role з Custom trust policy, де дозволяємо виконувати Assume від ролі external-secrets-operator-test-role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Statement1",
      "Effect": "Allow",
      "Action": [
          "sts:AssumeRole",
           "sts:TagSession"
         ],
 			"Principal": {
 			    "AWS": "arn:aws:iam::492***148:role/external-secrets-operator-test-role"
 			}
    }
  ]
}

До цієї ролі підключаємо нову політику:

AWS: Kubernetes та External Secrets Operator для AWS Secrets Manager

І зберігаємо нову роль як external-secrets-operator-test-application-role:

Тепер повертаємось до нашого SecretStore, і в spec.provider.aws додаємо параметр role:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: test-secret-store
  namespace: ops-test-ns
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      role: arn:aws:iam::492***148:role/external-secrets-operator-test-application-role

Оновлюємо SecretStore :

$ kk apply -f test-secretstore.yml 
secretstore.external-secrets.io/test-secret-store configured

Для перевірки видалимо старий ExternalSecret:

$ kk delete externalsecret test-external-secret
externalsecret.external-secrets.io "test-external-secret" deleted

Створимо ще раз:

$ kk apply -f test-externalsecret.yml 
externalsecret.external-secrets.io/test-external-secret created

І дивимось статус:

$ kk get externalsecret
NAME                   STORE               REFRESH INTERVAL   STATUS         READY
test-external-secret   test-secret-store   1h                 SecretSynced   True

А тепер давайте спробуємо використати інший секрет з AWS Secrets Manager – “test/rds/kraken“, до якого ми не давали дозволу в IAM Policy external-secrets-operator-test-application-policy:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: test-external-secret
  namespace: ops-test-ns
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: test-secret-store
    kind: SecretStore
  target:
    name: test-kubernetes-secret
  ...
  dataFrom:
  - extract:
      key: test/rds/kraken

Деплоїмо, перевіряємо:

$ kk get externalsecret
NAME                   STORE               REFRESH INTERVAL   STATUS              READY
test-external-secret   test-secret-store   1h                 SecretSyncedError   False

І тепер STATUS == SecretSyncedError.

В логах це добре видно – “external-secrets-operator-test-application-role is not authorized to perform: secretsmanager:GetSecretValue on resource: test/rds/kraken“:

external-secrets-5859d8dc69-2fgc8:external-secrets {... "msg":"could not get secret data from provider","ExternalSecret":{"name":"test-external-secret","namespace":"ops-test-ns"},"error":"AccessDeniedException: User: arn:aws:sts::492***148:assumed-role/external-secrets-operator-test-application-role/1724248118674412261 is not authorized to perform: secretsmanager:GetSecretValue on resource: test/rds/kraken because no identity-based policy allows the secretsmanager:GetSecretValue action\n\tstatus code: 400 ...}

Висновки

Поки що мені External Secrets Operator прям дуже сподобався.

По-перше – дійсно набагато менше коду маніфестів для створення ресурсів.

По-друге – він нормально працює з EKS Pod Identity.

Третє – це дуже гнучка система розподілення доступів до секретів.

Четверте – що за допомогою одного оператора можна створювати Kubernetes Secrets з різних провайдерів.

Та і взагалі простіша система, бо не потрібно мати DaemonSet з подами на кожній WorkerNode, як це реалізовано в secrets-store-csi-driver-provider-aws.

Виглядає дуже прикольно, тому будемо мігрувати на нього.