ExternalSecrets Operator part 1

Данное расширение позволяет синхронизировать секреты из других провайдеров с секретами в k8s. Эта статья - первая часть, посвящённая данному решению.

·

6 min read

Установка в k8s.

helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets \
   external-secrets/external-secrets \
   -n external-secrets \
   --create-namespace
------------------------------------------------------------------------
helm list -n external-secrets
NAME                NAMESPACE           REVISION    UPDATED                                 STATUS      CHART                     APP VERSION
external-secrets    external-secrets    1           2023-08-21 15:28:29.095992 +0300 MSK    deployed    external-secrets-0.9.2    v0.9.2

ExternalSecret Operator создаёт несколько сущностей.

kubectl api-resources
clusterexternalsecrets            ces                 external-secrets.io/v1beta1            false        ClusterExternalSecret
clustersecretstores               css                 external-secrets.io/v1beta1            false        ClusterSecretStore
externalsecrets                   es                  external-secrets.io/v1beta1            true         ExternalSecret
secretstores                      ss                  external-secrets.io/v1beta1            true         SecretStore

Создаваемые ресурсы.

kubectl get po -n external-secret
NAME                                                READY   STATUS    RESTARTS   AGE
external-secrets-5b6fbc88f9-286m6                   1/1     Running   0          13m
external-secrets-cert-controller-7b7d4c9bcd-7gnq4   1/1     Running   0          13m
external-secrets-webhook-7446c9896-br9ql            1/1     Running   0          13m
kubectl get deploy -n external-secret
NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
external-secrets                   1/1     1            1           31m
external-secrets-cert-controller   1/1     1            1           31m
external-secrets-webhook           1/1     1            1           31m
kubectl get svc -n external-secret
NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
external-secrets-webhook   ClusterIP   10.75.239.144   <none>        443/TCP   17m
kubectl get crd
NAME                                             CREATED AT
clusterexternalsecrets.external-secrets.io       2023-08-16T09:50:37Z
clustersecretstores.external-secrets.io          2023-08-16T09:50:37Z
externalsecrets.external-secrets.io              2023-08-16T09:50:37Z
secretstores.external-secrets.io                 2023-08-16T09:50:37Z
kubectl get validatingwebhookconfigurations
NAME                      WEBHOOKS   AGE
externalsecret-validate   1          19m
secretstore-validate      2          19m
kubectl get secret -n external-secret
NAME                                     TYPE                 DATA   AGE
external-secrets-webhook                 Opaque               4      22m
sa-creds                                 Opaque               1      22m
sh.helm.release.v1.external-secrets.v1   helm.sh/release.v1   1      22m
kubectl get cm -n external-secret
NAME               DATA   AGE
kube-root-ca.crt   1      22m
kubectl get sa -n external-secret
NAME                               SECRETS   AGE
default                            0         23m
external-secrets                   0         23m
external-secrets-cert-controller   0         23m
external-secrets-webhook           0         23m
kubectl get role -n external-secret
NAME                              CREATED AT
external-secrets-leaderelection   2023-08-16T09:50:37Z

kubectl describe role external-secrets-leaderelection -n external-secret
Name:         external-secrets-leaderelection
Labels:       app.kubernetes.io/instance=external-secrets
              app.kubernetes.io/managed-by=Helm
              app.kubernetes.io/name=external-secrets
              app.kubernetes.io/version=v0.5.5
              helm.sh/chart=external-secrets-0.5.5
Annotations:  meta.helm.sh/release-name: external-secrets
              meta.helm.sh/release-namespace: external-secret
PolicyRule:
  Resources                   Non-Resource URLs  Resource Names                 Verbs
  ---------                   -----------------  --------------                 -----
  configmaps                  []                 []                             [create]
  leases.coordination.k8s.io  []                 []                             [get create update patch]
  configmaps                  []                 [external-secrets-controller]

Troubleshooting

При возникновении проблем с синхронизацией секретов необходимо будет посмотреть логи пода external-secrets. Там будет указана ошибка.

#Пример
kubectl logs external-secrets-644849d5c9-j9984 -n external-secrets

Провайдеры.

Fake Provider.

Используется только в рамках тестирования работоспособности сервиса.

kubectl apply -f - <<< '
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: fake
spec:
  provider:
    fake:
      data:
      - key: "/foo/bar"
        value: "HELLO1"
        version: "v1"
      - key: "/foo/bar"
        value: "HELLO2"
        version: "v2"
      - key: "/foo/baz"
        valueMap:
          foo: example
          other: thing'
kubectl get css
NAME   AGE   STATUS   CAPABILITIES   READY
fake   11s   Valid    ReadWrite      True
kubectl apply -f - <<< '
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: fake-secret
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: fake
    kind: ClusterSecretStore
  target:
    name: secret-to-be-created
  data:
  - secretKey: foo_bar
    remoteRef:
      key: /foo/bar
      version: v1
  dataFrom:
  - extract:
      key: /foo/baz'
kubectl get es
NAME              STORE          REFRESH INTERVAL   STATUS              READY
fake-secret       fake           1h                 SecretSynced        True
kubectl get secret secret-to-be-created --output=json | jq --raw-output ".data"
{
  "foo": "ZXhhbXBsZQ==",
  "foo_bar": "SEVMTE8x",
  "other": "dGhpbmc="
}
--------------------------------------------------------------------------
apiVersion: v1
kind: Secret
metadata:
  name: secret-to-be-created
  namespace: default
data:
  foo_bar: SEVMTE8x # HELLO1  (via data)
  foo: ZXhhbXBsZQ== # example (via dataFrom)
  other: dGhpbmc=   # thing   (via dataFrom)

ExternalSecret Operator + YC Certificate Manager

