vault-ops/infra/archiv/setup-vault-agent-mtls-client-config2.sh
2025-10-06 07:25:33 +02:00

162 lines
5.9 KiB
Bash
Executable file

#!/usr/bin/env bash
set -Eeuo pipefail
#
# setup-vault-agent-mtls-client-config2.sh
#
# Zweck:
# Ein *Client-mTLS*-Zertifikat aus Vault ausstellen und NUR nach
# /home/<user>/vault/mtls/{agent.key, agent.crt, ca.crt} schreiben.
# KEIN Agent, KEIN systemd.
#
# Liest standardmäßig ./config/apps.yaml (felder: environments.<env>.pki_mount, apps[].{name,user,internal_cn,issue_ttl})
# lässt sich durch Flags überschreiben.
#
# Usage:
# sudo -E ./setup-vault-agent-mtls-client-config2.sh \
# --env test --app nctest [--config ./config/apps.yaml] \
# [--user nctest] [--cn nctest.int.privsec.ch] [--ttl 24h] \
# [--role client-nctest] [--pki pki-test] [--outdir /home/nctest/vault/mtls] \
# [--ensure-role]
#
# Auth:
# Nutzt VAULT_TOKEN, oder (falls leer) VAULT_ADMIN_TOKEN. VAULT_ADDR muss gesetzt sein.
# ---------- pretty logs ----------
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}"; }
warn(){ echo -e "🟨 ${Y}${B}$*${R}"; }
die(){ echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }
need(){ command -v "$1" >/dev/null 2>&1 || die "missing binary: $1"; }
need vault; need jq; need openssl
# ---------- args ----------
CFG="./config/apps.yaml"; ENV_NAME="test"; APPN=""
OVR_USER=""; OVR_CN=""; OVR_TTL=""; OVR_ROLE=""; OVR_PKI=""; OUTDIR=""
ENSURE_ROLE=0
while [[ $# -gt 0 ]]; do
case "$1" in
--config) CFG="$2"; shift 2;;
--env) ENV_NAME="$2"; shift 2;;
--app) APPN="$2"; shift 2;;
--user) OVR_USER="$2"; shift 2;;
--cn) OVR_CN="$2"; shift 2;;
--ttl) OVR_TTL="$2"; shift 2;;
--role) OVR_ROLE="$2"; shift 2;;
--pki) OVR_PKI="$2"; shift 2;;
--outdir) OUTDIR="$2"; shift 2;;
--ensure-role) ENSURE_ROLE=1; shift;;
-h|--help) sed -n '1,200p' "$0"; exit 0;;
*) die "unknown arg: $1";;
esac
done
[[ -n "$APPN" ]] || die "--app ist erforderlich"
# ---------- auth/env ----------
if [[ -z "${VAULT_TOKEN:-}" && -n "${VAULT_ADMIN_TOKEN:-}" ]]; then
export VAULT_TOKEN="$VAULT_ADMIN_TOKEN"
fi
: "${VAULT_TOKEN:?Setze VAULT_TOKEN oder VAULT_ADMIN_TOKEN}"
: "${VAULT_ADDR:?Setze VAULT_ADDR (z.B. https://127.0.0.1:22300)}"
# ---------- config laden (falls vorhanden) ----------
PKI_MOUNT=""; APP_USER=""; CN=""; TTL=""
if [[ -r "$CFG" ]]; then
CFG_ABS="$(readlink -f "$CFG")"
CFGJSON="$(python3 - <<PY
import yaml, json; print(json.dumps(yaml.safe_load(open("$CFG_ABS","r",encoding="utf-8"))))
PY
)" || die "YAML parse failed: $CFG"
jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }
jqapp(){ echo "$CFGJSON" | jq -r ".apps[] | select(.name==\"$APPN\")$1"; }
PKI_MOUNT="$(jqenv '.pki_mount')"; [[ "$PKI_MOUNT" == "null" ]] && PKI_MOUNT=""
APP_USER="$(jqapp '.user')"; [[ "$APP_USER" == "null" ]] && APP_USER=""
CN="$(jqapp '.internal_cn')"; [[ "$CN" == "null" ]] && CN=""
TTL="$(jqapp '.issue_ttl')"; [[ "$TTL" == "null" ]] && TTL=""
fi
# ---------- overrides & defaults ----------
[[ -n "$OVR_PKI" ]] && PKI_MOUNT="$OVR_PKI"
[[ -n "$OVR_USER" ]] && APP_USER="$OVR_USER"
[[ -n "$OVR_CN" ]] && CN="$OVR_CN"
[[ -n "$OVR_TTL" ]] && TTL="$OVR_TTL"
[[ -n "$PKI_MOUNT" ]] || die "pki_mount unbekannt (Flag --pki oder in apps.yaml)"
[[ -n "$APP_USER" ]] || die "user unbekannt (Flag --user oder in apps.yaml)"
[[ -n "$CN" ]] || CN="${APPN}-agent"
[[ -n "$TTL" ]] || TTL="720h"
ROLE="${OVR_ROLE:-client-${APPN}}"
HOME_DIR="/home/${APP_USER}"
OUTDIR="${OUTDIR:-${HOME_DIR}/vault/mtls}"
# ---------- user/outdir ----------
id -u "${APP_USER}" >/dev/null 2>&1 || die "Linux-User fehlt: ${APP_USER}"
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${OUTDIR}"
ok "Vault=${VAULT_ADDR} PKI=${PKI_MOUNT} Role=${ROLE}"
ok "User=${APP_USER} CN=${CN} TTL=${TTL}"
ok "Out=${OUTDIR}"
# ---------- optional: role anlegen/aktualisieren ----------
if [[ $ENSURE_ROLE -eq 1 ]]; then
warn "ENSURE_ROLE aktiv → upsert PKI-Role ${ROLE}"
vault write "${PKI_MOUNT}/roles/${ROLE}" \
allow_any_name=true server_flag=false client_flag=true \
key_type=ec key_bits=256 max_ttl=720h >/dev/null
fi
# ---------- zertifikat anfordern ----------
RESP_JSON="$(mktemp)"
cleanup(){ rm -f "$RESP_JSON"; }
trap cleanup EXIT
set +e
vault write -format=json "${PKI_MOUNT}/issue/${ROLE}" \
"common_name=${CN}" "ttl=${TTL}" >"$RESP_JSON" 2>&1
RC=$?
set -e
if [[ $RC -ne 0 ]]; then
echo "Vault issue call failed:" >&2
cat "$RESP_JSON" >&2
exit 1
fi
# Falls Vault Fehler-JSON in STDERR schrieb, steht jetzt trotzdem im File.
# Versuchen wir, gültiges JSON zu parsen und Daten zu extrahieren:
if ! jq -e . >/dev/null 2>&1 <"$RESP_JSON"; then
echo "Kein gültiges JSON vom Issue-Endpoint:" >&2
cat "$RESP_JSON" >&2
exit 1
fi
CERT="$(jq -r '.data.certificate // ""' "$RESP_JSON")"
KEY="$(jq -r '.data.private_key // ""' "$RESP_JSON")"
ISS_CA="$(jq -r '(.data.issuing_ca // (.data.ca_chain[0] // ""))' "$RESP_JSON")"
[[ -n "${CERT// }" ]] || die "leeres Zertifikat vom PKI-Endpoint"
[[ -n "${KEY// }" ]] || die "leerer Private Key vom PKI-Endpoint"
if [[ -z "${ISS_CA// }" ]]; then
# Fallback: Issuer direkt vom Mount lesen
ISS_CA="$(vault read -field=certificate "${PKI_MOUNT}/cert/ca" 2>/dev/null || true)"
[[ -n "${ISS_CA// }" ]] || die "keine Issuing CA im Response und am Mount"
fi
# ---------- schreiben mit owner/mode ----------
printf '%s\n' "$KEY" | sudo install -m 0600 -o "${APP_USER}" -g "${APP_USER}" /dev/stdin "${OUTDIR}/agent.key"
printf '%s\n' "$CERT" | sudo install -m 0644 -o "${APP_USER}" -g "${APP_USER}" /dev/stdin "${OUTDIR}/agent.crt"
printf '%s\n' "$ISS_CA"| sudo install -m 0644 -o "${APP_USER}" -g "${APP_USER}" /dev/stdin "${OUTDIR}/ca.crt"
ok "geschrieben:"
echo " - ${OUTDIR}/agent.key (0600)"
echo " - ${OUTDIR}/agent.crt (0644)"
echo " - ${OUTDIR}/ca.crt (0644)"
# ---------- mini-check ----------
openssl x509 -in "${OUTDIR}/agent.crt" -noout -subject -issuer -enddate | sed 's/^/ 🔎 /'