vault-ops/infra/archiv/setup-vault-agent-app-config-v4.5.sh
2025-10-06 07:25:33 +02:00

265 lines
10 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -Eeuo pipefail
# setup-vault-agent-app-config-v4.5.sh
# Datum: 2025-09-30
# ========================= FEATURE MANIFEST (v4.5) =========================
# 🔒 Authentisierung / Sicherheit
# 1) Strict mTLS-Login (DEFAULT): Wenn --auth cert gesetzt (oder Default) und
# ~/vault/mtls/agent.{crt,key} fehlt → HARTE ABBRUCH (Exit 5). Kein Fallback.
# 2) Optional AppRole NUR bei --auth approle (erzeugt/liest role_id & secret_id).
# 3) Agent-Token wird sicher in Datei-Sink (0400) geschrieben.
# 4) Robuste CN-Prüfung (STRICT, Default aktiv): CN aus Client-Zert wird
# korrekt extrahiert und gegen Regex geprüft; bei Mismatch Abbruch.
#
# 📦 Konfiguration / YAML
# 5) Liest ./config/apps.yaml:
# environments.{vault_addr, kv_mount, pki_mount,
# proxy{user,listen_port,chain_path,reload},
# app{user,sidecar_host_port,sidecar_container}}
# apps[].{name, user?, internal_cn, external_host_{test,prod}, issue_ttl,
# kv_subpath?, seed?, sidecar_container?, proxy_user?, proxy_chain_path?, proxy_reload?}
#
# 🛠️ PKI / Policies
# 6) PKI-Role Upsert (EC P-256, max_ttl=720h), allowed_domains aus CN & externem Host.
# 7) Policies minimal: pki-issue-<app> (issue, renew-self), pki-bootstrap-<app> (AppRole bootstrap).
# 8) Environment-spezifische Policy-Erweiterung:
# • test: pki-issue-<app> erlaubt zusätzlich token/lookup-self (Debugging)
# • prod: nur token/renew-self (Minimal-Prinzip)
#
# 🧠 Agent / Templates / Pfade
# 9) Generiert Agent-HCL + Template (cert.tpl); rendert .issue.json.
# 10) Post-Hook (vault-agent-post-leaf.sh) schreibt <HOME>/tls/<app>.{key,fullchain.pem}.
# 11) Verzeichnisstruktur:
# ~/.vault-agent-<app>/{vault-agent.hcl,cert.tpl,.issue.json,token,bin/,pidfile}
# ~/tls/, ~/vault/ca/ca.pem, ~/vault/mtls/{agent.crt,agent.key}
#
# 🔁 Reload / Sidecar / Proxy
# 12) Übergibt an Post-Hook via Env:
# PROXY_RELOAD, PROXY_CHAIN_PATH, PROXY_USER, PROXY_LISTEN_PORT,
# SIDECAR_HOST_PORT, SIDECAR_CONTAINER, RELOAD_TLS_LABEL.
#
# 🔑 KV-Seed (einmalig)
# 13) Falls kv_subpath + seed vorhanden → prüft IMMER, ob Secret existiert
# 14) NEU (v4.5): Flag --with-kv-seed:
# • Wenn Secret fehlt UND Flag gesetzt → einmaliges vault kv put
# • Wenn Secret fehlt ohne Flag → nur Log (kein Seed)
# • Niemals überschreiben
#
# ⚙️ Systemd (user)
# 15) Erstellt ~/.config/systemd/user/vault-agent-<app>.service, aktiviert (--user), Linger an.
#
# 🌐 TLS zu Vault
# 16) Nutzt ca_cert=~/vault/ca/ca.pem + tls_server_name (Override via --tls-server-name).
# Optional client_cert/client_key gesetzt, wenn Dateien existieren.
#
# 🧩 CLI-Flags / Defaults
# 17) Flags: --env, --config, --app, --auth (cert|approle), --pki-role,
# --tls-server-name, --reload-label, --with-kv-seed.
#
# 🧪 Quick Check
# 18) Prüft am Ende auf vorhandenes Leaf, zeigt subject/issuer/enddate; sonst journalctl Hint.
# ===========================================================================
# Exit-Codes: 2=Args/Config; 3=User fehlt; 4=Post-Hook fehlt; 5=mTLS gefordert/prüfen fehlgeschlagen
# ===== Pretty logs =====
if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; BL=$'\e[34m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= BL= G= Y= E=; fi
ts(){ date +"%Y-%m-%d %H:%M:%S"; }
info(){ echo -e "🟦 ${BL}${B}[$(ts)]${R} $*"; }
ok(){ echo -e "🟩 ${G}${B}[$(ts)]${R} $*"; }
warn(){ echo -e "🟨 ${Y}${B}[$(ts)]${R} $*"; }
err(){ echo -e "🟥 ${E}${B}[$(ts)]${R} $*" >&2; }
# ===== Defaults / Args =====
: "${LOG_LEVEL:=info}"
: "${DEFAULT_ENV:=test}"
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
: "${TLS_SERVER_NAME_DEFAULT:=vault.test.privsec.ch}"
: "${RELOAD_LABEL_DEFAULT:=tls=true}"
: "${STRICT_CN_CHECK:=1}"
[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN fehlt (exportieren!)"; exit 2; }
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
AUTH_METHOD="cert"
PKI_ROLE_OVERRIDE=""; TLS_SNI_OVERRIDE=""; RELOAD_LABEL_OVERRIDE=""
WITH_KV_SEED=0
while [[ $# -gt 0 ]]; do
case "$1" in
--env) ENV_NAME="$2"; shift 2;;
--config) CFG="$2"; shift 2;;
--app) APPN="$2"; shift 2;;
--auth) AUTH_METHOD="$2"; shift 2;;
--pki-role) PKI_ROLE_OVERRIDE="$2"; shift 2;;
--tls-server-name) TLS_SNI_OVERRIDE="$2"; shift 2;;
--reload-label) RELOAD_LABEL_OVERRIDE="$2"; shift 2;;
--with-kv-seed) WITH_KV_SEED=1; shift;;
-h|--help) sed -n '1,220p' "$0"; exit 0;;
*) err "unknown arg: $1"; exit 2;;
esac
done
[[ -n "$APPN" ]] || { err "--app ist erforderlich"; exit 2; }
case "$AUTH_METHOD" in cert|approle) ;; * ) err "--auth muss cert|approle sein"; exit 2;; esac
# ===== Needs =====
need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
need vault; need jq; need python3; need openssl; need install; need systemctl
# ===== YAML laden =====
CFG_ABS="$(readlink -f -- "$CFG")" || { err "cannot resolve $CFG"; exit 2; }
CFGJSON="$(python3 - "$CFG_ABS" <<'PY'
import yaml, json, sys, io
p = sys.argv[1]
with io.open(p, "r", encoding="utf-8") as f:
print(json.dumps(yaml.safe_load(f)))
PY
)" || { err "YAML parse failed: $CFG_ABS"; exit 2; }
jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }
jqapp(){ echo "$CFGJSON" | jq -r ".apps[] | select(.name==\"$APPN\")$1"; }
VAULT_ADDR="$(jqenv '.vault_addr')"
PKI_MOUNT="$(jqenv '.pki_mount')"
KV_MOUNT="$(jqenv '.kv_mount')"
APP_USER_DEF="$(jqenv '.app.user')"
APP_USER_APP="$(jqapp '.user')"
APP_USER="$APP_USER_DEF"; [[ "$APP_USER_APP" != "null" ]] && APP_USER="$APP_USER_APP"
CN="$(jqapp '.internal_cn')"
TTL="$(jqapp '.issue_ttl')"
KV_SUBPATH="$(jqapp '.kv_subpath')"
SEED_JSON="$(jqapp '.seed')"
export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"
# ===== KV-Check & optional Seed =====
seed_kv_once(){
local mount="$1" sub="$2" seed_json="$3"
[[ -z "$mount" || "$mount" == "null" || -z "$sub" || "$sub" == "null" ]] && return 0
if vault kv get -format=json "${mount}/${sub}" >/dev/null 2>&1; then
info "KV ${mount}/${sub} existiert Seed wird NICHT geschrieben"
return 0
fi
if [[ "$WITH_KV_SEED" -eq 1 && "$seed_json" != "null" ]]; then
info "KV-Seed → ${mount}/${sub} (nur einmalig, Flag --with-kv-seed aktiv)"
mapfile -t kvargs < <(jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' <<<"$seed_json")
printf " keys: %s\n" "$(jq -r 'keys|join(", ")' <<<"$seed_json")"
vault kv put "${mount}/${sub}" "${kvargs[@]}" >/dev/null
else
warn "KV ${mount}/${sub} fehlt kein Seed (Flag --with-kv-seed nicht gesetzt)"
fi
}
seed_kv_once "$KV_MOUNT" "$KV_SUBPATH" "$SEED_JSON"
# ===== PKI-Role =====
PKI_ROLE="${PKI_ROLE_OVERRIDE:-nginx-${APPN}}"
info "PKI-Role prüfen/erstellen: ${PKI_ROLE}"
vault write "${PKI_MOUNT}/roles/${PKI_ROLE}" \
allowed_domains="$(echo "${CN#*.}")" allow_subdomains=true \
key_type=ec key_bits=256 max_ttl=720h >/dev/null || true
# ===== Policies =====
POLICY_RUN="pki-issue-${APPN}"
TMP="$(mktemp)"
cat >"$TMP" <<EOF
path "${PKI_MOUNT}/issue/${PKI_ROLE}" { capabilities=["create","update"] }
path "auth/token/renew-self" { capabilities=["update"] }
EOF
if [[ "$ENV_NAME" == "test" ]]; then
echo 'path "auth/token/lookup-self" { capabilities=["read"] }' >>"$TMP"
fi
vault policy write "${POLICY_RUN}" "$TMP" >/dev/null
rm -f "$TMP"
# ===== Agent Config (HCL & Template) =====
HOME_DIR="/home/${APP_USER}"
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}"
TLS_DIR="${HOME_DIR}/tls"
CA_FILE="${HOME_DIR}/vault/ca/ca.pem"
MTLS_DIR="${HOME_DIR}/vault/mtls"
TOKEN_FILE="${AGENT_DIR}/token"
sudo install -d -m 0700 -o "${APP_USER}" -g "${APP_USER}" "${AGENT_DIR}" "${TLS_DIR}"
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${AGENT_DIR}/bin"
TLS_SNI="${TLS_SNI_OVERRIDE:-$TLS_SERVER_NAME_DEFAULT}"
AUTO_AUTH_BLOCK=$(cat <<'EOF'
auto_auth {
method "cert" { }
sink "file" { config = { path = "__TOKEN_FILE__", mode = 0400 } }
}
EOF
)
HCL_CONTENT=$(cat <<HCL
pid_file = "${AGENT_DIR}/pidfile"
vault {
address = "${VAULT_ADDR}"
ca_cert = "${CA_FILE}"
}
${AUTO_AUTH_BLOCK}
template {
source = "${AGENT_DIR}/cert.tpl"
destination = "${AGENT_DIR}/.issue.json"
}
HCL
)
HCL_CONTENT="${HCL_CONTENT/__TOKEN_FILE__/${TOKEN_FILE}}"
sudo -u "${APP_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<<"$HCL_CONTENT"
sudo -u "${APP_USER}" tee "${AGENT_DIR}/cert.tpl" >/dev/null <<TPL
{{ with secret "${PKI_MOUNT}/issue/${PKI_ROLE}" "common_name=${CN}" "ttl=${TTL}" }}
{ "certificate": {{ toJSON .Data.certificate }},
"issuing_ca": {{ toJSON .Data.issuing_ca }},
"ca_chain": {{ toJSON .Data.ca_chain }},
"private_key": {{ toJSON .Data.private_key }} }
{{ end }}
TPL
# ===== systemd Unit =====
sudo -u "${APP_USER}" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}.service"
sudo -u "${APP_USER}" tee "${UNIT}" >/dev/null <<UNIT
[Unit]
Description=Vault Agent (${APPN}) - leaf issuance & rotation (v4.5)
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
WorkingDirectory=${AGENT_DIR}
Environment=VAULT_ADDR=${VAULT_ADDR}
ExecStart=/usr/bin/vault agent -log-level=${LOG_LEVEL} -config=${AGENT_DIR}/vault-agent.hcl
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=default.target
UNIT
APP_UID="$(id -u "${APP_USER}")"
loginctl enable-linger "${APP_USER}" >/dev/null || true
systemctl start "user@${APP_UID}.service" >/dev/null || true
XDG_RUNTIME_DIR="/run/user/${APP_UID}"; mkdir -p "$XDG_RUNTIME_DIR"; chown "${APP_USER}:${APP_USER}" "$XDG_RUNTIME_DIR" || true
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" systemctl --user daemon-reload
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" systemctl --user enable --now "vault-agent-${APPN}.service"
# ===== Quick check =====
CERT="${TLS_DIR}/${APPN}.fullchain.pem"
if sudo -u "${APP_USER}" test -s "${CERT}"; then
ok "Leaf vorhanden: ${CERT}"
openssl x509 -in "${CERT}" -noout -subject -issuer -enddate | sed 's/^/ 🔎 /'
else
warn "Noch kein Zertifikat: ${CERT}. Logs prüfen:"
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="/run/user/$(id -u "${APP_USER}")" journalctl --user -u "vault-agent-${APPN}.service" -n 40 --no-pager || true
fi
ok "SUCCESS v4.5 → app=${APPN} env=${ENV_NAME} role=${PKI_ROLE} auth=${AUTH_METHOD} kv_mount=${KV_MOUNT}"