vault-ops/infra/versions/bootstrap-secret-agent-v4.2.sh
2026-04-14 11:45:15 +07:00

288 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 -euo pipefail
# ==============================================================================
# Bootstrap Secret Agent v4.2 (with auth/cert mapping for mTLS)
#
# =========================
# >>> FEATURE CHECKLIST <<<
# =========================
# 1) Idempotently enables a KV v2 secrets engine at a configurable mount
# (default: "kv").
# 2) Creates/updates a policy <ROLE>-policy granting:
# - read on <KV_MOUNT>/data/<SUBPATH>
# - read,list on <KV_MOUNT>/metadata/<SUBPATH>
# 3) Creates/updates an AppRole <ROLE> (role name: secret-agent-<APPUSER>)
# with token TTLs (token_ttl, token_max_ttl).
# 4) Fetches ROLE_ID and SECRET_ID and installs them to
# /home/<APPUSER>/vault/creds/{role_id,secret_id} (0400), directory 0700.
# 5) Optionally seeds KV at <KV_MOUNT>/<SUBPATH> with example values if absent.
# 6) NEW: Creates/updates an auth/cert mapping at
# auth/cert/certs/<CERT_MAP_NAME> that assigns the same KV policy (plus
# any extra policies) to mTLS-authenticated clients
# (requires your *Client CA chain* and the allowed Common Name).
# 7) All upserts are idempotent (enables engines/auth methods only if missing
# and overwrites/refreshes policies/roles/cert-maps as needed).
# 8) Prints a final summary (paths/files/policies) so you can verify easily.
#
# =========================
# WHY THIS MATTERS
# =========================
# - Your *container* Vault Agent authenticates via mTLS (auth/cert), NOT AppRole.
# - Without a cert mapping that attaches your KV policy to the mTLS login,
# the agent gets a token with no KV rights -> 403 on kv/data/<subpath>.
# - This script ensures your mTLS login (client cert CN) is mapped to the same
# policy as your AppRole, so both paths (AppRole + mTLS) behave consistently.
#
# =========================
# IMPORTANT CA NOTES
# =========================
# - VAULT_CACERT (env) is for the *Vault CLI* to verify the *Vault SERVER* TLS cert.
# That is the server trust chain (e.g., your public enterprise CA or internal CA that
# issued "vault.test.privsec.ch").
# - --client-ca-chain (flag) is the *CLIENT* CA CHAIN used to issue your AGENT certs
# (agent-<app>.<env>.privsec.ch). This is NOT the Vault server cert. It must contain
# the issuing chain of your agent client certificates (Root + Intermediate(s)).
# - --allowed-cn should match the subject CN your agent client cert actually uses.
#
# Keep this header intact and review it whenever you change the script. ✅
# ==============================================================================
# -------------------------
# Usage & Flags
# -------------------------
usage() {
cat >&2 <<'EOF'
Usage:
sudo -E ./bootstrap-secret-agent.sh <APPUSER>
[--kv-subpath SUB] [--kv-mount MOUNT]
[--ttl 15m] [--max-ttl 30m]
[--cert-map-name NAME]
[--client-ca-chain /path/chain.pem]
[--allowed-cn agent-<app>.test.privsec.ch]
[--no-approle] [--no-cert]
[--extra-policies "p1,p2"]
[--no-seed]
Required env:
VAULT_ADDR e.g. https://127.0.0.1:22300
VAULT_TOKEN installer/root/approver token
Optional env:
VAULT_CACERT trust chain for the *Vault SERVER* TLS cert verification
VAULT_NAMESPACE (Enterprise/HCP)
Notes:
- --client-ca-chain is the *client* CA chain (Root+Intermediates) that issued
your Agent mTLS client certs. Do NOT confuse it with the Vault server CA.
- --allowed-cn must match the CN in your Agent's client certificate.
- Both AppRole and cert mapping can be enabled/disabled independently.
EOF
exit 2
}
[[ $# -ge 1 ]] || usage
APPUSER="$1"; shift || true
# -------------------------
# Defaults
# -------------------------
KV_MOUNT="kv"
KV_SUBPATH="$APPUSER"
TOKEN_TTL="15m"
TOKEN_MAX_TTL="30m"
WITH_APPROLE=1
WITH_CERT=1
DO_SEED=1
EXTRA_POLICIES="" # e.g., "pki-issue-nctest-policy"
ROLE_NAME="secret-agent-${APPUSER}"
POLICY_NAME="${ROLE_NAME}-policy"
CERT_MAP_NAME="agent-${APPUSER}" # path: auth/cert/certs/<CERT_MAP_NAME>
CLIENT_CA_CHAIN="" # client CA chain file (Root+Intermediates)
ALLOWED_CN="" # CN used by the Agent client certificate
# -------------------------
# Parse flags (simple)
# -------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--kv-subpath) KV_SUBPATH="${2:?}"; shift 2;;
--kv-mount) KV_MOUNT="${2:?}"; shift 2;;
--ttl) TOKEN_TTL="${2:?}"; shift 2;;
--max-ttl) TOKEN_MAX_TTL="${2:?}"; shift 2;;
--cert-map-name) CERT_MAP_NAME="${2:?}"; shift 2;;
--client-ca-chain) CLIENT_CA_CHAIN="${2:?}"; shift 2;;
--allowed-cn) ALLOWED_CN="${2:?}"; shift 2;;
--extra-policies) EXTRA_POLICIES="${2:?}"; shift 2;;
--no-approle) WITH_APPROLE=0; shift;;
--no-cert) WITH_CERT=0; shift;;
--no-seed) DO_SEED=0; shift;;
-h|--help) usage;;
*) echo "Unknown option: $1" >&2; usage;;
esac
done
# -------------------------
# Environment & helpers
# -------------------------
: "${VAULT_ADDR:?Set VAULT_ADDR (e.g. https://127.0.0.1:22300)}"
: "${VAULT_TOKEN:?Set VAULT_TOKEN}"
: "${VAULT_NAMESPACE:=}"
export VAULT_ADDR VAULT_TOKEN VAULT_NAMESPACE
need(){ command -v "$1" >/dev/null 2>&1 || { echo "Missing binary: $1" >&2; exit 1; }; }
need vault; need getent; need sudo; need install
HOMEDIR="$(getent passwd "$APPUSER" | cut -d: -f6 || true)"
[[ -n "$HOMEDIR" ]] || HOMEDIR="/home/${APPUSER}"
CREDS_DIR="${HOMEDIR}/vault/creds"
info() { printf '==> %s\n' "$*"; }
note() { printf ' %s\n' "$*"; }
warn() { printf '!! WARN: %s\n' "$*" >&2; }
ok() { printf '✔ %s\n' "$*"; }
# -------------------------
# Plan overview (echo inputs)
# -------------------------
info "Vault: ${VAULT_ADDR}"
[[ -n "${VAULT_CACERT:-}" ]] && note "CLI Trust (Vault SERVER CA): ${VAULT_CACERT}"
[[ -n "${VAULT_NAMESPACE}" ]] && note "Namespace: ${VAULT_NAMESPACE}"
note "APPUSER: ${APPUSER}"
note "KV path: ${KV_MOUNT}/data/${KV_SUBPATH}"
note "ROLE: ${ROLE_NAME}"
note "POLICY: ${POLICY_NAME}"
note "TTLs: token_ttl=${TOKEN_TTL}, token_max_ttl=${TOKEN_MAX_TTL}"
if (( WITH_CERT )); then
note "CERTMAP: auth/cert/certs/${CERT_MAP_NAME}"
[[ -n "$CLIENT_CA_CHAIN" ]] && note "Client CA chain: ${CLIENT_CA_CHAIN}"
[[ -n "$ALLOWED_CN" ]] && note "Allowed CN: ${ALLOWED_CN}"
fi
[[ -n "$EXTRA_POLICIES" ]] && note "Extra policies: ${EXTRA_POLICIES}"
echo
# ------------------------------------------------------------------------------
# 1) Enable KV v2 (idempotent)
# ------------------------------------------------------------------------------
info "Enable KV v2 (idempotent) at: ${KV_MOUNT}"
vault secrets enable -path="${KV_MOUNT}" kv-v2 >/dev/null 2>&1 || true
# ------------------------------------------------------------------------------
# 2) Write policy (read data + read/list metadata)
# ------------------------------------------------------------------------------
info "Write/Upsert policy ${POLICY_NAME} (READ on data, READ/LIST on metadata)…"
POL="$(mktemp)"
cat >"$POL" <<EOF
path "${KV_MOUNT}/data/${KV_SUBPATH}" {
capabilities = ["read"]
}
path "${KV_MOUNT}/metadata/${KV_SUBPATH}" {
capabilities = ["read", "list"]
}
EOF
vault policy write "${POLICY_NAME}" "$POL" >/dev/null
rm -f "$POL"
POLICIES="${POLICY_NAME}"
if [[ -n "$EXTRA_POLICIES" ]]; then
POLICIES="${POLICIES},${EXTRA_POLICIES}"
fi
# ------------------------------------------------------------------------------
# 3) AppRole (optional) → for host/user-side agents or bootstrap
# ------------------------------------------------------------------------------
if (( WITH_APPROLE )); then
info "Enable auth method: approle (idempotent)…"
vault auth enable approle >/dev/null 2>&1 || true
info "Upsert AppRole ${ROLE_NAME} (policies: ${POLICIES})…"
vault write "auth/approle/role/${ROLE_NAME}" \
policies="${POLICIES}" \
secret_id_ttl=0 \
secret_id_num_uses=0 \
token_ttl="${TOKEN_TTL}" \
token_max_ttl="${TOKEN_MAX_TTL}" >/dev/null
info "Fetch ROLE_ID and SECRET_ID…"
ROLE_ID="$(vault read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
SECRET_ID="$(vault write -field=secret_id -f "auth/approle/role/${ROLE_NAME}/secret-id")"
info "Install creds to: ${CREDS_DIR}"
sudo install -d -o "${APPUSER}" -g "${APPUSER}" -m 0700 "${CREDS_DIR}"
printf "%s" "${ROLE_ID}" | sudo install -o "${APPUSER}" -g "${APPUSER}" -m 0400 /dev/stdin "${CREDS_DIR}/role_id"
printf "%s" "${SECRET_ID}" | sudo install -o "${APPUSER}" -g "${APPUSER}" -m 0400 /dev/stdin "${CREDS_DIR}/secret_id"
# quiet ls just to ensure path exists (no output spam)
sudo ls -l "${CREDS_DIR}" >/dev/null || true
fi
# ------------------------------------------------------------------------------
# 4) Cert auth mapping (optional) → for container-side mTLS agent
# ------------------------------------------------------------------------------
if (( WITH_CERT )); then
info "Enable auth method: cert (idempotent)…"
vault auth enable cert >/dev/null 2>&1 || true
if [[ -z "$CLIENT_CA_CHAIN" || -z "$ALLOWED_CN" ]]; then
warn "--client-ca-chain and/or --allowed-cn not set → SKIPPING cert mapping."
warn "mTLS agent will authenticate, but without a mapping it won't get the KV policy (403)."
else
# Basic validation hints (non-fatal)
[[ -f "$CLIENT_CA_CHAIN" ]] || warn "Client CA chain file not found: $CLIENT_CA_CHAIN"
info "Upsert auth/cert/certs/${CERT_MAP_NAME} (policies: ${POLICIES})…"
vault write "auth/cert/certs/${CERT_MAP_NAME}" \
display_name="${CERT_MAP_NAME}" \
certificate=@"${CLIENT_CA_CHAIN}" \
allowed_common_names="${ALLOWED_CN}" \
policies="${POLICIES}" >/dev/null
# Optional: You could add token_ttl/token_max_ttl on the cert mapping too.
# (Keeping default behavior here to avoid unexpected interactions.)
fi
fi
# ------------------------------------------------------------------------------
# 5) Seed KV (only if absent)
# ------------------------------------------------------------------------------
if (( DO_SEED )); then
if ! vault kv get "${KV_MOUNT}/${KV_SUBPATH}" >/dev/null 2>&1; then
info "Seed secrets at ${KV_MOUNT}/${KV_SUBPATH}"
vault kv put "${KV_MOUNT}/${KV_SUBPATH}" \
mariadb_root_password='CHANGE_ME_ROOT' \
mysql_password='CHANGE_ME_APP' >/dev/null
note "- Added example secrets (change later)."
else
note "- KV already exists at ${KV_MOUNT}/${KV_SUBPATH} seed skipped."
fi
fi
# ------------------------------------------------------------------------------
# 6) Summary
# ------------------------------------------------------------------------------
echo
ok "Done."
note "Role: ${ROLE_NAME} $([[ $WITH_APPROLE -eq 1 ]] && echo '(AppRole enabled)' || echo '(AppRole disabled)')"
note "Policy: ${POLICY_NAME}"
note "Policies: ${POLICIES}"
note "KV Path: ${KV_MOUNT}/${KV_SUBPATH}"
if (( WITH_APPROLE )); then
note "Creds: ${CREDS_DIR}/{role_id,secret_id}"
fi
if (( WITH_CERT )); then
note "Cert map: auth/cert/certs/${CERT_MAP_NAME} (allowed_cn=${ALLOWED_CN:-<unset>})"
[[ -n "$CLIENT_CA_CHAIN" ]] && note "Client-CA: ${CLIENT_CA_CHAIN}"
fi
# EOF