457 lines
20 KiB
Bash
Executable file
457 lines
20 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
# setup-vault-agent-app-config-v4.3.sh
|
||
# Datum: 2025-09-29
|
||
|
||
# ========================= FEATURE MANIFEST (v4.3) =========================
|
||
# 🔒 Authentisierung / Sicherheit
|
||
# 1) Strict mTLS-Login (DEFAULT): Wenn --auth cert gesetzt (oder Default) und
|
||
# ~/vault/mtls/agent.{crt,key} fehlt → HARTE ABBRUCH (Exit 5). Kein Fallback.
|
||
# 2) Optional AppRole NUR bei --auth approle (erzeugt/liest role_id & secret_id).
|
||
# 3) Agent-Token wird sicher in Datei-Sink (0400) geschrieben.
|
||
#
|
||
# 📦 Konfiguration / YAML
|
||
# 4) Liest ./config/apps.yaml:
|
||
# environments.{vault_addr, kv_mount, pki_mount,
|
||
# proxy{user,listen_port,chain_path,reload},
|
||
# app{user,sidecar_host_port,sidecar_container}}
|
||
# apps[].{name, user?, internal_cn, external_host_{test,prod}, issue_ttl,
|
||
# kv_subpath?, seed?, sidecar_container?, proxy_user?, proxy_chain_path?, proxy_reload?}
|
||
#
|
||
# 🛠️ PKI / Policies
|
||
# 5) PKI-Role Upsert (EC P-256, max_ttl=720h), allowed_domains aus CN & externem Host.
|
||
# 6) Policies minimal: pki-issue-<app> (issue, renew-self), pki-bootstrap-<app> (AppRole bootstrap).
|
||
#
|
||
# 🧠 Agent / Templates / Pfade
|
||
# 7) Generiert Agent-HCL + Template (cert.tpl); rendert .issue.json.
|
||
# 8) Post-Hook (vault-agent-post-leaf.sh) schreibt <HOME>/tls/<app>.{key,fullchain.pem}.
|
||
# 9) Verzeichnisstruktur:
|
||
# ~/.vault-agent-<app>/{vault-agent.hcl,cert.tpl,.issue.json,token,bin/,pidfile}
|
||
# ~/tls/, ~/vault/ca/ca.pem, ~/vault/mtls/{agent.crt,agent.key}
|
||
#
|
||
# 🔁 Reload / Sidecar / Proxy
|
||
# 10) Übergibt an Post-Hook via Env:
|
||
# PROXY_RELOAD, PROXY_CHAIN_PATH, PROXY_USER, PROXY_LISTEN_PORT,
|
||
# SIDECAR_HOST_PORT, SIDECAR_CONTAINER, RELOAD_TLS_LABEL.
|
||
#
|
||
# 🔑 KV-Seed (einmalig)
|
||
# 11) Falls kv_subpath + seed vorhanden und Secret fehlt → einmaliges vault kv put (kein Overwrite).
|
||
#
|
||
# ⚙️ Systemd (user)
|
||
# 12) Erstellt ~/.config/systemd/user/vault-agent-<app>.service, aktiviert (--user), Linger an.
|
||
#
|
||
# 🌐 TLS zu Vault
|
||
# 13) Nutzt ca_cert=~/vault/ca/ca.pem + tls_server_name (Override via --tls-server-name).
|
||
# Optional client_cert/client_key gesetzt, wenn Dateien existieren.
|
||
#
|
||
# 🧩 CLI-Flags / Defaults
|
||
# 14) Flags: --env, --config, --app, --auth (cert|approle), --pki-role,
|
||
# --tls-server-name, --reload-label. Defensive Defaults.
|
||
#
|
||
# 🧪 Quick Check
|
||
# 15) Prüft am Ende auf vorhandenes Leaf, zeigt subject/issuer/enddate; sonst journalctl Hint.
|
||
#
|
||
# 🆕 Cert-Auth Mapping (PKI) – Auto-Manage
|
||
# 16) Legt/updated auth/cert/certs/<MAP> für PKI:
|
||
# - liest CN aus ~/vault/mtls/agent.crt (Allowed CN)
|
||
# - verwendet CA aus ~/vault/ca/ca.pem (override via --cert-map-cafile)
|
||
# - Policies: stellt sicher, dass pki-issue-<app> enthalten ist
|
||
# - Strategien: --cert-map-strategy merge|replace (Default: merge)
|
||
# - Map-Name: --cert-map-name (Default: agent-<app>-pki)
|
||
# ===========================================================================
|
||
#
|
||
# Exit-Codes: 2=Args/Config; 3=User fehlt; 4=Post-Hook fehlt; 5=mTLS gefordert aber Material fehlt
|
||
|
||
# ===== 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_DEFAULT:=vault.test.privsec.ch}"
|
||
: "${RELOAD_LABEL_DEFAULT:=tls=true}"
|
||
|
||
# Cert-Map defaults
|
||
: "${CERT_MAP_NAME_DEFAULT:=}"
|
||
: "${CERT_MAP_STRATEGY_DEFAULT:=merge}" # merge|replace
|
||
: "${CERT_MAP_TTL_DEFAULT:=24h}"
|
||
: "${CERT_MAP_CAFILE_DEFAULT:=}"
|
||
|
||
[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN fehlt (exportieren!)"; exit 2; }
|
||
|
||
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
|
||
AUTH_METHOD="cert" # cert|approle
|
||
PKI_ROLE_OVERRIDE=""; TLS_SNI_OVERRIDE=""; RELOAD_LABEL_OVERRIDE=""
|
||
CERT_MAP_NAME="$CERT_MAP_NAME_DEFAULT"
|
||
CERT_MAP_STRATEGY="$CERT_MAP_STRATEGY_DEFAULT"
|
||
CERT_MAP_TTL="$CERT_MAP_TTL_DEFAULT"
|
||
CERT_MAP_CAFILE="$CERT_MAP_CAFILE_DEFAULT"
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--env) ENV_NAME="$2"; shift 2;;
|
||
--config) CFG="$2"; shift 2;;
|
||
--app) APPN="$2"; shift 2;;
|
||
--auth) AUTH_METHOD="$2"; shift 2;;
|
||
--pki-role) PKI_ROLE_OVERRIDE="$2"; shift 2;;
|
||
--tls-server-name) TLS_SNI_OVERRIDE="$2"; shift 2;;
|
||
--reload-label) RELOAD_LABEL_OVERRIDE="$2"; shift 2;;
|
||
# 🆕 Cert-Map mgmt
|
||
--cert-map-name) CERT_MAP_NAME="$2"; shift 2;;
|
||
--cert-map-strategy) CERT_MAP_STRATEGY="$2"; shift 2;;
|
||
--cert-map-ttl) CERT_MAP_TTL="$2"; shift 2;;
|
||
--cert-map-cafile) CERT_MAP_CAFILE="$2"; shift 2;;
|
||
-h|--help) sed -n '1,220p' "$0"; exit 0;;
|
||
*) err "unknown arg: $1"; exit 2;;
|
||
esac
|
||
done
|
||
[[ -n "$APPN" ]] || { err "--app ist erforderlich"; exit 2; }
|
||
case "$AUTH_METHOD" in cert|approle) ;; * ) err "--auth muss cert|approle sein"; exit 2;; esac
|
||
case "${CERT_MAP_STRATEGY}" in merge|replace) ;; * ) err "--cert-map-strategy: merge|replace"; exit 2;; esac
|
||
|
||
# ===== 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 - "$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"; }
|
||
|
||
VAULT_ADDR="$(jqenv '.vault_addr')"
|
||
PKI_MOUNT="$(jqenv '.pki_mount')"
|
||
KV_MOUNT="$(jqenv '.kv_mount')"
|
||
|
||
# Env-level defaults
|
||
APP_USER_DEF="$(jqenv '.app.user')"
|
||
SIDECAR_HOST_PORT_DEF="$(jqenv '.app.sidecar_host_port')"
|
||
SIDECAR_CONTAINER_DEF="$(jqenv '.app.sidecar_container')"
|
||
PROXY_USER_DEF="$(jqenv '.proxy.user')"
|
||
PROXY_LISTEN_PORT_DEF="$(jqenv '.proxy.listen_port')"
|
||
PROXY_CHAIN_PATH_DEF="$(jqenv '.proxy.chain_path')"
|
||
PROXY_RELOAD_DEF="$(jqenv '.proxy.reload')"
|
||
|
||
# App-level fields & overrides
|
||
APP_USER_APP="$(jqapp '.user')"
|
||
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=""
|
||
KV_SUBPATH="$(jqapp '.kv_subpath')"
|
||
SEED_JSON="$(jqapp '.seed')"
|
||
|
||
SIDECAR_CONTAINER_APP="$(jqapp '.sidecar_container')"
|
||
PROXY_USER_APP="$(jqapp '.proxy_user')"
|
||
PROXY_CHAIN_PATH_APP="$(jqapp '.proxy_chain_path')"
|
||
PROXY_RELOAD_APP="$(jqapp '.proxy_reload')"
|
||
|
||
APP_USER="$APP_USER_DEF"; [[ "$APP_USER_APP" != "null" ]] && APP_USER="$APP_USER_APP"
|
||
SIDECAR_HOST_PORT="$SIDECAR_HOST_PORT_DEF"; [[ "$SIDECAR_HOST_PORT" == "null" ]] && SIDECAR_HOST_PORT=""
|
||
SIDECAR_CONTAINER="$SIDECAR_CONTAINER_DEF"; [[ "$SIDECAR_CONTAINER_APP" != "null" ]] && SIDECAR_CONTAINER="$SIDECAR_CONTAINER_APP"; [[ "$SIDECAR_CONTAINER" == "null" ]] && SIDECAR_CONTAINER=""
|
||
PROXY_USER="$PROXY_USER_DEF"; [[ "$PROXY_USER_APP" != "null" ]] && PROXY_USER="$PROXY_USER_APP"
|
||
PROXY_LISTEN_PORT="$PROXY_LISTEN_PORT_DEF"; [[ "$PROXY_LISTEN_PORT" == "null" ]] && PROXY_LISTEN_PORT=""
|
||
PROXY_CHAIN_PATH="$PROXY_CHAIN_PATH_DEF"; [[ "$PROXY_CHAIN_PATH_APP" != "null" ]] && PROXY_CHAIN_PATH="$PROXY_CHAIN_PATH_APP"
|
||
PROXY_RELOAD="$PROXY_RELOAD_DEF"; [[ "$PROXY_RELOAD_APP" != "null" ]] && PROXY_RELOAD="$PROXY_RELOAD_APP"
|
||
|
||
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" && "$CN" != "null" && "$TTL" != "null" && -n "$APP_USER" ]] \
|
||
|| { err "incomplete config: vault_addr/pki_mount/app.user/internal_cn/issue_ttl"; exit 2; }
|
||
|
||
export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"
|
||
|
||
# ===== Helpers =====
|
||
base_domain(){ local h="${1:-}"; [[ "$h" == *.* ]] && printf '%s\n' "${h#*.}" || printf '\n'; }
|
||
uniq_csv(){ tr ',' '\n' | awk 'NF' | sort -u | paste -sd, -; }
|
||
derive_allowed_domains(){ printf '%s\n%s\n' "$(base_domain "$1")" "$(base_domain "$2")" | uniq_csv; }
|
||
|
||
extract_cn(){
|
||
# robust CN-Parse (supports comma- oder slash-Notation)
|
||
local cert="$1"
|
||
local subj; subj="$(openssl x509 -in "$cert" -noout -subject 2>/dev/null || true)"
|
||
[[ -n "$subj" ]] || return 1
|
||
# try comma-style
|
||
echo "$subj" | sed -n 's/^subject= *//; s,/, ,g; s, ,\n,g; p' | awk -F= '$1=="CN"{print $2}' | head -n1
|
||
}
|
||
|
||
ensure_pki_role(){
|
||
local mount="$1" role="$2" doms="$3" max="${4:-720h}"
|
||
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 || true
|
||
}
|
||
|
||
ensure_cert_map(){
|
||
# Creates/updates auth/cert/certs/<map> for PKI Agent mTLS
|
||
local map_name="$1" cafile="$2" allowed_cn="$3" need_policy="$4" ttl="$5" strategy="$6"
|
||
info "ensure cert-auth map: ${map_name} (strategy=${strategy}, ttl=${ttl})"
|
||
vault auth enable cert >/dev/null 2>&1 || true
|
||
|
||
local exist_json=""
|
||
if exist_json="$(vault read -format=json "auth/cert/certs/${map_name}" 2>/dev/null || true)"; then
|
||
# merge or replace
|
||
local current_pols current_cns
|
||
current_pols="$(jq -r '.data.policies // [] | .[]' <<<"$exist_json" 2>/dev/null || true)"
|
||
current_cns="$(jq -r '.data.allowed_common_names // [] | .[]' <<<"$exist_json" 2>/dev/null || true)"
|
||
|
||
if [[ "$strategy" == "merge" ]]; then
|
||
# union
|
||
local merged_pols merged_cns
|
||
merged_pols="$( { printf '%s\n' $current_pols; echo "$need_policy"; } | awk 'NF' | sort -u | paste -sd, -)"
|
||
merged_cns="$( { printf '%s\n' $current_cns; echo "$allowed_cn"; } | awk 'NF' | sort -u | paste -sd, -)"
|
||
vault write "auth/cert/certs/${map_name}" \
|
||
display_name="${map_name}" \
|
||
certificate=@"${cafile}" \
|
||
allowed_common_names="${merged_cns}" \
|
||
policies="${merged_pols}" \
|
||
token_ttl="${ttl}" >/dev/null
|
||
ok "cert-map updated (merge): ${map_name} | policies=[${merged_pols}] | CNs=[${merged_cns}]"
|
||
else
|
||
# replace everything with our values (keeps name)
|
||
vault write "auth/cert/certs/${map_name}" \
|
||
display_name="${map_name}" \
|
||
certificate=@"${cafile}" \
|
||
allowed_common_names="${allowed_cn}" \
|
||
policies="${need_policy}" \
|
||
token_ttl="${ttl}" >/dev/null
|
||
ok "cert-map replaced: ${map_name} | policies=[${need_policy}] | CNs=[${allowed_cn}]"
|
||
fi
|
||
else
|
||
# create fresh
|
||
vault write "auth/cert/certs/${map_name}" \
|
||
display_name="${map_name}" \
|
||
certificate=@"${cafile}" \
|
||
allowed_common_names="${allowed_cn}" \
|
||
policies="${need_policy}" \
|
||
token_ttl="${ttl}" >/dev/null
|
||
ok "cert-map created: ${map_name} | policies=[${need_policy}] | CNs=[${allowed_cn}]"
|
||
fi
|
||
}
|
||
|
||
# ===== 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"
|
||
MTLS_DIR="${HOME_DIR}/vault/mtls"
|
||
CA_FILE="${CA_DIR}/ca.pem"
|
||
RID="${AGENT_DIR}/role_id"
|
||
SID="${AGENT_DIR}/secret_id"
|
||
TOKEN_FILE="${AGENT_DIR}/token"
|
||
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
|
||
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${MTLS_DIR}" >/dev/null 2>&1 || true
|
||
|
||
# ===== PKI Role & Policies =====
|
||
PKI_ROLE="${PKI_ROLE_OVERRIDE:-nginx-${APPN}}"
|
||
EXT_HOST="$EXT_TEST"; [[ "$ENV_NAME" == "prod" ]] && EXT_HOST="$EXT_PROD"
|
||
ALLOWED="$(derive_allowed_domains "$CN" "$EXT_HOST")"
|
||
ensure_pki_role "$PKI_MOUNT" "$PKI_ROLE" "$ALLOWED" "720h"
|
||
|
||
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"] }
|
||
EOF
|
||
vault policy write "${POLICY_RUN}" "$TMP" >/dev/null
|
||
cat >"$TMP" <<EOF
|
||
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
|
||
rm -f "$TMP"
|
||
|
||
# ===== Auth-Setup (Strict mTLS; AppRole nur explizit) =====
|
||
ROLE_NAME="${APPN}-pki-issue"
|
||
if [[ "$AUTH_METHOD" == "approle" ]]; then
|
||
info "Auth=AppRole (explizit via --auth approle)"
|
||
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
|
||
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}'"
|
||
fi
|
||
|
||
# ===== CA-Trust prüfen =====
|
||
if ! sudo -u "${APP_USER}" test -s "${CA_FILE}"; then
|
||
warn "CA fehlt: ${CA_FILE} → HTTPS Verify ggü. Vault kann scheitern."
|
||
fi
|
||
|
||
# ===== KV-Seed (einmalig, wenn konfiguriert) =====
|
||
seed_kv_once(){
|
||
local mount="$1" sub="$2" seed_json="$3"
|
||
[[ -z "$mount" || "$mount" == "null" || -z "$sub" || "$sub" == "null" || "$seed_json" == "null" ]] && return 0
|
||
if vault kv get -format=json "${mount}/${sub}" >/dev/null 2>&1; then
|
||
info "KV ${mount}/${sub} existiert – Seed wird NICHT überschrieben"
|
||
return 0
|
||
fi
|
||
info "KV-Seed → ${mount}/${sub} (nur einmalig)"
|
||
mapfile -t kvargs < <(jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' <<<"$seed_json")
|
||
printf " keys: %s\n" "$(jq -r 'keys|join(", ")' <<<"$seed_json")"
|
||
vault kv put "${mount}/${sub}" "${kvargs[@]}" >/dev/null
|
||
}
|
||
seed_kv_once "$KV_MOUNT" "$KV_SUBPATH" "$SEED_JSON"
|
||
|
||
# ===== HCL + Template (Strict mTLS / optional AppRole) =====
|
||
TLS_SNI="${TLS_SNI_OVERRIDE:-$TLS_SERVER_NAME_DEFAULT}"
|
||
PARAMS="\"common_name=${CN}\" \"ttl=${TTL}\""; [[ -n "$EXT_HOST" ]] && PARAMS="$PARAMS \"alt_names=${EXT_HOST}\""
|
||
|
||
AUTO_AUTH_BLOCK=""
|
||
if [[ "$AUTH_METHOD" == "cert" ]]; then
|
||
if ! ( sudo -u "${APP_USER}" test -s "${MTLS_DIR}/agent.crt" && sudo -u "${APP_USER}" test -s "${MTLS_DIR}/agent.key" ); then
|
||
err "Auth=cert gefordert, aber mTLS fehlt: ${MTLS_DIR}/agent.{crt,key}"
|
||
exit 5
|
||
fi
|
||
info "Auth=cert (mTLS) – strikt, kein Fallback"
|
||
AUTO_AUTH_BLOCK=$(cat <<'EOF'
|
||
auto_auth {
|
||
method "cert" { }
|
||
sink "file" { config = { path = "__TOKEN_FILE__", mode = 0400 } }
|
||
}
|
||
EOF
|
||
)
|
||
elif [[ "$AUTH_METHOD" == "approle" ]]; then
|
||
AUTO_AUTH_BLOCK=$(cat <<'EOF'
|
||
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 } }
|
||
}
|
||
EOF
|
||
)
|
||
fi
|
||
|
||
TLS_EXTRA=$'\n tls_server_name = "'"${TLS_SNI}"$'"\n'
|
||
# Optional mTLS zum Vault-Server selbst, falls vorhanden:
|
||
if sudo -u "${APP_USER}" test -s "${MTLS_DIR}/agent.crt" && sudo -u "${APP_USER}" test -s "${MTLS_DIR}/agent.key"; then
|
||
TLS_EXTRA+=$' client_cert = "'"${MTLS_DIR}/agent.crt"\"$'\n'
|
||
TLS_EXTRA+=$' client_key = "'"${MTLS_DIR}/agent.key"\"$'\n'
|
||
fi
|
||
|
||
HCL_CONTENT=$(cat <<HCL
|
||
pid_file = "${AGENT_DIR}/pidfile"
|
||
|
||
vault {
|
||
address = "${VAULT_ADDR}"
|
||
ca_cert = "${CA_FILE}"${TLS_EXTRA}
|
||
}
|
||
|
||
__AUTO_AUTH__
|
||
|
||
template {
|
||
source = "${AGENT_DIR}/cert.tpl"
|
||
destination = "${AGENT_DIR}/.issue.json"
|
||
command = "OUTDIR='${TLS_DIR}' RELOAD_TLS_LABEL='${RELOAD_LABEL_OVERRIDE:-$RELOAD_LABEL_DEFAULT}' PROXY_RELOAD='${PROXY_RELOAD}' PROXY_CHAIN_PATH='${PROXY_CHAIN_PATH}' PROXY_USER='${PROXY_USER}' PROXY_LISTEN_PORT='${PROXY_LISTEN_PORT}' SIDECAR_HOST_PORT='${SIDECAR_HOST_PORT}' SIDECAR_CONTAINER='${SIDECAR_CONTAINER}' APP_NAME='${APPN}' ${POST_LOCAL}"
|
||
}
|
||
HCL
|
||
)
|
||
HCL_CONTENT="${HCL_CONTENT/__AUTO_AUTH__/${AUTO_AUTH_BLOCK}}"
|
||
HCL_CONTENT="${HCL_CONTENT/__TOKEN_FILE__/${TOKEN_FILE}}"
|
||
HCL_CONTENT="${HCL_CONTENT/__RID__/${RID}}"
|
||
HCL_CONTENT="${HCL_CONTENT/__SID__/${SID}}"
|
||
|
||
sudo -u "${APP_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<<"$HCL_CONTENT"
|
||
|
||
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 bereitstellen =====
|
||
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 fehlt: $POST_SRC (benötigt, um Key/Fullchain zu schreiben & Reload auszuführen)"; exit 4
|
||
fi
|
||
|
||
# ===== 🆕 Cert-Auth Map für PKI sicherstellen (nur bei mTLS-Auth) =====
|
||
if [[ "$AUTH_METHOD" == "cert" ]]; then
|
||
# CN aus Agent-Client-Zert lesen
|
||
CLIENT_CN="$(sudo -u "${APP_USER}" bash -c 'openssl x509 -in "'"${MTLS_DIR}/agent.crt"'" -noout -subject' | sed -n 's/^subject= *//p' | sed 's,/, ,g' | awk -F= '$1=="CN"{print $2}' | head -n1 || true)"
|
||
if [[ -z "$CLIENT_CN" ]]; then
|
||
err "Konnte CN aus ${MTLS_DIR}/agent.crt nicht lesen"; exit 5
|
||
fi
|
||
# Map-Name defaulten, falls nicht übergeben
|
||
[[ -n "$CERT_MAP_NAME" ]] || CERT_MAP_NAME="agent-${APPN}-pki"
|
||
# CA-Datei für Cert-Map (override möglich)
|
||
CM_CA="${CERT_MAP_CAFILE:-$CA_FILE}"
|
||
[[ -r "$CM_CA" ]] || { err "Cert-Map CA nicht lesbar: ${CM_CA}"; exit 2; }
|
||
ensure_cert_map "$CERT_MAP_NAME" "$CM_CA" "$CLIENT_CN" "$POLICY_RUN" "$CERT_MAP_TTL" "$CERT_MAP_STRATEGY"
|
||
else
|
||
info "Auth=AppRole → Cert-Auth-Map wird übersprungen"
|
||
fi
|
||
|
||
# ===== systemd (user) =====
|
||
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 (v4.3)
|
||
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} (kommt i.d.R. kurz darauf). Logs:"
|
||
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="/run/user/$(id -u "${APP_USER}")" journalctl --user -u "vault-agent-${APPN}.service" -n 60 --no-pager || true
|
||
fi
|
||
|
||
ok "SUCCESS v4.3 → app=${APPN} env=${ENV_NAME} role=${PKI_ROLE} auth=${AUTH_METHOD} kv_mount=${KV_MOUNT} allowed_domains=${ALLOWED} cert_map=${CERT_MAP_NAME} strategy=${CERT_MAP_STRATEGY}"
|
||
|