288 lines
11 KiB
Bash
Executable file
288 lines
11 KiB
Bash
Executable file
#!/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
|