Helm: helm-secrets – шифрование sensitive данных с AWS KMS и деплой из Jenkins

Автор: | 15/05/2020

В продолжение поста Helm: пошаговое создание чарта и деплоймента из Jenkins – теперь нам надо добавить шифрование данных, что бы не хранить пароли и различные приватные ключи в открытом виде в Github-репозиториях – даже пусть и приватных.

Судя по гуглу, чуть ли не единственный вариант – это использование helm-secrets (а подскажите в комментариях – кто чем ещё пользуется?).

Из плюшек – возможность работать с ключами из AWS KMS, и автоматом подставлять данные в шаблоны.

Суть простая: берёт GPG/PGP/KMS ключ, им шифруется файл. Всё 🙂

Под капотом использует Mozilla SOPS (SOPS – Secrets OPerationS).

Что выполним – установим плагин, создадим AWS KMS ключ, создадим файл с паролем, обновим уже задеплоенный хельм-релиз.

В конце – добавим работу с плагином Jenkins-джобу из предыдущего поста.

helm-secrets

Переходим в репозиторий, в каталог с файлами чарта:

[simterm]

$ cd k8s/bttrm-apps-backend/
$ ll
total 24
-rw-r--r-- 1 setevoy setevoy 1109 May 14 08:21 Chart.yaml
drwxr-xr-x 2 setevoy setevoy 4096 May 14 08:21 charts
drwxr-xr-x 2 setevoy setevoy 4096 May 14 09:27 secrets
drwxr-xr-x 2 setevoy setevoy 4096 May 15 15:37 templates
-rw-r--r-- 1 setevoy setevoy 1225 May 15 10:01 values.yaml

[/simterm]

Установка

helm-secrets & sops на Arch Linux

Вообще установка должна выполняться через helm plugin – но на Arch Linux не сработало.

Сначала ругнулось на права:

[simterm]

$ helm plugin install https://github.com/futuresimple/helm-secrets
which: no dpkg in (/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/home/setevoy/go/bin)
mv: cannot create regular file '/usr/local/bin/sops': Permission denied
Error: plugin install hook for "secrets" exited with error

[/simterm]

Потом ещё раз на права – да и устанвавливать от sudo в /root – бред:

[simterm]

$ sudo helm plugin install https://github.com/futuresimple/helm-secrets
which: no dpkg in (/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/home/setevoy/go/bin)
/root/.local/share/helm/plugins/helm-secrets/install-binary.sh: line 62: /tmp/sops: Permission denied
Error: plugin install hook for "secrets" exited with error

[/simterm]

Потому – ставим сам sops из AUR:

[simterm]

$ yay -S sops

[/simterm]

И читаем документацию по helm plugin – куда он вообще устанвливает файлы плагинов?

$XDG_DATA_HOME/helm/plugins – ага.

$XDG_DATA_HOME должен вести на $HOME/.local/share – проверяем:

[simterm]

$ ll $HOME/.local/share/helm/plugins/
total 4
lrwxrwxrwx 1 setevoy setevoy 76 May 15 08:30 helm-secrets -> /home/setevoy/.cache/helm/plugins/https-github.com-futuresimple-helm-secrets

[/simterm]

Смотрим содержимое:

[simterm]

$ ls -l /home/setevoy/.cache/helm/plugins/https-github.com-futuresimple-helm-secrets/
total 68
-rw-r--r-- 1 setevoy setevoy 11337 May 15 08:30 LICENSE
-rw-r--r-- 1 setevoy setevoy 20057 May 15 08:30 README.md
drwxr-xr-x 4 setevoy setevoy  4096 May 15 08:30 example
-rwxr-xr-x 1 setevoy setevoy  2346 May 15 08:30 install-binary.sh
-rw-r--r-- 1 setevoy setevoy   338 May 15 08:30 plugin.yaml
-rwxr-xr-x 1 setevoy setevoy 12388 May 15 08:30 secrets.sh
-rwxr-xr-x 1 setevoy setevoy  4621 May 15 08:30 test.sh

[/simterm]

В общем-то вся работа плагина построена на bash-скриптах. Прелестно.

Сами скрипты описаны в документации Moving parts of project.

Ладно.

Что плагин вообще выполняет, какие действия?

Читаем файл plugin.yaml:

[simterm]

