Зберігання даних доступу у Kubernetes Secrets має важливий недолік, бо вони доступні тільки всередені самого Kubernetes кластеру.
Щоб зробити їх доступними зовнішнім сервісам – можемо використати Hashicorp Vault і інтегрувати його з Kubernetes за допомогою таких рішень, як vault-k8s
, або скористуватись сервісами від AWS – Secrets Manager або Parameter Store.
Інтеграція AWS Secrets Manager та Parameter Store в Kubernetes дасть нам можливість створювати новий тип ресурсів – SecretProviderClass
, який ми зможемо підключати до Kubernetes Pods у вигляді файлів або змінних оточення.
Для цього нам знадобляться AWS Secrets and Configuration Provider (ASCP) та Kubernetes Secrets Store CSI Driver.
Зміст
AWS Secrets and Configuration Provider vs Hashicorp Vault
Я давно не користувався Vault, але щодо питання “Що використовувати” – то тут вибір між “сетапити, конфігурити та менеджити Hashicorp Vault самому” (встановлення Helm-чарту та конфігурація доступів) або “використати готове рішення від AWS” (по суті, потрібно тільки налаштувати IAM-ролі).
Також враховуйте, що використання AWS сервісів (suprize!) платне, тож якщо ви плануєте мати тисячі секретів – то мабуть краще таки з Vault.
Крім того, Vault сам по собі дає набагато більше можливостей, наприклад – генерація тимчасових токенів для сервісів, плюс наскільки пам’ятаю – Kubernetes Pods можуть отримувати параметри з Vault без необхідності в створенні Kubernetes Secrets, тоді як при використанні AWS Secrets and Configuration Provider (ASCP) та Kubernetes Secrets Store CSI Driver для підключення змінних будуть створюватиcm звичайні Kubernetes Secrets.
Втім, на нашому проекті вже використовуються Secrets Manager та Parameter Store, сенсу в Vault поки не бачу, тож інтегруємо наші секрети до кластеру в AWS Elastic Kubernetes Service.
AWS Secrets Manager vs Parameter Store
Детальніше про різницю між ними можна почитати тут – AWS — Difference between Secrets Manager and Parameter Store (Systems Manager), тут кратенько.
Загальні риси:
- обидва використовують AWS KMS для шифрування даних
- обидва являють собою Key/Value Store
- обидва підтримують versioning
Різниця:
- вартість:
- Secrets Manager: бере $0.40 за кожен секрет та $0.05 за кожні 10,000 API запросів
- Parameter Store: за Standard не бере грошей за зберігання, при higher throughput – коштує $0.05 за кожні 10,000 API запросів, при Advanced parameters – $0.05 за зберігання та $0.05 за кожні 10,000 API запросів
- ротація секретів:
- Secrets Manager: має вбудований механізм ротації та інтегрує його з сервісами (RDS, DocumentDB, etc)
- Parameter Store: маєте імплементувати ротацію самостійно
- Cross-account Access:
- Secrets Manager: підтримує
- Parameter Store: не підтримує
- Cross-Regions Replication:
- Secrets Manager: підтримує
- Parameter Store: не підтримує
- розмір даних:
- Secrets Manager: до 10KB на кожен секрет
- Parameter Store: 4KB на кожен запис (8KB при Advanced Parameters)
- ліміти кількості:
- Secrets Manager: 500,000 на регіон та акаунт
- Parameter Store: 10,000 на регіон та акаунт
Встановлення Secrets Store CSI Driver
Отже, для інтеграції нам потрібні два сервіси – Secrets Store CSI Driver та AWS Secrets and Configuration Provider.
Першим додаємо Secrets Store CSI Driver.
За його допомогою зможемо підключати секрети/параметри з AWS файлами або змінними до Kubernetes Pods.
Додаємо Helm-чарт і встановлюємо з опцією syncSecret.enabled=true
для створення RBAC-ролей для роботи з Kubernetes Secrets та їх синхронізації з секретами AWS під час ротації даних (див. Sync as Kubernetes Secret):
[simterm]
$ helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts $ helm -n kube-system install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver --set syncSecret.enabled=true
[/simterm]
Перевіряємо поди:
[simterm]
$ kubectl -n kube-system get pod | grep secret csi-secrets-store-secrets-store-csi-driver-kzmcx 3/3 Running 0 31s csi-secrets-store-secrets-store-csi-driver-t7bqc 3/3 Running 0 31s
[/simterm]
Встановлення AWS Secrets and Configuration Provider
Додаємо репозиторій та встановлюємо чарт:
[simterm]
$ helm repo add aws-secrets-manager https://aws.github.io/secrets-store-csi-driver-provider-aws $ helm install -n kube-system secrets-provider-aws aws-secrets-manager/secrets-store-csi-driver-provider-aws
[/simterm]
Перевіряємо поди:
[simterm]
$ kubectl -n kube-system get pod | grep secret csi-secrets-store-secrets-store-csi-driver-kzmcx 3/3 Running 0 9m csi-secrets-store-secrets-store-csi-driver-t7bqc 3/3 Running 0 9m secrets-provider-aws-secrets-store-csi-driver-provider-awskq5g8 1/1 Running 0 23s secrets-provider-aws-secrets-store-csi-driver-provider-awsksq9d 1/1 Running 0 23s
[/simterm]
Та глянемо CSIDriver:
[simterm]
$ kubectl get csidriver NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE ebs.csi.aws.com true false false <unset> false Persistent 46h efs.csi.aws.com false false false <unset> false Persistent 4d secrets-store.csi.k8s.io false true false <unset> false Ephemeral 10m
[/simterm]
Далі – налаштуємо IAM для IRSA.
IAM Policy та IAM Role для ServiceAccount
Щоб Kubernetes Pod зміг отримати доступ до AWS SecretManager та Parameter Store використаємо IRSA – створимо ServiceAacount, який буде використовувати IAM Role з IAM Policy, яка буде мати дозволи на виклик Secrets Manager та Parameter Store (див. AWS: EKS, OpenID Connect та ServiceAccounts).
Описуємо політику:
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "secretsmanager:DescribeSecret", "secretsmanager:GetSecretValue", "ssm:DescribeParameters", "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath" ], "Effect": "Allow", "Resource": "*" } ] }
Створюємо її в IAM:
[simterm]
$ aws iam create-policy --policy-name ascp-iam-policy --policy-document file://ascp-policy.json { "Policy": { "PolicyName": "ascp-policy", "PolicyId": "ANPAXFIUAIGSBPFEDKZZT", "Arn": "arn:aws:iam::492***148:policy/ascp-iam-policy", ...
[/simterm]
Описуємо Trust policy для ролі:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "sts:AssumeRoleWithWebIdentity", "Principal": { "Federated": "arn:aws:iam::492***148:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/2DC***124" }, "Condition": { "StringEquals": { "oidc.eks.us-east-1.amazonaws.com/id/2DC***124:aud": "sts.amazonaws.com", "oidc.eks.us-east-1.amazonaws.com/id/2DC***124:sub": "system:serviceaccount:default:ascp-test-serviceaccount" } } } ] }
Створюємо саму роль з цією політикою довіри:
[simterm]
aws iam create-role --role-name ascp-iam-role --assume-role-policy-document file://ascp-trust.json { "Role": { "Path": "/", "RoleName": "ascp-iam-role", "RoleId": "AROAXFIUAIGSLDCB3L4AR", "Arn": "arn:aws:iam::492***148:role/ascp-iam-role", ...
[/simterm]
До ролі підключаємо політику ascp-iam-policy
:
[simterm]
$ aws iam attach-role-policy --role-name ascp-iam-role --policy-arn=arn:aws:iam::492***148:policy/ascp-iam-policy
[/simterm]
Тепер можемо створити SecretProviderClass та Pod, який буде його використовувати.
Створення SecretProviderClass
Додамо SecretProviderClass, який буде отримувати строку з Secrets Manager та строку Parameter Store, які потім підключимо до Kubernetes Pod.
Створюємо секрет в Secrets Manager:
[simterm]
$ aws secretsmanager create-secret --name ascp-secret-test-string --secret-string "secretLine" { "ARN": "arn:aws:secretsmanager:us-east-1:492***148:secret:ascp-secret-test-string-DNweNg", "Name": "ascp-secret-test-string", "VersionId": "9d4f490d-edcc-4ee0-b43d-5b4e25fa271b" }
[/simterm]
Додаємо запис до Parameter Store:
[simterm]
$ aws ssm put-parameter --name ascp-ssm-test-param --value 'paramLine' --type "String" { "Version": 1, "Tier": "Standard" }
[/simterm]
Далі описуємо сам SecretProviderClass з двома objects
– в parameters.objects.objectName
– ім’я об’єкту в Secrets Manager або Parameter Store, а в objectType
вказуємо звідки беремо цей об’єкт:
--- apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: aspc-test-secret-class spec: provider: aws parameters: objects: | - objectName: "ascp-test-string" objectType: "secretsmanager" - objectName: "ascp-ssm-test-param" objectType: "ssmparameter"
Переходимо до поду.
Підключення SecretProviderClass в Pod файлом
Додаємо ServiceAccount з IAM-ролью, яку створили раніше, та Pod з цим ServiceAccount:
--- apiVersion: v1 kind: ServiceAccount metadata: name: ascp-test-serviceaccount namespace: annotations: eks.amazonaws.com/role-arn: arn:aws:iam::492***148:role/ascp-iam-role --- apiVersion: v1 kind: Pod metadata: name: ascp-test-pod spec: containers: - name: my-aws-cli image: amazon/aws-cli:latest command: ['sleep', '36000'] volumeMounts: - name: ascp-test-secret-volume mountPath: /mnt/ascp-secret readOnly: true restartPolicy: Never serviceAccountName: ascp-test-serviceaccount volumes: - name: ascp-test-secret-volume csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: aspc-test-secret-class
Деплоїмо:
[simterm]
$ kubectl apply -f ascp-test.yaml serviceaccount/ascp-test-serviceaccount created secretproviderclass.secrets-store.csi.x-k8s.io/aspc-test-secret-class created pod/ascp-test-pod created
[/simterm]
Перевіряємо под:
[simterm]
$ kk describe pod ascp-test-pod ... Mounts: /mnt/ascp-secret from ascp-test-secret-volume (ro) ... Volumes: ... ascp-test-secret-volume: Type: CSI (a Container Storage Interface (CSI) volume source) Driver: secrets-store.csi.k8s.io FSType: ReadOnly: true VolumeAttributes: secretProviderClass=aspc-test-secret-class ...
[/simterm]
Та зміст каталогу /mnt/ascp-secret
:
[simterm]
$ kk exec -ti ascp-test-pod -- ls -l /mnt/ascp-secret total 8 -rw-r--r-- 1 root root 10 Jul 17 09:32 ascp-secret-test-string -rw-r--r-- 1 root root 9 Jul 17 09:32 ascp-ssm-test-param
[/simterm]
І зміст файлів:
[simterm]
$ kk exec -ti ascp-test-pod -- cat /mnt/ascp-secret/ascp-secret-test-string secretLine $ kk exec -ti ascp-test-pod -- cat /mnt/ascp-secret/ascp-ssm-test-param paramLine
[/simterm]
Підключення SecretProviderClass в Pod змінною оточення
Підключення файлами може бути непоганим рішенням для якихось .env
файлів, але як щодо звичайних змінних? Наприклад – передати пароль для DB_PASSWORD
.
Для цього до SecretProviderClass додаємо secretObjects
– тоді Kubernetes Secrets Store CSI Driver створить звичайний Kubernetes Secret, котрий зможемо підключити в под :
--- apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: aspc-test-secret-class spec: provider: aws parameters: objects: | - objectName: "ascp-secret-test-string" objectType: "secretsmanager" - objectName: "ascp-ssm-test-param" objectType: "ssmparameter" secretObjects: - secretName: aspc-test-kube-secret type: Opaque data: - objectName: ascp-secret-test-string key: kube-secret-key
Тут:
secretObjects.secretName
: ім’я Kubernetes Secret, який буде створеноsecretObjects.secretName.data.objectName
: має збігатись зparameters.objects.objectName
secretObjects.secretName.data.key
: ключ для Kubernetes Secret –data.kube-secret-key
Деплоїмо, перевіряємо Kubernetes Secret:
[simterm]
$ kk get secret aspc-test-kube-secret -o yaml apiVersion: v1 data: kube-secret-key: c2VjcmV0TGluZQ== ...
[/simterm]
І значення kube-secret-key
:
[simterm]
$ echo c2VjcmV0TGluZQ== | base64 -d secretLine
[/simterm]
Тепер підключимо до поду – додаємо spec.containers.env
з valueFrom.secretKeyRef
:
--- apiVersion: v1 kind: Pod metadata: name: ascp-test-pod spec: containers: - name: my-aws-cli image: amazon/aws-cli:latest command: ['sleep', '36000'] env: - name: SECRET valueFrom: secretKeyRef: name: aspc-test-kube-secret key: kube-secret-key volumeMounts: - name: ascp-test-secret-volume mountPath: /mnt/ascp-secret readOnly: true restartPolicy: Never serviceAccountName: ascp-test-serviceaccount volumes: - name: ascp-test-secret-volume csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: aspc-test-secret-class
Деплоїмо, перевіряємо:
[simterm]
$ kk exec -ti ascp-test-pod -- printenv | grep SECRET SECRET=secretLine
[/simterm]
Або:
[simterm]
$ kk exec -ti ascp-test-pod -- bash bash-4.2# echo $SECRET secretLine
[/simterm]
При цьому маємо підключати і сам volumes
та volumeMounts
, як робили це для підключення секретів файлом.
Створення SecretProviderClass з JSON
Якщо дані в Secrets Manager та Parameter Store зберігаються в JSON – то для SecretProviderClass маємо використовувати jmesPath
.
Створимо ще один секрет в Secrets Manager:
[simterm]
$ aws secretsmanager create-secret --name ascp-secret-test-json --secret-string '{"username":"admin", "password":"foobar"}' { "ARN": "arn:aws:secretsmanager:us-east-1:492***148:secret:ascp-secret-test-json-iOtcBf", "Name": "ascp-secret-test-json", "VersionId": "32666608-6416-46cf-8b93-cf090eef1bc5" }
[/simterm]
Перевіряємо його:
Оновлюємо наш SecretProviderClass:
--- apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: aspc-test-secret-class spec: provider: aws parameters: objects: | - objectName: "ascp-secret-test-string" objectType: "secretsmanager" - objectName: "ascp-ssm-test-param" objectType: "ssmparameter" - objectName: "ascp-secret-test-json" objectType: "secretsmanager" jmesPath: - path: "username" objectAlias: "ascp-test-username" - path: "password" objectAlias: "ascp-test-password" secretObjects: - secretName: aspc-test-kube-secret type: Opaque data: - objectName: ascp-secret-test-string key: kube-secret-key - secretName: aspc-test-kube-secret-json type: Opaque data: - objectName: ascp-test-username key: kube-secret-user - objectName: ascp-test-password key: kube-secret-pass
Тут:
- в
parameters.objects.objectName: "ascp-secret-test-json"
викликаємоjmesPath
, який парсить наш секрет і отримує значення довх полів –username
таpassword
, для яких створює дваobjectAlias
– ascp-test-username та ascp-test-password - в
secretObjects.secretName: aspc-test-kube-secret-json
додаємоdata
з двомаobjectName
, в яких використуємоobjectAlias
зparameters
Обновлюємо наш Kubernetes Pod – додаємо два secretKeyRef
з ключами kube-secret-user
та kube-secret-user
з секрету aspc-test-kube-secret-json
:
--- apiVersion: v1 kind: Pod metadata: name: ascp-test-pod spec: containers: - name: my-aws-cli image: amazon/aws-cli:latest command: ['sleep', '36000'] env: - name: SECRET valueFrom: secretKeyRef: name: aspc-test-kube-secret key: kube-secret-key - name: USER valueFrom: secretKeyRef: name: aspc-test-kube-secret-json key: kube-secret-user - name: PASS valueFrom: secretKeyRef: name: aspc-test-kube-secret-json key: kube-secret-pass volumeMounts: - name: ascp-test-secret-volume mountPath: /mnt/ascp-secret readOnly: true restartPolicy: Never serviceAccountName: ascp-test-serviceaccount volumes: - name: ascp-test-secret-volume csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: aspc-test-secret-class
Деплоїмо, та перевіряємо Kubernetes Secrets:
[simterm]
$ kk get secret NAME TYPE DATA AGE aspc-test-kube-secret Opaque 1 2s aspc-test-kube-secret-json Opaque 2 2s
[/simterm]
І значення з aspc-test-kube-secret-json
:
[simterm]
$ kk get secret aspc-test-kube-secret-json -o yaml apiVersion: v1 data: kube-secret-pass: Zm9vYmFy kube-secret-user: YWRtaW4= ...
[/simterm]
Та змінні оточення в поді:
[simterm]
$ kk exec -ti ascp-test-pod -- printenv | grep 'SECRET\|USER\|PASS' SECRET=secretLine USER=admin PASS=foobar
[/simterm]
Все є.
Готово.