Subsections of Hashicorp Vault

HashiCorp Vault mit Client Config

Hinweis

Tokens und Zugangsdaten sind unbedingt sicher aufzubewahren. Verliert man diese, kann die Vault unbrauchbar werden und die Daten verloren gehen!

Konfiguration

Docker

Zuerst legen wir die Ordnerstruktur an:

mkdir -p /srv/docker/vault/config
mkdir -p /srv/docker/vault/file
mkdir -p /srv/docker/vault/logs

Danach benötigen wir typischerweise ein docker-compose.yml

version: "3"
services:
  vault:
    image: hashicorp/vault
    container_name: vault
    volumes:
      - /srv/docker/vault/config:/vault/config
      - /srv/docker/vault/file:/vault/file
      - /srv/docker/vault/logs:/vault/logs
    cap_add:
      - IPC_LOCK
    restart: unless-stopped
    networks:
      default:
        ipv4_address: 172.16.3.2
    command: server

networks:
  default:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.16.3.0/24
    driver_opts:
      com.docker.network.bridge.name: br_vault

Und dazu noch die Vault Config:

ui            = true
api_addr      = "http://0.0.0.0:8200"
disable_mlock = true

storage "file" {
  path = "/vault/file"
}

listener "tcp" {
  address       = "0.0.0.0:8200"
  tls_disable = 1
}

Danach kann der Container gestartet werden:

docker-compose up -d

Vault Ersteinrichtung mit CLI

Wenn die Vault gestartet wurde und erreichbar ist, muss die Einrichtung einmalig durchgeführt werden.
Hierzu kann man den CLI Client nutzen und die URL des Containers temporär setzen.

Schlüsselmenge

Mit -key-shares= gibt man die Menge der Schlüssel an die man erzeugen lassen möchte und mit -key-threshold= wie viele benötigt werden um beim Neustart die Vault zu öffnen. Beispiel: 10 Nutzer, von denen mindestens 3 benötigt werden um die Vault zu öffnen: -key-shares=10 -key-threshold=3

Sind nicht genügend Schlüssel zum öffnen verfügbar (können über den Browser eingegeben werden), bleibt die Vault dicht!

Schlüsselmenge

Ich kann es nicht oft genug erwähnen: SCHLÜSSEL / TOKEN SICHERN

export VAULT_ADDR=http://172.16.3.2:8200
vault operator init -key-shares=1 -key-threshold=1

Unseal Key 1: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Initial Root Token: hvs.xxxxxxxxxxxxxxxxxxxxx

# MAN BEACHTE:
Vault does not store the generated root key. Without at least 1 keys to
reconstruct the root 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.

Nginx

Wie so häufig wird nginx als Proxy davor gestellt, hierfür ist keine große Konfiguration nötig und es kann eine einfache Proxy-Config genutzt werden:

