229 lines
10 KiB
Bash
Executable file
229 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
# setup-vault-agent-mtls-client-config-v4.5.sh
|
||
# Datum: 2025-09-29
|
||
|
||
# ========================= FEATURE MANIFEST (mTLS v4.5) ======================
|
||
# 🔒 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) Standard: CN=agent-<app>-<suffix>.<env>.privsec.ch (ttl=720h)
|
||
# • <suffix> via --cn-suffix (Default = <home-base>)
|
||
# b) Neu: 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>
|
||
# ✅ Neu: --mapping-policy <policy> (Default: pki-issue-<app>)
|
||
# - name des Mappings via --mapping-name (Default: <app>-<env>-<suffix> bzw. frei wählbar)
|
||
#
|
||
# 🔧 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> (default: == home-base),
|
||
# --cn <exact-cn> (übersteuert cn-suffix),
|
||
# --mapping-name <name> (default: <app>-<env>-<suffix>),
|
||
# --mapping-policy <policy> (default: pki-issue-<app>)
|
||
#
|
||
# 🚦 Exit-Codes
|
||
# 13) 2=Args/Env unvollständig; 3=Linux-User fehlt; 6=PKI-Issue fehlgeschlagen.
|
||
# ============================================================================
|
||
|
||
# ================================ EXAMPLES ==================================
|
||
# --- PKI (UNIX-User, z. B. /home/nctest/vault = PKI) ---
|
||
# • Standard (BASE=~/vault, Policy=pki-issue-<app>, Mapping=<app>-<env>-vault):
|
||
# sudo -E ./setup-vault-agent-mtls-client-config-v4.5.sh --app nctest --env test
|
||
# • BASE benennen & Suffix anpassen:
|
||
# sudo -E ./setup-vault-agent-mtls-client-config-v4.5.sh --app nctest --env test --home-base vault-pki --cn-suffix pki --mapping-name nctest-test-pki
|
||
#
|
||
# --- KV (Sidecar, eigenes Verzeichnis, exakter CN & KV-Policy) ---
|
||
# • Mapping / Policy für SECRET-AGENT:
|
||
# sudo -E ./setup-vault-agent-mtls-client-config-v4.5.sh \
|
||
# --app nctest --env test --home-base vault-sidecar \
|
||
# --cn "agent-nctest.test.privsec.ch" \
|
||
# --mapping-name "agent-nctest" \
|
||
# --mapping-policy "secret-agent-nctest-policy"
|
||
#
|
||
# Quick-Tests nach Lauf:
|
||
# • CRT anzeigen: openssl x509 -in ~/<BASE>/mtls/agent.crt -noout -subject -issuer -dates
|
||
# • Mapping check: vault read auth/cert/certs/<mapping-name>
|
||
# • Live-Verify: vault-tls-check --addr 127.0.0.1:22300 --sni vault.test.privsec.ch \
|
||
# --cafile ~/<BASE>/ca/ca.pem --mtls-cert ~/<BASE>/mtls/agent.crt --mtls-key ~/<BASE>/mtls/agent.key
|
||
# ============================================================================
|
||
|
||
# ===== 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"
|
||
CN_SUFFIX=""; CN_EXACT=""
|
||
MAPPING_NAME=""; MAPPING_POLICY=""
|
||
|
||
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) sed -n '1,300p' "$0"; exit 0;;
|
||
*) 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 =====
|
||
: "${CN_SUFFIX:=${HOME_BASE}}"
|
||
if [[ -n "$CN_EXACT" ]]; then
|
||
CN_AGENT="$CN_EXACT"
|
||
else
|
||
CN_AGENT="agent-${APPN}-${CN_SUFFIX}.${ENV_NAME}.privsec.ch"
|
||
fi
|
||
: "${MAPPING_NAME:=${APPN}-${ENV_NAME}-${CN_SUFFIX}}"
|
||
: "${MAPPING_POLICY:=pki-issue-${APPN}}"
|
||
|
||
ROLE="agent-mtls-${APPN}-${CN_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"
|
||
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})"
|
||
|
||
# ===== 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.5)"
|
||
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}"
|
||
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=\"https://127.0.0.1:22300\" VAULT_CACERT=\"${CA_FILE}\" \\"
|
||
echo " vault login -method=cert -client-cert \"${CRT}\" -client-key \"${KEY}\""
|
||
|