Vault HA + Consul HA + k8s

В данной статье будет показан процесс установки HashiCorp Vault с связке с Consul, который выступает хранилищем для Vault, в HA режиме с включенным tls шифрованием в Kubernetes кластер.
Исходные данные:
— Kubernetes кластер (например yandex)
— В кластере установлен nginx ingress controller, в статье класс определен как nginx-internal
Для UI существуют TLS/SSL сертификаты.
— Установлен helm 3
— есть доступ к кластеру через kubectl с правами администратора.
— есть возможность запустить docker контейнер

Установка Consul

Скачать исходники чартов consul-k8s https://github.com/hashicorp/consul-k8s/tree/main/charts/consul

Либо через helm, но доступ из России сейчас запрещен:

$ helm repo add hashicorp https://helm.releases.hashicorp.com
$ helm pull hashicorp/consul

Создать namespace:

$ kubectl create namespace consul

Создать secret для ssl сертификатов для ingress контроллера.

Допустим у вас есть ssl сертификаты от https://letsencrypt.org/ :
Создать фаил secret-tls-consul.yaml :

apiVersion: v1
kind: Secret
metadata:
  name: secret-tls
data:
  tls.crt: 
  tls.key: 
type: kubernetes.io/tls

где:
вместо подставить заначение:
$ cat fullchain1.pem | base64 -w0
вместо подставить значение:
$ cat privkey1.pem | base64 -w0

Применить данный манифест:

$ kubectl -n consul apply -f secret-tls-consul.yaml

Создать фаил values-work.yaml где переопределить некоторые переменные:

global:
  enabled: true
  logLevel: "info"
  logJSON: true
  datacenter: vault-storage
  gossipEncryption:
    autoGenerate: false
    secretName: consul-consul-gossip-encryption-key
    secretKey: key
  tls:
    enabled: true
    enableAutoEncrypt: true
 
server:
  enabled: "-"
  replicas: 3
  storage: 5Gi
  storageClass: yc-network-ssd
  connect: true
 
client:
  enabled: false
 
connectInject:
  enabled: false
 
ui:
  enabled: "-"
  service:
    enabled: true
  ingress:
    enabled: true
 
    annotations: |
      "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS"
 
    ingressClassName: nginx-internal
    pathType: Prefix
 
    hosts:
    - host: consul.k8s.domain.com
 
    tls:
      - secretName: secret-tls
        hosts:
          - consul.k8s.domain.com

Основные моменты файла:
gossipEncryption : включаем шифрование, указываем имя секрета, где будет храниться ключ. Сам ключ создается так ( нужно сгенерировать):

$ docker run --rm -ti --entrypoint=/bin/sh hashicorp/consul:latest
/ # consul keygen
$ kubectl -n consul create secret generic \
     consul-consul-gossip-encryption-key --from-literal=key=

replicas: 3: количество реплик, должно быть достаточное количество нод в k8s либо включен автоскейлинг
storageClass: yc-network-ssd — один из классов в yandex. Острожно, у него RECLAIMPOLICY=Delete, это означает, что если удалить pvc, то диск, который будет создан, тоже будет удален.
enableAutoEncrypt: true: благодаря этой строчке будет создан самоподписной CA (секреты:  consul-consul-ca-cert,consul-consul-ca-key), а также сертифкат и ключ для самого consul (секрет: consul-consul-server-cert).

Можно попробовать запустить:

$ helm -n consul upgrade --install consul -f values-work.yaml ./consul-1.3.1

где ./consul-1.3.1 — папка с чартом consul

$ kubectl -n consul get pods,pvc
NAME                         READY   STATUS    RESTARTS   AGE
pod/consul-consul-server-0   1/1     Running   0          30m
pod/consul-consul-server-1   1/1     Running   0          30m
pod/consul-consul-server-2   1/1     Running   0          30m

NAME                                                       STATUS  
persistentvolumeclaim/data-consul-consul-consul-server-0   Bound   
persistentvolumeclaim/data-consul-consul-consul-server-1   Bound   
persistentvolumeclaim/data-consul-consul-consul-server-2   Bound