$ cat /home/setevoy/.cache/helm/plugins/https-github.com-futuresimple-helm-secrets/plugin.yaml 
name: "secrets"
version: "2.0.2"
usage: "Secrets encryption in Helm for Git storing"
description: |-
  This plugin provides secrets values encryption for Helm charts secure storing
command: "$HELM_PLUGIN_DIR/secrets.sh"
useTunnel: true
hooks:
  install: "$HELM_PLUGIN_DIR/install-binary.sh"
  update: "$HELM_PLUGIN_DIR/install-binary.sh"

[/simterm]

command: "$HELM_PLUGIN_DIR/secrets.sh" – ага…

Посмотрим скрипт secrets.sh:

[simterm]

$ cat /home/setevoy/.cache/helm/plugins/https-github.com-futuresimple-helm-secrets/secrets.sh | wc -l
534

[/simterm]

21-й век – мы полагаемся на километровый баш-скрипт для работы с важными данными.

Окей… Прелестно.

Настройка шифрования

За настройку ключей используемых для encrypt/decrypt действий отвечает файл .sops.yaml в каталоге чарта.

Примеры настроек – Using .sops.yaml conf to select KMS/PGP for new files.

Переходим к созданию ключа, которым будем шифровать наши данные.

AWS KMS

Документация – https://aws.amazon.com/ru/kms/getting-started.

Создаём KMS ключ, тип – дефолтный, симетричный (см. Using symmetric and asymmetric keys):

Задаём его администратора:

Далее надо добавить пользователей, у которых будет доступ к ключу.

Так как мы будем использовать файл из Jenkins, который работает на EC2 и доступ в AWS получает через свой EC2 Instance Profile – то надо добавить роль, которая используется им для аутентификации и авторизации в нашем AWS-аккаунте.

Находим профиль:

Находим, и подключаем её:

Подтверждаем сгенерированную политику:

Кстати:

...
            "Principal": {
                "AWS": "arn:aws:iam::534***385:root"
            },
            "Action": "kms:*",
            "Resource": "*"
...

ARN arn:aws:iam::534***385:root указывает на всех пользователей AWS-аккаунта, насколько я помню из поста AWS Elastic Kubernetes Service: RBAC-авторизация через AWS IAM и RBAC группы.

И таки да – у коллеги доступ к ключу есть, хотя явно user-прав ему не назначал. Окей – имеем это ввиду.

Ключ готов:

.sops.yaml config

В корне каталога с файлами чарта создаём файл .sops.yaml – это файл настроек для SOPS, который он будет использовать для работы с нашими секретами.

В примерах есть варианты использования различных ключей для различных файлов секретов – можно использовать различные ключи для различных файлов.

Пока используем один дефолтный для всех – создаём правило:

---
creation_rules:
  - kms: 'arn:aws:kms:eu-west-1:534***385:key/620b89fe-***-25b435611e8b'

Файл секретов

В каталоге чарта bttrm-apps-backend создаём файл для паролей – secrets.yaml.

Тут есть подводный камень, на который я напоролся – по-умолчанию helm-secrets будет искать файл по маске secrets.*.yaml – см. Usage and examples:

By convention, files containing secrets are named secrets.yaml, or anything beginning with “secrets.” and ending with “.yaml”. E.g. secrets.test.yaml and secrets.prod.yaml.

Т.е. назвать файл passwords.yaml – не вариант, по крайней мере без подолнительных телодвижений.

Итак, сейчас в файле values.yaml имеются пароли открытым текстом:

...
image:
  registry: "docker.io"
  username: "bttrm"
  password: "pass"
  repository: "bttrm"
  name: "bttrm-apps"
  tag: "120"
...

Вырезаем из values.yaml строку image.password – переносим в файл secrets.yaml:

image:
  password: "pass"

Или, что бы сделать тут пример более простым и наглядным – добавляем в secrets.yaml новое значение, например:

test:
    secret: testecret

И создаём манифест Kubernetes Secret, в моём случае они создаётся из шаблона bttrm-apps-backend/templates/bttrm-apps-secrets.yaml, где bttrm-apps-backend – каталог чарта:

---
apiVersion: v1
kind: Secret
metadata:
  name: test-secret
type: Opaque
stringData:
  example-secret: {{ .Values.test.secret }}

В stringData обращаемся к .Values, как обычно.

helm secrets – AccessDeniedException

Шифруем файл секретов:

[simterm]