ExternalSecret позволяет использовать внешнюю, по отношению к кластеру Kubernetes, секретницу. В данном примере используется интеграция с YC CM.

Предполагаю, что SA есть и IAM ключ для него уже сгенерирован. Если нет, то можно воспользоваться инструкцией: https://teletype.in/@cameda/w93lPMPQWS4

На всякий случай. Генерацию IAM ключа для SA можно выполнить так:

yc iam key create --service-account-id $SA --output key.json

Создаётся файл key.json, который можно использовать в дальнейшем.

Создаём самоподписанный сертификат и добавляем его в CM.

openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout key.pem \
  -out cert.pem \
  -days 365 \
  -subj '/CN=example.com'
yc cm certificate create \
  --name camedakey \
  --chain cert.pem \
  --key key.pem
yc cm certificate get camedakey
id: fpqtr8jd2jd07a8u60vp
folder_id:
created_at: "2023-08-21T13:31:28.078Z"
name: camedakey
type: IMPORTED
domains:
  - example.com
status: ISSUED
issuer: CN=example.com
subject: CN=example.com
serial: 7d358e9654ba7771df479b778093800eebc9600e
updated_at: "2023-08-21T13:31:28.078Z"
issued_at: "2023-08-21T13:31:28.078Z"
not_after: "2024-08-20T13:29:48Z"
not_before: "2023-08-21T13:29:48Z"
yc cm certificate add-access-binding \
  --id fpqtr8jd2jd07a8u60vp \
  --service-account-name cameda-service \
  --role certificate-manager.certificates.downloader

Посмотрим содержимое сертификата.

yc certificate-manager certificate content \
  --id fpqtr8jd2jd07a8u60vp \
  --chain cert.pem \
  --key key.pem

Создаём секрет на основе файла key.json.

kubectl create secret generic yc-auth --from-file=authorized-key=key.json
# Проверяем, что содержимое соответствует файлу.
kubectl get secret yc-auth --output=json | jq --raw-output ".data[]" | base64 -d

Создаём SecretStore на основе секрета.

kubectl apply -f - <<< '
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: cm-store
spec:
  provider:
    yandexcertificatemanager:
      auth:
        authorizedKeySecretRef:
          name: yc-auth
          key: authorized-key'
kubectl get ss
NAME           AGE    STATUS   CAPABILITIES   READY
cm-store       143m   Valid    ReadOnly       True

Создаём ExternalSecret, использующий SecretStore.

kubectl apply -f - <<< '
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: cm-secret
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: cm-store
    kind: SecretStore
  target:
    name: cm-secret # the target k8s secret name
    template:
      type: kubernetes.io/tls
  data:
    - secretKey: tls.crt # the target k8s secret key
      remoteRef:
        key: fpqtr8jd2jd07a8u60vp # the certificate ID
        property: chain
    - secretKey: tls.key # the target k8s secret key
      remoteRef:
        key: fpqtr8jd2jd07a8u60vp # the certificate ID
        property: privateKey'

В secretKey указываем именно tls.crt и tls.key, а не cert.pem и key.pem.
Иначе будет ошибка.

kubectl get es
NAME              STORE          REFRESH INTERVAL   STATUS         READY
cm-secret         cm-store       30m                SecretSynced   True
#Проверим содержимое сертификата.
kubectl get secret cm-secret --output=json | jq --raw-output ".data[]" | base64 -d

Всё норм! Секрет создался и синконулся. Если декодировать его, то получим те же данные, что и в CM.

ExternalSecret Operator + YC LockBox

ExternalSecret позволяет использовать внешнюю, по отношению к кластеру Kubernetes, секретницу. В YC этой секретницей является Lockbox.

Предполагаю, что SA есть и IAM ключ для него уже сгенерирован. Если нет, то можно воспользоваться инструкцией: https://teletype.in/@cameda/w93lPMPQWS4

На всякий случай. Генерацию IAM ключа для SA можно выполнить так:

yc iam key create --service-account-id $SA --output key.json

Создаётся файл key.json, который можно использовать в дальнейшем.

KMS ключ сгенерировал здесь: https://teletype.in/@cameda/vBIMDOZi7vV

Lockbox был создан по этому мануалу: https://teletype.in/@cameda/M1Rm88sCB7t

Создаём секрет на основе файла key.json.

kubectl create secret generic yc-auth --from-file=authorized-key=key.json
# Проверяем, что содержимое соответствует файлу.
kubectl get secret yc-auth --output=json | jq --raw-output ".data[]" | base64 -d

Создаём SecretStore на основе секрета.

kubectl apply -f - <<< '
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: secret-store
spec:
  provider:
    yandexlockbox:
      auth:
        authorizedKeySecretRef:
          name: yc-auth
          key: authorized-key'
kubectl get ss
NAME           AGE   STATUS
secret-store   4s    Valid

SecretStore в статусе Valid. Значит создалось нормально.

Создаём ExternalSecret.

kubectl apply -f - <<< '
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-secret
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: secret-store
    kind: SecretStore
  target:
    name: k8s-secret
  data:
  - secretKey: ssh
    remoteRef:
      key: e6qad2p5dog8d6e07mol
      property: ssh'

secret-store - имя созданного SecretStore для обеспечения доступа к Lockbox;
k8s-secret - имя создаваемого секрета;
e6qa5j84t1gaq1fc1lgr - идентификатор секрета в Lockbox;
ssh - ключ секрета.

kubectl get es
NAME              STORE          REFRESH INTERVAL   STATUS
external-secret   secret-store   1h                 SecretSynced
# Проверяем, что содержимое ключа отдаётся верно.
kubectl get secret k8s-secret --output=json | jq --raw-output ".data.ssh" | base64 -d