server {

  ######################################
  # NGINX vhost listen, logs, ssl, ...
  ######################################

  # OPTIONALER Schutz um die Vault mit vorangehendem Basic Auth zu schützen
  #auth_basic              "Vault realm";
  #auth_basic_user_file    /etc/nginx/htpasswd/vault.htpasswd;

  location / {
      proxy_pass                              http://172.16.3.2:8200;
      proxy_set_header    X-Real-IP           $remote_addr; # pass on real client's IP
      proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
      proxy_set_header    X-Forwarded-Proto   $scheme;
      proxy_read_timeout  900;
  }

Danach kann die Nginx Config getestet und geladen werden:

nginx -t && nginx -s reload

Konfiguration Vault Adresse im Client

Nachdem die Vault über Nginx erreichbar gemacht wurde, kann die Adresse des Clients angepasst werden:

ohne Basic Auth
export VAULT_ADDR=https://vault.mydomain.com
lokal auf dem Docker Host

Falls man nicht über Nginx gehen möchte …

export VAULT_ADDR=http://172.16.3.2
Mit zusätzlichem Basic Auth
export VAULT_ADDR=https://USERNAME:PASSWORD@vault.mydomain.com

Login & Tests

Um die Anleitung nicht zu kompliziert werden zu lassen, sollen verschiedene Authentifizierungsverfahren hier nicht beleuchtet werden.
Der Standardlogin mit gerade erzeugtem Root-Token funktioniert wie folgt:

vault login
Token (will be hidden): <HIER TOKEN (beginnt mit hvs.) EINGEBEN>
Vault Status prüfen

Die Vault ist aktuell noch verschlossen (Sealed)

vault status

Key                Value
---                -----
Initialized        true
Sealed             true
Total Shares       1
Threshold          1
Unseal Progress    0/1
Unseal Nonce       n/a
Vault öffnen

Öffnen der Vault mit X Schlüsseln

vault operator unseal
Unseal Key (will be hidden): <UNSEAL TOKEN EINGEBEN>

Key             Value
---             -----
Sealed          false

Let's Encrypt Zertifikate mit Vault synchronisieren

Hinweis

Die Scripts benötigen einen gültigen Vault Token (siehe Vorbereitung)! Außerdem muss ein KV-Secret Store eingerichtet sein. In diesem Beispiel lautet der Secret-Store certbot.

Vorbereitung

Installation Komponenten

Für das Script werden JQ und Vault (CLI Client) benötigt:

pacman -Syu vault jq

Offizielle Anleitung: https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-install

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install vault jq
Vault Login

Durch den Login wird der Token in die Datei /root/.vault-token geschrieben. Dieser wird von den Scripts und den weiteren Vorbereitungen benötigt.

export VAULT_ADDR=https://vault.domain.com
vault login
KV Store

Vault muss einen KV-Store bereitstellen, dieser lautet in den Scripts certbot. Ist ein anderer Namen gewünscht, müssen alle Pfade angepasst werden.

Nun wird der Secret Store angelegt und die Rotation von maximal 7 Versionen eingestellt, wodurch nur insgesamt 7 Versionen behalten werden und ältere automatisch gelöscht werden.

vault secrets enable -path=certbot kv
vault write certbot/config max_versions=7

Cert Download

Script zum downloaden der Zertifikate

Script auf dem Server anlegen /usr/local/sbin/update_cert.sh:

#!/bin/bash

# cancel if any error occurs
set -e

if [[ $# -ne 1 ]];then
    echo "usage: $0 DOMAIN"
    exit 1
fi
DOMAIN="${1}"
CERT_DIR="/etc/nginx/ssl/${DOMAIN}/"

export VAULT_ADDR

# load certificate, chain + key from vault
json=$(vault kv get -format=json certbot/certificates/${DOMAIN})

mkdir -p ${CERT_DIR}

# join cert + chain -> (fullchain)
jq -r '.data.data.cert + .data.data.chain' <(echo $json) > ${CERT_DIR}/fullchain.pem
jq -r '.data.data.privkey' <(echo $json) > ${CERT_DIR}/privkey.pem

# OPTIONAL: reload nginx if config is okay
#nginx -t && nginx -s reload

Berechtigung korrigieren, damit das Script ausführbar ist

chown -R root:root /usr/local/sbin/update_cert.sh
chmod 700 /usr/local/sbin/update_cert.sh

Cronjob / Systemd Timer

Unbedingt den Hostname vault.domain.com anpassen!

Cronjob
crontab -e
0 2 * * * VAULT_ADDR=https://vault.domain.com /usr/local/sbin/update_cert.sh cert.mydomain.com
1 2 * * * VAULT_ADDR=https://vault.domain.com /usr/local/sbin/update_cert.sh mydomain.com
Systemd Unit / Timer

Service Unit /etc/systemd/system/cert_update@.service

[Unit]
Description=Download Certbot files
Requires=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update_cert.sh %i
Environment="VAULT_ADDR=https://vault.domain.com"

Service Timer /etc/systemd/system/cert_update@.timer

[Unit]
Description=Daily renewal of cert

[Timer]
OnCalendar=01:00:00
Persistent=true
RandomizedDelaySec=60

[Install]
WantedBy=timers.target

Danach kann der Timer für jedes Zertifikat aktiviert werden:

systemctl daemon-reload
systemctl enable --now cert_update@my-domain.com.timer
systemctl enable --now cert_update@cert2.my-domain.com.timer

Cert Upload

Script zum uploaden der Zertifikate

Das Script kann mehrere Zertifikate mit einem Aufruf verarbeiten, diese müssen lediglich als Parameter übergeben werden.
In diesem Beispiel wird allerdings für jedes Zertifikat ein eigener Cronjob / Systemd-Timer angelegt, geht aber auch anders …

Das Script unter /usr/local/sbin/push_certs_to_vault.sh anlegen:

#!/bin/bash

export VAULT_ADDR
# script supports multiple domains via parameter  
DOMAINS="$*"

for domain in ${DOMAINS};do
    vault kv put "certbot/certificates/${domain}" "cert=@/etc/letsencrypt/live/${domain}/cert.pem" "chain=@/etc/letsencrypt/live/${domain}/chain.pem" "privkey=@/etc/letsencrypt/live/${domain}/privkey.pem"
done

Berechtigung korrigieren, damit das Scripts ausführbar ist:

chown -R root:root /usr/local/sbin/push_certs_to_vault.sh
chmod 700 /usr/local/sbin/push_certs_to_vault.sh
Cronjob

Unbedingt den Hostname vault.domain.com anpassen!

crontab -e
0 2 * * * VAULT_ADDR=https://vault.domain.com /usr/local/sbin/update_cert.sh cert.mydomain.com
1 2 * * * VAULT_ADDR=https://vault.domain.com /usr/local/sbin/update_cert.sh mydomain.com
Systemd Unit / Timer

Service Unit /etc/systemd/system/push_cert@.service

[Unit]
Description=Upload Certbot files
Requires=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/push_certs_to_vault.sh %i
Environment="VAULT_ADDR=https://vault.domain.com"

Service Timer /etc/systemd/system/push_cert@.timer

[Unit]
Description=Daily upload of certs

[Timer]
OnCalendar=00:00:00
Persistent=true
RandomizedDelaySec=30

[Install]
WantedBy=timers.target

Danach kann der Timer für jedes Zertifikat aktiviert werden:

systemctl daemon-reload
systemctl enable --now push_cert@my-domain.com.timer
systemctl enable --now push_cert@cert2.my-domain.com.timer

Anmerkung

Wieso das Script nicht als Certbot Hook eingebunden wird

Während des Cert Renewals muss die Vault verfügbar sein. Ist dies nicht gegeben, würde der Upload nicht funktionieren und auch nicht nachgeholt.
Deshalb habe ich mich dazu entschieden diese einfach täglich hochzuladen.
In Vault werden inerhalb der Secret Engine rotierte Inhalte gelöscht, sodass der tägliche Upload nicht problematisch ist und man Gefahr läuft die Vault vollzumüllen.

Wenn die Vault zusätzlich mit Basic Auth geschützt ist

Wie in der Anleitung zur Einrichtung beschrieben, muss hier nur die URL ergänzt werden.
Aus VAULT_ADDR=https://vault.domain.com wird dann VAULT_ADDR=https://USERNAME:PASSWORD@vault.domain.com.