$ helm secrets enc secrets.yaml 
Encrypting secrets.yaml
Could not generate data key: [failed to encrypt new data key with master key "arn:aws:kms:eu-west-1:534***385:key/620b89fe-8365-45a6-aad6-25b435611e8b": Failed to call KMS encryption service: AccessDeniedException: 
        status code: 400, request id: 699332aa-492b-47b4-b9ab-50f83dbcc2a4]
Error: plugin "secrets" exited with error

[/simterm]

А, окей.

Проблема, собственно, ясна сразу – SOPS пытается обратиться к AWS, используя default профиль из ~/.aws/credentials, в котором у меня записан мой личный аккаунт AWS, а не рабочий.

Можно добавить aws_profile в .sops.yaml:

---
creation_rules:
  - kms: 'arn:aws:kms:eu-west-1:534***385:key/620b89fe-***-25b435611e8b'
    aws_profile: 'arseniy'

Но тогда этот же профиль будет использоваться при деплое чарта из Jenkins.

Поэтому – задаём профиль переменной окружения:

[simterm]

$ AWS_PROFILE=arseniy

[/simterm]

И ещё раз пробуем зашифровать данные в чарте bttrm-apps-backend:

[simterm]

$ helm secrets enc bttrm-apps-backend/secrets.yaml 
Encrypting bttrm-apps-backend/secrets.yaml
Already encrypted: bttrm-apps-backend/secrets.yaml

[/simterm]

Проверяем содержимое secrets.yaml теперь:

image:
    password: ENC[AES256_GCM,data:1t7 .. PyY,iv:9mzTyB ... 9KG7+Hg=,tag:6KJ ... gvA==,type:str]
test:
    secret: ENC[AES256_GCM,data:l4c ... vCy,iv:9riw ... 0fvQ=,tag:G3p ... oSw==,type:str]
sops:
    kms:
    -   arn: arn:aws:kms:eu-west-1:534***385:key/620b89fe- ... -25b435611e8b
        created_at: '2020-05-15T06:33:44Z'
        enc: AQICAHj9F0HBsgu ... kb6GOxJkiOMZSSxOtA==
...

В строках image.password и test.secret теперь содержатся зашифрованные значения, а затем – описание того, как SOPS его шифровал.

Просмотреть содержимое можно с помощью view:

[simterm]

$ helm secrets view bttrm-apps-backend/secrets.yaml 
image:
    password: pass
test: 
    secret: testecret

[/simterm]

Отредактировать файл – с помощью edit:

[simterm]

$ helm secrets edit bttrm-apps-backend/secrets.yaml

[/simterm]

Test deploy

Что бы задеплоить – просто вызываем helm c install или upgrade, или как в нашем случае в Jenkins – upgrade --install, но добавляем secretshelm вызовет скрипт secrets.sh, который выполнит всю работу:

[simterm]

$ helm secrets upgrade --install --namespace bttrm-apps-dev-1-ns --create-namespace --atomic bttrm-apps-backend bttrm-apps-backend/ -f bttrm-apps-backend/values.yaml -f bttrm-apps-backend/secrets.yaml
Release "bttrm-apps-backend" has been upgraded. Happy Helming!
...

[/simterm]

Тут может быть интересно посмотреть процессы, которые запускаются во время вызова helm secrets upgrade:

[simterm]

$ ps aux | grep helm
setevoy    74906  0.0  0.3 1270888 49984 pts/1   Sl+  17:05   0:00 helm secrets upgrade --install --namespace bttrm-apps-dev-1-ns --create-namespace --atomic bttrm-apps-backend bttrm-apps-backend/ -f bttrm-apps-backend/values.yaml -f bttrm-apps-backend/secrets.yaml
setevoy    74914  0.0  0.0   4436  3672 pts/1    S+   17:05   0:00 bash /home/setevoy/.local/share/helm/plugins/helm-secrets/secrets.sh upgrade --install --create-namespace --atomic bttrm-apps-backend bttrm-apps-backend/ -f bttrm-apps-backend/values.yaml -f bttrm-apps-backend/secrets.yaml
setevoy    74963  0.8  0.5 1271768 96876 pts/1   Sl+  17:06   0:00 helm upgrade bttrm-apps-backend bttrm-apps-backend/ --install --create-namespace --atomic -f bttrm-apps-backend/values.yaml -f bttrm-apps-backend/secrets.yaml.dec

[/simterm]

Т.е:

  1. мы вызываем helm secrets upgrade
  2. затем вызывается bash /home/setevoy/.local/share/helm/plugins/helm-secrets/secrets.sh upgrade
  3. который вызывает helm upgrade

