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

249 lines
11 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-mtls-client-config-v4.7.sh
# Datum: 2025-10-01
#
# ========================= FEATURE MANIFEST (mTLS v4.7) ======================
# 🔒 Zweck / Sicherheit
# 1) Bereitet STRICT mTLS-Login für den Vault-Agent vor (auth/cert).
# 2) Keine AppRole, kein Fallback dieses Script erzeugt NUR Client-mTLS & Mapping.
#
# 📦 Konfiguration / YAML
# 3) Liest ./config/apps.yaml:
# environments.<env>.pki_mount
# environments.<env>.app.user (Default-App-User)
# apps[].{name, user?} (per-App User-Override)
#
# 🧩 Pfade (kompatibel zu Agent v4.2+)
# 4) Schreibt Login-mTLS nach: ~/<BASE>/mtls/{agent.crt,agent.key} (BASE=default "vault")
# 5) Schreibt Vault-TLS-CA nach: ~/<BASE>/ca/ca.pem
# ✅ BASE via --home-base <ordner> wählbar (z. B. "vault-sidecar" oder "vault-pki").
#
# 🛠️ PKI / Client-Zert
# 6) Erzwingt/aktualisiert CLIENT-Rolle (client_flag=true, server_flag=false, EC P-256).
# 7) Stellt Client-Zert aus:
# a) **NEU Default (v4.7):** CN=agent-<app>.<env>.privsec.ch (ohne Suffix), ttl=720h
# • Optionaler Suffix via --cn-suffix → CN=agent-<app>-<suffix>.<env>.privsec.ch
# b) EXAKTER CN via --cn "<wert>" (übersteuert a)
#
# 🔐 Vault auth/cert (Mapping)
# 8) Aktiviert auth/cert (idempotent) und legt Mapping an:
# - certificate = Issuing CA der Client-Rolle
# - allowed_common_names = CN
# - policies = <mapping-policy> (Default: pki-issue-<app>)
# - name des Mappings via --mapping-name (Default **NEU**: agent-<app>)
# - Fügt automatisch Marker-Policy **marker-cert-auth** hinzu (Observability)
#
# 🔧 Ownership / Permissions
# 9) KEY 0600, CRT 0644, CA 0644; Owner = Linux-User aus YAML.
#
# 🧪 Ausgabe / Checks
# 10) OpenSSL-Checks (subject/issuer/dates) + farbige Zusammenfassung.
# 11) Warnung, falls Policy pki-issue-<app> noch nicht existiert (Agent-Setup erzeugt sie i.d.R.).
#
# 🧰 CLI-Flags / Defaults
# 12) Flags: --env (default: test), --config (default: ./config/apps.yaml),
# --app (erforderlich), --pki-mount (optional Override),
# --home-base <ordner> (default: vault),
# --cn-suffix <suffix> (**NEU Default leer**; v4.5 hatte default==home-base),
# --cn <exact-cn> (übersteuert),
# --mapping-name <name> (**NEU Default: agent-<app>**),
# --mapping-policy <policy> (default: pki-issue-<app>)
#
# 🚦 Exit-Codes
# 13) 2=Args/Env unvollständig; 3=Linux-User fehlt; 6=PKI-Issue 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; }
# ===== Dependencies =====
need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
need vault; need jq; need python3; need install; need openssl
# ===== Defaults / Args =====
: "${DEFAULT_ENV:=test}"
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
: "${DEFAULT_HOME_BASE:=vault}"
[[ -n "${VAULT_ADDR:-}" && -n "${VAULT_TOKEN:-}" ]] || { err "VAULT_ADDR/VAULT_TOKEN required"; exit 2; }
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
PKI_MOUNT_OVERRIDE=""; HOME_BASE="$DEFAULT_HOME_BASE"
# **NEU**: CN-Suffix standardmäßig leer (vorher == home-base)
CN_SUFFIX=""
CN_EXACT=""
# **NEU**: Mapping-Name default agent-<app>
MAPPING_NAME=""
MAPPING_POLICY=""
usage(){ sed -n '1,220p' "$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;;
--pki-mount) PKI_MOUNT_OVERRIDE="$2"; shift 2;;
--home-base) HOME_BASE="$2"; shift 2;;
--cn-suffix) CN_SUFFIX="$2"; shift 2;;
--cn) CN_EXACT="$2"; shift 2;;
--mapping-name) MAPPING_NAME="$2"; shift 2;;
--mapping-policy) MAPPING_POLICY="$2"; shift 2;;
-h|--help) usage;;
*) err "unknown arg: $1"; exit 2;;
esac
done
[[ -n "$APPN" ]] || { err "--app required"; exit 2; }
[[ -n "$HOME_BASE" ]] || { err "--home-base must be non-empty"; exit 2; }
case "$HOME_BASE" in /*|*/*|*..*) err "--home-base must be a simple name under the user's home (no '/', no '..')"; exit 2;; esac
# ===== 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"; }
PKI_MOUNT="${PKI_MOUNT_OVERRIDE:-$(jqenv '.pki_mount')}"
APP_USER_DEF="$(jqenv '.app.user')"
APP_USER_APP="$(jqapp '.user')"
APP_USER="$APP_USER_DEF"; [[ "$APP_USER_APP" != "null" ]] && APP_USER="$APP_USER_APP"
[[ "$PKI_MOUNT" != "null" && -n "$PKI_MOUNT" ]] || { err "pki_mount missing (YAML/env or --pki-mount)"; exit 2; }
[[ -n "$APP_USER" && "$APP_USER" != "null" ]] || { err "app user missing (YAML)"; exit 2; }
id -u "$APP_USER" >/dev/null 2>&1 || { err "Linux-User $APP_USER fehlt"; exit 3; }
# ===== Pfade (BASE-Override) =====
HOME_DIR="/home/${APP_USER}"
BASE_DIR="${HOME_DIR}/${HOME_BASE}"
MTLS_DIR="${BASE_DIR}/mtls"
CA_DIR="${BASE_DIR}/ca"
CRT="${MTLS_DIR}/agent.crt"
KEY="${MTLS_DIR}/agent.key"
CA_FILE="${CA_DIR}/ca.pem"
sudo install -d -m 0755 -o "$APP_USER" -g "$APP_USER" "$BASE_DIR" "$CA_DIR" "$MTLS_DIR"
info "BASE=${BASE_DIR} MTLS=${MTLS_DIR} CA=${CA_DIR}"
# ===== CN / Mapping Defaults (NEU Defaults) =====
# - CN default: agent-<app>.<env>.privsec.ch (ohne Suffix)
# - Mapping-Name default: agent-<app>
# - Mapping-Policy default: pki-issue-<app>
: "${MAPPING_NAME:=agent-${APPN}}"
: "${MAPPING_POLICY:=pki-issue-${APPN}}"
# CN mit optionalem Suffix oder exakt
if [[ -n "$CN_EXACT" ]]; then
CN_AGENT="$CN_EXACT"
else
SUF=""; [[ -n "$CN_SUFFIX" ]] && SUF="-$CN_SUFFIX"
CN_AGENT="agent-${APPN}${SUF}.${ENV_NAME}.privsec.ch"
fi
# Rollen-Name folgt der Suffix-Logik (nur kosmetisch); v4.5-Kompat: agent-mtls-<app>[-<suffix>]
ROLE_SUFFIX=""; [[ -n "$CN_SUFFIX" ]] && ROLE_SUFFIX="-$CN_SUFFIX"
ROLE="agent-mtls-${APPN}${ROLE_SUFFIX}"
info "CN=${CN_AGENT} role=${PKI_MOUNT}/roles/${ROLE} mapping=auth/cert/certs/${MAPPING_NAME} policy=${MAPPING_POLICY}"
# ===== PKI-Clientrolle & Zertifikat =====
info "Upsert PKI client role…"
vault write "${PKI_MOUNT}/roles/${ROLE}" \
allowed_domains="${CN_AGENT}" \
allow_bare_domains=true \
allow_subdomains=false \
server_flag=false client_flag=true \
key_type="ec" key_bits=256 max_ttl="720h" >/dev/null || true
info "Issue client certificate…"
ISSUE_JSON="$(vault write -format=json "${PKI_MOUNT}/issue/${ROLE}" common_name="${CN_AGENT}" ttl=720h)" \
|| { err "PKI issue failed (role=${ROLE})"; exit 6; }
# Dateien schreiben (Owner, Rechte)
echo "$ISSUE_JSON" | jq -r '.data.private_key' | sudo install -m 0600 -o "$APP_USER" -g "$APP_USER" /dev/stdin "$KEY"
echo "$ISSUE_JSON" | jq -r '.data.certificate' | sudo install -m 0644 -o "$APP_USER" -g "$APP_USER" /dev/stdin "$CRT"
echo "$ISSUE_JSON" | jq -r '.data.issuing_ca' | sudo install -m 0644 -o "$APP_USER" -g "$APP_USER" /dev/stdin "$CA_FILE" || true
ok "mTLS material written → KEY=${KEY} CRT=${CRT} CA=${CA_FILE}"
# ===== auth/cert aktivieren & Mapping setzen =====
info "Enable auth/cert & upsert mapping…"
vault auth enable cert >/dev/null 2>&1 || true
TMP="$(mktemp)"; jq -r '.data.issuing_ca' <<<"$ISSUE_JSON" > "$TMP"
# 1) Erstschreib: nur Standard-Policy (wie bisher)
vault write "auth/cert/certs/${MAPPING_NAME}" \
certificate=@"$TMP" \
allowed_common_names="${CN_AGENT}" \
policies="${MAPPING_POLICY}" \
token_ttl="24h" >/dev/null
rm -f "$TMP"
ok "Mapping updated → auth/cert/certs/${MAPPING_NAME} (policies=${MAPPING_POLICY})"
# ===== Marker-Policy automatisch hinzufügen (Observability) =====
# - idempotent: existiert sie nicht, wird sie minimal angelegt
# - anschließend wird das Mapping um marker-cert-auth erweitert (Union)
if ! vault policy read marker-cert-auth >/dev/null 2>&1; then
cat > /tmp/marker-cert-auth.hcl <<'EOF'
# marker-cert-auth — harmlose Marker-Policy (für Audit/Scanner)
path "sys/capabilities-self" { capabilities = ["update"] }
EOF
vault policy write marker-cert-auth /tmp/marker-cert-auth.hcl >/dev/null
ok "Marker-Policy erstellt → marker-cert-auth"
fi
# bestehende Policies am Mapping holen + vereinigen
EXIST_POLS="$(vault read -format=json "auth/cert/certs/${MAPPING_NAME}" | jq -r '(.data.policies // []) | join(",")')"
# Union bilden (bestehend + mapping_policy + marker-cert-auth)
NEW_POLS="$(printf '%s\n%s\n%s\n' "$EXIST_POLS" "$MAPPING_POLICY" "marker-cert-auth" \
| tr ',' '\n' | sed '/^$/d' | awk '!seen[$0]++' | paste -sd',' -)"
info "Mapping-Policies erweitern → ${NEW_POLS}"
TMP2="$(mktemp)"; jq -r '.data.certificate' <<<"$(vault read -format=json "auth/cert/certs/${MAPPING_NAME}")" > "$TMP2"
vault write "auth/cert/certs/${MAPPING_NAME}" \
certificate=@"$TMP2" \
allowed_common_names="${CN_AGENT}" \
policies="${NEW_POLS}" >/dev/null
rm -f "$TMP2"
ok "Marker-Policy hinzugefügt → auth/cert/certs/${MAPPING_NAME} (policies=${NEW_POLS})"
# ===== Hinweis, falls Standard-Policy noch nicht existiert =====
if [[ "$MAPPING_POLICY" == "pki-issue-${APPN}" ]] && ! vault policy read "pki-issue-${APPN}" >/dev/null 2>&1; then
warn "Policy pki-issue-${APPN} existiert noch nicht das Agent-Setup v4.2+ erzeugt sie i.d.R. automatisch."
fi
# ===== Zusammenfassung / Checks =====
echo
ok "Certificate summary:"
openssl x509 -in "$CRT" -noout -subject -issuer -dates | sed 's/^/ 🔎 /'
if [[ -s "$CA_FILE" ]]; then
info "CA bundle peek:"
( set +e; openssl x509 -in "$CA_FILE" -noout -subject -issuer -enddate 2>/dev/null | sed 's/^/ 📜 /'; true )
fi
echo
ok "SUCCESS (v4.7)"
echo " User: ${APP_USER}"
echo " BASE: ${BASE_DIR}"
echo " CN: ${CN_AGENT}"
echo " Role: ${PKI_MOUNT}/roles/${ROLE}"
echo " Mapping: auth/cert/certs/${MAPPING_NAME}"
echo " Policy: ${MAPPING_POLICY} (+ marker-cert-auth)"
echo " Files: ${KEY} (0600), ${CRT} (0644), ${CA_FILE} (0644)"
echo
info "Next:"
echo " • Mount BASE for container if needed: -v ${BASE_DIR}:${BASE_DIR}:ro"
echo " • Test login via mTLS (example):"
echo " VAULT_ADDR=\"${VAULT_ADDR}\" VAULT_CACERT=\"${CA_FILE}\" \\"
echo " vault login -method=cert -client-cert \"${CRT}\" -client-key \"${KEY}\""