512 lines
22 KiB
Bash
Executable file
512 lines
22 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
# setup-vault-agent-app-config-v4.8.sh
|
||
# Datum: 2025-09-30
|
||
|
||
# ========================= FEATURE MANIFEST (v4.8) =========================
|
||
# 🔒 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).
|
||
#
|
||
# 🧩 CLI-Flags / Defaults
|
||
# 17) Flags: --env, --config, --app, --auth (cert|approle), --pki-role,
|
||
# --tls-server-name, --reload-label, --with-kv-seed,
|
||
# --cert-mapping, --debug-policy, --debug-policy-name
|
||
# (debug-policy: auto|on|off; default auto; name default "debug-policy")
|
||
# ===========================================================================
|
||
|
||
# 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; }
|
||
|
||
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
|
||
|
||
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;;
|
||
-h|--help) sed -n '1,320p' "$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 "$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}"
|
||
}
|
||
|
||
# ===== 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
|
||
# (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
|
||
|
||
# ===== 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.8)
|
||
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 aus v4.7) =====
|
||
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.8 → app=${APPN} env=${ENV_NAME} role=${PKI_ROLE} auth=${AUTH_METHOD} kv_mount=${KV_MOUNT} allowed_domains=${ALLOWED} cert_mapping=${CERT_MAPPING} debug_policy=${DEBUG_POLICY_NAME} mode=${DEBUG_POLICY_MODE}"
|
||
|