Маємо на проекті новий 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:
Створення IAM Role для EKS Pod Identity
Переходимо в IAM Role, створюємо нову роль, в Use case вибираємо EKS – Pod Identity:
Підключаємо створену вище IAM Policy external-secrets-operator-test-policy:
Зберігаємо нову роль з ім’ям external-secrets-operator-test-role, в Trust policy маємо "Service": "pods.eks.amazonaws.com"
:
Створення 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:
Окей – тут все готово. Тепер 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:
Зберігаємо його з ім’ям test-aws-secret:
І маємо секрет зі значенням {"secret_key_1":"secret_value_1","secret_key_2":"secret_value_2"}
:
Спочатку отримаємо всю строку, а потім подивимось, як її можна додавати в 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 ManagersecretStoreRef
: який SecretStore використовувати для доступу в AWS Secrets Manager (можна вказати окремо вdata.sourceRef.storeRef
)target
:name
: ім’я Kubernetes Secret, який буде створеноcreationPolicy
: “власник” Kubernetes Secret:Owner
: видаляє Kubernetes Secret, якщо видаляється відповідний ExternalSecretMerge
: не створює 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 SecretremoteRef
: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.
Схематично це можна відобразити так:
- в ролі 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:
Створимо нову 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*" ] } ] }
Зберігаємо:
Створюємо нову 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" } } ] }
До цієї ролі підключаємо нову політику:
І зберігаємо нову роль як 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
.
Виглядає дуже прикольно, тому будемо мігрувати на нього.