О чем это

Тут некоторе заметки по поводу использования 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

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
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

Включим секреты 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
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"

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.

Конфигмап содержит три конфига:

  • для 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'а с тремя контейнерами:

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/

Enter your comment. Wiki syntax is allowed:
 
  • devops/hashicorp_vault_workflow.txt
  • Last modified: 2020/08/13 19:48
  • by admin