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

457 lines
20 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.3.sh
# Datum: 2025-09-29
# ========================= FEATURE MANIFEST (v4.3) =========================
# 🔒 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.
#
# 📦 Konfiguration / YAML
# 4) 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
# 5) PKI-Role Upsert (EC P-256, max_ttl=720h), allowed_domains aus CN & externem Host.
# 6) Policies minimal: pki-issue-<app> (issue, renew-self), pki-bootstrap-<app> (AppRole bootstrap).
#
# 🧠 Agent / Templates / Pfade
# 7) Generiert Agent-HCL + Template (cert.tpl); rendert .issue.json.
# 8) Post-Hook (vault-agent-post-leaf.sh) schreibt <HOME>/tls/<app>.{key,fullchain.pem}.
# 9) 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
# 10) Ü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)
# 11) Falls kv_subpath + seed vorhanden und Secret fehlt → einmaliges vault kv put (kein Overwrite).
#
# ⚙️ Systemd (user)
# 12) Erstellt ~/.config/systemd/user/vault-agent-<app>.service, aktiviert (--user), Linger an.
#
# 🌐 TLS zu Vault
# 13) 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
# 14) Flags: --env, --config, --app, --auth (cert|approle), --pki-role,
# --tls-server-name, --reload-label. Defensive Defaults.
#
# 🧪 Quick Check
# 15) Prüft am Ende auf vorhandenes Leaf, zeigt subject/issuer/enddate; sonst journalctl Hint.
#
# 🆕 Cert-Auth Mapping (PKI) Auto-Manage
# 16) Legt/updated auth/cert/certs/<MAP> für PKI:
# - liest CN aus ~/vault/mtls/agent.crt (Allowed CN)
# - verwendet CA aus ~/vault/ca/ca.pem (override via --cert-map-cafile)
# - Policies: stellt sicher, dass pki-issue-<app> enthalten ist
# - Strategien: --cert-map-strategy merge|replace (Default: merge)
# - Map-Name: --cert-map-name (Default: agent-<app>-pki)
# ===========================================================================
#
# Exit-Codes: 2=Args/Config; 3=User fehlt; 4=Post-Hook fehlt; 5=mTLS gefordert aber Material fehlt
# ===== 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}"
# Cert-Map defaults
: "${CERT_MAP_NAME_DEFAULT:=}"
: "${CERT_MAP_STRATEGY_DEFAULT:=merge}" # merge|replace
: "${CERT_MAP_TTL_DEFAULT:=24h}"
: "${CERT_MAP_CAFILE_DEFAULT:=}"
[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN fehlt (exportieren!)"; exit 2; }
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
AUTH_METHOD="cert" # cert|approle
PKI_ROLE_OVERRIDE=""; TLS_SNI_OVERRIDE=""; RELOAD_LABEL_OVERRIDE=""
CERT_MAP_NAME="$CERT_MAP_NAME_DEFAULT"
CERT_MAP_STRATEGY="$CERT_MAP_STRATEGY_DEFAULT"
CERT_MAP_TTL="$CERT_MAP_TTL_DEFAULT"
CERT_MAP_CAFILE="$CERT_MAP_CAFILE_DEFAULT"
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;;
# 🆕 Cert-Map mgmt
--cert-map-name) CERT_MAP_NAME="$2"; shift 2;;
--cert-map-strategy) CERT_MAP_STRATEGY="$2"; shift 2;;
--cert-map-ttl) CERT_MAP_TTL="$2"; shift 2;;
--cert-map-cafile) CERT_MAP_CAFILE="$2"; shift 2;;
-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
case "${CERT_MAP_STRATEGY}" in merge|replace) ;; * ) err "--cert-map-strategy: merge|replace"; 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(){
# robust CN-Parse (supports comma- oder slash-Notation)
local cert="$1"
local subj; subj="$(openssl x509 -in "$cert" -noout -subject 2>/dev/null || true)"
[[ -n "$subj" ]] || return 1
# try comma-style
echo "$subj" | sed -n 's/^subject= *//; s,/, ,g; s, ,\n,g; p' | awk -F= '$1=="CN"{print $2}' | head -n1
}
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
}
ensure_cert_map(){
# Creates/updates auth/cert/certs/<map> for PKI Agent mTLS
local map_name="$1" cafile="$2" allowed_cn="$3" need_policy="$4" ttl="$5" strategy="$6"
info "ensure cert-auth map: ${map_name} (strategy=${strategy}, ttl=${ttl})"
vault auth enable cert >/dev/null 2>&1 || true
local exist_json=""
if exist_json="$(vault read -format=json "auth/cert/certs/${map_name}" 2>/dev/null || true)"; then
# merge or replace
local current_pols current_cns
current_pols="$(jq -r '.data.policies // [] | .[]' <<<"$exist_json" 2>/dev/null || true)"
current_cns="$(jq -r '.data.allowed_common_names // [] | .[]' <<<"$exist_json" 2>/dev/null || true)"
if [[ "$strategy" == "merge" ]]; then
# union
local merged_pols merged_cns
merged_pols="$( { printf '%s\n' $current_pols; echo "$need_policy"; } | awk 'NF' | sort -u | paste -sd, -)"
merged_cns="$( { printf '%s\n' $current_cns; echo "$allowed_cn"; } | awk 'NF' | sort -u | paste -sd, -)"
vault write "auth/cert/certs/${map_name}" \
display_name="${map_name}" \
certificate=@"${cafile}" \
allowed_common_names="${merged_cns}" \
policies="${merged_pols}" \
token_ttl="${ttl}" >/dev/null
ok "cert-map updated (merge): ${map_name} | policies=[${merged_pols}] | CNs=[${merged_cns}]"
else
# replace everything with our values (keeps name)
vault write "auth/cert/certs/${map_name}" \
display_name="${map_name}" \
certificate=@"${cafile}" \
allowed_common_names="${allowed_cn}" \
policies="${need_policy}" \
token_ttl="${ttl}" >/dev/null
ok "cert-map replaced: ${map_name} | policies=[${need_policy}] | CNs=[${allowed_cn}]"
fi
else
# create fresh
vault write "auth/cert/certs/${map_name}" \
display_name="${map_name}" \
certificate=@"${cafile}" \
allowed_common_names="${allowed_cn}" \
policies="${need_policy}" \
token_ttl="${ttl}" >/dev/null
ok "cert-map created: ${map_name} | policies=[${need_policy}] | CNs=[${allowed_cn}]"
fi
}
# ===== 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="$(cd -- "$(dirname -- "$0")" && pwd)/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
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"
# ===== 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
# ===== 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
# ===== KV-Seed (einmalig, wenn konfiguriert) =====
seed_kv_once(){
local mount="$1" sub="$2" seed_json="$3"
[[ -z "$mount" || "$mount" == "null" || -z "$sub" || "$sub" == "null" || "$seed_json" == "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
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
}
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
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
info "Auth=cert (mTLS) strikt, kein Fallback"
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
# ===== 🆕 Cert-Auth Map für PKI sicherstellen (nur bei mTLS-Auth) =====
if [[ "$AUTH_METHOD" == "cert" ]]; then
# CN aus Agent-Client-Zert lesen
CLIENT_CN="$(sudo -u "${APP_USER}" bash -c 'openssl x509 -in "'"${MTLS_DIR}/agent.crt"'" -noout -subject' | sed -n 's/^subject= *//p' | sed 's,/, ,g' | awk -F= '$1=="CN"{print $2}' | head -n1 || true)"
if [[ -z "$CLIENT_CN" ]]; then
err "Konnte CN aus ${MTLS_DIR}/agent.crt nicht lesen"; exit 5
fi
# Map-Name defaulten, falls nicht übergeben
[[ -n "$CERT_MAP_NAME" ]] || CERT_MAP_NAME="agent-${APPN}-pki"
# CA-Datei für Cert-Map (override möglich)
CM_CA="${CERT_MAP_CAFILE:-$CA_FILE}"
[[ -r "$CM_CA" ]] || { err "Cert-Map CA nicht lesbar: ${CM_CA}"; exit 2; }
ensure_cert_map "$CERT_MAP_NAME" "$CM_CA" "$CLIENT_CN" "$POLICY_RUN" "$CERT_MAP_TTL" "$CERT_MAP_STRATEGY"
else
info "Auth=AppRole → Cert-Auth-Map wird übersprungen"
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.3)
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} (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
ok "SUCCESS v4.3 → app=${APPN} env=${ENV_NAME} role=${PKI_ROLE} auth=${AUTH_METHOD} kv_mount=${KV_MOUNT} allowed_domains=${ALLOWED} cert_map=${CERT_MAP_NAME} strategy=${CERT_MAP_STRATEGY}"