632 lines
26 KiB
Bash
Executable file
632 lines
26 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
# setup-vault-agent-app-config-v5.0.sh
|
||
# Datum: 2025-11-26
|
||
|
||
# ========================= FEATURE MANIFEST (v5.0) =========================
|
||
# 🔒 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)
|
||
#
|
||
# 🆕 v5.0 Env-spezifisches tls_server_name-Default
|
||
# 18) tls_server_name-Default richtet sich jetzt nach ENV_NAME, falls kein --tls-server-name
|
||
# gesetzt ist:
|
||
# • ENV=test → vault.test.privsec.ch
|
||
# • ENV=prod → vault.prod.privsec.ch
|
||
# • sonst → vault.<env>.privsec.ch
|
||
# Override weiterhin möglich über:
|
||
# • Flag --tls-server-name
|
||
# • (intern) Variable TLS_SNI_OVERRIDE
|
||
# ===========================================================================
|
||
|
||
# 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
|
||
|
||
# 🆕 v5.0: Env-spezifisches Default für tls_server_name
|
||
# Nur anwenden, wenn kein explizites --tls-server-name gesetzt wurde.
|
||
if [[ -z "$TLS_SNI_OVERRIDE" ]]; then
|
||
case "$ENV_NAME" in
|
||
prod)
|
||
TLS_SERVER_NAME_DEFAULT="vault.prod.privsec.ch"
|
||
;;
|
||
test)
|
||
TLS_SERVER_NAME_DEFAULT="vault.test.privsec.ch"
|
||
;;
|
||
*)
|
||
TLS_SERVER_NAME_DEFAULT="vault.${ENV_NAME}.privsec.ch"
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
# ===== 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 (v5.0)
|
||
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 v5.0 → 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}"
|