Самоподписные SSL:
При использовании опции enableAutoEncrypt: true Сертификат для consul создается сроком на два года, чтобы его обновлять автоматом необходимо периодически переустанавливать сам consul. К тому же выпускается от стороннего производителя, примерно такой:

Issuer: C = US, ST = CA, L = San Francisco, street = 101 Second Street, postalCode = 94105, O = HashiCorp Inc., CN = Consul Agent CA

Необходимо создать свой CA и свои сертификаты, заполнить по необходимости, или взять уже существующие:

$ openssl genrsa -out rootCA.key 4096
$ openssl req -x509 -new -key rootCA.key -days 10000 -out rootCA.crt
........
Country Name (2 letter code) [AU]:RU
State or Province Name (full name) [Some-State]:Moscow
Locality Name (eg, city) []:Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Domain LTD
Organizational Unit Name (eg, section) []:devops
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:devops@domain.com

Создать фаил consul.conf:

[req]
default_bits = 4096
prompt = no
default_md = sha256
 
req_extensions = v3_req
distinguished_name = dn
 
[dn]
C = RU
ST = Moscow
L = Moscow
CN = server.vault-storage.consul
 
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

Создать фаил consul.ext:

subjectAltName=DNS:consul-consul-server, DNS:*.consul-consul-server, DNS:*.consul-consul-server.consul, DNS:consul-consul-server.consul, DNS:*.consul-consul-server.consul.svc, DNS:consul-consul-server.consul.svc, DNS:*.server.vault-storage.consul, DNS:server.vault-storage.consul, DNS:localhost, IP:127.0.0.1

Список этих subjectAltName можно подсмотреть в сертификате, который был сгенерирован автоматически.

Сгенерировать ключ и выпустить/подписать сертифкат:

$ openssl genrsa -out consul.key 4096
 
$ openssl req -new \
    -key consul.key \
    -out consul.csr \
    -config consul.conf
 
$ openssl x509 -req \
    -in consul.csr \
    -CA rootCA.crt -CAkey rootCA.key \
    -CAcreateserial -out consul.crt \
    -extfile consul.ext \
    -days 5000

Создать секреты с этими ключом с сертификатами:

$ kubectl -n consul create secret generic root-ca --from-file=tls.crt=rootCA.crt
$ kubectl -n consul create secret tls consul --key consul.key --cert consul.crt

Поменять values-work.yaml для использования этих секретов:

global:
  enabled: true
  logLevel: "info"
  logJSON: true
  datacenter: vault-storage
  gossipEncryption:
    autoGenerate: false
    secretName: consul-consul-gossip-encryption-key
    secretKey: key
  tls:
    enabled: true
    enableAutoEncrypt: false
    caCert:
      # The name of the Kubernetes secret.
      secretName: root-ca
      # The key of the Kubernetes secret.
      secretKey: tls.crt
    verify: true
 
server:
  enabled: "-"
  replicas: 3
  storage: 5Gi
  storageClass: yc-network-ssd
  connect: true
  serverCert:
    # The name of the Vault secret that holds the PEM encoded server certificate.
    # @type: string
    secretName: consul
 
client:
  enabled: false
 
connectInject:
  enabled: false
 
ui:
  enabled: "-"
  service:
    enabled: true
  ingress:
    enabled: true
 
    annotations: |
      "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS"
 
    ingressClassName: nginx-internal
    pathType: Prefix
 
    hosts:
    - host: consul.k8s.domain.com
 
    tls:
      - secretName: secret-tls
        hosts:
          - consul.k8s.domain.com

Удалить ненужные секреты и запустить с новыми параметрами:

$ kubectl -n consul delete secrets consul-consul-ca-cert consul-consul-ca-key consul-consul-server-cert
$ helm -n consul upgrade --install consul -f values-work.yaml ./consul-1.3.1

