249 lines
11 KiB
Bash
Executable file
249 lines
11 KiB
Bash
Executable file
#!/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}\""
|
||
|