Зберігання даних доступу у 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.objectNamesecretObjects.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]
Все є.
Готово.
