vault-ops/infra/archiv/setup-vault-agent-proxy-config-v4.8.sh
Blade34242 e0b3c80819 Update
2025-11-26 08:56:50 +01:00

397 lines
16 KiB
Bash
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-proxy-config-v4.8.sh
# Datum: 2025-10-01
#
# ========================= FEATURE MANIFEST (Proxy v4.8) ======================
# 🔒 Auth-Optionen
# 1) **Default: auth/cert (mTLS)** — erwartet, dass dein mTLS-Client-Script
# (setup-vault-agent-mtls-client-config-*.sh) zuvor für **--app <app>**
# gelaufen ist und unter ~/<user>/vault/mtls/agent.{crt,key} existieren.
# • Strikter CN-Check (Standard an) gegen ^agent-<app>(-[a-z0-9]+)?\.<env>\.privsec\.ch$
# 2) **Alternative: AppRole** — mit --auth approle wird eine eigene AppRole
# (<app>-pki-ca) erstellt und role_id/secret_id im Agent-Verzeichnis hinterlegt.
#
# 📦 YAML/Config
# 3) Liest ./config/apps.yaml und verwendet:
# environments.{vault_addr, vault_sni, pki_mount, proxy{user,chain_path,reload}}
# apps[].{name, proxy_user?, proxy_chain_path?, proxy_reload?, cert_map_name?}
#
# 🧩 Aufgabe dieses Agents
# 4) Periodisch **ISSUING CA (Intermediate)** aus ${PKI_MOUNT}/cert/ca lesen,
# mit **Root** zu einer **Kette** kombinieren und nach proxy.chain_path schreiben.
# 5) Post-Hook `vault-agent-post-chain.sh` triggert Reload via Label (z. B. podman).
#
# 🔐 Policies & Rollen
# 6) Erstellt/aktualisiert Policy **pki-ca-read-<app>** (Read auf <pki_mount>/cert/ca & Renew-Self).
# 7) Für **--auth approle** zusätzlich **pki-ca-bootstrap-<app>** (Role-/Secret-ID).
# 8) **NEU v4.8 bei auth/cert**:
# • Cert-Mapping **auth/cert/certs/<mapping>** wird **automatisch** um
# Policy **pki-ca-read-<app>** erweitert (idempotent).
# Mapping-Name: apps[].cert_map_name oder Default **agent-<app>**
# Bestehende Policies (z. B. pki-issue-<app>, marker-cert-auth) bleiben erhalten.
#
# 🌐 TLS ggü. Vault (Server-Verify)
# 9) vault { ca_cert = <Root-Kopie> ; tls_server_name = <vault_sni> }
# • Root-Kopie wird aus /home/vault/tls-<env>/root_ca.pem gespiegelt nach ~/<user>/vault/ca/ca.pem.
#
# 🧰 Verzeichnisse/Dateien
# 10) Agent unter ~/<user>/.vault-agent-<app>-ca/{vault-agent.hcl, ca.tpl, token, bin/...}
# Kette nach proxy.chain_path (z. B. /home/proxytest/nginx/ca/current-ca-chain.pem)
#
# ⚙️ Systemd (user)
# 11) Legt **~/.config/systemd/user/vault-agent-<app>-ca.service** an und enabled/started ihn.
#
# 🆕 v4.7/4.8 Additiv (bestehendes Verhalten unverändert)
# 12) **Force-Restart** nach Setup → sofort neues Token & frisches Rendering (analog App-Script).
# 13) **CLI-Token-Symlink** ~/.vault-token → konsistente CLI wie beim App-Agent.
# 14) **Kurze Statusausgabe** direkt nach dem Restart (ohne Pager).
# 15) Am Ende Hinweise: Logs & TLS-Verify.
# =============================================================================
#
# Exit-Codes: 2=Args/Config; 3=Linux-User fehlt; 4=Post-Hook fehlt; 5=mTLS fehlt/ungenügend
# ===== 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; }
# ===== Needs =====
need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
# ===== Defaults / Args =====
: "${LOG_LEVEL:=info}"
: "${DEFAULT_ENV:=test}"
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
: "${STRICT_CN_CHECK:=1}"
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""; AUTH_METHOD="cert" # cert|approle
TLS_SNI_OVERRIDE=""; CHAIN_PATH_OVERRIDE=""; RELOAD_LABEL_OVERRIDE=""
usage(){ sed -n '1,260p' "$0"; exit 2; }
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;;
--tls-server-name) TLS_SNI_OVERRIDE="$2"; shift 2;;
--chain-path) CHAIN_PATH_OVERRIDE="$2"; shift 2;;
--reload-label) RELOAD_LABEL_OVERRIDE="$2"; shift 2;;
-h|--help) usage;;
*) err "unknown arg: $1"; usage;;
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
# ===== Binaries =====
need vault; need jq; need python3; need install; need systemctl; need getent; need openssl
# ===== 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')"
VAULT_SNI_DEF="$(jqenv '.vault_sni')"; [[ "$VAULT_SNI_DEF" == "null" ]] && VAULT_SNI_DEF="vault.${ENV_NAME}.privsec.ch"
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" ]] || { err "incomplete env config: vault_addr/pki_mount"; exit 2; }
PROXY_USER_DEF="$(jqenv '.proxy.user')"
CHAIN_PATH_DEF="$(jqenv '.proxy.chain_path')"
RELOAD_DEF="$(jqenv '.proxy.reload')"
PROXY_USER_APP="$(jqapp '.proxy_user')"
CHAIN_PATH_APP="$(jqapp '.proxy_chain_path')"
RELOAD_APP="$(jqapp '.proxy_reload')"
CERT_MAP_NAME_YAML="$(jqapp '.cert_map_name')"
PROXY_USER="$PROXY_USER_DEF"; [[ "$PROXY_USER_APP" != "null" ]] && PROXY_USER="$PROXY_USER_APP"
[[ -n "$PROXY_USER" && "$PROXY_USER" != "null" ]] || { err "proxy.user fehlt in YAML"; exit 2; }
CHAIN_PATH="$CHAIN_PATH_DEF"; [[ "$CHAIN_PATH_APP" != "null" ]] && CHAIN_PATH="$CHAIN_PATH_APP"
[[ -n "$CHAIN_PATH_OVERRIDE" ]] && CHAIN_PATH="$CHAIN_PATH_OVERRIDE"
[[ -n "$CHAIN_PATH" && "$CHAIN_PATH" != "null" ]] || { err "proxy.chain_path fehlt in YAML/CLI"; exit 2; }
RELOAD_LABEL="${RELOAD_APP:-$RELOAD_DEF}"; [[ "$RELOAD_LABEL" == "null" || -z "$RELOAD_LABEL" ]] && RELOAD_LABEL="tls=true"
TLS_SNI="${TLS_SNI_OVERRIDE:-$VAULT_SNI_DEF}"
# Cert-Mapping-Name: YAML (cert_map_name) oder Default agent-<app>
CERT_MAPPING="agent-${APPN}"
if [[ -n "$CERT_MAP_NAME_YAML" && "$CERT_MAP_NAME_YAML" != "null" ]]; then
CERT_MAPPING="$CERT_MAP_NAME_YAML"
fi
export VAULT_ADDR
# ===== Helfer für Policies am Cert-Mapping =====
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(",")
'
}
ensure_cert_mapping_has_policy(){
local mapping="$1" pol="$2"
local j
if ! j="$(vault read -format=json "auth/cert/certs/${mapping}")"; then
warn "Cert-Mapping nicht gefunden: auth/cert/certs/${mapping} Policy ${pol} kann nicht angehängt werden"
return 0
fi
local merged
merged="$(merge_policies "$j")"
if [[ -z "$merged" ]]; then
merged="$pol"
elif grep -q -E "(^|,)${pol}(,|$)" <<<"$merged"; then
info "Cert-Mapping ${mapping}: Policy ${pol} bereits vorhanden"
return 0
else
merged="${merged},${pol}"
fi
# Zertifikat & allowed_common_names auslesen, damit wir sie beim write nicht verlieren
local crt allowed tmp
crt="$(printf '%s' "$j" | jq -r '.data.certificate // ""')"
allowed="$(printf '%s' "$j" | jq -r '(.data.allowed_common_names // []) | join(",")')"
if [[ -n "$crt" ]]; then
tmp="$(mktemp)"
printf '%s\n' "$crt" > "$tmp"
if [[ -n "$allowed" ]]; then
vault write "auth/cert/certs/${mapping}" \
certificate=@"$tmp" \
allowed_common_names="${allowed}" \
policies="${merged}" token_policies="${merged}" >/dev/null
else
vault write "auth/cert/certs/${mapping}" \
certificate=@"$tmp" \
policies="${merged}" token_policies="${merged}" >/dev/null
fi
rm -f "$tmp"
else
vault write "auth/cert/certs/${mapping}" \
policies="${merged}" token_policies="${merged}" >/dev/null
fi
ok "Cert-Mapping aktualisiert: auth/cert/certs/${mapping} (Policies: ${merged})"
}
# ===== User/Dirs/Paths =====
id -u "$PROXY_USER" >/dev/null 2>&1 || { err "Linux-User $PROXY_USER fehlt"; exit 3; }
HOME_DIR="/home/${PROXY_USER}"
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}-ca"
RID="${AGENT_DIR}/role_id"; SID="${AGENT_DIR}/secret_id"; TOKEN_FILE="${AGENT_DIR}/token"
BIN_DIR="${AGENT_DIR}/bin"; POST_LOCAL="${BIN_DIR}/vault-agent-post-chain.sh"
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}-ca.service"
# Trust (für Server-Verify): Root vom Vault-Host spiegeln
CA_DIR="${HOME_DIR}/vault/ca"; CA_FILE="${CA_DIR}/ca.pem"
ROOT_SRC="/home/vault/tls-${ENV_NAME}/root_ca.pem"
# ===== Ensure dirs & copy Root =====
sudo install -d -m 0700 -o "$PROXY_USER" -g "$PROXY_USER" "$AGENT_DIR"
sudo install -d -m 0755 -o "$PROXY_USER" -g "$PROXY_USER" "$BIN_DIR"
sudo install -d -m 0755 -o "$PROXY_USER" -g "$PROXY_USER" "$(dirname "$CHAIN_PATH")"
sudo install -d -m 0755 -o "$PROXY_USER" -g "$PROXY_USER" "$CA_DIR"
if [[ -r "$ROOT_SRC" ]]; then
sudo install -D -m 0644 -o "$PROXY_USER" -g "$PROXY_USER" "$ROOT_SRC" "$CA_FILE"
ok "Root CA gespiegelt → $CA_FILE"
else
warn "Root nicht gefunden: $ROOT_SRC → trage später manuell nach $CA_FILE ein"
fi
# ===== Policies =====
CA_POLICY_NAME="pki-ca-read-${APPN}"
TMP="$(mktemp)"
cat >"$TMP" <<EOF
path "$PKI_MOUNT/cert/ca" { capabilities=["read"] }
path "auth/token/renew-self" { capabilities=["update"] }
EOF
vault policy write "${CA_POLICY_NAME}" "$TMP" >/dev/null || true
# NEU v4.8: Cert-Mapping automatisch um CA-Policy erweitern (idempotent)
if [[ -n "$CERT_MAPPING" ]]; then
info "Cert-Mapping check → auth/cert/certs/${CERT_MAPPING} (+${CA_POLICY_NAME})"
ensure_cert_mapping_has_policy "$CERT_MAPPING" "$CA_POLICY_NAME" || true
fi
cat >"$TMP" <<EOF
path "auth/approle/role/${APPN}-pki-ca/role-id" { capabilities=["read"] }
path "auth/approle/role/${APPN}-pki-ca/secret-id" { capabilities=["create","update"] }
EOF
vault policy write "pki-ca-bootstrap-${APPN}" "$TMP" >/dev/null || true
rm -f "$TMP"
# ===== Auth: cert (strict) oder approle =====
AUTO_AUTH_BLOCK=""
TLS_EXTRA=$'\n tls_server_name = "'"${TLS_SNI}"$'"\n'
if [[ "$AUTH_METHOD" == "cert" ]]; then
MTLS_DIR="${HOME_DIR}/vault/mtls"
CRT="${MTLS_DIR}/agent.crt"; KEY="${MTLS_DIR}/agent.key"
if ! ( sudo -u "$PROXY_USER" test -s "$CRT" && sudo -u "$PROXY_USER" test -s "$KEY" ); then
err "Auth=cert gefordert, aber mTLS fehlt: ${MTLS_DIR}/agent.{crt,key}"; exit 5
fi
# Strikter CN-Check
CN_ACT="$(openssl x509 -in "$CRT" -noout -subject -nameopt RFC2253 | sed -n 's/^subject=//; s/.*CN=\([^,]*\).*/\1/p')"
EXP_RE="^agent-${APPN}(-[a-z0-9]+)?\\.${ENV_NAME}\\.privsec\\.ch$"
if [[ "$STRICT_CN_CHECK" == "1" ]]; then
if ! [[ "$CN_ACT" =~ $EXP_RE ]]; then
err "mTLS CN '$CN_ACT' passt nicht zu $EXP_RE"; exit 5
else
ok "mTLS CN ok: $CN_ACT"
fi
else
[[ "$CN_ACT" =~ $EXP_RE ]] && info "CN passt (tolerant)" || warn "CN abweichend (tolerant)"
fi
TLS_EXTRA+=$' client_cert = "'"${CRT}"$'"\n'
TLS_EXTRA+=$' client_key = "'"${KEY}"$'"\n'
AUTO_AUTH_BLOCK=$(cat <<'EOF'
auto_auth {
method "cert" { }
sink "file" { config = { path = "__TOKEN_FILE__", mode = 0400 } }
}
EOF
)
else
export VAULT_TOKEN="${VAULT_ADMIN_TOKEN:-${VAULT_TOKEN:-}}"
: "${VAULT_TOKEN:?VAULT_TOKEN oder VAULT_ADMIN_TOKEN erforderlich für --auth approle}"
vault auth enable approle >/dev/null 2>&1 || true
vault write "auth/approle/role/${APPN}-pki-ca" \
policies="${CA_POLICY_NAME}" 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/${APPN}-pki-ca/role-id")"
SID_VAL="$(vault write -f -field=secret_id "auth/approle/role/${APPN}-pki-ca/secret-id")"
sudo bash -c "umask 077; printf '%s' '${RID_VAL}' > '${RID}'; chown ${PROXY_USER}:${PROXY_USER} '${RID}'; chmod 600 '${RID}'"
sudo bash -c "umask 077; printf '%s' '${SID_VAL}' > '${SID}'; chown ${PROXY_USER}:${PROXY_USER} '${SID}'; chmod 600 '${SID}'"
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
# ===== HCL + Template =====
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}/ca.tpl"
destination = "${AGENT_DIR}/.ca.json"
command = "CHAIN_FILE='${CHAIN_PATH}' ROOT_FILE='${CA_FILE}' RELOAD_TLS_LABEL='${RELOAD_LABEL}' ${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 "$PROXY_USER" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<<"$HCL_CONTENT"
sudo -u "$PROXY_USER" tee "${AGENT_DIR}/ca.tpl" >/dev/null <<TPL
{{ with secret "${PKI_MOUNT}/cert/ca" }}
{ "issuing_ca": {{ toJSON .Data.certificate }} }
{{ end }}
TPL
# ===== Post-Hook bereitstellen =====
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
POST_SRC="${SCRIPT_DIR}/scripts/vault-agent-post-chain.sh"
if [[ -r "$POST_SRC" ]]; then
sudo /usr/bin/install -m 0755 -o "$PROXY_USER" -g "$PROXY_USER" -D "$POST_SRC" "$POST_LOCAL"
ok "post-hook installiert: ${POST_LOCAL}"
else
err "Post-Hook fehlt: $POST_SRC"; exit 4
fi
# ===== systemd (user) =====
sudo -u "$PROXY_USER" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
sudo -u "$PROXY_USER" tee "$UNIT" >/dev/null <<UNIT
[Unit]
Description=Vault Agent (${APPN}-ca) - refresh proxy CA chain (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
PROXY_UID="$(id -u "$PROXY_USER")"
loginctl enable-linger "$PROXY_USER" >/dev/null || true
systemctl start "user@${PROXY_UID}.service" >/dev/null || true
XDG_RUNTIME_DIR="/run/user/${PROXY_UID}"; mkdir -p "$XDG_RUNTIME_DIR"; chown "${PROXY_USER}:${PROXY_USER}" "$XDG_RUNTIME_DIR" || true
sudo -u "$PROXY_USER" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" systemctl --user daemon-reload
sudo -u "$PROXY_USER" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" systemctl --user enable --now "vault-agent-${APPN}-ca.service"
# 🆕 v4.7: Force-Restart & Status (sofort neues Token/Render, analog App-Script)
info "service restart (erzwinge Neustart des user-services)"
sudo -u "$PROXY_USER" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" systemctl --user restart "vault-agent-${APPN}-ca.service"
info "service status (kurz, ohne Pager)"
sudo -u "$PROXY_USER" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" systemctl --user --no-pager --full status "vault-agent-${APPN}-ca.service" || true
# 🆕 v4.7: Default-CLI-Token-Symlink (ergonomisch wie App-Agent)
sudo -u "$PROXY_USER" ln -sf "${TOKEN_FILE}" "${HOME_DIR}/.vault-token" || true
info "default CLI token symlinked → ${HOME_DIR}/.vault-token -> ${TOKEN_FILE}"
# ===== Quick result =====
if sudo -u "$PROXY_USER" test -s "$CHAIN_PATH"; then
ok "CA chain present: ${CHAIN_PATH}"
else
warn "CA chain not yet present → check: journalctl --user -u vault-agent-${APPN}-ca.service -e -n 60"
fi
cat <<OUT
Next steps / Checks:
• Logs: sudo -u ${PROXY_USER} journalctl --user -u vault-agent-${APPN}-ca.service -e -n 60
• TLS OK: openssl s_client -connect 127.0.0.1:22300 -servername ${TLS_SNI} \\
-CAfile ${CA_FILE} -brief </dev/null | sed -n '1,10p'
OUT