289 lines
12 KiB
Bash
Executable file
289 lines
12 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
# setup-vault-agent-app-config_v3.sh
|
|
# Version: v3 (2025-09-28)
|
|
#
|
|
# Zweck:
|
|
# Für eine App (z.B. nctest/apptest) einen Vault Agent (user systemd) einrichten,
|
|
# der regelmäßig ein LEAF-Zertifikat aus PKI ausstellt & rotiert.
|
|
# - Liest ./config/apps.yaml (envs + apps)
|
|
# - Upsert PKI-Role mit korrekten allowed_domains (aus internal_cn + external_host_*)
|
|
# - Erstellt Policies + AppRole (periodischer Service-Token)
|
|
# - Schreibt role_id/secret_id unter ~/.vault-agent-<app>
|
|
# - Vault-Agent HCL + Template (inkl. alt_names bei ext host)
|
|
# - nutzt Post-Hook (scripts/vault-agent-post-leaf.sh) → schreibt <HOME>/tls/<app>.{key,fullchain.pem}
|
|
#
|
|
# Anforderungen: vault, jq, python3, openssl, systemctl, install
|
|
#
|
|
# Nutzung:
|
|
# VAULT_ADMIN_TOKEN=hvs.XXX \
|
|
# sudo -E ./setup-vault-agent-app-config_v3.sh --env test --config ./config/apps.yaml --app nctest
|
|
#
|
|
# Wichtige Env/Flags:
|
|
# --env test|prod (Default: test)
|
|
# --config <pfad> (Default: ./config/apps.yaml)
|
|
# --app <name> (Pflicht)
|
|
# --tls-server-name STR (Override SNI/Hostname für TLS; Default: vault.test.local)
|
|
# --pki-role NAME (Default: nginx-<app>)
|
|
# --reload-label STR (Env für Post-Hook; Default: "tls=true")
|
|
# LOG_LEVEL=info|debug (Default: info)
|
|
|
|
# ===== 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; }
|
|
|
|
# ===== Defaults / Args =====
|
|
: "${LOG_LEVEL:=info}"
|
|
: "${DEFAULT_ENV:=test}"
|
|
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
|
|
: "${TLS_SERVER_NAME:=vault.test.privsec.ch}" # kann via --tls-server-name übersteuert werden
|
|
: "${RELOAD_LABEL:=tls=true}" # env für Post-Hook (Container-Reload via Label)
|
|
|
|
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
|
|
PKI_ROLE_OVERRIDE=""; TLS_SERVER_NAME_OVERRIDE=""; RELOAD_LABEL_OVERRIDE=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--env) ENV_NAME="$2"; shift 2;;
|
|
--config) CFG="$2"; shift 2;;
|
|
--app) APPN="$2"; shift 2;;
|
|
--pki-role) PKI_ROLE_OVERRIDE="$2"; shift 2;;
|
|
--tls-server-name) TLS_SERVER_NAME_OVERRIDE="$2"; shift 2;;
|
|
--reload-label) RELOAD_LABEL_OVERRIDE="$2"; shift 2;;
|
|
-h|--help)
|
|
sed -n '1,200p' "$0"; exit 0;;
|
|
*) err "unknown arg: $1"; exit 2;;
|
|
esac
|
|
done
|
|
[[ -n "$APPN" ]] || { err "--app ist erforderlich"; exit 2; }
|
|
[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN fehlt (exportieren!)"; exit 2; }
|
|
|
|
# ===== Binaries / needs =====
|
|
need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
|
|
need vault; need jq; need python3; need openssl; need install; need systemctl
|
|
|
|
# ===== YAML laden =====
|
|
CFG_ABS="$(readlink -f "$CFG")" || { err "cannot resolve $CFG"; exit 2; }
|
|
CFGJSON="$(python3 - <<PY
|
|
import yaml, json, sys
|
|
with open("$CFG_ABS","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"; }
|
|
|
|
VAULT_ADDR="$(jqenv '.vault_addr')"
|
|
PKI_MOUNT="$(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"
|
|
|
|
CN="$(jqapp '.internal_cn')"
|
|
TTL="$(jqapp '.issue_ttl')"
|
|
EXT_TEST="$(jqapp '.external_host_test')"; [[ "$EXT_TEST" == "null" ]] && EXT_TEST=""
|
|
EXT_PROD="$(jqapp '.external_host_prod')"; [[ "$EXT_PROD" == "null" ]] && EXT_PROD=""
|
|
EXT_HOST="$EXT_TEST"; [[ "$ENV_NAME" == "prod" ]] && EXT_HOST="$EXT_PROD"
|
|
|
|
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" && "$CN" != "null" && "$TTL" != "null" && -n "$APP_USER" ]] \
|
|
|| { err "incomplete config in apps.yaml (vault_addr/pki_mount/app.user/internal_cn/issue_ttl)"; exit 2; }
|
|
|
|
[[ -n "$EXT_HOST" ]] || warn "kein external_host_* für ${APPN} in ${ENV_NAME} (SAN bleibt leer → nur CN)"
|
|
|
|
PKI_ROLE="${PKI_ROLE_OVERRIDE:-nginx-${APPN}}"
|
|
TLS_SERVER_NAME="${TLS_SERVER_NAME_OVERRIDE:-$TLS_SERVER_NAME}"
|
|
RELOAD_LABEL="${RELOAD_LABEL_OVERRIDE:-$RELOAD_LABEL}"
|
|
|
|
export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"
|
|
|
|
# ===== Helpers (v3) ====
|
|
#
|
|
base_domain() {
|
|
local h="${1:-}"
|
|
if [[ -z "$h" || "$h" != *.* ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
# entfernt nur das erste Label inkl. Punkt
|
|
printf "%s\n" "${h#*.}" # z.B. nctest.int.privsec.ch -> int.privsec.ch
|
|
}
|
|
uniq_csv(){ tr ',' '\n' | awk 'NF' | sort -u | paste -sd, -; }
|
|
derive_allowed_domains(){ local cn="$1" ext="$2"; printf '%s\n%s\n' "$(base_domain "$cn")" "$(base_domain "$ext")" | uniq_csv; }
|
|
ensure_pki_role(){
|
|
local mount="$1" role="$2" doms="$3" max="${4:-720h}"
|
|
if [[ -z "$doms" ]]; then doms="$(derive_allowed_domains "$CN" "")"; fi
|
|
info "upsert PKI role ${role} (allowed_domains=${doms})"
|
|
vault write "${mount}/roles/${role}" \
|
|
allowed_domains="${doms}" allow_subdomains=true allow_bare_domains=true \
|
|
allow_wildcard_certificates=false server_flag=true client_flag=false \
|
|
key_type="ec" key_bits=256 max_ttl="${max}" >/dev/null
|
|
}
|
|
|
|
# ===== User/Dirs/Paths =====
|
|
id -u "${APP_USER}" >/dev/null 2>&1 || { err "Linux-User ${APP_USER} fehlt"; exit 3; }
|
|
HOME_DIR="/home/${APP_USER}"
|
|
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}"
|
|
TLS_DIR="${HOME_DIR}/tls"
|
|
CA_DIR="${HOME_DIR}/vault/ca"
|
|
CA_FILE="${CA_DIR}/ca.pem" # Root oder Chain (dein Distribute-Skript)
|
|
RID="${AGENT_DIR}/role_id"
|
|
SID="${AGENT_DIR}/secret_id"
|
|
TOKEN_FILE="${AGENT_DIR}/token"
|
|
#POST_LOCAL="${AGENT_DIR}/bin/vault-agent-post.sh"
|
|
#POST_SRC="$(cd -- "$(dirname -- "$0")" && pwd)/scripts/vault-agent-post-leaf.sh"
|
|
POST_LOCAL="${AGENT_DIR}/bin/vault-agent-post-leaf.sh"
|
|
POST_SRC="$(cd -- "$(dirname -- "$0")" && pwd)/scripts/vault-agent-post-leaf.sh"
|
|
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}.service"
|
|
|
|
sudo install -d -m 0700 -o "${APP_USER}" -g "${APP_USER}" "${AGENT_DIR}" "${TLS_DIR}"
|
|
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${AGENT_DIR}/bin"
|
|
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${CA_DIR}" >/dev/null 2>&1 || true
|
|
|
|
# ===== PKI-Role (Domänen) =====
|
|
ALLOWED="$(derive_allowed_domains "$CN" "$EXT_HOST")"
|
|
ensure_pki_role "$PKI_MOUNT" "$PKI_ROLE" "$ALLOWED" "720h"
|
|
|
|
# ===== Policies =====
|
|
POLICY_BOOT="pki-bootstrap-${APPN}"
|
|
POLICY_RUN="pki-issue-${APPN}"
|
|
TMP="$(mktemp)"
|
|
cat >"$TMP" <<EOF
|
|
path "${PKI_MOUNT}/issue/${PKI_ROLE}" { capabilities=["create","update"] }
|
|
path "auth/token/renew-self" { capabilities=["update"] }
|
|
path "auth/approle/role/${APPN}-pki-issue/role-id" { capabilities=["read"] }
|
|
path "auth/approle/role/${APPN}-pki-issue/secret-id" { capabilities=["create","update"] }
|
|
EOF
|
|
vault policy write "${POLICY_BOOT}" "$TMP" >/dev/null
|
|
cat >"$TMP" <<EOF
|
|
path "${PKI_MOUNT}/issue/${PKI_ROLE}" { capabilities=["create","update"] }
|
|
path "auth/token/renew-self" { capabilities=["update"] }
|
|
EOF
|
|
vault policy write "${POLICY_RUN}" "$TMP" >/dev/null
|
|
rm -f "$TMP"
|
|
|
|
# ===== AppRole (periodic token; bind_secret_id) =====
|
|
ROLE_NAME="${APPN}-pki-issue"
|
|
vault auth enable approle >/dev/null 2>&1 || true
|
|
vault write "auth/approle/role/${ROLE_NAME}" \
|
|
policies="${POLICY_RUN}" bind_secret_id=true \
|
|
token_type=service token_period=24h secret_id_ttl=0 secret_id_num_uses=0 >/dev/null
|
|
|
|
# ===== role_id / secret_id speichern =====
|
|
RID_VAL="$(vault read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
|
|
SID_VAL="$(vault write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
|
|
sudo bash -c "umask 077; printf '%s' '${RID_VAL}' > '${RID}'; chown ${APP_USER}:${APP_USER} '${RID}'; chmod 600 '${RID}'"
|
|
sudo bash -c "umask 077; printf '%s' '${SID_VAL}' > '${SID}'; chown ${APP_USER}:${APP_USER} '${SID}'; chmod 600 '${SID}'"
|
|
|
|
# ===== CA-Trust prüfen (für TLS zum Vault) =====
|
|
if ! sudo -u "${APP_USER}" test -s "${CA_FILE}"; then
|
|
warn "CA file nicht gefunden für ${APP_USER}: ${CA_FILE} (Agent HTTPS Verify kann scheitern)."
|
|
warn "→ verteile Root/Chain zuerst: ./distribute_ca_to_agents.sh --env ${ENV_NAME} --which chain --users ${APP_USER}"
|
|
fi
|
|
|
|
# ===== Agent HCL + Template =====
|
|
# Optionales Client-mTLS (nur wenn Dateien existieren)
|
|
TLS_EXTRA=$'\n tls_server_name = "'"${TLS_SERVER_NAME}"$'"\n'
|
|
if sudo -u "${APP_USER}" test -s "${HOME_DIR}/vault/mtls/agent.crt" && sudo -u "${APP_USER}" test -s "${HOME_DIR}/vault/mtls/agent.key"; then
|
|
TLS_EXTRA+=$' client_cert = "'"${HOME_DIR}/vault/mtls/agent.crt"\"$'\n'
|
|
TLS_EXTRA+=$' client_key = "'"${HOME_DIR}/vault/mtls/agent.key"\"$'\n'
|
|
else
|
|
warn "kein Client-mTLS unter ${HOME_DIR}/vault/mtls → Agent nutzt CA-only TLS."
|
|
fi
|
|
|
|
sudo -u "${APP_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<HCL
|
|
pid_file = "${AGENT_DIR}/pidfile"
|
|
|
|
vault {
|
|
address = "${VAULT_ADDR}"
|
|
ca_cert = "${CA_FILE}"${TLS_EXTRA}
|
|
}
|
|
|
|
auto_auth {
|
|
method "approle" {
|
|
config = {
|
|
role_id_file_path = "${RID}"
|
|
secret_id_file_path = "${SID}"
|
|
remove_secret_id_file_after_reading = false
|
|
}
|
|
}
|
|
sink "file" {
|
|
config = {
|
|
path = "${TOKEN_FILE}"
|
|
mode = 0400
|
|
}
|
|
}
|
|
}
|
|
|
|
template {
|
|
source = "${AGENT_DIR}/cert.tpl"
|
|
destination = "${AGENT_DIR}/.issue.json"
|
|
command = "OUTDIR='${TLS_DIR}' RELOAD_TLS_LABEL='${RELOAD_LABEL}' ${POST_LOCAL}"
|
|
}
|
|
HCL
|
|
|
|
PARAMS="\"common_name=${CN}\" \"ttl=${TTL}\""
|
|
[[ -n "$EXT_HOST" ]] && PARAMS="$PARAMS \"alt_names=${EXT_HOST}\""
|
|
|
|
sudo -u "${APP_USER}" tee "${AGENT_DIR}/cert.tpl" >/dev/null <<TPL
|
|
{{ with secret "${PKI_MOUNT}/issue/${PKI_ROLE}" ${PARAMS} }}
|
|
{ "certificate": {{ toJSON .Data.certificate }},
|
|
"issuing_ca": {{ toJSON .Data.issuing_ca }},
|
|
"ca_chain": {{ toJSON .Data.ca_chain }},
|
|
"private_key": {{ toJSON .Data.private_key }} }
|
|
{{ end }}
|
|
TPL
|
|
|
|
# ===== Post-Hook installieren (dein vorhandenes Script) =====
|
|
if [[ -r "$POST_SRC" ]]; then
|
|
sudo /usr/bin/install -m 0755 -o "${APP_USER}" -g "${APP_USER}" -D "$POST_SRC" "${POST_LOCAL}"
|
|
ok "post-hook installiert: ${POST_LOCAL}"
|
|
else
|
|
err "Post-Hook nicht gefunden: $POST_SRC (erwartet scripts/vault-agent-post-leaf.sh neben diesem Script)"
|
|
exit 4
|
|
fi
|
|
|
|
# ===== systemd (user) Unit =====
|
|
sudo -u "${APP_USER}" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
|
|
sudo -u "${APP_USER}" tee "${UNIT}" >/dev/null <<UNIT
|
|
[Unit]
|
|
Description=Vault Agent (${APPN}) - leaf issuance & rotation (v3)
|
|
Wants=network-online.target
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
WorkingDirectory=${AGENT_DIR}
|
|
Environment=VAULT_ADDR=${VAULT_ADDR}
|
|
ExecStart=/usr/bin/vault agent -log-level=${LOG_LEVEL} -config=${AGENT_DIR}/vault-agent.hcl
|
|
Restart=on-failure
|
|
RestartSec=5s
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
UNIT
|
|
|
|
APP_UID="$(id -u "${APP_USER}")"
|
|
loginctl enable-linger "${APP_USER}" >/dev/null || true
|
|
systemctl start "user@${APP_UID}.service" >/dev/null || true
|
|
XDG_RUNTIME_DIR="/run/user/${APP_UID}"; mkdir -p "$XDG_RUNTIME_DIR"; chown "${APP_USER}:${APP_USER}" "$XDG_RUNTIME_DIR" || true
|
|
|
|
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" systemctl --user daemon-reload
|
|
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" systemctl --user enable --now "vault-agent-${APPN}.service"
|
|
|
|
# ===== Quick check =====
|
|
CERT="${TLS_DIR}/${APPN}.fullchain.pem"
|
|
if sudo -u "${APP_USER}" test -s "${CERT}"; then
|
|
ok "Leaf vorhanden: ${CERT}"
|
|
openssl x509 -in "${CERT}" -noout -subject -issuer -enddate | sed 's/^/ 🔎 /'
|
|
else
|
|
warn "Noch kein Zertifikat: ${CERT} (Agent rendert ggf. gleich). Logs:"
|
|
journalctl --user -u "vault-agent-${APPN}.service" -n 60 --no-pager || true
|
|
fi
|
|
|
|
ok "SUCCESS v3 → app=${APPN} env=${ENV_NAME} role=${PKI_ROLE} allowed_domains=${ALLOWED}"
|
|
|