О чем это
Тут некоторе заметки по поводу использования HashiCorp Vault.
HashiCorp Vault реализует API для безопасного доступа к сенситивным данным. Он шифрует/дешифрует их и передает/извлекает в/из хранилища, в качестве которого могут выступать как обычные файлы, так и базы данных (mysql), key-value хранилища (etcd), но рекомендуемым является Hashicorp Consul.
Все эксперименты провожу в отдельном неймспйсе:
kubectl create ns vault
Установка Hashicorp Vault
Установим Consul
helm upgrade --install consul -n vault ./consul-helm/ --set server.replicas=1 --set server.bootstrapExpect=1
Установим Vault:
helm upgrade --install vault -n vault ./vault-helm/ --set server.standalone.enabled=false --set server.ha.enabled=true --set server.ha.replicas=1 --set ui.enabled=true --set ui.serviceType: "ClusterIP"
Убедимся, что всё запустилось:
kubectl get po -n vault
Инициализация Vault
kubectl exec -it -n vault vault-0 -- vault operator init --key-shares=1 --key-threshold=1
В ответ получим что-то такое:
Unseal Key 1: EEvC8nTJ7gw1SQtMJIly6JKtk6SWm+DyJWGycq/ECq4= Initial Root Token: s.Ta90nQ3ynlOEqkWm4g0WiJRJ Vault initialized with 1 key shares and a key threshold of 1. Please securely distribute the key shares printed above. When the Vault is re-sealed, restarted, or stopped, you must supply at least 1 of these keys to unseal it before it can start servicing requests. Vault does not store the generated master key. Without at least 1 key to reconstruct the master key, Vault will remain permanently sealed! It is possible to generate new unseal keys, provided you have a quorum of existing unseal keys shares. See "vault operator rekey" for more information.
Теперь поглядим статус Vault:
kubectl exec -it -n vault vault-0 -- vault status -tls-skip-verify
И получим такое:
Key Value --- ----- Seal Type shamir Initialized true Sealed true Total Shares 1 Threshold 1 Unseal Progress 0/1 Unseal Nonce n/a Version 1.4.2 HA Enabled true command terminated with exit code 2
Тут видно, что Vault запечатан (Sealed true). Распечатаем его:
kubectl exec -it vault-0 -n vault -- vault operator unseal
Если Vault развернут в HA-конфигурации, то распечатать нужно каждую реплику.
kubectl exec -it vault-0 -- vault operator unseal
Работа с данными в Vault
Для начала нужно залогиниться в Vault (получить токен для доступа к API):
kubectl exec -it vault-0 -n vault -- vault login
Тут нужно ввести root token, полученный при инициализации Vault.
Теперь запросим список хрянящихся в Vault авторизаций:
kubectl exec -it -n vault vault-0 -- vault auth list
Создадим путь (директорию) для хранения секретов:
kubectl exec -it -n vault vault-0 -- vault secrets enable --path=otus kv
Посмотрим список существующих секретов:
kubectl exec -it -n vault vault-0 -- vault secrets list --detailed
Создадим пару секретов в нашей “директории”
kubectl exec -it -n vault vault-0 -- vault kv put otus/otus-ro/config username='otus' password='asajkjkahs' kubectl exec -it -n vault vault-0 -- vault kv put otus/otus-rw/config username='otus' password='asajkjkahs'
И теперь прочитаем их:
kubectl exec -it -n vault vault-0 -- vault read otus/otus-ro/config kubectl exec -it -n vault vault-0 -- vault kv get otus/otus-rw/config
Интеграция с Kubernetes
Базовый вариант использования Vault с Kubernetes подразумевает чтение секретов из Vault. Для этого используются init-контейнер vault-agent, который читает из Vault секрет и помещает его в файл-шаблон.
Включим авторизацию через Kubernetes:
kubectl exec -it -n vault vault-0 -- vault auth enable kubernetes
И убедимся, что она включилась:
kubectl exec -it -n vault vault-0 -- vault auth list
Создадим ServiceAcccount
kubectl create serviceaccount -n vault vault-auth
И дадим ей права с помощью CLusterRoleBinding:
kubectl apply -f - <<EOF apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: role-tokenreview-binding namespace: vault roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: system:auth-delegator subjects: - kind: ServiceAccount name: vault-auth namespace: vault EOF
Теперь мы сконфигурируем в Vault параметры аутентификации для доступа к Kubernetes:
export VAULT_SA_NAME=$(kubectl get sa vault-auth -n vault -o jsonpath="{.secrets[*]['name']}") export SA_JWT_TOKEN=$(kubectl get secret $VAULT_SA_NAME -n vault -o jsonpath="{.data.token}" | base64 --decode; echo) export SA_CA_CRT=$(kubectl get secret $VAULT_SA_NAME -n vault -o jsonpath="{.data['ca\.crt']}" | base64 --decode; echo) #export K8S_HOST=$(more ~/.kube/config | grep server |awk '/http/ {print $NF}') export K8S_HOST="https://kub:6443"
И сконфигурируем аутентификацию (вернее поместим полученные значения в Vault):
kubectl exec -it -n vault vault-0 -- vault write auth/kubernetes/config token_reviewer_jwt="$SA_JWT_TOKEN" kubernetes_host="$K8S_HOST" kubernetes_ca_cert="$SA_CA_CRT"
Дальше нужно дать права на доступ к секретам (вернее к “директориям”) в Vault. Для этого - создади файлик политики:
tee otus-policy.hcl <<EOF path "otus/otus-ro/*" { capabilities = ["read", "list"] } path "otus/otus-rw/*" { capabilities = ["read", "create", "list", "update"] } EOF
скопируем его в pod vault'а:
kubectl -n vault cp otus-policy.hcl vault-0:/tmp/
и создадим с его помощью политику:
kubectl -n vault exec -it vault-0 -- vault policy write otus-policy /tmp/otus-policy.hcl
а также - создаим роль в vault:
kubectl -n vault exec -it vault-0 -- vault write auth/kubernetes/role/otus bound_service_account_names=vault-auth bound_service_account_namespaces=default policies=otus-policy ttl=24h
Проверка аутентификации и авторизации pod'а в Vault
Сейчас нужно запустить pod с ServiceAccount, которая была создана ранее и попытаться залогиниться в Vault с помощью токена этой ServiceAccount:
kubectl run -n vault --generator=run-pod/v1 tmp --rm -i --tty --serviceaccount=vault-auth --image alpine:3.7
В результате выполнения этой команды моявится приглашение командной строки внутри запущенного контейнера. Установим в контейнере curl и jq
apk add curl jq
И попытаемся залогиниться в Vault (также внутри тестового контейнера):
VAULT_ADDR=http://vault:8200 KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) curl --request POST --data '{"jwt": "'$KUBE_TOKEN'", "role": "otus"}' $VAULT_ADDR/v1/auth/kubernetes/login | jq TOKEN=$(curl -k -s --request POST --data '{"jwt": "'$KUBE_TOKEN'", "role": "otus"}' $VAULT_ADDR/v1/auth/kubernetes/login | jq '.auth.client_token' | awk -F\" '{print $2}')
В итоге у нас должно получиться значение токена:
echo $TOKEN s.ZmPegZEMIbB3pzBavjSWDLab
И теперь с помощью этого токена можно почитать секреты:
curl --header "X-Vault-Token:s.pPjvLHcbKsNoWo7zAAuhMoVK" $VAULT_ADDR/v1/otus/otus-ro/config curl --header "X-Vault-Token:s.pPjvLHcbKsNoWo7zAAuhMoVK" $VAULT_ADDR/v1/otus/otus-rw/config
или записать секреты:
curl --request POST --data '{"bar": "baz"}' --header "X-Vault-Token:s.pPjvLHcbKsNoWo7zAAuhMoVK" $VAULT_ADDR/v1/otus/otus-rw/config
Теперь можно сконфигурировать тестовый под, который сможет забрать секреты из Vault. Объекты создадим на базе примеров: https://github.com/hashicorp/vault-guides/tree/master/identity/vault-agent-k8s-demo
Configmap
kubectl apply -f - <<EOF apiVersion: v1 data: vault-agent-config.hcl: | # Comment this out if running as sidecar instead of initContainer exit_after_auth = true pid_file = "/home/vault/pidfile" auto_auth { method "kubernetes" { mount_path = "auth/kubernetes" config = { role = "otus" } } sink "file" { config = { path = "/home/vault/.vault-token" } } } consul-template-config.hcl: | vault { renew_token = false vault_agent_token_file = "/home/vault/.vault-token" retry { backoff = "1s" } } template { destination = "/etc/secrets/index.html" contents = <<EOT <html> <body> <p>Some secrets:</p> {{- with secret "otus/otus-ro/config" }} <ul> <li><pre>username: {{ .Data.username }}</pre></li> <li><pre>password: {{ .Data.password }}</pre></li> </ul> {{ end }} </body> </html> EOT } kind: ConfigMap metadata: name: example-vault-agent-config namespace: vault EOF
pod
kubectl apply -f - <<EOF apiVersion: v1 kind: Pod metadata: name: vault-agent-example namespace: vault spec: serviceAccountName: vault-auth volumes: - configMap: items: - key: vault-agent-config.hcl path: vault-agent-config.hcl - key: consul-template-config.hcl path: consul-template-config.hcl name: example-vault-agent-config name: config - emptyDir: {} name: shared-data - emptyDir: {} name: vault-token initContainers: - args: - agent - -config=/etc/vault/vault-agent-config.hcl - -log-level=debug env: - name: VAULT_ADDR value: http://vault:8200 image: vault name: vault-agent volumeMounts: - mountPath: /etc/vault name: config - name: vault-token mountPath: /home/vault containers: - image: nginx name: nginx-container ports: - containerPort: 80 volumeMounts: - mountPath: /usr/share/nginx/html name: shared-data - name: consul-template image: hashicorp/consul-template:alpine imagePullPolicy: Always volumeMounts: - name: vault-token mountPath: /home/vault - name: config mountPath: /etc/consul-template - name: shared-data mountPath: /etc/secrets env: - name: HOME value: /home/vault - name: VAULT_ADDR value: http://vault:8200 args: ["-config=/etc/consul-template/consul-template-config.hcl"] EOF
Запуск Certification Authority на базе Vault
создадим CA на базе vault
Включим секреты pki и сконфигурирум некоторые параметры:
kubectl exec -it -n vault vault-0 -- vault secrets enable pki kubectl exec -it -n vault vault-0 -- vault secrets tune -max-lease-ttl=87600h pki
Сгенерируем сертификат CA:
kubectl exec -it -n vault vault-0 -- vault write -field=certificate pki/root/generate/internal common_name="example.ru" ttl=87600h > CA_cert.crt
Пропишем ссылки для CA и проверки отозванных сертификатов
kubectl exec -it -n vault vault-0 -- vault write pki/config/urls issuing_certificates="http://vault:8200/v1/pki/ca" сrl_distribution_points="http://vault:8200/v1/pki/crl"
Создадим промежуточный сертификат
kubectl exec -it -n vault vault-0 -- vault secrets enable --path=pki_int pki kubectl exec -it -n vault vault-0 -- vault secrets tune -max-lease-ttl=87600h pki_int kubectl exec -it -n vault vault-0 -- vault write -format=json pki_int/intermediate/generate/internal common_name="example.ru Intermediate Authority" | jq -r '.data.csr' > pki_intermediate.csr
пропишем промежуточный сертификат в vault:
kubectl cp pki_intermediate.csr -n vault vault-0:/tmp/ kubectl exec -it -n vault vault-0 -- vault write -format=json pki/root/sign-intermediate csr=@/tmp/pki_intermediate.csr format=pem_bundle ttl="43800h" | jq -r '.data.certificate' > intermediate.cert.pem kubectl cp intermediate.cert.pem -n vault vault-0:/tmp/ kubectl exec -it -n vault vault-0 -- vault write pki_int/intermediate/set-signed certificate=@/tmp/intermediate.cert.pem
Создадим и отзовем новые сертификаты
Создадим роль для выдачи сертификатов
kubectl exec -it -n vault vault-0 -- vault write pki_int/roles/example-dot-ru allowed_domains="example.ru" allow_subdomains=true max_ttl="720h"
Создадим и отзовем сертификат
kubectl exec -it -n vault vault-0 -- vault write pki_int/issue/example-dot-ru common_name="test.example.ru" ttl="24h" kubectl exec -it -n vault vault-0 -- vault write pki_int/revoke serial_number="71:a8:4f:4c:bd:74:c6:d8:ea:27:64:cb:53:ef:80:1a:6b:c8:be:e3"
Автоматическое обновление сертификатов с помощью consul-template
https://medium.com/hashicorp-engineering/certificates-issuing-and-renewal-with-vault-and-consul-template-18e766228dac
https://learn.hashicorp.com/tutorials/nomad/vault-pki-nomad
Как все работает.
- в деплойменте пода помимио прикладного контейнера (nginx) прописываются два дополнительных контейнера. Первый - init-контейнер vault-agent, который берет конфигурацию из конфигмапа и получает токен. Этот токен в дальнейшем использует второй контейнер - consul-template, который занимается тем, что обновляет данные в соответствии с темплейтом записаным в конфигмап. Обновленные данные он кладет в папку, которая смонтирована в него и в прикладной под с nginx.
- Для того, чтобы все работало в Vault должна быть заведена политика, которая бы разрешала доступ для заданной роли в путь, где выпускаются сертификаты.
Создадим файл политики, которая разрешить выпуск сертификатов с помощью промежуточного CA:
tee cert_issue_policy.hcl <<EOF path "pki_int/issue/*" { capabilities = ["create", "read", "update", "list"] } EOF
скопируем файл политики в vault:
kubectl -n vault cp cert_issue_policy.hcl vault-0:/tmp/
и создадим с него помощью политику:
kubectl -n vault exec -it vault-0 -- vault policy write cert-issue-policy /tmp/cert_issue_policy.hcl
а также - создадим роль в vault, которую привяжем к ServiceAccount (с правами которого будет работать pod) и назначим этой роли созданную ранее политику, разрешающую выпуск сертов:
kubectl -n vault exec -it vault-0 -- vault write auth/kubernetes/role/cert-issue-role bound_service_account_names=vault-auth bound_service_account_namespaces=vault policies=cert-issue-policy ttl=24h
А теперь создадим объекты в K8S.
configmap
Конфигмап содержит три конфига:
- для init-контейнера vault-agent, который аутентифицируется с помощью ServiceAccount и получит токен для контейнера consul-template
- для consul-templateв котором описано где взять токен, поученный vault-agent, что именно забрать из vault (сертификат и ключ), куда это потом положить (в файлы) и что сделать после этого (послать сигнал nginx для обновления конфига).
- конфиг nginx, в котором настроен HTTPS-сервер, использующий файлы сертификата и ключа, полученные из Vault.
apiVersion: v1 kind: ConfigMap metadata: name: nginx-cert-renew-config namespace: vault data: vault-agent-config.hcl: | # Comment this out if running as sidecar instead of initContainer exit_after_auth = true pid_file = "/home/vault/pidfile" auto_auth { method "kubernetes" { mount_path = "auth/kubernetes" config = { role = "cert-issue-role" } } sink "file" { config = { path = "/home/vault/.vault-token" } } } consul-template-config.hcl: | vault { renew_token = false vault_agent_token_file = "/home/vault/.vault-token" retry { backoff = "1s" } } template { contents = "{{ with secret \"pki_int/issue/example-dot-ru\" \"common_name=test.example.ru\" \"ttl=2m\"}}{{ .Data.certificate }}{{ end }}" destination="/etc/secrets/tls.crt" command="sh -c 'killall -s HUP nginx || true'" } template { contents = "{{ with secret \"pki_int/issue/example-dot-ru\" \"common_name=test.example.ru\" \"ttl=2m\"}}{{ .Data.private_key }}{{ end }}" destination="/etc/secrets/tls.key" } template { contents = "{{ with secret \"pki_int/issue/example-dot-ru\" \"common_name=test.example.ru\" \"ttl=2m\"}}{{ .Data.issuing_ca }}{{ end }}" destination="/etc/secrets/int_ca.crt" } nginx_ssl.conf: | server { listen 443 ssl; server_name test.example.ru; ssl_certificate /etc/nginx/ssl/tls.crt; ssl_certificate_key /etc/nginx/ssl/tls.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; location / { root /usr/share/nginx/html; index index.html index.htm; } }
Pod
Манифест Pod'а с тремя контейнерами:
apiVersion: v1 kind: Pod metadata: name: nginx-cert-renew-example namespace: vault labels: app: nginx spec: shareProcessNamespace: true serviceAccountName: vault-auth volumes: - configMap: items: - key: vault-agent-config.hcl path: vault-agent-config.hcl - key: consul-template-config.hcl path: consul-template-config.hcl name: nginx-cert-renew-config name: config - configMap: items: - key: nginx_ssl.conf path: nginx_ssl.conf name: nginx-cert-renew-config name: nginx-config - emptyDir: {} name: shared-data - emptyDir: {} name: vault-token initContainers: - args: - agent - -config=/etc/vault/vault-agent-config.hcl - -log-level=debug env: - name: VAULT_ADDR value: http://vault:8200 image: vault imagePullPolicy: IfNotPresent name: vault-agent volumeMounts: - mountPath: /etc/vault name: config - name: vault-token mountPath: /home/vault containers: - image: nginx imagePullPolicy: IfNotPresent name: nginx-container ports: - containerPort: 80 volumeMounts: - mountPath: /etc/nginx/ssl/ name: shared-data - mountPath: /etc/nginx/conf.d/ name: nginx-config - name: consul-template image: hashicorp/consul-template:alpine imagePullPolicy: IfNotPresent securityContext: runAsUser: 0 capabilities: add: - SYS_PTRACE volumeMounts: - name: vault-token mountPath: /home/vault - name: config mountPath: /etc/consul-template - name: shared-data mountPath: /etc/secrets env: - name: HOME value: /home/vault - name: VAULT_ADDR value: http://vault:8200 args: ["-config=/etc/consul-template/consul-template-config.hcl"]
Тут стоит обратить внимание на:
shareProcessNamespace: true
Эта конструкция позволяет иметь процессам всех контейнеров общий namespace, чтобы можно было управлять процессом nginx из контейнера consul-template.
Также важно, чтобы у контейнера consul-template был такой securityContext:
securityContext: runAsUser: 0 capabilities: add: - SYS_PTRACE
Это значит, что процессы этого контейнера будут исполняться с id=0 (то есть иметь права root с точки зрения всех процессов pod'а) и у процессов этого контейнера будут права посылать сигналы в другеи контейнеры (SYS_PTRACE). Подробнее тут - https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace/
Discussion