Удалить все поды consul, так как из-за новых сертификатов, они не смогут взаимодействовать друг с другом:

$ kubectl -n consul delete pods --all

Закрыть токеном UI consul

Привести конфигурацию values-work.yaml к виду:

global:
  enabled: true
  logLevel: "info"
  logJSON: true
  datacenter: vault-storage
  gossipEncryption:
    autoGenerate: false
    secretName: consul-consul-gossip-encryption-key
    secretKey: key
  tls:
    enabled: true
    enableAutoEncrypt: false
    caCert:
      # The name of the Kubernetes secret.
      secretName: root-ca
      # The key of the Kubernetes secret.
      secretKey: tls.crt
    verify: true
 
server:
  enabled: "-"
  replicas: 3
  storage: 5Gi
  storageClass: yc-network-ssd
  connect: true
  serverCert:
    # The name of the Vault secret that holds the PEM encoded server certificate.
    # @type: string
    secretName: consul
 
  extraConfig: |
        {
          "addresses": {
            "http": "0.0.0.0"
          },
          "acl": {
            "enabled": true,
            "default_policy": "deny",
            "down_policy": "extend-cache",
            "enable_token_persistence": true
          }
        }
 
client:
  enabled: false
 
connectInject:
  enabled: false
 
ui:
  enabled: "-"
  service:
    enabled: true
  ingress:
    enabled: true
 
    annotations: |
      "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS"
 
    ingressClassName: nginx-internal
    pathType: Prefix
 
    hosts:
    - host: consul.k8s.domain.com
 
    tls:
      - secretName: secret-tls
        hosts:
          - consul.k8s.domain.com

Применить/перезапустить:

$ helm -n consul upgrade --install consul -f values-work.yaml ./consul-1.3.1 

Дождаться пока перезапустятся все поды, и будет выбран лидер — можно смотреть по логам:

$ kubectl -n consul logs consul-consul-server-2 -f

Запустить генерацию bootstrap токена:

$ kubectl -n consul exec -ti consul-consul-server-0 -- sh -c "consul acl bootstrap | grep -i secretid"
Defaulted container "consul" out of: consul, locality-init (init)
SecretID:         

Токен (SecretID) необходимо сохранить в безопасном месте.

Через web интерфейс проверить что все работает: авторизоваться используя полученный токен.

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

$ kubectl -n consul exec -ti consul-consul-server-0 -- \
            cat /consul/data/serf/local.keyring
[""]

Ключ должен находиться в списке всех этих файлов (в каждом поде). Это тот самый ключ из секрета consul-consul-gossip-encryption-key, здесь он будет указан без base64 кодировки.

Создать политику и токен для Vault:

Создание Политики

Создание Политики

Создание Токена в связке с Политикой

Создание Токена в связке с Политикой

Токен понадобится далее.

Установка Vault

Создать tls/ssl сертификаты

Создать файлы:
vault.conf:

[req]
default_bits = 4096
prompt = no
default_md = sha256
 
req_extensions = v3_req
distinguished_name = dn
 
[dn]
C = RU
ST = Russia
L = Moscow
CN = vault-server-tls.vault.svc
 
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

vault.ext:

subjectAltName=DNS:vault,DNS:localhost,DNS:vault-0.vault-internal,DNS:vault-1.vault-internal,DNS:vault-2.vault-internal,IP:127.0.0.1

Создать ключ и сертифкат:

$ openssl genrsa -out vault.key 4096
 
$ openssl req -new \
    -key vault.key \
    -out vault.csr \
    -config vault.conf
 
$ openssl x509 -req \
    -in vault.csr \
    -CA rootCA.crt -CAkey rootCA.key \
    -CAcreateserial -out vault.crt \
    -extfile vault.ext \
    -days 5000

Где rootCA.key и rootCA.crt — ключ и сертифкат CA созданные при установке consul

Создать secret-ы из сертификатов:

$ kubectl create ns vault
$ kubectl -n vault create secret tls vault --key vault.key --cert vault.crt
$ kubectl -n vault create secret generic root-ca --from-file=tls.crt=rootCA.crt

