vault-ops/infra/versions/setup-vault-agent-app-config-v4.9.sh
2026-04-14 11:45:15 +07:00

606 lines
26 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.9.sh
# Datum: 2025-10-02
# ========================= FEATURE MANIFEST (v4.9) =========================
# 🔒 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) (Bestand) In test-Umgebungen enthält pki-issue-<app> weiterhin den Debug-Pfad (lookup-self).
# 9) Zwei-Policy-Debug:
# • Separate Policy (Standardname: "debug-policy") wird
# in ENV=test automatisch am Cert-Mapping angehängt,
# in ENV=prod NICHT automatisch angehängt.
# • Override per Flag: --debug-policy auto|on|off (Default: auto)
# auto: test → anhängen, prod → nicht anhängen
# on: erzwingt anhängen
# off: erzwingt entfernen
# • Policy wird bei Bedarf idempotent erstellt (lookup-self + renew-self).
#
# 🧠 Agent / Templates / Pfade
# 10) Generiert Agent-HCL + Template (cert.tpl); rendert .issue.json.
# 11) Post-Hook (vault-agent-post-leaf.sh) schreibt <HOME>/tls/<app>.{key,fullchain.pem}.
# 12) 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
# 13) Ü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, Flag-gesteuert)
# 14) Immer prüfen und loggen, ob Secret existiert; Seed nur wenn --with-kv-seed
# gesetzt und Secret fehlt; niemals überschreiben.
#
# ⚙️ Systemd (user)
# 15) Erstellt ~/.config/systemd/user/vault-agent-<app>.service, aktiviert (--user), Linger an.
# 16) Erzwingt Neustart, zeigt Status & tailt Journal (konfigurierbar via JOURNAL_TAIL_LINES).
#
# 🆕 v4.9 Optional: **KV-Access ergänzen** (nur mit Flag, keine Default-Änderung!)
# 17) --with-kv-access:
# • Aktiviert idempotent KV v2 auf <kv_mount>
# • Erstellt/aktualisiert KV-Policy "secret-agent-<app>-policy" (lesend auf data/<kv_subpath>, read/list auf metadata/<kv_subpath>)
# • Hängt diese KV-Policy am **bestehenden** cert-mapping (auth/cert/certs/<mapping-name>) an
# Mapping-Name wie bisher (Default agent-<app> oder via --cert-mapping)
# • Bei --auth approle: KV-Policy zusätzlich in die AppRole <app>-pki-issue mergen
# • Kein automatisches Seeding (weiterhin getrennt via --with-kv-seed)
# ===========================================================================
# 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}"
: "${JOURNAL_TAIL_LINES:=80}"
#[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN fehlt (exportieren!)"; exit 2; }
: "${VAULT_ADMIN_TOKEN:=${VAULT_TOKEN:-}}"
[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN oder VAULT_TOKEN fehlt"; exit 2; }
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
AUTH_METHOD="cert" # cert|approle
PKI_ROLE_OVERRIDE=""; TLS_SNI_OVERRIDE=""; RELOAD_LABEL_OVERRIDE=""
WITH_KV_SEED=0
CERT_MAPPING_OVERRIDE=""
DEBUG_POLICY_MODE="auto" # auto|on|off
DEBUG_POLICY_NAME="debug-policy" # separater, sichtbarer Name
# 🆕 v4.9-Flags (nur optional, ändert Default-Verhalten nicht)
WITH_KV_ACCESS=0
KV_POLICY_NAME_OVERRIDE=""
usage_extra=$'# 17) --with-kv-access → KV v2 aktivieren + KV-Policy erstellen + am cert-mapping anhängen (optional)\n# --kv-policy-name NAME → Name der KV-Policy (Default: secret-agent-<app>-policy)\n'
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;;
--cert-mapping) CERT_MAPPING_OVERRIDE="$2"; shift 2;;
--debug-policy) DEBUG_POLICY_MODE="$2"; shift 2;;
--debug-policy-name) DEBUG_POLICY_NAME="$2"; shift 2;;
# 🆕 v4.9
--with-kv-access) WITH_KV_ACCESS=1; shift;;
--kv-policy-name) KV_POLICY_NAME_OVERRIDE="$2"; shift 2;;
-h|--help)
sed -n '1,200p' "$0"
printf "%s" "$usage_extra"
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
case "$DEBUG_POLICY_MODE" in auto|on|off) ;; * ) err "--debug-policy muss auto|on|off 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')"
# Env-level defaults
APP_USER_DEF="$(jqenv '.app.user')"
SIDECAR_HOST_PORT_DEF="$(jqenv '.app.sidecar_host_port')"
SIDECAR_CONTAINER_DEF="$(jqenv '.app.sidecar_container')"
PROXY_USER_DEF="$(jqenv '.proxy.user')"
PROXY_LISTEN_PORT_DEF="$(jqenv '.proxy.listen_port')"
PROXY_CHAIN_PATH_DEF="$(jqenv '.proxy.chain_path')"
PROXY_RELOAD_DEF="$(jqenv '.proxy.reload')"
# App-level fields & overrides
APP_USER_APP="$(jqapp '.user')"
CN="$(jqapp '.internal_cn')"
TTL="$(jqapp '.issue_ttl')"
EXT_TEST="$(jqapp '.external_host_test')"; [[ "$EXT_TEST" == "null" ]] && EXT_TEST=""
EXT_PROD="$(jqapp '.external_host_prod')"; [[ "$EXT_PROD" == "null" ]] && EXT_PROD=""
KV_SUBPATH="$(jqapp '.kv_subpath')"
SEED_JSON="$(jqapp '.seed')"
SIDECAR_CONTAINER_APP="$(jqapp '.sidecar_container')"
PROXY_USER_APP="$(jqapp '.proxy_user')"
PROXY_CHAIN_PATH_APP="$(jqapp '.proxy_chain_path')"
PROXY_RELOAD_APP="$(jqapp '.proxy_reload')"
APP_USER="$APP_USER_DEF"; [[ "$APP_USER_APP" != "null" ]] && APP_USER="$APP_USER_APP"
SIDECAR_HOST_PORT="$SIDECAR_HOST_PORT_DEF"; [[ "$SIDECAR_HOST_PORT" == "null" ]] && SIDECAR_HOST_PORT=""
SIDECAR_CONTAINER="$SIDECAR_CONTAINER_DEF"; [[ "$SIDECAR_CONTAINER_APP" != "null" ]] && SIDECAR_CONTAINER="$SIDECAR_CONTAINER_APP"; [[ "$SIDECAR_CONTAINER" == "null" ]] && SIDECAR_CONTAINER=""
PROXY_USER="$PROXY_USER_DEF"; [[ "$PROXY_USER_APP" != "null" ]] && PROXY_USER="$PROXY_USER_APP"
PROXY_LISTEN_PORT="$PROXY_LISTEN_PORT_DEF"; [[ "$PROXY_LISTEN_PORT" == "null" ]] && PROXY_LISTEN_PORT=""
PROXY_CHAIN_PATH="$PROXY_CHAIN_PATH_DEF"; [[ "$PROXY_CHAIN_PATH_APP" != "null" ]] && PROXY_CHAIN_PATH="$PROXY_CHAIN_PATH_APP"
PROXY_RELOAD="$PROXY_RELOAD_DEF"; [[ "$PROXY_RELOAD_APP" != "null" ]] && PROXY_RELOAD="$PROXY_RELOAD_APP"
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" && "$CN" != "null" && "$TTL" != "null" && -n "$APP_USER" ]] \
|| { err "incomplete config: vault_addr/pki_mount/app.user/internal_cn/issue_ttl"; exit 2; }
export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"
# ===== Helpers =====
base_domain(){ local h="${1:-}"; [[ "$h" == *.* ]] && printf '%s\n' "${h#*.}" || printf '\n'; }
uniq_csv(){ tr ',' '\n' | awk 'NF' | sort -u | paste -sd, -; }
derive_allowed_domains(){ printf '%s\n%s\n' "$(base_domain "$1")" "$(base_domain "$2")" | uniq_csv; }
extract_cn(){
openssl x509 -in "$1" -noout -subject -nameopt RFC2253 \
| sed -n 's/^subject=//; s/.*CN=\([^,]*\).*/\1/p'
}
ensure_pki_role(){
local mount="$1" role="$2" doms="$3" max="${4:-720h}"
info "upsert PKI role ${role} (allowed_domains=${doms})"
vault write "${mount}/roles/${role}" \
allowed_domains="${doms}" allow_subdomains=true allow_bare_domains=true \
allow_wildcard_certificates=false server_flag=true client_flag=false \
key_type="ec" key_bits=256 max_ttl="${max}" >/dev/null || true
}
# Zert-Mapping-Name ableiten/übersteuern
CERT_MAPPING="${CERT_MAPPING_OVERRIDE:-agent-${APPN}}"
# Merge/Update der Mapping-Policies (policies + token_policies gleichzeitig)
merge_policies(){
local json="$1"
printf '%s' "$json" | jq -r '
def to_arr(x):
if x == null then []
elif (x|type)=="string" then (x|split(",")|map(gsub("\\s+";""))|map(select(length>0)))
else x
end;
.data as $d
| ((to_arr($d.policies) + to_arr($d.token_policies)) | unique)
| join(",")
'
}
mapping_attach_debug(){
local cert="$1" name="$2"
local j; if ! j="$(vault read -format=json "auth/cert/certs/${cert}")"; then
warn "Cert-Mapping nicht gefunden: auth/cert/certs/${cert} überspringe Debug-Attach"
return 0
fi
local merged; merged="$(merge_policies "$j")"
if [[ -z "$merged" ]]; then
merged="$name"
elif ! grep -q -E "(^|,)\Q${name}\E(,|$)" <<<"$merged"; then
merged="${merged},${name}"
fi
info "debug-policy attach → cert='${cert}' policy='${name}'"
vault write "auth/cert/certs/${cert}" policies="$merged" token_policies="$merged" >/dev/null
ok "debug-policy aktiv: ${name} @ ${cert}"
}
mapping_detach_debug(){
local cert="$1" name="$2"
local j; if ! j="$(vault read -format=json "auth/cert/certs/${cert}")"; then
warn "Cert-Mapping nicht gefunden: auth/cert/certs/${cert} nichts zu entfernen"
return 0
fi
local merged; merged="$(merge_policies "$j")"
local filtered; filtered="$(tr ',' '\n' <<<"$merged" | grep -v -x "$name" | paste -sd, -)"
info "debug-policy remove → cert='${cert}' policy='${name}'"
vault write "auth/cert/certs/${cert}" policies="${filtered}" token_policies="${filtered}" >/dev/null
ok "debug-policy entfernt: ${name} @ ${cert}"
}
ensure_debug_policy_exists(){
# idempotent: anlegen/überschreiben mit minimalem Inhalt (lookup-self + renew-self)
if vault policy read "${DEBUG_POLICY_NAME}" >/dev/null 2>&1; then
info "debug-policy vorhanden: ${DEBUG_POLICY_NAME}"
return 0
fi
info "debug-policy fehlt lege an: ${DEBUG_POLICY_NAME}"
local tmp; tmp="$(mktemp)"
cat >"$tmp" <<'HCL'
path "auth/token/lookup-self" { capabilities = ["read"] }
path "auth/token/renew-self" { capabilities = ["update"] }
HCL
vault policy write "${DEBUG_POLICY_NAME}" "$tmp" >/dev/null
rm -f "$tmp"
ok "debug-policy erstellt: ${DEBUG_POLICY_NAME}"
}
# 🆕 v4.9: KV-Access Helfer (idempotent, nur wenn --with-kv-access)
ensure_kv_mount_v2(){
local m="$1"
[[ -z "$m" || "$m" == "null" ]] && { warn "KV-Mount nicht gesetzt → skip enable"; return 0; }
if vault secrets list -format=json | jq -e --arg m "$m/" 'has($m)' >/dev/null 2>&1; then
info "KV v2 bereits aktiv auf: ${m}"
else
info "Enable KV v2 auf: ${m}"
vault secrets enable -path="$m" kv-v2 >/dev/null 2>&1 || true
fi
}
ensure_kv_policy(){
local m="$1" sub="$2" pol="$3"
[[ -z "$m" || "$m" == "null" || -z "$sub" || "$sub" == "null" ]] && { info "KV-Policy: unvollständige Angaben (mount/subpath) → skip"; return 0; }
local tmp; tmp="$(mktemp)"
cat >"$tmp" <<EOF
path "${m}/data/${sub}" { capabilities = ["read"] }
path "${m}/metadata/${sub}" { capabilities = ["read", "list"] }
EOF
info "Write/Upsert KV-Policy ${pol} (READ data, READ/LIST metadata) …"
vault policy write "${pol}" "$tmp" >/dev/null
rm -f "$tmp"
ok "KV-Policy bereit: ${pol}"
}
mapping_attach_policy(){
local cert="$1" pol="$2"
local j; if ! j="$(vault read -format=json "auth/cert/certs/${cert}")"; then
warn "Cert-Mapping fehlt: auth/cert/certs/${cert} KV-Policy nicht anhängbar"
return 0
fi
local merged; merged="$(merge_policies "$j")"
if [[ -z "$merged" ]]; then
merged="$pol"
elif ! grep -q -E "(^|,)\Q${pol}\E(,|$)" <<<"$merged"; then
merged="${merged},${pol}"
else
info "Cert-Mapping ${cert}: Policy ${pol} bereits vorhanden"
return 0
fi
info "Attach KV-Policy → cert='${cert}' policy='${pol}'"
vault write "auth/cert/certs/${cert}" policies="$merged" token_policies="$merged" >/dev/null
ok "KV-Policy angehängt: ${pol} @ ${cert}"
}
approle_attach_policy(){
local role="$1" pol="$2"
# Nur Policies mergen; andere Rolleigenschaften unverändert lassen
local j; if ! j="$(vault read -format=json "auth/approle/role/${role}")"; then
warn "AppRole ${role} nicht gefunden skip KV-Policy-Anhang"
return 0
fi
local have; have="$(jq -r '.data.policies // ""' <<<"$j")"
if grep -q -E "(^|,)\Q${pol}\E(,|$)" <<<"${have}"; then
info "AppRole ${role}: Policy ${pol} bereits vorhanden"
return 0
fi
local merged="${have}"; [[ -n "$merged" ]] && merged="${merged},${pol}" || merged="${pol}"
info "AppRole-Policies mergen: ${role}${pol}"
vault write "auth/approle/role/${role}" policies="${merged}" >/dev/null
ok "AppRole aktualisiert: ${role} (+${pol})"
}
# ===== User/Dirs/Paths =====
id -u "${APP_USER}" >/dev/null 2>&1 || { err "Linux-User ${APP_USER} fehlt"; exit 3; }
HOME_DIR="/home/${APP_USER}"
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}"
TLS_DIR="${HOME_DIR}/tls"
CA_DIR="${HOME_DIR}/vault/ca"
MTLS_DIR="${HOME_DIR}/vault/mtls"
CA_FILE="${CA_DIR}/ca.pem"
RID="${AGENT_DIR}/role_id"
SID="${AGENT_DIR}/secret_id"
TOKEN_FILE="${AGENT_DIR}/token"
POST_LOCAL="${AGENT_DIR}/bin/vault-agent-post-leaf.sh"
POST_SRC="${INFRA_DIR}/scripts/vault-agent-post-leaf.sh"
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}.service"
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"
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${CA_DIR}" >/dev/null 2>&1 || true
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${MTLS_DIR}" >/dev/null 2>&1 || true
# ===== PKI Role & Policies =====
PKI_ROLE="${PKI_ROLE_OVERRIDE:-nginx-${APPN}}"
EXT_HOST="$EXT_TEST"; [[ "$ENV_NAME" == "prod" ]] && EXT_HOST="$EXT_PROD"
ALLOWED="$(derive_allowed_domains "$CN" "$EXT_HOST")"
ensure_pki_role "$PKI_MOUNT" "$PKI_ROLE" "$ALLOWED" "720h"
POLICY_BOOT="pki-bootstrap-${APPN}"
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
# (Bestand) lookup-self in test-Env beibehalten NICHT entfernt, nur ergänzt durch separate Debug-Policy
if [[ "$ENV_NAME" == "test" ]]; then
echo 'path "auth/token/lookup-self" { capabilities=["read"] }' >>"$TMP"
fi
vault policy write "${POLICY_RUN}" "$TMP" >/dev/null
cat >"$TMP" <<EOF
path "auth/approle/role/${APPN}-pki-issue/role-id" { capabilities=["read"] }
path "auth/approle/role/${APPN}-pki-issue/secret-id" { capabilities=["create","update"] }
EOF
vault policy write "${POLICY_BOOT}" "$TMP" >/dev/null
rm -f "$TMP"
# ===== Debug-Policy (separat) nach Modus anhängen/entfernen =====
# auto: test→attach, prod→detach (keine Änderung falls nicht vorhanden)
case "$DEBUG_POLICY_MODE" in
auto)
ensure_debug_policy_exists
if [[ "$ENV_NAME" == "test" ]]; then
mapping_attach_debug "${CERT_MAPPING}" "${DEBUG_POLICY_NAME}"
else
mapping_detach_debug "${CERT_MAPPING}" "${DEBUG_POLICY_NAME}"
fi
;;
on)
ensure_debug_policy_exists
mapping_attach_debug "${CERT_MAPPING}" "${DEBUG_POLICY_NAME}"
;;
off)
mapping_detach_debug "${CERT_MAPPING}" "${DEBUG_POLICY_NAME}"
;;
esac
# ===== Auth-Setup (Strict mTLS; AppRole nur explizit) =====
ROLE_NAME="${APPN}-pki-issue"
if [[ "$AUTH_METHOD" == "approle" ]]; then
info "Auth=AppRole (explizit via --auth approle)"
vault auth enable approle >/dev/null 2>&1 || true
vault write "auth/approle/role/${ROLE_NAME}" \
policies="${POLICY_RUN}" bind_secret_id=true \
token_type=service token_period=24h secret_id_ttl=0 secret_id_num_uses=0 >/dev/null
RID_VAL="$(vault read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
SID_VAL="$(vault write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
sudo bash -c "umask 077; printf '%s' '${RID_VAL}' > '${RID}'; chown ${APP_USER}:${APP_USER} '${RID}'; chmod 600 '${RID}'"
sudo bash -c "umask 077; printf '%s' '${SID_VAL}' > '${SID}'; chown ${APP_USER}:${APP_USER} '${SID}'; chmod 600 '${SID}'"
fi
# ===== 🆕 v4.9: Optional KV-Access vorbereiten (nur bei Flag) =====
KV_POLICY_NAME="${KV_POLICY_NAME_OVERRIDE:-secret-agent-${APPN}-policy}"
if (( WITH_KV_ACCESS )); then
info "KV-Access aktiviert (--with-kv-access)"
ensure_kv_mount_v2 "${KV_MOUNT}"
ensure_kv_policy "${KV_MOUNT}" "${KV_SUBPATH}" "${KV_POLICY_NAME}"
mapping_attach_policy "${CERT_MAPPING}" "${KV_POLICY_NAME}"
if [[ "$AUTH_METHOD" == "approle" ]]; then
approle_attach_policy "${ROLE_NAME}" "${KV_POLICY_NAME}"
fi
else
info "KV-Access: AUS (kein Flag) bestehendes Verhalten bleibt unverändert"
fi
# ===== CA-Trust prüfen =====
if ! sudo -u "${APP_USER}" test -s "${CA_FILE}"; then
warn "CA fehlt: ${CA_FILE} → HTTPS Verify ggü. Vault kann scheitern."
fi
# ===== mTLS Material vorhanden? + CN strikt prüfen =====
if [[ "$AUTH_METHOD" == "cert" ]]; then
if ! ( sudo -u "${APP_USER}" test -s "${MTLS_DIR}/agent.crt" && sudo -u "${APP_USER}" test -s "${MTLS_DIR}/agent.key" ); then
err "Auth=cert gefordert, aber mTLS fehlt: ${MTLS_DIR}/agent.{crt,key}"
exit 5
fi
CRT_CN="$(extract_cn "${MTLS_DIR}/agent.crt" || true)"
if [[ -z "$CRT_CN" ]]; then
err "Konnte CN aus ${MTLS_DIR}/agent.crt nicht lesen"
exit 5
fi
EXPECTED_RE="^agent-${APPN}(-[a-z0-9]+)?\.${ENV_NAME}\.privsec\.ch$"
if [[ "${STRICT_CN_CHECK}" == "1" ]]; then
if ! [[ "$CRT_CN" =~ $EXPECTED_RE ]]; then
err "mTLS client CN '${CRT_CN}' passt NICHT zu ${EXPECTED_RE} → Abbruch (STRICT_CN_CHECK=1)."
exit 5
else
ok "mTLS client CN '${CRT_CN}' passt zu ${EXPECTED_RE}"
fi
else
if ! [[ "$CRT_CN" =~ $EXPECTED_RE ]]; then
warn "mTLS client CN '${CRT_CN}' passt nicht zu ${EXPECTED_RE}; weiter (tolerant, STRICT_CN_CHECK=0)."
else
info "mTLS client CN '${CRT_CN}' passt zu ${EXPECTED_RE}"
fi
fi
info "Auth=cert (mTLS) strikt, kein Fallback"
fi
# ===== KV-Seed (einmalig, kontrolliert per Flag) =====
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 überschrieben"
return 0
fi
if [[ "$WITH_KV_SEED" -eq 1 && "$seed_json" != "null" ]]; then
info "KV-Seed → ${mount}/${sub} (nur einmalig)"
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"
# ===== HCL + Template (Strict mTLS / optional AppRole) =====
TLS_SNI="${TLS_SNI_OVERRIDE:-$TLS_SERVER_NAME_DEFAULT}"
PARAMS="\"common_name=${CN}\" \"ttl=${TTL}\""; [[ -n "$EXT_HOST" ]] && PARAMS="$PARAMS \"alt_names=${EXT_HOST}\""
AUTO_AUTH_BLOCK=""
if [[ "$AUTH_METHOD" == "cert" ]]; then
AUTO_AUTH_BLOCK=$(cat <<'EOF'
auto_auth {
method "cert" { }
sink "file" { config = { path = "__TOKEN_FILE__", mode = 0400 } }
}
EOF
)
elif [[ "$AUTH_METHOD" == "approle" ]]; then
AUTO_AUTH_BLOCK=$(cat <<'EOF'
auto_auth {
method "approle" {
config = {
role_id_file_path = "__RID__"
secret_id_file_path = "__SID__"
remove_secret_id_file_after_reading = false
}
}
sink "file" { config = { path = "__TOKEN_FILE__", mode = 0400 } }
}
EOF
)
fi
TLS_EXTRA=$'\n tls_server_name = "'"${TLS_SNI}"$'"\n'
# Optional mTLS zum Vault-Server selbst, falls vorhanden:
if sudo -u "${APP_USER}" test -s "${MTLS_DIR}/agent.crt" && sudo -u "${APP_USER}" test -s "${MTLS_DIR}/agent.key"; then
TLS_EXTRA+=$' client_cert = "'"${MTLS_DIR}/agent.crt"\"$'\n'
TLS_EXTRA+=$' client_key = "'"${MTLS_DIR}/agent.key"\"$'\n'
fi
HCL_CONTENT=$(cat <<HCL
pid_file = "${AGENT_DIR}/pidfile"
vault {
address = "${VAULT_ADDR}"
ca_cert = "${CA_FILE}"${TLS_EXTRA}
}
__AUTO_AUTH__
template {
source = "${AGENT_DIR}/cert.tpl"
destination = "${AGENT_DIR}/.issue.json"
command = "OUTDIR='${TLS_DIR}' RELOAD_TLS_LABEL='${RELOAD_LABEL_OVERRIDE:-$RELOAD_LABEL_DEFAULT}' PROXY_RELOAD='${PROXY_RELOAD}' PROXY_CHAIN_PATH='${PROXY_CHAIN_PATH}' PROXY_USER='${PROXY_USER}' PROXY_LISTEN_PORT='${PROXY_LISTEN_PORT}' SIDECAR_HOST_PORT='${SIDECAR_HOST_PORT}' SIDECAR_CONTAINER='${SIDECAR_CONTAINER}' APP_NAME='${APPN}' ${POST_LOCAL}"
}
HCL
)
HCL_CONTENT="${HCL_CONTENT/__AUTO_AUTH__/${AUTO_AUTH_BLOCK}}"
HCL_CONTENT="${HCL_CONTENT/__TOKEN_FILE__/${TOKEN_FILE}}"
HCL_CONTENT="${HCL_CONTENT/__RID__/${RID}}"
HCL_CONTENT="${HCL_CONTENT/__SID__/${SID}}"
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}" ${PARAMS} }}
{ "certificate": {{ toJSON .Data.certificate }},
"issuing_ca": {{ toJSON .Data.issuing_ca }},
"ca_chain": {{ toJSON .Data.ca_chain }},
"private_key": {{ toJSON .Data.private_key }} }
{{ end }}
TPL
# ===== Post-Hook bereitstellen =====
if [[ -r "$POST_SRC" ]]; then
sudo /usr/bin/install -m 0755 -o "${APP_USER}" -g "${APP_USER}" -D "$POST_SRC" "${POST_LOCAL}"
ok "post-hook installiert: ${POST_LOCAL}"
else
err "Post-Hook fehlt: $POST_SRC (benötigt, um Key/Fullchain zu schreiben & Reload auszuführen)"; exit 4
fi
# ===== systemd (user) =====
sudo -u "${APP_USER}" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
sudo -u "${APP_USER}" tee "${UNIT}" >/dev/null <<UNIT
[Unit]
Description=Vault Agent (${APPN}) - leaf issuance & rotation (v4.9)
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"
# ===== Erzwinge Neustart + Status + Log-Tail (Bestand) =====
info "service restart (erzwinge Neustart des user-services)"
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" systemctl --user restart "vault-agent-${APPN}.service"
info "service status (kurz, ohne Pager)"
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" systemctl --user status "vault-agent-${APPN}.service" --no-pager -l | sed 's/^/ 📄 /'
# ===== Quick check (unverändert) =====
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} (kommt i.d.R. kurz darauf). Logs:"
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="/run/user/$(id -u "${APP_USER}")" journalctl --user -u "vault-agent-${APPN}.service" -n 60 --no-pager || true
fi
info "journal tail (letzte ${JOURNAL_TAIL_LINES} Zeilen, ohne Pager)"
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" journalctl --user -u "vault-agent-${APPN}.service" -n "${JOURNAL_TAIL_LINES}" --no-pager | sed 's/^/ 🪵 /'
ok "SUCCESS v4.9 → app=${APPN} env=${ENV_NAME} role=${PKI_ROLE} auth=${AUTH_METHOD} kv_mount=${KV_MOUNT} kv_access=$WITH_KV_ACCESS allowed_domains=${ALLOWED} cert_mapping=${CERT_MAPPING} debug_policy=${DEBUG_POLICY_NAME} mode=${DEBUG_POLICY_MODE}"