AWS: Kubernetes – інтеграція AWS Secrets Manager та Parameter Store

Автор |  17/07/2023
 

Зберігання даних доступу у 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, для яких створює два objectAliasascp-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]

Все є.

Готово.