vault-ops/infra/versions/03_issue_vault_server_cert-v1.0.sh
2026-04-14 11:45:15 +07:00

176 lines
7.4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -Eeuo pipefail
# 03_issue_vault_server_cert.sh (portable)
#
# - Liest env-spezifische Pfade/Owner aus YAML (mit smarten Fallbacks)
# - Alles via Flags überschreibbar
# - Schreibt cert/key/fullchain in TLSDIR, ca_chain nach server_chain_path
#
# Beispiele:
# VAULT_TOKEN=hvs.XXX ./03_issue_vault_server_cert.sh \
# --env test --config ./config/apps.yaml \
# --cn vault.test.privsec.ch --dns "vault.test.privsec.ch,localhost" \
# --ips "127.0.0.1,::1" --ttl 720h
#
# # Ad-hoc andere User/Dirs (ohne config ändern):
# VAULT_TOKEN=hvs.XXX ./03_issue_vault_server_cert.sh \
# --env prod --config ./config/apps.yaml --cn vault.prod.privsec.ch \
# --vault-user vaultprd --tls-dir /home/vaultprd/tls-prod \
# --offline-root /home/vaultprd/offline-root/prod
if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= G= Y= E=; fi
ok(){ echo -e "🟩 ${G}${B}$*${R}"; }
info(){ echo -e "🟨 ${Y}${B}$*${R}"; }
die(){ echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }
: "${VAULT_TOKEN:?Set VAULT_TOKEN}"
# ---- Defaults ----
CFG="./config/apps.yaml"
ENV_NAME="test"
CN="vault.test.privsec.ch"
DNS="" # wird zu "<CN>,localhost" wenn leer
IPS="127.0.0.1,::1"
TTL="720h"
ROOT_DIR_OVERRIDE=""
TLS_DIR_OVERRIDE=""
VAULT_USER_OVERRIDE=""
VAULT_GROUP_OVERRIDE=""
SERVER_CHAIN_OVERRIDE=""
DRY_RUN=0
# ---- Args ----
while [[ $# -gt 0 ]]; do
case "$1" in
--config) CFG="$2"; shift 2;;
--env) ENV_NAME="$2"; shift 2;;
--cn) CN="$2"; shift 2;;
--dns) DNS="$2"; shift 2;;
--ips) IPS="$2"; shift 2;;
--ttl) TTL="$2"; shift 2;;
--root-dir|--offline-root) ROOT_DIR_OVERRIDE="$2"; shift 2;;
--tls-dir) TLS_DIR_OVERRIDE="$2"; shift 2;;
--vault-user) VAULT_USER_OVERRIDE="$2"; shift 2;;
--vault-group) VAULT_GROUP_OVERRIDE="$2"; shift 2;;
--server-chain-path) SERVER_CHAIN_OVERRIDE="$2"; shift 2;;
--dry-run) DRY_RUN=1; shift;;
-h|--help) sed -n '1,200p' "$0"; exit 0;;
*) die "Unknown arg: $1";;
esac
done
[[ -n "$DNS" ]] || DNS="${CN},localhost"
need(){ command -v "$1" >/dev/null || die "missing: $1"; }
need vault; need jq; need python3; need sudo; need install
# PyYAML vorhanden?
python3 - <<'PY' >/dev/null 2>&1 || die "python3-yaml (PyYAML) nicht installiert"
import yaml
PY
# Config laden
CFG_ABS="$(readlink -f "$CFG")" || die "cannot readlink $CFG"
CFGJSON="$(python3 - <<PY
import yaml, json
print(json.dumps(yaml.safe_load(open("$CFG_ABS","r",encoding="utf-8"))))
PY
)"
jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }
VAULT_ADDR="$(jqenv '.vault_addr')"; [[ "$VAULT_ADDR" != "null" ]] || die "missing environments.$ENV_NAME.vault_addr"
INT_MOUNT="$(jqenv '.pki_mount')"; [[ "$INT_MOUNT" != "null" ]] || die "missing environments.$ENV_NAME.pki_mount"
VAULT_SNI_CFG="$(jqenv '.vault_sni')"; [[ "$VAULT_SNI_CFG" == "null" ]] && VAULT_SNI_CFG=""
ENV_TLS_DIR="$(jqenv '.tls_dir')"; [[ "$ENV_TLS_DIR" == "null" ]] && ENV_TLS_DIR=""
ENV_SERVER_CHAIN_PATH="$(jqenv '.server_chain_path')"; [[ "$ENV_SERVER_CHAIN_PATH" == "null" ]] && ENV_SERVER_CHAIN_PATH=""
ENV_VAULT_USER="$(jqenv '.vault_user')"; [[ "$ENV_VAULT_USER" == "null" ]] && ENV_VAULT_USER=""
ENV_VAULT_GROUP="$(jqenv '.vault_group')";[[ "$ENV_VAULT_GROUP" == "null" ]] && ENV_VAULT_GROUP=""
ENV_OFFLINE_ROOT_DIR="$(jqenv '.offline_root_dir')"; [[ "$ENV_OFFLINE_ROOT_DIR" == "null" ]] && ENV_OFFLINE_ROOT_DIR=""
export VAULT_ADDR
# Wichtig: Vault CLI nutzt VAULT_TLS_SERVER_NAME für SNI/Servername
[[ -n "$VAULT_SNI_CFG" ]] && export VAULT_TLS_SERVER_NAME="$VAULT_SNI_CFG"
# Owner/Group bestimmen
VAULT_OS_USER="${VAULT_USER_OVERRIDE:-${ENV_VAULT_USER:-vault}}"
VAULT_OS_GROUP="${VAULT_GROUP_OVERRIDE:-${ENV_VAULT_GROUP:-$VAULT_OS_USER}}"
# User muss existieren
id "$VAULT_OS_USER" >/dev/null 2>&1 || die "unix user '$VAULT_OS_USER' not found"
# TLS-Zielverzeichnis bestimmen (Prio: Flag > tls_dir > dirname(server_chain_path) > /home/<user>/tls-<env> > /home/vault/tls-<env>)
if [[ -n "$TLS_DIR_OVERRIDE" ]]; then
TLSDIR="$TLS_DIR_OVERRIDE"
elif [[ -n "$ENV_TLS_DIR" ]]; then
TLSDIR="$ENV_TLS_DIR"
elif [[ -n "$ENV_SERVER_CHAIN_PATH" ]]; then
TLSDIR="$(dirname "$ENV_SERVER_CHAIN_PATH")"
elif [[ -n "$VAULT_OS_USER" ]]; then
TLSDIR="/home/${VAULT_OS_USER}/tls-${ENV_NAME}"
else
TLSDIR="/home/vault/tls-${ENV_NAME}"
fi
# server_chain_path bestimmen (Prio: Flag > config > TLSDIR/ca_chain.pem)
SERVER_CHAIN_PATH="${SERVER_CHAIN_OVERRIDE:-${ENV_SERVER_CHAIN_PATH:-${TLSDIR}/ca_chain.pem}}"
# Offline-Root bestimmen (Prio: Flag > config > /home/<user>/offline-root/<env> > Fallback ~/vault/offline-root/<env> wenn vorhanden)
ME_HOME="$(cd ~ && pwd)"
ROOT_DIR="${ROOT_DIR_OVERRIDE:-${ENV_OFFLINE_ROOT_DIR:-/home/${VAULT_OS_USER}/offline-root/${ENV_NAME}}}"
if [[ ! -d "$ROOT_DIR" && -d "${ME_HOME}/vault/offline-root/${ENV_NAME}" ]]; then
ROOT_DIR="${ME_HOME}/vault/offline-root/${ENV_NAME}"
fi
ROOT_CRT="${ROOT_DIR}/root-ca.pem"
info "Effective config: env=$ENV_NAME, owner=${VAULT_OS_USER}:${VAULT_OS_GROUP}"
info " VAULT_ADDR=$VAULT_ADDR VAULT_TLS_SERVER_NAME=${VAULT_TLS_SERVER_NAME:-<none>} INT_MOUNT=$INT_MOUNT"
info " TLSDIR=$TLSDIR"
info " server_chain_path=$SERVER_CHAIN_PATH"
info " offline_root_dir=$ROOT_DIR"
[[ $DRY_RUN -eq 1 ]] && { info "--dry-run: exit vor Ausgabe"; exit 0; }
[[ -s "$ROOT_CRT" ]] || die "root-ca.pem not found at $ROOT_CRT (Flag --offline-root setzen oder environments.$ENV_NAME.offline_root_dir)"
# Verzeichnisse anlegen
sudo install -d -m 0755 -o "$VAULT_OS_USER" -g "$VAULT_OS_GROUP" "$TLSDIR"
sudo install -d -m 0755 -o "$VAULT_OS_USER" -g "$VAULT_OS_GROUP" "$(dirname "$SERVER_CHAIN_PATH")"
# PKI-Role sicherstellen (Domain aus CN ableiten)
BASE="$(echo "$CN" | sed 's/^[^.]*\.//')" # z.B. vault.test.privsec.ch -> test.privsec.ch
vault write "$INT_MOUNT/roles/vault-server" \
key_type="ec" allow_ip_sans=true \
allowed_domains="$BASE" allow_subdomains=true allow_bare_domains=true \
server_flag=true client_flag=false max_ttl="2160h" >/dev/null 2>&1 || true
# Zertifikat ausstellen
ARGS=( "common_name=${CN}" "ttl=${TTL}" )
[[ -n "$DNS" ]] && ARGS+=( "alt_names=${DNS}" )
[[ -n "$IPS" ]] && ARGS+=( "ip_sans=${IPS}" )
RESP="$(vault write -format=json "$INT_MOUNT/issue/vault-server" "${ARGS[@]}")" \
|| die "vault issue failed"
CERT="$(echo "$RESP" | jq -r '.data.certificate')"
KEY="$(echo "$RESP" | jq -r '.data.private_key')"
[[ -n "$CERT" && -n "$KEY" ]] || die "issue response missing certificate/private_key"
# Intermediate-CA holen
INT_CRT="$(vault read -field=certificate "$INT_MOUNT/cert/ca")"
[[ -n "$INT_CRT" ]] || die "failed to fetch intermediate from $INT_MOUNT/cert/ca"
# Dateien schreiben
echo "$KEY" | sudo install -D -m 0600 -o "$VAULT_OS_USER" -g "$VAULT_OS_GROUP" /dev/stdin "$TLSDIR/server.key"
echo "$CERT" | sudo install -D -m 0644 -o "$VAULT_OS_USER" -g "$VAULT_OS_GROUP" /dev/stdin "$TLSDIR/server.crt"
{ echo "$CERT"; echo "$INT_CRT"; } \
| sudo install -D -m 0644 -o "$VAULT_OS_USER" -g "$VAULT_OS_GROUP" /dev/stdin "$TLSDIR/fullchain.crt"
{ echo "$INT_CRT"; cat "$ROOT_CRT"; } \
| sudo install -D -m 0644 -o "$VAULT_OS_USER" -g "$VAULT_OS_GROUP" /dev/stdin "$SERVER_CHAIN_PATH"
ok "Wrote:"
ok " - $TLSDIR/server.key (0600)"
ok " - $TLSDIR/server.crt (0644)"
ok " - $TLSDIR/fullchain.crt (server + intermediate)"
ok " - $SERVER_CHAIN_PATH (intermediate + root)"