Проверяем наш секрет в Kubernetes:

[simterm]

$ kk -n bttrm-apps-dev-1-ns get secret test-secret -o yaml
apiVersion: v1
data:
  example-secret: dGVzdGVjcmV0
...

[/simterm]

И значение dGVzdGVjcmV0:

[simterm]

$ echo dGVzdGVjcmV0 | base64 --decode
testecret

[/simterm]

Переходим к Jenkins.

Jenkins

Теперь надо всё это добавить в Jenkinsfile, который писали в предыдущем посте.

Если сейчас удалить релиз:

[simterm]

$ helm -n bttrm-apps-dev-1-ns delete bttrm-apps-backend
release "bttrm-apps-backend" uninstalled

[/simterm]

А потом попробовать задеплоить его заново из Jenkins – то получим ошибку, т.к. helm не сможет получить данные, которые мы вынесли из values.yaml в secrets.yaml:

[simterm]

+ helm upgrade --install --namespace bttrm-apps-dev-1-ns --create-namespace --atomic bttrm-apps-backend bttrm-apps-backend-20.tgz
Release "bttrm-apps-backend" does not exist. Installing it now.
Error: unable to build kubernetes objects from release manifest: error validating "": error validating data: [unknown object type "nil" in Secret.stringData.backend-apple-cert-passphrase, unknown object type "nil" in Secret.stringData.backend-apple-sigin-key-id, unknown object type "nil" in Secret.stringData.backend-db-password, unknown object type "nil" in Secret.stringData.backend-private-key, unknown object type "nil" in Secret.stringData.backend-secret]

[/simterm]

Что надо сделать – добавить в Докер-образ, который используется для билда, плагин helm-secrets, а потом обновить стейдж stage(“Helm install”) в скрипте пайплайна – добавить вызов secrets и -f secrets.yaml.

Jenkins Docker build-image

Сейчас Dockefile для сборки образа, который используется для билда и деплоя выглядит так:

FROM bitnami/minideb:stretch
  
RUN apt update && install_packages ca-certificates wget
RUN install_packages curl python-pip python-setuptools jq git

RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
RUN chmod +x ./kubectl
RUN mv ./kubectl /usr/local/bin/kubectl

WORKDIR /tmp
RUN curl  --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
RUN mv /tmp/eksctl /usr/local/bin

RUN pip install ansible boto3 awscli

WORKDIR /tmp
RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
RUN /bin/bash get_helm.sh

USER root

Добавляем установку плагина – там исходный образ bitnami/minideb:stretch – SOPS должен установится нормально, а не как на Arch Linux:

...
RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
RUN /bin/bash get_helm.sh
RUN helm plugin install https://github.com/futuresimple/helm-secrets 

USER root

Собираем:

[simterm]

$ docker build -t bttrm/kubectl-aws:4.1 .
...
Step 14/15 : RUN helm plugin install https://github.com/futuresimple/helm-secrets
 ---> Running in 9c8b1e891e86
/usr/bin/dpkg
/root/.local/share/helm/plugins/helm-secrets/install-binary.sh: line 57: sudo: command not found
Error: plugin install hook for "secrets" exited with error
The command '/bin/sh -c helm plugin install https://github.com/futuresimple/helm-secrets' returned a non-zero code: 1

[/simterm]

Ага, счаз.

Окей – поставим SOPS из PIP:

...
RUN mv /tmp/eksctl /usr/local/bin 

RUN pip install ansible boto3 awscli sops

WORKDIR /tmp
...

Правда, во время сборки Helm сказал, что:

[simterm]

...
sops is already installed:
INFO: You're using Sops 1 written in Python. Sops 2 was rewritten in Go. Consider installing it with: $ go get -u go.mozilla.org/sops/cmd/sops
sops 1.18
...

[/simterm]

Посмотрим – как пойдёт во время билда в Jenkins.

Jenkinsfile

Обновляем скрипт пайплайна – добавляем secrets и -f bttrm-apps-backend/secrets.yaml:

...
        stage("Helm install") {
                    ...
                    sh "helm secrets upgrade --install --namespace ${AWS_EKS_NAMESPACE} --create-namespace --atomic ${APP_CHART_NAME} ${APP_CHART_NAME}-${RELEASE_VERSION}.tgz -f bttrm-apps-backend/secrets.yaml"
                }
            }   
        }
...

Запускаем:

Готово.

Ссылки по теме