Скачать с github исходники helm для Vault https://github.com/hashicorp/vault-helm.git

Создать secret для ssl сертификатов для ingress контроллера.

Допустим у вас есть ssl сертификаты от https://letsencrypt.org/ :
Создать фаил secret-tls-vault.yaml :

apiVersion: v1
kind: Secret
metadata:
  name: secret-tls
data:
  tls.crt: 
  tls.key: 
type: kubernetes.io/tls

где:
вместо подставить заначение:
$ cat fullchain1.pem | base64 -w0
вместо подставить значение:
$ cat privkey1.pem | base64 -w0

Применить данный манифест:

$ kubectl -n vault apply -f secret-tls-vault.yaml

Создать фаил values-work.yaml,  где переопределить некоторые значения:

global:
  tlsDisable: false
 
server:
  standalone:
    enabled: false
  dataStorage:
    enabled: false
  extraEnvironmentVars:
    VAULT_CACERT: /vault/userconfig/root-ca/tls.crt
  extraVolumes:
    - type: secret
      name: root-ca
    - type: secret
      name: vault
  ha:
    enabled: true
    replicas: 3
    config: |
      ui = true
      listener "tcp" {
        tls_disable = 0
        address = "[::]:8200"
        cluster_address = "[::]:8201"
        tls_cert_file = "/vault/userconfig/vault/tls.crt"
        tls_key_file = "/vault/userconfig/vault/tls.key"
      }
      storage "consul" {
        path = "vault"
        address = "consul-consul-server.consul.svc:8501"
        scheme = "https"
        tls_ca_file = "/vault/userconfig/root-ca/tls.crt"
        token = ""
      }
      service_registration "kubernetes" {}
 
  ingress:
    enabled: yes
    annotations:
      nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
 
    ingressClassName: nginx-internal
 
    hosts:
      - host: vault.k8s.domain.com
    tls:
      - secretName: secret-tls
        hosts:
          - vault.k8s.domain.com
 
injector:
  enabled: false
 
ui:
  enabled: false

Основные моменты файла:

tls_disable = 0: включение шифрования трафика
token = "": токен, который был создан в Consul для Vault.
address = "consul-consul-server.consul.svc:8501": имя consul-consul-server.consul.svc должно быть прописано в ssl сертификате для consul.

Установить:

$ helm upgrade -n vault --install vault -f values-work.yaml ./vault-0.27.0

Запустить инициализацию, подставить свои значения для количества ключей и количества для ключей для unseal:

$ kubectl -n vault exec -ti vault-0 -- /bin/sh
/ $ vault operator init -key-shares=3 -key-threshold=2

Сохранить ключи и токен root в надежном месте!

Распечатать каждый под Vault необходимое количество раз (значение -key-threshold):

$ kubectl -n vault exec -ti vault-0 -- /bin/sh
/ $ vault operator unseal
Unseal Key (will be hidden):
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       3
Threshold          2
Unseal Progress    1/2
Unseal Nonce       cb2c64c9-ca57-0c98-2141-e99235f05853
Version            1.15.2
Build Date         2023-11-06T11:33:28Z
Storage Type       consul
HA Enabled         true

Успешным итогом будет такой вывод для каждого пода:

$ kubectl -n vault exec -ti vault-0 -- vault status
Key                      Value
---                      -----
Recovery Seal Type       shamir
Initialized              true
Sealed                   false
Total Recovery Shares    3
Threshold                2
Version                  1.15.2
Build Date               2023-11-06T11:33:28Z
Storage Type             consul
Cluster Name             vault-cluster-51496cc6
Cluster ID               b77b3b96-9bfa-11a9-21b9-bcdc331bfe00
HA Enabled               true

Проверить доступность через UI

Можно пользоваться!

Для настройки автоматического Unseal здесь на Хабре есть несколько замечательных статей. Повторять их нет необходимости.

© Habrahabr.ru