
===== ./distribute_ca_to_agents.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail

# Distribute a CA file from /home/vault/tls-<env>/... to /home/<user>/vault/ca/ca.pem
# Safe/idempotent: skips if identical; use --force to overwrite.
#
# Examples:
#   ./distribute_ca_to_agents.sh --src "/home/vault/tls-test/root_ca.pem" --users "nctest apptest proxytest"
#   ./distribute_ca_to_agents.sh --env test --which chain --users-file ./agents.txt
#     (uses /home/vault/tls-test/ca_chain.pem)
#
# Options:
#   --src <path>         explicit source PEM (overrides --env/--which)
#   --env <name>         e.g. test|prod (builds /home/vault/tls-<env>/<file>)
#   --which root|chain   pick root_ca.pem (default) or ca_chain.pem
#   --users "u1 u2"      space-separated users
#   --users-file <file>  one user per line; # comments OK
#   --force              overwrite even if target exists
#   -h|--help            show help

# 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; }

need(){ command -v "$1" >/dev/null 2>&1 || { err "missing: $1"; exit 2; }; }
need sudo; need install; need getent
command -v openssl >/dev/null 2>&1 || warn "openssl not found → skip PEM parse check"

SRC=""; ENV_NAME=""; WHICH="root"; USERS=""; USERS_FILE=""; FORCE=0
while [[ $# -gt 0 ]]; do
  case "$1" in
    --src) SRC="$2"; shift 2;;
    --env) ENV_NAME="$2"; shift 2;;
    --which) WHICH="$2"; shift 2;;
    --users) USERS="$2"; shift 2;;
    --users-file) USERS_FILE="$2"; shift 2;;
    --force) FORCE=1; shift;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) err "unknown arg: $1"; exit 2;;
  esac
done

# resolve source
if [[ -z "$SRC" ]]; then
  [[ -n "$ENV_NAME" ]] || { err "Either --src or --env is required."; exit 2; }
  case "$WHICH" in
    root)  SRC="/home/vault/tls-${ENV_NAME}/root_ca.pem" ;;
    chain) SRC="/home/vault/tls-${ENV_NAME}/ca_chain.pem" ;;
    *) err "--which must be root|chain"; exit 2;;
  esac
fi
sudo test -r "$SRC" || { err "source not readable via sudo: $SRC"; exit 3; }
if command -v openssl >/dev/null 2>&1; then
  sudo openssl x509 -in "$SRC" -noout >/dev/null 2>&1 || warn "openssl can't parse a single cert from $SRC (bundle is still OK)"
fi

# users
USERS_ARR=()
[[ -n "$USERS" ]] && read -r -a USERS_ARR <<<"$USERS"
if [[ -n "$USERS_FILE" ]]; then
  [[ -r "$USERS_FILE" ]] || { err "users file not readable: $USERS_FILE"; exit 2; }
  while IFS= read -r line; do
    line="${line%%#*}"; line="$(echo "$line" | xargs || true)"
    [[ -z "$line" ]] && continue
    USERS_ARR+=("$line")
  done < "$USERS_FILE"
fi
(( ${#USERS_ARR[@]} > 0 )) || { err "No users specified. Use --users or --users-file."; exit 2; }

info "Source:      $SRC"
info "Users:       ${USERS_ARR[*]}"
info "Target name: ca.pem"
info "Mode:        $([[ $FORCE -eq 1 ]] && echo 'force overwrite' || echo 'skip if exists / identical')"
echo

COPIED=0; SKIPPED=0; MISSING=0
for u in "${USERS_ARR[@]}"; do
  if ! id -u "$u" >/dev/null 2>&1; then
    warn "user not found: $u → skip"; ((MISSING++)); continue
  fi
  HOME_DIR="$(getent passwd "$u" | cut -d: -f6)"; [[ -n "$HOME_DIR" ]] || HOME_DIR="/home/$u"
  DEST_DIR="${HOME_DIR}/vault/ca"; DEST="${DEST_DIR}/ca.pem"

  sudo install -d -m 0755 -o "$u" -g "$u" "$DEST_DIR" >/dev/null
  if [[ -f "$DEST" && $FORCE -eq 0 ]] && sudo cmp -s "$SRC" "$DEST"; then
    ok "[$u] up-to-date → ${DEST}"; ((SKIPPED++)); continue
  fi
  if [[ -f "$DEST" && $FORCE -eq 0 ]]; then
    ok "[$u] exists → skip (use --force to overwrite): ${DEST}"; ((SKIPPED++)); continue
  fi
  if sudo install -m 0644 -o "$u" -g "$u" "$SRC" "$DEST"; then
    ok "[$u] wrote ${DEST}"; ((COPIED++))
  else
    err "[$u] failed to write ${DEST}"
  fi
done

echo
ok "Done. Copied: ${COPIED}, Skipped: ${SKIPPED}, Missing users: ${MISSING}"


===== ./setup-vault-agent-app-config.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail

# ========= Pretty logging =========
if [[ -t 1 ]]; then
  BOLD=$'\e[1m'; DIM=$'\e[2m'; RESET=$'\e[0m'
  BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'
else
  BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
fi
ts(){ date +"%Y-%m-%d %H:%M:%S"; }
info(){ echo -e "🟦 ${BLUE}${BOLD}[$(ts)]${RESET} $*"; }
ok(){   echo -e "🟩 ${GREEN}${BOLD}[$(ts)]${RESET} $*"; }
warn(){ echo -e "🟨 ${YELLOW}${BOLD}[$(ts)]${RESET} $*"; }
err(){  echo -e "🟥 ${RED}${BOLD}[$(ts)]${RESET} $*" >&2; }

# ========= Defaults / Args =========
: "${LOG_LEVEL:=info}"
: "${VAULT_BIN:=/usr/bin/vault}"
: "${SYSTEMD_BIN:=/usr/bin/systemctl}"
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
: "${DEFAULT_ENV:=test}"

if [[ -z "${VAULT_ADMIN_TOKEN:-}" ]]; then
  err "VAULT_ADMIN_TOKEN missing. Example:
  sudo env VAULT_ADMIN_TOKEN=hvs.XXX $0 --env test --config ./config/apps.yaml --app apptest"
  exit 2
fi

ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --env)    ENV_NAME="$2"; shift 2;;
    --config) CFG="$2";      shift 2;;
    --app)    APPN="$2";     shift 2;;
    -h|--help) echo "usage: $0 [--env test|prod] [--config ./config/apps.yaml] --app <name>"; exit 0;;
    *) err "unknown arg: $1"; exit 2;;
  esac
done
[[ -n "$APPN" ]] || { err "--app is required"; exit 2; }

need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
need python3; need jq; need openssl; need "$VAULT_BIN"

SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
POST_SRC="${SCRIPT_DIR}/scripts/vault-agent-post.sh"
[[ -r "$POST_SRC" ]] || { err "POST_SRC not readable: $POST_SRC"; exit 4; }

# ========= Load config (YAML → JSON) =========
CFG_ABS="$(readlink -f "$CFG")"
CFGJSON="$(python3 - <<PY
import yaml, json, sys
with open("$CFG_ABS","r",encoding="utf-8") as f:
    data=yaml.safe_load(f)
print(json.dumps(data))
PY
)"
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="$(jqenv '.app.user')"
COMMON_NAME="$(jqapp '.internal_cn')"
ISSUE_TTL="$(jqapp '.issue_ttl')"
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" && "$APP_USER" != "null" && "$COMMON_NAME" != "null" && "$ISSUE_TTL" != "null" ]] || { err "incomplete config"; exit 2; }

export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"

ROLE_NAME="${APPN}-pki-issue"
POLICY_NAME="pki-issue-${APPN}"
PKI_ROLE="nginx-${APPN}"

HOME_DIR="/home/${APP_USER}"
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}"
TLS_DIR="${HOME_DIR}/tls"
ROLE_ID_FILE="${AGENT_DIR}/role_id"
SECRET_ID_FILE="${AGENT_DIR}/secret_id"
POST="${AGENT_DIR}/bin/vault-agent-post.sh"
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}.service"

info "using config: ${BOLD}${CFG_ABS}${RESET}  env=${BOLD}${ENV_NAME}${RESET}"
info "APP=${BOLD}${APPN}${RESET} USER=${BOLD}${APP_USER}${RESET} VAULT=${VAULT_ADDR} PKI=${PKI_MOUNT}"

id -u "${APP_USER}" >/dev/null 2>&1 || { err "user ${APP_USER} missing"; exit 3; }
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${HOME_DIR}"
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"

info "upsert policy ${POLICY_NAME}"
POL="$(mktemp)"; cat >"$POL" <<EOF
path "${PKI_MOUNT}/issue/${PKI_ROLE}" { capabilities=["create","update"] }
path "auth/approle/role/${ROLE_NAME}/role-id"   { capabilities=["read"] }
path "auth/approle/role/${ROLE_NAME}/secret-id" { capabilities=["create","update"] }
path "auth/token/renew-self" { capabilities=["update"] }
EOF
${VAULT_BIN} policy write "${POLICY_NAME}" "$POL" >/dev/null; rm -f "$POL"

info "upsert PKI role ${PKI_ROLE}"
base_domain="${COMMON_NAME#*.}"
${VAULT_BIN} write "${PKI_MOUNT}/roles/${PKI_ROLE}" \
  allowed_domains="${base_domain}" allow_subdomains=true allow_bare_domains=true \
  allow_wildcard_certificates=false max_ttl="720h" >/dev/null || true

info "upsert approle ${ROLE_NAME}"
${VAULT_BIN} write "auth/approle/role/${ROLE_NAME}" \
  policies="${POLICY_NAME}" secret_id_ttl=0 secret_id_num_uses=0 \
  token_ttl=24h token_max_ttl=0 bind_secret_id=true >/dev/null

ROLE_ID="$(${VAULT_BIN} read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
SECRET_ID="$(${VAULT_BIN} write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
info "RoleID: ${BOLD}${ROLE_ID}${RESET}"
info "SecretID: ${BOLD}${SECRET_ID:0:6}********${RESET}"

sudo bash -c "umask 077; printf '%s\n' '${ROLE_ID}'   > '${ROLE_ID_FILE}'; chown ${APP_USER}:${APP_USER} '${ROLE_ID_FILE}'; chmod 600 '${ROLE_ID_FILE}'"
sudo bash -c "umask 077; printf '%s\n' '${SECRET_ID}' > '${SECRET_ID_FILE}'; chown ${APP_USER}:${APP_USER} '${SECRET_ID_FILE}'; chmod 600 '${SECRET_ID_FILE}'"

info "write agent hcl/tpl (embed values, no env{} in template)"
sudo -u "${APP_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<HCL
pid_file = "${AGENT_DIR}/pidfile"
auto_auth {
  method "approle" { config = { role_id_file_path="${ROLE_ID_FILE}" secret_id_file_path="${SECRET_ID_FILE}" } }
  sink "file" { config = { path = "${AGENT_DIR}/token" } }
}
template {
  source      = "${AGENT_DIR}/cert.tpl"
  destination = "${AGENT_DIR}/.issue.json"
  command     = "OUTDIR='${TLS_DIR}' RELOAD_TLS_LABEL='tls=true' ${POST}"
}
HCL

sudo -u "${APP_USER}" tee "${AGENT_DIR}/cert.tpl" >/dev/null <<TPL
{{ with secret "${PKI_MOUNT}/issue/${PKI_ROLE}" "common_name=${COMMON_NAME}" "ttl=${ISSUE_TTL}" }}
{ "certificate": {{ toJSON .Data.certificate }}, "issuing_ca": {{ toJSON .Data.issuing_ca }}, "ca_chain": {{ toJSON .Data.ca_chain }}, "private_key": {{ toJSON .Data.private_key }} }
{{ end }}
TPL

info "install post script (overwrite)"
sudo /usr/bin/install -m 0755 -o "${APP_USER}" -g "${APP_USER}" -D "$POST_SRC" "$POST"

info "systemd user unit (overwrite)"
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
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
WorkingDirectory=${AGENT_DIR}
Environment=VAULT_ADDR=${VAULT_ADDR}
ExecStartPre=/bin/sh -lc 'echo "🚀 [unit][${APPN}] starting at \$(date -Is)"'
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
${SYSTEMD_BIN} 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}" ${SYSTEMD_BIN} --user daemon-reload
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user enable --now "vault-agent-${APPN}.service"

CERT="${TLS_DIR}/${APPN}.fullchain.pem"
if sudo -u "${APP_USER}" test -s "${CERT}"; then
  SIZE="$(sudo -u "${APP_USER}" wc -c < "${CERT}" || echo 0)"
  ok "TLS fullchain present: ${BOLD}${CERT}${RESET} (${SIZE} bytes)"
  openssl x509 -in "${CERT}" -noout -subject -issuer -enddate | sed "s/^/   🔎 /" || true
else
  err "TLS fullchain not found: ${CERT}"
fi
ok "SUCCESS app ${BOLD}${APPN}${RESET} (${ENV_NAME})"


===== ./setup-vault-agent-app-config2.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
#
# setup-vault-agent-app-config.sh
#
# Purpose:
#   Create a per-user systemd Vault Agent that issues & rotates a LEAF cert for one app.
#   - Creates PKI role, minimal policies, AppRole with a periodic token
#   - Stores role_id/secret_id under the app user's home
#   - Renders JSON via template; calls YOUR post-hook to write PEMs
#   - Uses a SAFE file sink (no /dev/null) to avoid temp-file errors
#
# Usage (run as admin, keep VAULT_ADMIN_TOKEN in env):
#   VAULT_ADMIN_TOKEN=hvs.XXXX \
#   sudo -E ./setup-vault-agent-app-config.sh --env test --config ./config/apps.yaml --app nctest
#
# Requirements: vault, python3, jq, openssl, systemd user instance for target user

# ---- Pretty logging
if [[ -t 1 ]]; then BOLD=$'\e[1m'; RESET=$'\e[0m'; BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m';
else BOLD=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""; fi
ts(){ date +"%Y-%m-%d %H:%M:%S"; }
info(){ echo -e "🟦 ${BLUE}${BOLD}[$(ts)]${RESET} $*"; }
ok(){   echo -e "🟩 ${GREEN}${BOLD}[$(ts)]${RESET} $*"; }
warn(){ echo -e "🟨 ${YELLOW}${BOLD}[$(ts)]${RESET} $*"; }
err(){  echo -e "🟥 ${RED}${BOLD}[$(ts)]${RESET} $*" >&2; }

# ---- Defaults
: "${LOG_LEVEL:=info}"
: "${VAULT_BIN:=/usr/bin/vault}"
: "${SYSTEMD_BIN:=/usr/bin/systemctl}"
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
: "${DEFAULT_ENV:=test}"

[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN missing"; exit 2; }

# ---- Args
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --env)    ENV_NAME="$2"; shift 2;;
    --config) CFG="$2";      shift 2;;
    --app)    APPN="$2";     shift 2;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) err "unknown arg: $1"; exit 2;;
  esac
done
[[ -n "$APPN" ]] || { err "--app is required"; exit 2; }

# ---- Binaries
need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
need python3; need jq; need "$VAULT_BIN"; need openssl

SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
POST_SRC="${SCRIPT_DIR}/scripts/vault-agent-post.sh"  # your existing hook (unchanged)
[[ -r "$POST_SRC" ]] || { err "post script missing: $POST_SRC"; exit 4; }

# ---- Load YAML
CFG_ABS="$(readlink -f "$CFG")"
CFGJSON="$(python3 - <<PY
import yaml, json; print(json.dumps(yaml.safe_load(open("$CFG_ABS","r",encoding="utf-8"))))
PY
)"
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=""

[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" && "$CN" != "null" && "$TTL" != "null" && -n "$APP_USER" ]] \
  || { err "missing required fields"; exit 2; }
EXT_HOST="$EXT_TEST"; [[ "$ENV_NAME" == "prod" ]] && EXT_HOST="$EXT_PROD"

export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"

ROLE_NAME="${APPN}-pki-issue"
POLICY_BOOT="pki-bootstrap-${APPN}"
POLICY_RUN="pki-issue-${APPN}"
PKI_ROLE="nginx-${APPN}"

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"                 # distribute your CA chain here (root or chain)
RID="${AGENT_DIR}/role_id"
SID="${AGENT_DIR}/secret_id"
TOKEN_FILE="${AGENT_DIR}/token"            # << SAFE sink path (fixes /dev/null issue)
POST="${AGENT_DIR}/bin/vault-agent-post.sh"
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}.service"
OUTDIR="${HOME_DIR}/vault/mtls"

info "env=${BOLD}${ENV_NAME}${RESET} VAULT=${VAULT_ADDR} PKI=${PKI_MOUNT}"
info "app=${BOLD}${APPN}${RESET} user=${BOLD}${APP_USER}${RESET} CN=${BOLD}${CN}${RESET} SAN=${BOLD}${EXT_HOST:-<none>}${RESET} TTL=${BOLD}${TTL}${RESET}"

# ---- Ensure user & dirs
id -u "${APP_USER}" >/dev/null 2>&1 || { err "linux user ${APP_USER} missing"; exit 3; }
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 (allow domains from CN + optional external host)
dom_int="${CN#*.}"; dom_ext=""
[[ -n "$EXT_HOST" ]] && dom_ext="${EXT_HOST#*.}"
ALLOWED="$(printf "%s\n%s\n" "$dom_int" "$dom_ext" | awk 'NF' | sort -u | paste -sd, -)"

info "upsert PKI role ${PKI_ROLE} (allowed_domains=${ALLOWED})"
${VAULT_BIN} write "${PKI_MOUNT}/roles/${PKI_ROLE}" \
  allowed_domains="${ALLOWED}" allow_subdomains=true allow_bare_domains=true \
  allow_wildcard_certificates=false server_flag=true client_flag=false \
  max_ttl="720h" >/dev/null || true

# ---- Policies
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/${ROLE_NAME}/role-id"   { capabilities=["read"] }
path "auth/approle/role/${ROLE_NAME}/secret-id" { capabilities=["create","update"] }
EOF
${VAULT_BIN} 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_BIN} policy write "${POLICY_RUN}" "$TMP" >/dev/null
rm -f "$TMP"

# ---- AppRole (periodic token; bind_secret_id)
${VAULT_BIN} auth enable approle >/dev/null 2>&1 || true
${VAULT_BIN} 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

# ---- Save role_id / secret_id for the agent
RID_VAL="$(${VAULT_BIN} read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
SID_VAL="$(${VAULT_BIN} 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}'"

# ---- Warn if CA missing (HTTPS to Vault needs trust)
if ! sudo -u "${APP_USER}" test -s "${CA_FILE}"; then
  warn "CA file not found for ${APP_USER}: ${CA_FILE} (agent TLS may fail)."
  warn "Distribute it with:  ./distribute_ca_to_agents.sh --src /home/vault/tls-${ENV_NAME}/root_ca.pem --users ${APP_USER}"
fi

# ---- Agent HCL & template (SAFE file sink inside AGENT_DIR)
PARAMS="\"common_name=${CN}\" \"ttl=${TTL}\""; [[ -n "$EXT_HOST" ]] && PARAMS="$PARAMS \"alt_names=${EXT_HOST}\""

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_server_name = "vault.test.local"
client_cert = "${OUTDIR}/agent.crt"
client_key  = "${OUTDIR}/agent.key"
}

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='tls=true' ${AGENT_DIR}/bin/vault-agent-post-leaf.sh"
}
HCL

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

# ---- Install your post hook
sudo /usr/bin/install -m 0755 -o "${APP_USER}" -g "${APP_USER}" -D "$POST_SRC" "${POST}"

# ---- Systemd user service (kept simple to avoid capability issues)
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
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

# ---- Enable lingering & start user manager
APP_UID="$(id -u "${APP_USER}")"
loginctl enable-linger "${APP_USER}" >/dev/null || true
${SYSTEMD_BIN} 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

# ---- Start/enable unit
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user daemon-reload
sudo -u "${APP_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --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 "cert ready: ${CERT}"
  openssl x509 -in "${CERT}" -noout -subject -issuer -enddate | sed 's/^/   🔎 /'
else
  warn "not yet rendered: ${CERT} (check: journalctl --user -u vault-agent-${APPN})"
fi


===== ./setup-vault-agent-app-config-v3.sh =====
#!/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}"


===== ./05_issue_admin_client_cert.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
# 05_issue_admin_client_cert.sh
#
# Purpose:
#   Issue an ADMIN mTLS client certificate from the environment's Intermediate PKI in Vault
#   and store it under $HOME/vault/tls-admin (for VAULT_CLIENT_CERT/KEY).
#
# Usage:
#   VAULT_TOKEN=hvs.XXX \
#   ./05_issue_admin_client_cert.sh --env test --config ./config/apps.yaml \
#     --cn vault-admin-setup --ttl 180h

if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= G= Y= E=; fi
ok(){   echo -e "🟩 ${G}${B}$*${R}"; }
warn(){ echo -e "🟨 ${Y}${B}$*${R}"; }
die(){  echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }

need(){ command -v "$1" >/dev/null || die "missing: $1"; }
need vault; need jq; need python3; need install

: "${VAULT_TOKEN:?Set VAULT_TOKEN (or VAULT_ADMIN_TOKEN)}"
if [[ -z "${VAULT_TOKEN:-}" && -n "${VAULT_ADMIN_TOKEN:-}" ]]; then
  export VAULT_TOKEN="$VAULT_ADMIN_TOKEN"
fi

CFG="./config/apps.yaml"; ENV_NAME="test"; CN="vault-admin-setup"; TTL="180h"
while [[ $# -gt 0 ]]; do
  case "$1" in
    --config) CFG="$2"; shift 2;;
    --env)    ENV_NAME="$2"; shift 2;;
    --cn)     CN="$2"; shift 2;;
    --ttl)    TTL="$2"; shift 2;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) die "Unknown arg: $1";;
  esac
done

CFG_ABS="$(readlink -f "$CFG")" || die "cannot resolve $CFG"
CFGJSON="$(python3 - <<PY
import yaml, json; print(json.dumps(yaml.safe_load(open("$CFG_ABS","r",encoding="utf-8"))))
PY
)"
jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }

VAULT_ADDR="$(jqenv '.vault_addr')"
INT_MOUNT="$(jqenv '.pki_mount')"
[[ "$VAULT_ADDR" != "null" && "$INT_MOUNT" != "null" ]] || die "incomplete config: vault_addr/pki_mount"
export VAULT_ADDR

ROLE="admin-client"
ok "Using Vault @ ${VAULT_ADDR} (mount: ${INT_MOUNT}) role=${ROLE} CN=${CN} TTL=${TTL}"

# Tighten for prod if desired (allowed_domains/allowed_uri_sans); default keeps it simple.
vault write "${INT_MOUNT}/roles/${ROLE}" \
  server_flag=false client_flag=true \
  allow_any_name=true key_usage="DigitalSignature" ext_key_usage="ClientAuth" \
  max_ttl="720h" >/dev/null 2>&1 || true

RESP="$(vault write -format=json "${INT_MOUNT}/issue/${ROLE}" common_name="${CN}" ttl="${TTL}")" || die "issue failed"
CERT="$(echo "$RESP" | jq -r .data.certificate)"
KEY="$(echo  "$RESP" | jq -r .data.private_key)"
ISSUING_CA="$(vault read -field=certificate "${INT_MOUNT}/cert/ca")"

OUT="$HOME/vault/tls-admin"; mkdir -p "$OUT"
echo "${KEY}"        | install -m 0600 /dev/stdin "${OUT}/admin.key"
echo "${CERT}"       | install -m 0644 /dev/stdin "${OUT}/admin.crt"
echo "${ISSUING_CA}" | install -m 0644 /dev/stdin "${OUT}/issuing_ca.pem"

ok "Wrote:"
echo " - ${OUT}/admin.key (0600)"
echo " - ${OUT}/admin.crt (0644)"
echo " - ${OUT}/issuing_ca.pem (issuer of admin.crt)"

cat <<EOF
Use:
  export VAULT_CLIENT_CERT='${OUT}/admin.crt'
  export VAULT_CLIENT_KEY='${OUT}/admin.key'
  vault status
EOF


===== ./01_make_offline_root_ca.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
# 01_make_offline_root_ca.sh
#
# Purpose:
#   Create an OFFLINE Root CA (private key + self-signed cert) under your sudo user's home,
#   NOT inside Vault. This keeps the root key off the server/container (best practice).
#
# Output:
#   /home/<deinUser>/vault/offline-root/<env>/
#     - root-ca.key        (0600)  EC-P256, encrypted if ROOT_CA_PASSPHRASE is set
#     - root-ca.pem        (0644)  self-signed cert
#     - openssl.cnf        (0644)  with v3_ca section
#
# Usage:
#   ./01_make_offline_root_ca.sh --env test
#   ROOT_CA_PASSPHRASE='********' ./01_make_offline_root_ca.sh --env prod
#
# Security:
#   - Keep root-ca.key offline and backed up safely (HSM/USB safe).
#   - DO NOT copy the private key to /home/vault.

if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= G= Y= E=; fi
ok(){ echo -e "🟩 ${G}${B}$*${R}"; }; die(){ echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }

ENV_NAME="test"
while [[ $# -gt 0 ]]; do
  case "$1" in
    --env) ENV_NAME="$2"; shift 2;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) die "Unknown arg: $1";;
  esac
done

ME_HOME="$(cd ~ && pwd)"
OUTDIR="${ME_HOME}/vault/offline-root/${ENV_NAME}"
mkdir -p "${OUTDIR}"

# OpenSSL config (v3_ca)
cat > "${OUTDIR}/openssl.cnf" <<'CNF'
[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
x509_extensions    = v3_ca
prompt             = no

[ req_distinguished_name ]
C  = CH
O  = PrivSec
CN = PrivSec OFFLINE Root CA

[ v3_ca ]
basicConstraints = CA:true
keyUsage         = keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
CNF

# EC P-256 key (encrypted if passphrase provided)
if [[ -n "${ROOT_CA_PASSPHRASE:-}" ]]; then
  (umask 077; openssl ecparam -name prime256v1 -genkey \
    | openssl ec -aes256 -passout env:ROOT_CA_PASSPHRASE -out "${OUTDIR}/root-ca.key")
else
  (umask 077; openssl ecparam -name prime256v1 -genkey -out "${OUTDIR}/root-ca.key")
fi
chmod 600 "${OUTDIR}/root-ca.key"

# Self-signed cert (10 years)
if [[ -n "${ROOT_CA_PASSPHRASE:-}" ]]; then
  openssl req -x509 -new -nodes -key "${OUTDIR}/root-ca.key" -passin env:ROOT_CA_PASSPHRASE \
    -sha256 -days 3650 -out "${OUTDIR}/root-ca.pem" -config "${OUTDIR}/openssl.cnf"
else
  openssl req -x509 -new -key "${OUTDIR}/root-ca.key" \
    -sha256 -days 3650 -out "${OUTDIR}/root-ca.pem" -config "${OUTDIR}/openssl.cnf"
fi
chmod 0644 "${OUTDIR}/root-ca.pem"

ok "Offline Root created at: ${OUTDIR}
 - Keep ${OUTDIR}/root-ca.key SAFE and OFFLINE.
 - Only the public cert (root-ca.pem) will be referenced by other scripts."


===== ./setup-vault-agent-mtls-client-config2.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
#
# setup-vault-agent-mtls-client-config2.sh
#
# Zweck:
#   Ein *Client-mTLS*-Zertifikat aus Vault ausstellen und NUR nach
#   /home/<user>/vault/mtls/{agent.key, agent.crt, ca.crt} schreiben.
#   KEIN Agent, KEIN systemd.
#
# Liest standardmäßig ./config/apps.yaml (felder: environments.<env>.pki_mount, apps[].{name,user,internal_cn,issue_ttl})
# lässt sich durch Flags überschreiben.
#
# Usage:
#   sudo -E ./setup-vault-agent-mtls-client-config2.sh \
#     --env test --app nctest [--config ./config/apps.yaml] \
#     [--user nctest] [--cn nctest.int.privsec.ch] [--ttl 24h] \
#     [--role client-nctest] [--pki pki-test] [--outdir /home/nctest/vault/mtls] \
#     [--ensure-role]
#
# Auth:
#   Nutzt VAULT_TOKEN, oder (falls leer) VAULT_ADMIN_TOKEN. VAULT_ADDR muss gesetzt sein.

# ---------- pretty logs ----------
if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= G= Y= E=; fi
ok(){   echo -e "🟩 ${G}${B}$*${R}"; }
warn(){ echo -e "🟨 ${Y}${B}$*${R}"; }
die(){  echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }
need(){ command -v "$1" >/dev/null 2>&1 || die "missing binary: $1"; }

need vault; need jq; need openssl

# ---------- args ----------
CFG="./config/apps.yaml"; ENV_NAME="test"; APPN=""
OVR_USER=""; OVR_CN=""; OVR_TTL=""; OVR_ROLE=""; OVR_PKI=""; OUTDIR=""
ENSURE_ROLE=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --config) CFG="$2"; shift 2;;
    --env)    ENV_NAME="$2"; shift 2;;
    --app)    APPN="$2"; shift 2;;
    --user)   OVR_USER="$2"; shift 2;;
    --cn)     OVR_CN="$2"; shift 2;;
    --ttl)    OVR_TTL="$2"; shift 2;;
    --role)   OVR_ROLE="$2"; shift 2;;
    --pki)    OVR_PKI="$2"; shift 2;;
    --outdir) OUTDIR="$2"; shift 2;;
    --ensure-role) ENSURE_ROLE=1; shift;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) die "unknown arg: $1";;
  esac
done
[[ -n "$APPN" ]] || die "--app ist erforderlich"

# ---------- auth/env ----------
if [[ -z "${VAULT_TOKEN:-}" && -n "${VAULT_ADMIN_TOKEN:-}" ]]; then
  export VAULT_TOKEN="$VAULT_ADMIN_TOKEN"
fi
: "${VAULT_TOKEN:?Setze VAULT_TOKEN oder VAULT_ADMIN_TOKEN}"
: "${VAULT_ADDR:?Setze VAULT_ADDR (z.B. https://127.0.0.1:22300)}"

# ---------- config laden (falls vorhanden) ----------
PKI_MOUNT=""; APP_USER=""; CN=""; TTL=""
if [[ -r "$CFG" ]]; then
  CFG_ABS="$(readlink -f "$CFG")"
  CFGJSON="$(python3 - <<PY
import yaml, json; print(json.dumps(yaml.safe_load(open("$CFG_ABS","r",encoding="utf-8"))))
PY
)" || die "YAML parse failed: $CFG"

  jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }
  jqapp(){ echo "$CFGJSON" | jq -r ".apps[] | select(.name==\"$APPN\")$1"; }

  PKI_MOUNT="$(jqenv '.pki_mount')"; [[ "$PKI_MOUNT" == "null" ]] && PKI_MOUNT=""
  APP_USER="$(jqapp '.user')";      [[ "$APP_USER" == "null" ]] && APP_USER=""
  CN="$(jqapp '.internal_cn')";     [[ "$CN" == "null" ]] && CN=""
  TTL="$(jqapp '.issue_ttl')";      [[ "$TTL" == "null" ]] && TTL=""
fi

# ---------- overrides & defaults ----------
[[ -n "$OVR_PKI"  ]] && PKI_MOUNT="$OVR_PKI"
[[ -n "$OVR_USER" ]] && APP_USER="$OVR_USER"
[[ -n "$OVR_CN"   ]] && CN="$OVR_CN"
[[ -n "$OVR_TTL"  ]] && TTL="$OVR_TTL"

[[ -n "$PKI_MOUNT" ]] || die "pki_mount unbekannt (Flag --pki oder in apps.yaml)"
[[ -n "$APP_USER"  ]] || die "user unbekannt (Flag --user oder in apps.yaml)"
[[ -n "$CN"        ]] || CN="${APPN}-agent"
[[ -n "$TTL"       ]] || TTL="720h"

ROLE="${OVR_ROLE:-client-${APPN}}"

HOME_DIR="/home/${APP_USER}"
OUTDIR="${OUTDIR:-${HOME_DIR}/vault/mtls}"

# ---------- user/outdir ----------
id -u "${APP_USER}" >/dev/null 2>&1 || die "Linux-User fehlt: ${APP_USER}"
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${OUTDIR}"

ok "Vault=${VAULT_ADDR}  PKI=${PKI_MOUNT}  Role=${ROLE}"
ok "User=${APP_USER}  CN=${CN}  TTL=${TTL}"
ok "Out=${OUTDIR}"

# ---------- optional: role anlegen/aktualisieren ----------
if [[ $ENSURE_ROLE -eq 1 ]]; then
  warn "ENSURE_ROLE aktiv → upsert PKI-Role ${ROLE}"
  vault write "${PKI_MOUNT}/roles/${ROLE}" \
    allow_any_name=true server_flag=false client_flag=true \
    key_type=ec key_bits=256 max_ttl=720h >/dev/null
fi

# ---------- zertifikat anfordern ----------
RESP_JSON="$(mktemp)"
cleanup(){ rm -f "$RESP_JSON"; }
trap cleanup EXIT

set +e
vault write -format=json "${PKI_MOUNT}/issue/${ROLE}" \
  "common_name=${CN}" "ttl=${TTL}" >"$RESP_JSON" 2>&1
RC=$?
set -e

if [[ $RC -ne 0 ]]; then
  echo "Vault issue call failed:" >&2
  cat "$RESP_JSON" >&2
  exit 1
fi

# Falls Vault Fehler-JSON in STDERR schrieb, steht jetzt trotzdem im File.
# Versuchen wir, gültiges JSON zu parsen und Daten zu extrahieren:
if ! jq -e . >/dev/null 2>&1 <"$RESP_JSON"; then
  echo "Kein gültiges JSON vom Issue-Endpoint:" >&2
  cat "$RESP_JSON" >&2
  exit 1
fi

CERT="$(jq -r '.data.certificate // ""'  "$RESP_JSON")"
KEY="$(jq  -r '.data.private_key // ""'  "$RESP_JSON")"
ISS_CA="$(jq -r '(.data.issuing_ca // (.data.ca_chain[0] // ""))' "$RESP_JSON")"

[[ -n "${CERT// }" ]] || die "leeres Zertifikat vom PKI-Endpoint"
[[ -n "${KEY// }"  ]] || die "leerer Private Key vom PKI-Endpoint"
if [[ -z "${ISS_CA// }" ]]; then
  # Fallback: Issuer direkt vom Mount lesen
  ISS_CA="$(vault read -field=certificate "${PKI_MOUNT}/cert/ca" 2>/dev/null || true)"
  [[ -n "${ISS_CA// }" ]] || die "keine Issuing CA im Response und am Mount"
fi

# ---------- schreiben mit owner/mode ----------
printf '%s\n' "$KEY"   | sudo install -m 0600 -o "${APP_USER}" -g "${APP_USER}" /dev/stdin "${OUTDIR}/agent.key"
printf '%s\n' "$CERT"  | sudo install -m 0644 -o "${APP_USER}" -g "${APP_USER}" /dev/stdin "${OUTDIR}/agent.crt"
printf '%s\n' "$ISS_CA"| sudo install -m 0644 -o "${APP_USER}" -g "${APP_USER}" /dev/stdin "${OUTDIR}/ca.crt"

ok "geschrieben:"
echo "  - ${OUTDIR}/agent.key (0600)"
echo "  - ${OUTDIR}/agent.crt (0644)"
echo "  - ${OUTDIR}/ca.crt    (0644)"

# ---------- mini-check ----------
openssl x509 -in "${OUTDIR}/agent.crt" -noout -subject -issuer -enddate | sed 's/^/   🔎 /'


===== ./bootstrap_secret_agent2.sh =====
#!/usr/bin/env bash
set -euo pipefail

# Minimal, idempotent bootstrap for a Vault Agent AppRole used by containers.
# Usage:
#   sudo -E ./bootstrap_secret_agent.sh <APPUSER> [KV_SUBPATH]
# Env (required):
#   VAULT_ADDR=https://127.0.0.1:22300
#   VAULT_TOKEN=<admin/root/installer token>
#   VAULT_CACERT=<path to your Root CA or chain>   # recommended for HTTPS
# Optional:
#   VAULT_NAMESPACE=<hcp/enterprise namespace>

if [[ $# -lt 1 || $# -gt 2 ]]; then
  echo "Usage: sudo -E $0 <APPUSER> [KV_SUBPATH]" >&2
  exit 2
fi

APPUSER="$1"
KV_SUBPATH="${2:-$APPUSER}"     # default subpath = APPUSER (e.g. nctest)

: "${VAULT_ADDR:?Set VAULT_ADDR (https://…)}"
: "${VAULT_TOKEN:?Set VAULT_TOKEN}"
: "${VAULT_NAMESPACE:=}"

KV_MOUNT="kv"                    # adjust if you use a different KV mount
ROLE="secret-agent-${APPUSER}"
POLICY="${ROLE}-policy"

need(){ command -v "$1" >/dev/null 2>&1 || { echo "missing: $1" >&2; exit 1; }; }
need vault; need getent; need sudo; need install

export VAULT_ADDR VAULT_TOKEN VAULT_NAMESPACE

HOMEDIR="$(getent passwd "$APPUSER" | cut -d: -f6 || true)"
[[ -n "$HOMEDIR" ]] || HOMEDIR="/home/${APPUSER}"
CREDS_DIR="${HOMEDIR}/vault/creds"

echo "==> Vault:  $VAULT_ADDR"
[[ -n "$VAULT_CACERT" ]] && echo "==> CA:     $VAULT_CACERT"
[[ -n "$VAULT_NAMESPACE" ]] && echo "==> NS:     $VAULT_NAMESPACE"
echo "==> APP:    $APPUSER"
echo "==> KV:     ${KV_MOUNT}/data/${KV_SUBPATH}"
echo "==> ROLE:   $ROLE"
echo "==> POLICY: $POLICY"
echo

# 1) Enable KV v2 (idempotent)
vault secrets enable -path="${KV_MOUNT}" kv-v2 >/dev/null 2>&1 || true

# 2) Write read-only policy to that subpath
POL="$(mktemp)"
cat >"$POL" <<EOF
path "${KV_MOUNT}/data/${KV_SUBPATH}" {
  capabilities = ["read"]
}
EOF
vault policy write "${POLICY}" "$POL" >/dev/null
rm -f "$POL"

# 3) Enable AppRole and upsert the role
vault auth enable approle >/dev/null 2>&1 || true
vault write "auth/approle/role/${ROLE}" \
  policies="${POLICY}" \
  secret_id_ttl=0 \
  secret_id_num_uses=0 \
  token_ttl=15m \
  token_max_ttl=30m >/dev/null

# 4) Fetch ROLE_ID and SECRET_ID
ROLE_ID="$(vault read  -field=role_id  "auth/approle/role/${ROLE}/role-id")"
SECRET_ID="$(vault write -field=secret_id -f "auth/approle/role/${ROLE}/secret-id")"

# 5) Install cred files with correct owner/mode
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"
sudo ls -l "${CREDS_DIR}"

# 6) Optional: seed secrets if subpath not yet present
if ! vault kv get "${KV_MOUNT}/${KV_SUBPATH}" >/dev/null 2>&1; then
  echo "==> Seeding ${KV_MOUNT}/${KV_SUBPATH} with example values (change later)"
  vault kv put "${KV_MOUNT}/${KV_SUBPATH}" \
    mariadb_root_password='CHANGE_ME_ROOT' \
    mysql_password='CHANGE_ME_APP' >/dev/null
else
  echo "==> KV already present at ${KV_MOUNT}/${KV_SUBPATH} (skip seed)"
fi

echo
echo "✔ Installed role_id/secret_id in: ${CREDS_DIR}"
echo "   Role:    ${ROLE}"
echo "   Policy:  ${POLICY}"
echo "   KV Path: ${KV_MOUNT}/${KV_SUBPATH}"

===== ./distribute_ca_to_agents_v3.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
# distribute_ca_to_agents_v3.sh
#
# v3: Kann "intermediate" direkt aus Vault lesen (pki_mount/cert/ca),
#     oder lokal "root" / "chain" vom Vault-Host kopieren.
#
# Beispiele:
#   # Agents (Trust = INTERMEDIATE):
#   sudo -E ./distribute_ca_to_agents_v3.sh \
#     --env test --config ./config/apps.yaml \
#     --which intermediate --users "nctest apptest"
#
#   # Proxy einmalig mit Chain (I+R) befüllen:
#   sudo -E ./distribute_ca_to_agents_v3.sh \
#     --env test --config ./config/apps.yaml \
#     --which chain --users proxytest \
#     --dest "/home/proxytest/nginx/ca/current-ca-chain.pem"
#
# Optionen:
#   --env <name>               (z.B. test|prod)
#   --config <file>            (apps.yaml für vault_addr/pki_mount)
#   --which intermediate|root|chain
#   --src <pem>                (überschreibt env/which für root/chain)
#   --users "u1 u2 ..."        (Space-separiert)
#   --users-file <file>        (ein User pro Zeile)
#   --dest <PATH>              (Ziel-Datei; Default: /home/<user>/vault/ca/ca.pem)
#   --pki <mount>              (übersteuert pki_mount aus config)
#   --addr <URL>               (übersteuert vault_addr aus config)
#   --force                    (erzwingt Überschreiben)
#
# Erwartet für "intermediate":
#   VAULT_TOKEN im Env (oder VAULT_ADMIN_TOKEN) + VAULT_ADDR (aus config/--addr)
#
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; }

need(){ command -v "$1" >/dev/null 2>&1 || { err "missing: $1"; exit 2; }; }
need sudo; need install; need getent
command -v openssl >/dev/null 2>&1 || warn "openssl not found → PEM-Check wird übersprungen"
command -v jq >/dev/null 2>&1 || warn "jq nicht gefunden → nur Datei-Mode verfügbar"

ENV_NAME=""; CFG=""; WHICH=""; SRC=""
USERS=""; USERS_FILE=""; DEST_OVERRIDE=""
PKI_MOUNT_OVERRIDE=""; ADDR_OVERRIDE=""
FORCE=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --env)     ENV_NAME="$2"; shift 2;;
    --config)  CFG="$2"; shift 2;;
    --which)   WHICH="$2"; shift 2;;
    --src)     SRC="$2"; shift 2;;
    --users)   USERS="$2"; shift 2;;
    --users-file) USERS_FILE="$2"; shift 2;;
    --dest)    DEST_OVERRIDE="$2"; shift 2;;
    --pki)     PKI_MOUNT_OVERRIDE="$2"; shift 2;;
    --addr)    ADDR_OVERRIDE="$2"; shift 2;;
    --force)   FORCE=1; shift;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) err "unknown arg: $1"; exit 2;;
  esac
done

[[ -n "$WHICH" ]] || { err "--which intermediate|root|chain ist Pflicht"; exit 2; }
if [[ -z "$SRC" && -z "$ENV_NAME" ]]; then
  err "entweder --src ODER --env angeben"; exit 2
fi

# ---- Users einsammeln ----
USERS_ARR=()
[[ -n "$USERS" ]] && read -r -a USERS_ARR <<<"$USERS"
if [[ -n "$USERS_FILE" ]]; then
  [[ -r "$USERS_FILE" ]] || { err "users file not readable: $USERS_FILE"; exit 2; }
  while IFS= read -r line; do
    line="${line%%#*}"; line="$(echo "$line" | xargs || true)"
    [[ -z "$line" ]] && continue
    USERS_ARR+=("$line")
  done < "$USERS_FILE"
fi
(( ${#USERS_ARR[@]} > 0 )) || { err "keine Nutzer angegeben (nutze --users oder --users-file)"; exit 2; }

# ---- Config laden (falls nötig) ----
CFGJSON=""
if [[ -n "$CFG" ]]; then
  need python3; need jq
  CFG_ABS="$(readlink -f "$CFG" 2>/dev/null || true)"
  [[ -n "$CFG_ABS" && -r "$CFG_ABS" ]] || { err "config nicht lesbar: $CFG"; exit 2; }
  CFGJSON="$(python3 - <<PY
import yaml, json, sys
print(json.dumps(yaml.safe_load(open("$CFG_ABS","r",encoding="utf-8"))))
PY
)"
fi

jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }

VAULT_ADDR="${ADDR_OVERRIDE:-}"
PKI_MOUNT="${PKI_MOUNT_OVERRIDE:-}"

if [[ -z "$SRC" ]]; then
  case "$WHICH" in
    root|chain)
      SRC="/home/vault/tls-${ENV_NAME}/$([[ $WHICH == root ]] && echo root_ca.pem || echo ca_chain.pem)"
      ;;
    intermediate)
      [[ -n "$CFGJSON" ]] || { err "--config apps.yaml ist für 'intermediate' nötig"; exit 2; }
      [[ -n "$VAULT_ADDR" ]] || VAULT_ADDR="$(jqenv '.vault_addr')"
      [[ -n "$PKI_MOUNT"  ]] || PKI_MOUNT="$(jqenv '.pki_mount')"
      [[ -n "$VAULT_ADDR" && "$VAULT_ADDR" != "null" ]] || { err "vault_addr fehlt (config/--addr)"; exit 2; }
      [[ -n "$PKI_MOUNT" && "$PKI_MOUNT" != "null"   ]] || { err "pki_mount fehlt (config/--pki)"; exit 2; }
      export VAULT_ADDR
      if [[ -z "${VAULT_TOKEN:-}" && -n "${VAULT_ADMIN_TOKEN:-}" ]]; then
        export VAULT_TOKEN="$VAULT_ADMIN_TOKEN"
      fi
      : "${VAULT_TOKEN:?VAULT_TOKEN oder VAULT_ADMIN_TOKEN im Env erforderlich für --which intermediate}"
      need vault
      TMP_SRC="$(mktemp)"
      if vault read -format=json "${PKI_MOUNT}/cert/ca" | jq -r '.data.certificate' >"$TMP_SRC" && [[ -s "$TMP_SRC" ]]; then
        SRC="$TMP_SRC"
      else
        err "konnte Intermediate aus Vault nicht lesen (${PKI_MOUNT}/cert/ca)"
        exit 3
      fi
      ;;
    *) err "--which muss intermediate|root|chain sein"; exit 2;;
  esac
fi

sudo test -r "$SRC" || { err "Quelle nicht lesbar (via sudo): $SRC"; exit 3; }
if command -v openssl >/dev/null 2>&1; then
  if ! sudo openssl x509 -in "$SRC" -noout >/dev/null 2>&1; then
    warn "openssl konnte keinen einzelnen Cert-Block parsen (bei Chain normal)."
  fi
fi

info "Quelle:   $SRC"
[[ -n "$VAULT_ADDR" ]] && info "Vault:    $VAULT_ADDR"
[[ -n "$PKI_MOUNT"  ]] && info "PKI:      $PKI_MOUNT"
info "Modus:    $WHICH"
echo

# ---- Kopieren pro User ----
COPIED=0; SKIPPED=0; MISSING=0; ERRORS=0

for u in "${USERS_ARR[@]}"; do
  if ! id -u "$u" >/dev/null 2>&1; then
    warn "user nicht gefunden: $u → skip"; ((MISSING++)); continue
  fi
  HOME_DIR="$(getent passwd "$u" | cut -d: -f6)"; [[ -n "$HOME_DIR" ]] || HOME_DIR="/home/$u"

  # Default-Ziel
  DEST="${DEST_OVERRIDE}"
  if [[ -z "$DEST" ]]; then
    DEST_DIR="${HOME_DIR}/vault/ca"
    [[ "$WHICH" == "chain" ]] && DEST_FILE="ca-chain.pem" || DEST_FILE="ca.pem"
    DEST="${DEST_DIR}/${DEST_FILE}"
  fi

  # Wenn mehrere Nutzer und ein absoluter identischer --dest → verhindern Clash
  if (( ${#USERS_ARR[@]} > 1 )) && [[ -n "$DEST_OVERRIDE" && "$DEST_OVERRIDE" = /* ]]; then
    err "--dest ist absolut und mehrere Nutzer angegeben → Pfad-Kollision. Bitte ohne --dest oder je User separat ausführen."
    exit 2
  fi

  DEST_DIR="$(dirname "$DEST")"
  sudo install -d -m 0755 -o "$u" -g "$u" "$DEST_DIR" >/dev/null

  if [[ -f "$DEST" && $FORCE -eq 0 ]] && sudo cmp -s "$SRC" "$DEST"; then
    ok "[$u] up-to-date → $DEST"; ((SKIPPED++)); continue
  fi
  if [[ -f "$DEST" && $FORCE -eq 0 ]]; then
    ok "[$u] existiert → skip (nutze --force zum Überschreiben): $DEST"; ((SKIPPED++)); continue
  fi

  if sudo install -m 0644 -o "$u" -g "$u" "$SRC" "$DEST"; then
    ok "[$u] geschrieben: $DEST"
    ((COPIED++))
  else
    err "[$u] schreiben fehlgeschlagen: $DEST"
    ((ERRORS++))
  fi
done

echo
ok "Fertig. Copied=${COPIED}, Skipped=${SKIPPED}, MissingUsers=${MISSING}, Errors=${ERRORS}"


===== ./all_files.txt =====

===== ./bootstrap_secret_agent.sh =====
#!/usr/bin/env bash
set -euo pipefail

# ============
# Usage & ENV
# ============
if [[ $# -ne 1 ]]; then
  echo "Usage: sudo -E $0 <APPUSER>" >&2
  echo "Erfordert: VAULT_ADDR und VAULT_TOKEN im Environment." >&2
  exit 1
fi
APPUSER="$1"

: "${VAULT_ADDR:?Setze VAULT_ADDR, z.B. https://vault.example.com:8200}"
: "${VAULT_TOKEN:?Setze VAULT_TOKEN (Admin- oder Approver-Token)}"
: "${VAULT_NAMESPACE:=}"   # optional (HCP/Enterprise)

# Ableitungen aus APPUSER
ROLE="secret-agent-${APPUSER}"
POLICY="${ROLE}-policy"
KV_PATH="kv"                  # KV v2 Mount (bei Bedarf anpassen)
SECRET_SUBPATH="${APPUSER}"   # effektiv: kv/${APPUSER}

# Zielpfade für Creds
HOMEDIR="$(getent passwd "$APPUSER" | cut -d: -f6 || true)"
[[ -n "${HOMEDIR}" ]] || HOMEDIR="/home/${APPUSER}"
CREDS_DIR="${HOMEDIR}/vault/creds"

export VAULT_ADDR VAULT_TOKEN VAULT_NAMESPACE

need() { command -v "$1" >/dev/null 2>&1 || { echo "Fehlt: $1" >&2; exit 1; }; }
need vault; need getent; need sudo; need install

echo "==> Vault:           ${VAULT_ADDR}"
[[ -n "${VAULT_NAMESPACE}" ]] && echo "==> Namespace:       ${VAULT_NAMESPACE}"
echo "==> APPUSER:         ${APPUSER}"
echo "==> ROLE:            ${ROLE}"
echo "==> POLICY:          ${POLICY}"
echo "==> KV mount:        ${KV_PATH}"
echo "==> Secret subpath:  ${SECRET_SUBPATH}  (effektiv: ${KV_PATH}/${SECRET_SUBPATH})"
echo "==> Creds-Dir:       ${CREDS_DIR}"
echo

# ==============================
# 1) Backend/Policy/AppRole (idempotent)
# ==============================
echo "==> Enable KV v2 (idempotent)…"
vault secrets enable -path="${KV_PATH}" kv-v2 >/dev/null 2>&1 || true

echo "==> Write policy ${POLICY} (read-only auf ${KV_PATH}/data/${SECRET_SUBPATH})…"
POLICY_FILE="$(mktemp)"
cat >"${POLICY_FILE}" <<EOF
path "${KV_PATH}/data/${SECRET_SUBPATH}" {
  capabilities = ["read"]
}
EOF
vault policy write "${POLICY}" "${POLICY_FILE}" >/dev/null
rm -f "${POLICY_FILE}"

echo "==> Enable AppRole auth (idempotent)…"
vault auth enable approle >/dev/null 2>&1 || true

echo "==> Create/Update AppRole ${ROLE}…"
vault write "auth/approle/role/${ROLE}" \
  policies="${POLICY}" \
  secret_id_ttl=0 \
  secret_id_num_uses=0 \
  token_ttl=15m \
  token_max_ttl=30m >/dev/null

# ==============================
# 2) ROLE_ID & SECRET_ID holen
# ==============================
echo "==> Fetch role_id & secret_id…"
ROLE_ID="$(vault read  -field=role_id  "auth/approle/role/${ROLE}/role-id")"
SECRET_ID="$(vault write -field=secret_id -f "auth/approle/role/${ROLE}/secret-id")"
[[ -n "${ROLE_ID}" && -n "${SECRET_ID}" ]] || { echo "✖ ROLE_ID/SECRET_ID leer – Abbruch"; exit 1; }

# ==============================
# 3) Dateien beim APPUSER installieren
# ==============================
echo "==> 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"
sudo ls -l "${CREDS_DIR}"

# ==============================
# 4) (optional) Seed-Secrets anlegen, falls nicht vorhanden
# ==============================
if ! vault kv get "${KV_PATH}/${SECRET_SUBPATH}" >/dev/null 2>&1; then
  echo "==> Seed secrets at ${KV_PATH}/${SECRET_SUBPATH}…"
  vault kv put "${KV_PATH}/${SECRET_SUBPATH}" \
    mariadb_root_password='CHANGE_ME_ROOT' \
    mysql_password='CHANGE_ME_APP' >/dev/null
  echo "   - Beispiel-Secrets gesetzt (bitte später ändern)."
else
  echo "   - Secrets existieren bereits – übersprungen."
fi

echo
echo "✔ Fertig. ROLE_ID/SECRET_ID installiert unter: ${CREDS_DIR}"
echo "   Rolle:  ${ROLE}"
echo "   Policy: ${POLICY}"
echo "   Secret: ${KV_PATH}/${SECRET_SUBPATH}"


===== ./scripts/vault-agent-post-chain.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail


# ===== Logs =====
if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= G= Y= E=; fi
info(){ echo -e "🟦 ${G}${B}[INFO]${R} $*"; }
ok(){ echo -e "🟩 ${G}${B}[ OK ]${R} $*"; }
warn(){ echo -e "🟨 ${Y}${B}[WARN]${R} $*"; }
die(){ echo -e "🟥 ${E}${B}[FAIL]${R} $*" >&2; exit 1; }


# ===== Context =====
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
AGENT_DIR="$(dirname -- "$SCRIPT_DIR")" # …/.vault-agent-<app>-ca
JSON_CA="${JSON_CA:-${AGENT_DIR}/.ca.json}" # vom template erzeugt
ROOT_FILE="${ROOT_FILE:-$HOME/vault/ca/ca.pem}" # Root-Zert für Anhang
CHAIN_FILE="${CHAIN_FILE:-$HOME/nginx/ca/current-ca-chain.pem}" # Zielpfad
RELOAD_TLS_LABEL="${RELOAD_TLS_LABEL:-tls=true}" # Label für NGINX-Reload (leer = aus)


command -v jq >/dev/null || die "jq not found"


[[ -r "$JSON_CA" ]] || die "render JSON missing: $JSON_CA"
[[ -r "$ROOT_FILE" ]] || die "Root file missing: $ROOT_FILE"
mkdir -p "$(dirname "$CHAIN_FILE")"


# ===== Build chain =====
TMP="$(mktemp "${CHAIN_FILE}.XXXX")"; trap 'rm -f "$TMP"' EXIT
ISS_CA=$(jq -r '.issuing_ca // (.ca_chain[0] // .certificate // "")' "$JSON_CA")
[[ -n "${ISS_CA// }" ]] || die "no issuing_ca in $JSON_CA"
{
printf '%s\n' "$ISS_CA"
cat "$ROOT_FILE"
} > "$TMP"


install -m 0644 -D "$TMP" "$CHAIN_FILE"
ok "chain → $CHAIN_FILE"


# ===== Optional container reload (label) =====
if [[ -n "$RELOAD_TLS_LABEL" ]]; then
if command -v podman >/dev/null 2>&1; then
info "reload label: $RELOAD_TLS_LABEL"
mapfile -t CIDS < <(podman ps --filter "label=${RELOAD_TLS_LABEL}" --format '{{.ID}}' | sed '/^$/d') || true
if (( ${#CIDS[@]} == 0 )); then
warn "no containers with label $RELOAD_TLS_LABEL → skip reload"
else
for cid in "${CIDS[@]}"; do
if podman exec "$cid" sh -lc 'nginx -t >/dev/null 2>&1 && nginx -s reload' >/dev/null 2>&1; then
ok "reload OK in ${cid}"
else
warn "reload FAILED in ${cid}"
fi
done
fi
else
warn "podman not found → skip reload"
fi
fi

===== ./scripts/vault-agent-post2.sh.bk =====
#!/usr/bin/env bash
set -Eeuo pipefail

# ========= Pretty logging =========
if [[ -t 1 ]]; then
  BOLD=$'\e[1m'; DIM=$'\e[2m'; RESET=$'\e[0m'
  BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'
else
  BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
fi
info(){ echo -e "🟦 ${BLUE}${BOLD}[INFO]${RESET} $*"; }
ok(){   echo -e "🟩 ${GREEN}${BOLD}[ OK ]${RESET} $*"; }
warn(){ echo -e "🟨 ${YELLOW}${BOLD}[WARN]${RESET} $*"; }
err(){  echo -e "🟥 ${RED}${BOLD}[FAIL]${RESET} $*" >&2; }

# ========= Derive context =========
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
AGENT_DIR="$(dirname -- "$SCRIPT_DIR")"            # …/.vault-agent-<app>
APP_NAME="${APP_NAME:-${AGENT_DIR##*.vault-agent-}}"
VERSION="${VERSION:-post-2025-09-22.3}"

# Inputs (Agent rendert genau EINS davon)
JSON_LEAF="${AGENT_DIR}/.issue.json"    # App-Zert (leaf)
JSON_CHAIN="${AGENT_DIR}/.ca.json"      # Proxy-CA-Kette
JSON="$JSON_LEAF"; [[ -s "$JSON" ]] || JSON="$JSON_CHAIN"

# Outputs
OUTDIR="${OUTDIR:-$HOME/tls}"                                   # leaf out
CHAIN_FILE="${CHAIN_FILE:-$HOME/nginx/ca/current-ca-chain.pem}" # proxy out
RELOAD_TLS_LABEL="${RELOAD_TLS_LABEL:-tls=true}"                # container label

info "post v${VERSION} for ${BOLD}${APP_NAME}${RESET}"
command -v jq >/dev/null || { err "jq not found"; exit 1; }

umask 077
mkdir -p "$OUTDIR" 2>/dev/null || true
mkdir -p "$(dirname "$CHAIN_FILE")" 2>/dev/null || true

[[ -s "$JSON" ]] || { err "render JSON missing: $JSON_LEAF | $JSON_CHAIN"; exit 1; }

# ========= LEAF vs CHAIN =========
if jq -e 'has("private_key")' "$JSON" >/dev/null 2>&1; then
  # ----- LEAF (App-Zert) -----
  tmp="$(mktemp -d "$OUTDIR/.staging.XXXX")"; trap 'rm -rf "$tmp"' EXIT
  jq -r .private_key "$JSON" > "$tmp/${APP_NAME}.key"
  jq -r '
    .certificate,
    (if (.ca_chain|type=="array") then (.ca_chain|join("\n"))
     else if (.ca_chain|type=="string") then .ca_chain
     else .issuing_ca end end)
  ' "$JSON" > "$tmp/${APP_NAME}.fullchain.pem"

  install -m 600 "$tmp/${APP_NAME}.key"           "$OUTDIR/${APP_NAME}.key"
  install -m 644 "$tmp/${APP_NAME}.fullchain.pem" "$OUTDIR/${APP_NAME}.fullchain.pem"
  ok "leaf written → ${OUTDIR}/${APP_NAME}.{key,fullchain.pem}"

  subj="$(jq -r '.certificate' "$JSON" | openssl x509 -noout -subject 2>/dev/null || true)"
  [[ -n "$subj" ]] && info "subject: ${subj#subject=}"
else
  # ----- CHAIN (Proxy-CA-Kette) -----
  tmp="$(mktemp "${CHAIN_FILE}.XXXX")"; trap 'rm -f "$tmp"' EXIT
  jq -r '
    if has("ca_chain") then
      (if (.ca_chain|type=="array") then (.ca_chain|join("\n")) else .ca_chain end)
    elif has("issuing_ca") then .issuing_ca
    elif has("certificate") then .certificate
    else empty end
  ' "$JSON" > "$tmp"
  install -m 644 "$tmp" "$CHAIN_FILE"
  ok "chain written → ${CHAIN_FILE}"
fi

# ========= Optional NGINX reload via podman labels =========
if ! command -v podman >/dev/null 2>&1; then
  warn "podman not found → skip container reload"
  exit 0
fi
info "reload label: ${BOLD}${RELOAD_TLS_LABEL}${RESET}"
mapfile -t CIDS < <(podman ps --filter "label=${RELOAD_TLS_LABEL}" --format '{{.ID}}' | sed '/^$/d') || true
if (( ${#CIDS[@]} == 0 )); then
  warn "no containers with label ${RELOAD_TLS_LABEL} → skip reload"
  exit 0
fi
info "containers: ${BOLD}${CIDS[*]}${RESET}"
for cid in "${CIDS[@]}"; do
  if podman exec "$cid" sh -lc 'nginx -t >/dev/null 2>&1 && nginx -s reload' >/dev/null 2>&1; then
    ok "reload OK in ${cid}"
  else
    warn "reload FAILED in ${cid}"
  fi
done


===== ./scripts/vault-agent-post-leaf.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail


# ===== Logs =====
if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= G= Y= E=; fi
info(){ echo -e "🟦 ${G}${B}[INFO]${R} $*"; }
ok(){ echo -e "🟩 ${G}${B}[ OK ]${R} $*"; }
warn(){ echo -e "🟨 ${Y}${B}[WARN]${R} $*"; }
die(){ echo -e "🟥 ${E}${B}[FAIL]${R} $*" >&2; exit 1; }


# ===== Context =====
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
AGENT_DIR="$(dirname -- "$SCRIPT_DIR")" # …/.vault-agent-<app>
APP_NAME="${APP_NAME:-${AGENT_DIR##*.vault-agent-}}"
JSON_ISSUE="${JSON_ISSUE:-${AGENT_DIR}/.issue.json}" # vom template erzeugt
OUTDIR="${OUTDIR:-$HOME/tls}"
RELOAD_TLS_LABEL="${RELOAD_TLS_LABEL:-}" # optional: Container-Reload per Label


command -v jq >/dev/null || die "jq not found"
command -v openssl >/dev/null || warn "openssl not found → no x509 summary"


[[ -r "$JSON_ISSUE" ]] || die "render JSON missing: $JSON_ISSUE"


umask 077
mkdir -p "$OUTDIR"


# ===== Extract & Write =====
TMP="$(mktemp -d "$OUTDIR/.staging.XXXX")"; trap 'rm -rf "$TMP"' EXIT


# private key
jq -r '.private_key // empty' "$JSON_ISSUE" > "$TMP/${APP_NAME}.key"
[[ -s "$TMP/${APP_NAME}.key" ]] || die "no private_key in $JSON_ISSUE"


# fullchain = certificate + (ca_chain | issuing_ca)
jq -r '
.certificate,
(if (.ca_chain|type=="array") then (.ca_chain|join("\n"))
else if (.ca_chain|type=="string") then .ca_chain
else .issuing_ca end end)
' "$JSON_ISSUE" > "$TMP/${APP_NAME}.fullchain.pem"
[[ -s "$TMP/${APP_NAME}.fullchain.pem" ]] || die "empty fullchain build"


install -m 600 "$TMP/${APP_NAME}.key" "$OUTDIR/${APP_NAME}.key"
install -m 644 "$TMP/${APP_NAME}.fullchain.pem" "$OUTDIR/${APP_NAME}.fullchain.pem"
ok "leaf written → $OUTDIR/${APP_NAME}.{key,fullchain.pem}"


# optional: brief x509
if command -v openssl >/dev/null; then
subj=$(awk 'BEGIN{p=0} /-----BEGIN CERTIFICATE-----/{p=1} p{print} /-----END CERTIFICATE-----/{exit}' "$OUTDIR/${APP_NAME}.fullchain.pem" | openssl x509 -noout -subject 2>/dev/null || true)
[[ -n "$subj" ]] && info "subject: ${subj#subject=}"
fi


# ===== Optional container reload (label) =====
if [[ -n "$RELOAD_TLS_LABEL" ]]; then
if command -v podman >/dev/null 2>&1; then
info "reload label: $RELOAD_TLS_LABEL"
mapfile -t CIDS < <(podman ps --filter "label=${RELOAD_TLS_LABEL}" --format '{{.ID}}' | sed '/^$/d') || true
if (( ${#CIDS[@]} == 0 )); then
warn "no containers with label $RELOAD_TLS_LABEL → skip reload"
else
for cid in "${CIDS[@]}"; do
if podman exec "$cid" sh -lc 'nginx -t >/dev/null 2>&1 && nginx -s reload' >/dev/null 2>&1; then
ok "reload OK in ${cid}"
else
warn "reload FAILED in ${cid}"
fi
done
fi
else
warn "podman not found → skip reload"
fi
fi

===== ./scripts/vault-agent-post2.sh.bk2 =====
#!/usr/bin/env bash
set -Eeuo pipefail

# ===== Minimal logging =====
if [[ -t 1 ]]; then
  BOLD=$'\e[1m'; RESET=$'\e[0m'
  BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'
else BOLD=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""; fi
info(){ echo -e "🟦 ${BLUE}${BOLD}[INFO]${RESET} $*"; }
ok(){   echo -e "🟩 ${GREEN}${BOLD}[ OK ]${RESET} $*"; }
warn(){ echo -e "🟨 ${YELLOW}${BOLD}[WARN]${RESET} $*"; }
err(){  echo -e "🟥 ${RED}${BOLD}[FAIL]${RESET} $*" >&2; }

# ===== Context =====
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
AGENT_DIR="$(dirname -- "$SCRIPT_DIR")"      # …/.vault-agent-<app>
APP_NAME="${APP_NAME:-${AGENT_DIR##*.vault-agent-}}"

# Agent renders exactly one JSON:
JSON_LEAF="${AGENT_DIR}/.issue.json"         # contains .private_key for leaf
JSON_CHAIN="${AGENT_DIR}/.ca.json"           # CA chain payload
JSON="$JSON_LEAF"; [[ -s "$JSON" ]] || JSON="$JSON_CHAIN"
[[ -s "$JSON" ]] || { err "no render JSON: $JSON_LEAF | $JSON_CHAIN"; exit 1; }

# ===== Output targets (simple defaults) =====
OUTDIR="${OUTDIR:-$HOME/tls}"                                   # for leaf
CHAIN_FILE="${CHAIN_FILE:-$HOME/nginx/ca/current-ca-chain.pem}" # for chain
RELOAD_TLS_LABEL="tls=true"  # fixed global label (intended)

command -v jq >/dev/null || { err "jq not found"; exit 1; }
umask 077
mkdir -p "$OUTDIR" 2>/dev/null || true
mkdir -p "$(dirname "$CHAIN_FILE")" 2>/dev/null || true

# ===== Leaf vs Chain =====
if jq -e 'has("private_key")' "$JSON" >/dev/null 2>&1; then
  # ---- LEAF (key + fullchain) ----
  tmp="$(mktemp -d "$OUTDIR/.staging.XXXX")"; trap 'rm -rf "$tmp"' EXIT
  jq -r .private_key "$JSON" > "$tmp/${APP_NAME}.key"
  jq -r '
    .certificate,
    (if (.ca_chain|type=="array") then (.ca_chain|join("\n"))
     else if (.ca_chain|type=="string") then .ca_chain
     else .issuing_ca end end)
  ' "$JSON" > "$tmp/${APP_NAME}.fullchain.pem"

  install -m 600 "$tmp/${APP_NAME}.key"           "$OUTDIR/${APP_NAME}.key"
  install -m 644 "$tmp/${APP_NAME}.fullchain.pem" "$OUTDIR/${APP_NAME}.fullchain.pem"
  ok "leaf → ${OUTDIR}/${APP_NAME}.{key,fullchain.pem}"
else
  # ---- CA CHAIN (single file) ----
  tmp="$(mktemp "${CHAIN_FILE}.XXXX")"; trap 'rm -f "$tmp"' EXIT
  jq -r '
    if has("ca_chain") then
      (if (.ca_chain|type=="array") then (.ca_chain|join("\n")) else .ca_chain end)
    elif has("issuing_ca") then .issuing_ca
    elif has("certificate") then .certificate
    else empty end
  ' "$JSON" > "$tmp"
  [[ -s "$tmp" ]] || { err "empty chain payload"; exit 1; }
  install -m 644 "$tmp" "$CHAIN_FILE"
  ok "chain → ${CHAIN_FILE}"
fi

# ===== Podman label reload (only) =====
if ! command -v podman >/dev/null 2>&1; then
  warn "podman not found → skip reload"
  exit 0
fi

info "reload label: ${BOLD}${RELOAD_TLS_LABEL}${RESET}"
mapfile -t CIDS < <(podman ps --filter "label=${RELOAD_TLS_LABEL}" --format '{{.ID}}' | sed '/^$/d') || true
if (( ${#CIDS[@]} == 0 )); then
  warn "no containers with label ${RELOAD_TLS_LABEL} → skip reload"
  exit 0
fi
for cid in "${CIDS[@]}"; do
  if podman exec "$cid" sh -lc 'nginx -t >/dev/null 2>&1 && nginx -s reload' >/dev/null 2>&1; then
    ok "nginx reload OK in ${cid}"
  else
    warn "reload failed in ${cid}"
  fi
done


===== ./scripts/vault-agent-post.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail

# absolute binaries (no PATH surprises)
JQ=/usr/bin/jq
OPENSSL=/usr/bin/openssl
INSTALL=/usr/bin/install
PODMAN=/usr/bin/podman
CAT=/usr/bin/cat
PRINTF=/usr/bin/printf

# Inputs
CA_JSON_PATH="${CA_JSON_PATH:-$PWD/.ca.json}"     # written by the template in AGENT_DIR
ROOT_FILE="${ROOT_FILE:-$HOME/vault/ca/ca.pem}"   # copied by setup script
: "${CHAIN_FILE:?missing CHAIN_FILE}"             # passed from setup script (apps.yaml → chain_path)
RELOAD_TLS_LABEL="${RELOAD_TLS_LABEL:-}"

[[ -r "$CA_JSON_PATH" ]] || { echo "ERR: no CA_JSON_PATH: $CA_JSON_PATH" >&2; exit 1; }
[[ -r "$ROOT_FILE"    ]] || { echo "ERR: no ROOT_FILE:   $ROOT_FILE" >&2; exit 1; }

tmp="$(mktemp)"; trap 'rm -f "$tmp"' EXIT

# 1) read issuing CA (intermediate) from JSON
ISS_CA="$("$JQ" -r '.issuing_ca // (.ca_chain[0] // "")' "$CA_JSON_PATH")"
if [[ -z "${ISS_CA// }" ]]; then
  echo "ERR: issuing_ca/ca_chain missing in $CA_JSON_PATH" >&2
  exit 1
fi

# 2) build chain: Intermediate + Root
{
  "$PRINTF" '%s\n' "$ISS_CA"
  "$CAT" "$ROOT_FILE"
} > "$tmp"

# 3) atomic write to CHAIN_FILE
$INSTALL -m 0644 -D "$tmp" "$CHAIN_FILE"
echo "🟩 [OK] chain → $CHAIN_FILE"

# 4) optional reload all containers with label
if [[ -n "$RELOAD_TLS_LABEL" ]]; then
  echo "🟦 [INFO] reload label: $RELOAD_TLS_LABEL"
  ids="$($PODMAN ps -q --filter "label=$RELOAD_TLS_LABEL" || true)"
  if [[ -n "${ids// }" ]]; then
    while read -r id; do
      [[ -z "$id" ]] && continue
      $PODMAN exec "$id" nginx -s reload || $PODMAN restart "$id" || true
    done <<< "$ids"
  else
    echo "🟨 [WARN] no containers with label $RELOAD_TLS_LABEL → skip reload"
  fi
fi


===== ./setup-vault-agent-proxy-config2.sh.bk =====
#!/usr/bin/env bash
set -Eeuo pipefail
#
# setup-vault-agent-proxy-config2.sh
#
# Purpose:
#   Create a *user* systemd Vault Agent that periodically fetches the ISSUING CA
#   (chain) for a proxy and writes it to the desired path.
#   - Own AppRole (no clash with leaf issuer)
#   - Renders to ~/.vault-agent-<app>-ca/.ca.json
#   - Calls YOUR post-hook with CHAIN_FILE to write the chain
#
# Usage:
#   VAULT_ADMIN_TOKEN=hvs.XXXX \
#   sudo -E ./setup-vault-agent-proxy-config2.sh --env test --config ./config/apps.yaml --app nctest
#
# Requirements: vault, python3, jq, openssl (jq used by post-hook), setcap/getcap (optional)

if [[ -t 1 ]]; then
  BOLD=$'\e[1m'; RESET=$'\e[0m'; BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'
else BOLD=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""; fi
ts(){ date +"%Y-%m-%d %H:%M:%S"; }
info(){ echo -e "🟦 ${BLUE}${BOLD}[$(ts)]${RESET} $*"; }
ok(){   echo -e "🟩 ${GREEN}${BOLD}[$(ts)]${RESET} $*"; }
warn(){ echo -e "🟨 ${YELLOW}${BOLD}[$(ts)]${RESET} $*"; }
err(){  echo -e "🟥 ${RED}${BOLD}[$(ts)]${RESET} $*" >&2; }

: "${LOG_LEVEL:=info}"
: "${VAULT_BIN:=/usr/bin/vault}"
: "${SYSTEMD_BIN:=/usr/bin/systemctl}"
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
: "${DEFAULT_ENV:=test}"

[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN missing"; exit 2; }

ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --env)    ENV_NAME="$2"; shift 2;;
    --config) CFG="$2";      shift 2;;
    --app)    APPN="$2";     shift 2;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) err "unknown arg: $1"; exit 2;;
  esac
done
[[ -n "$APPN" ]] || { err "--app is required"; exit 2; }

need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
need python3; need jq; need "$VAULT_BIN"

SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
POST_SRC="${SCRIPT_DIR}/scripts/vault-agent-post.sh"
[[ -r "$POST_SRC" ]] || { err "post script missing: $POST_SRC"; exit 4; }

CFG_ABS="$(readlink -f "$CFG")"
CFGJSON="$(python3 - <<PY
import yaml, json
with open("$CFG_ABS","r",encoding="utf-8") as f:
    print(json.dumps(yaml.safe_load(f)))
PY
)"
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')"
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" ]] || { err "incomplete env config"; exit 2; }

PROXY_USER_DEF="$(jqenv '.proxy.user')"
CHAIN_PATH_DEF="$(jqenv '.proxy.chain_path')"
PROXY_USER_APP="$(jqapp '.proxy_user')"
CHAIN_PATH_APP="$(jqapp '.proxy_chain_path')"

PROXY_USER="$PROXY_USER_DEF"; [[ "$PROXY_USER_APP" != "null" ]] && PROXY_USER="$PROXY_USER_APP"
CHAIN_PATH="$CHAIN_PATH_DEF"; [[ "$CHAIN_PATH_APP" != "null" ]] && CHAIN_PATH="$CHAIN_PATH_APP"
[[ -n "$PROXY_USER" && -n "$CHAIN_PATH" ]] || { err "missing required fields (proxy.user / proxy.chain_path)"; exit 2; }

export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"

ROLE_NAME="${APPN}-pki-ca"
POLICY_BOOT="pki-ca-bootstrap-${APPN}"
POLICY_RUN="pki-ca-read-${APPN}"

HOME_DIR="/home/${PROXY_USER}"
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}-ca"
RID="${AGENT_DIR}/role_id"
SID="${AGENT_DIR}/secret_id"
TOKEN_FILE="${AGENT_DIR}/token"
POST="${AGENT_DIR}/bin/vault-agent-post.sh"
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}-ca.service"
OUTDIR="${HOME_DIR}/vault/mtls"
CA_DIR="${HOME_DIR}/vault/ca"
CA_FILE="${CA_DIR}/ca.pem"

info "env=${BOLD}${ENV_NAME}${RESET} VAULT=${VAULT_ADDR} PKI=${PKI_MOUNT}"
info "proxy for app=${BOLD}${APPN}${RESET} user=${BOLD}${PROXY_USER}${RESET}"
info "chain_path=${BOLD}${CHAIN_PATH}${RESET}"
id -u "${PROXY_USER}" >/dev/null 2>&1 || { err "linux user ${PROXY_USER} missing"; exit 3; }

sudo install -d -m 0700 -o "${PROXY_USER}" -g "${PROXY_USER}" "${AGENT_DIR}"
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "${AGENT_DIR}/bin"
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "$(dirname "${CHAIN_PATH}")"
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "${CA_DIR}"

if ! sudo -u "${PROXY_USER}" test -s "${CA_FILE}"; then
  warn "CA file not found for ${PROXY_USER}: ${CA_FILE} (agent HTTPS verify may fail). Distribute your root/chain first."
fi

TMP="$(mktemp)"
cat >"$TMP" <<EOF
path "auth/approle/role/${ROLE_NAME}/role-id"   { capabilities=["read"] }
path "auth/approle/role/${ROLE_NAME}/secret-id" { capabilities=["create","update"] }
EOF
${VAULT_BIN} policy write "${POLICY_BOOT}" "$TMP" >/dev/null

cat >"$TMP" <<EOF
path "${PKI_MOUNT}/cert/ca"  { capabilities=["read"] }
path "auth/token/renew-self" { capabilities=["update"] }
EOF
${VAULT_BIN} policy write "${POLICY_RUN}" "$TMP" >/dev/null
rm -f "$TMP"

${VAULT_BIN} auth enable approle >/dev/null 2>&1 || true
${VAULT_BIN} 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_BIN} read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
SID_VAL="$(${VAULT_BIN} write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
sudo bash -c "umask 077; printf '%s' '${RID_VAL}' > '${RID}'; chown ${PROXY_USER}:${PROXY_USER} '${RID}'; chmod 600 '${RID}'"
sudo bash -c "umask 077; printf '%s' '${SID_VAL}' > '${SID}'; chown ${PROXY_USER}:${PROXY_USER} '${SID}'; chmod 600 '${SID}'"

sudo -u "${PROXY_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<HCL
pid_file = "${AGENT_DIR}/pidfile"

vault {
  address = "${VAULT_ADDR}"
  ca_cert = "${CA_FILE}"
tls_server_name = "vault.test.local"
client_cert = "${OUTDIR}/agent.crt"
client_key  = "${OUTDIR}/agent.key"
}

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 = 0600
    }
  }
}

template {
  source      = "${AGENT_DIR}/ca.tpl"
  destination = "${AGENT_DIR}/.ca.json"
  command     = "CHAIN_FILE='${CHAIN_PATH}' RELOAD_TLS_LABEL='tls=true' ${POST}"
}
HCL

sudo -u "${PROXY_USER}" tee "${AGENT_DIR}/ca.tpl" >/dev/null <<TPL
{{ with secret "${PKI_MOUNT}/cert/ca" }}
{ "issuing_ca": {{ toJSON .Data.certificate }} }
{{ end }}
TPL

sudo /usr/bin/install -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" -D "$POST_SRC" "${POST}"

sudo -u "${PROXY_USER}" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
sudo -u "${PROXY_USER}" tee "${UNIT}" >/dev/null <<UNIT
[Unit]
Description=Vault Agent (${APPN}-ca) - refresh proxy CA chain
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

# Hardening
#NoNewPrivileges=true
#PrivateTmp=true
#ProtectSystem=strict
#ProtectHome=read-only
#ReadOnlyPaths=${CA_DIR}
#ReadWritePaths=${AGENT_DIR} $(dirname "${CHAIN_PATH}")
#ProtectKernelTunables=true
#ProtectKernelModules=true
#ProtectControlGroups=true
#LockPersonality=true
#MemoryDenyWriteExecute=true
#RestrictSUIDSGID=true
#RestrictNamespaces=true


[Install]
WantedBy=default.target
UNIT

# OPTIONAL: remove file caps from vault binary (prevents 218/CAPABILITIES)
if command -v getcap >/dev/null 2>&1; then
  VBIN="$(command -v vault || echo "$VAULT_BIN")"
  CAPS="$(getcap "$VBIN" 2>/dev/null || true)"
  if [[ -n "${CAPS// }" ]]; then
    info "vault binary has file capabilities → ${CAPS}"
    if [[ $EUID -eq 0 ]]; then
      setcap -r "$VBIN" && ok "removed file capabilities from $VBIN" || warn "setcap -r failed (non-fatal)"
    else
      warn "run as root to remove caps: sudo setcap -r '$VBIN'"
    fi
  fi
fi

PROXY_UID="$(id -u "${PROXY_USER}")"
loginctl enable-linger "${PROXY_USER}" >/dev/null || true
${SYSTEMD_BIN} start "user@${PROXY_UID}.service" >/dev/null || true
XDG_RUNTIME_DIR="/run/user/${PROXY_UID}"
mkdir -p "$XDG_RUNTIME_DIR" && chown "${PROXY_USER}:${PROXY_USER}" "$XDG_RUNTIME_DIR" || true

sudo -u "${PROXY_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user daemon-reload
sudo -u "${PROXY_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user enable --now "vault-agent-${APPN}-ca.service"

if sudo -u "${PROXY_USER}" test -s "${CHAIN_PATH}"; then
  ok "CA chain present: ${CHAIN_PATH}"
else
  warn "CA chain not found yet: ${CHAIN_PATH} → check journal"
fi


===== ./03_issue_vault_server_cert.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
# 03_issue_vault_server_cert.sh
#
# Purpose:
#   Issue a Vault server cert from the environment's Intermediate in Vault,
#   and write files into /home/vault/tls-<env>.
#
# It composes:
#   - fullchain.crt  = server cert + intermediate
#   - ca_chain.pem   = intermediate + root (root from your offline-root location)
#
# Usage:
#   VAULT_TOKEN=hvs.XXX ./03_issue_vault_server_cert.sh \
#     --env test --config ./config/apps.yaml \
#     --cn vault.test.privsec.ch \
#     --dns "vault.test.privsec.ch,localhost" \
#     --ips "127.0.0.1,::1" \
#     --ttl 720h
#
# NOTE:
#   If your offline-root is elsewhere, pass --root-dir <path>.

if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; E=$'\e[31m'; else B= R= G= E=; fi
ok(){ echo -e "🟩 ${G}${B}$*${R}"; }
die(){ echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }

: "${VAULT_TOKEN:?Set VAULT_TOKEN}"

# ---- Defaults (updated) ----
CFG="./config/apps.yaml"
ENV_NAME="test"
CN="vault.test.privsec.ch"
DNS=""                               # if empty, will default to "<CN>,localhost"
IPS="127.0.0.1,::1"
TTL="720h"
ROOT_DIR_OVERRIDE=""

# ---- Args ----
while [[ $# -gt 0 ]]; do
  case "$1" in
    --config)   CFG="$2"; shift 2;;
    --env)      ENV_NAME="$2"; shift 2;;
    --cn)       CN="$2"; shift 2;;
    --dns)      DNS="$2"; shift 2;;
    --ips)      IPS="$2"; shift 2;;
    --ttl)      TTL="$2"; shift 2;;
    --root-dir) ROOT_DIR_OVERRIDE="$2"; shift 2;;
    -h|--help)  sed -n '1,200p' "$0"; exit 0;;
    *) die "Unknown arg: $1";;
  esac
done

# If DNS not provided, default to "<CN>,localhost"
[[ -n "$DNS" ]] || DNS="${CN},localhost"

need(){ command -v "$1" >/dev/null || { die "missing: $1"; }; }
need vault; need jq; need python3; need sudo

CFG_ABS="$(readlink -f "$CFG")"
CFGJSON="$(python3 - <<PY
import yaml, json; print(json.dumps(yaml.safe_load(open("$CFG_ABS","r",encoding="utf-8"))))
PY
)"
jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }

VAULT_ADDR="$(jqenv '.vault_addr')"
INT_MOUNT="$(jqenv '.pki_mount')"
[[ "$VAULT_ADDR" != "null" && "$INT_MOUNT" != "null" ]] || die "incomplete config"
export VAULT_ADDR

ME_HOME="$(cd ~ && pwd)"
ROOT_DIR="${ROOT_DIR_OVERRIDE:-${ME_HOME}/vault/offline-root/${ENV_NAME}}"
ROOT_CRT="${ROOT_DIR}/root-ca.pem"
[[ -s "$ROOT_CRT" ]] || die "root-ca.pem not found at $ROOT_CRT (run your offline-root step)"

TLSDIR="/home/vault/tls-${ENV_NAME}"
sudo install -d -m 0755 -o vault -g vault "${TLSDIR}"

# ---- Ensure PKI role exists (based on CN base domain) ----
BASE="$(echo "${CN}" | sed 's/^[^.]*\.//')"   # e.g. vault.test.privsec.ch -> test.privsec.ch
vault write "${INT_MOUNT}/roles/vault-server" \
  key_type="ec" allow_ip_sans=true \
  allowed_domains="${BASE}" allow_subdomains=true allow_bare_domains=true \
  server_flag=true client_flag=false max_ttl="2160h" >/dev/null 2>&1 || true

# ---- Issue cert ----
ARGS=( "common_name=${CN}" "ttl=${TTL}" )
[[ -n "$DNS" ]] && ARGS+=( "alt_names=${DNS}" )
[[ -n "$IPS" ]] && ARGS+=( "ip_sans=${IPS}" )

RESP="$(vault write -format=json "${INT_MOUNT}/issue/vault-server" "${ARGS[@]}")" \
  || die "vault issue failed"

CERT="$(echo "$RESP" | jq -r '.data.certificate')"
KEY="$(echo "$RESP"  | jq -r '.data.private_key')"
[[ -n "$CERT" && -n "$KEY" ]] || die "issue response missing certificate/private_key"

# fetch intermediate public
INT_CRT="$(vault read -field=certificate "${INT_MOUNT}/cert/ca")"
[[ -n "$INT_CRT" ]] || die "failed to fetch intermediate from ${INT_MOUNT}/cert/ca"

# ---- Write files ----
echo "${KEY}"  | sudo install -m 0600 -o vault -g vault /dev/stdin "${TLSDIR}/server.key"
echo "${CERT}" | sudo install -m 0644 -o vault -g vault /dev/stdin "${TLSDIR}/server.crt"
{ echo "${CERT}"; echo "${INT_CRT}"; } \
  | sudo install -m 0644 -o vault -g vault /dev/stdin "${TLSDIR}/fullchain.crt"
{ echo "${INT_CRT}"; cat "${ROOT_CRT}"; } \
  | sudo install -m 0644 -o vault -g vault /dev/stdin "${TLSDIR}/ca_chain.pem"

ok "Wrote:
 - ${TLSDIR}/server.key        (0600)
 - ${TLSDIR}/server.crt        (0644)
 - ${TLSDIR}/fullchain.crt     (server + intermediate)
 - ${TLSDIR}/ca_chain.pem      (intermediate + root)"


===== ./setup-vault-agent-proxy-config.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail

# ========= Pretty logging =========
if [[ -t 1 ]]; then
  BOLD=$'\e[1m'; DIM=$'\e[2m'; RESET=$'\e[0m'
  BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'
else
  BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
fi
ts(){ date +"%Y-%m-%d %H:%M:%S"; }
info(){ echo -e "🟦 ${BLUE}${BOLD}[$(ts)]${RESET} $*"; }
ok(){   echo -e "🟩 ${GREEN}${BOLD}[$(ts)]${RESET} $*"; }
warn(){ echo -e "🟨 ${YELLOW}${BOLD}[$(ts)]${RESET} $*"; }
err(){  echo -e "🟥 ${RED}${BOLD}[$(ts)]${RESET} $*" >&2; }

# ========= Defaults / Args =========
: "${LOG_LEVEL:=info}"
: "${VAULT_BIN:=/usr/bin/vault}"
: "${SYSTEMD_BIN:=/usr/bin/systemctl}"
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
: "${DEFAULT_ENV:=test}"

if [[ -z "${VAULT_ADMIN_TOKEN:-}" ]]; then
  err "VAULT_ADMIN_TOKEN missing. Example:
  sudo env VAULT_ADMIN_TOKEN=hvs.XXX $0 --env test --config ./config/apps.yaml --app apptest"
  exit 2
fi

ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --env)    ENV_NAME="$2"; shift 2;;
    --config) CFG="$2";      shift 2;;
    --app)    APPN="$2";     shift 2;;
    -h|--help) echo "usage: $0 [--env test|prod] [--config ./config/apps.yaml] --app <name>"; exit 0;;
    *) err "unknown arg: $1"; exit 2;;
  esac
done
[[ -n "$APPN" ]] || { err "--app is required"; exit 2; }

need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
need python3; need jq; need "$VAULT_BIN"

SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
POST_SRC="${SCRIPT_DIR}/scripts/vault-agent-post.sh"
[[ -r "$POST_SRC" ]] || { err "POST_SRC not readable: $POST_SRC"; exit 4; }

# ========= Load config =========
CFG_ABS="$(readlink -f "$CFG")"
CFGJSON="$(python3 - <<PY
import yaml, json, sys
with open("$CFG_ABS","r",encoding="utf-8") as f:
    data=yaml.safe_load(f)
print(json.dumps(data))
PY
)"
jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }

VAULT_ADDR="$(jqenv '.vault_addr')"
PKI_MOUNT="$(jqenv '.pki_mount')"
PROXY_USER="$(jqenv '.proxy.user')"
CHAIN_PATH="$(jqenv '.proxy.chain_path')"
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" && "$PROXY_USER" != "null" && "$CHAIN_PATH" != "null" ]] || { err "incomplete env config"; exit 2; }

export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"

ROLE_NAME="${APPN}-pki-issue"
POLICY_NAME="pki-issue-${APPN}"

HOME_DIR="/home/${PROXY_USER}"
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}"
ROLE_ID_FILE="${AGENT_DIR}/role_id"
SECRET_ID_FILE="${AGENT_DIR}/secret_id"
POST="${AGENT_DIR}/bin/vault-agent-post.sh"
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}.service"

info "using config: ${BOLD}${CFG_ABS}${RESET}  env=${BOLD}${ENV_NAME}${RESET}"
info "PROXY app=${BOLD}${APPN}${RESET} user=${BOLD}${PROXY_USER}${RESET} chain=${CHAIN_PATH}"

id -u "${PROXY_USER}" >/dev/null 2>&1 || { err "user ${PROXY_USER} missing"; exit 3; }
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "${HOME_DIR}"
sudo install -d -m 0700 -o "${PROXY_USER}" -g "${PROXY_USER}" "${AGENT_DIR}"
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "${AGENT_DIR}/bin"
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "$(dirname "${CHAIN_PATH}")"

info "policy upsert ${POLICY_NAME}"
POL="$(mktemp)"; cat >"$POL" <<EOF
path "${PKI_MOUNT}/cert/ca" { capabilities=["read"] }
path "auth/approle/role/${ROLE_NAME}/role-id"   { capabilities=["read"] }
path "auth/approle/role/${ROLE_NAME}/secret-id" { capabilities=["create","update"] }
path "auth/token/renew-self" { capabilities=["update"] }
EOF
${VAULT_BIN} policy write "${POLICY_NAME}" "$POL" >/dev/null; rm -f "$POL"

info "approle upsert ${ROLE_NAME}"
${VAULT_BIN} write "auth/approle/role/${ROLE_NAME}" \
  policies="${POLICY_NAME}" secret_id_ttl=0 secret_id_num_uses=0 \
  token_ttl=24h token_max_ttl=0 bind_secret_id=true >/dev/null

ROLE_ID="$(${VAULT_BIN} read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
SECRET_ID="$(${VAULT_BIN} write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
info "RoleID: ${BOLD}${ROLE_ID}${RESET}"
info "SecretID: ${BOLD}${SECRET_ID:0:6}********${RESET}"

sudo bash -c "umask 077; printf '%s\n' '${ROLE_ID}'   > '${ROLE_ID_FILE}'; chown ${PROXY_USER}:${PROXY_USER} '${ROLE_ID_FILE}'; chmod 600 '${ROLE_ID_FILE}'"
sudo bash -c "umask 077; printf '%s\n' '${SECRET_ID}' > '${SECRET_ID_FILE}'; chown ${PROXY_USER}:${PROXY_USER} '${SECRET_ID_FILE}'; chmod 600 '${SECRET_ID_FILE}'"

info "agent hcl/tpl (chain only, no env{} in template)"
sudo -u "${PROXY_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<HCL
pid_file = "${AGENT_DIR}/pidfile"
auto_auth {
  method "approle" { config = { role_id_file_path="${ROLE_ID_FILE}" secret_id_file_path="${SECRET_ID_FILE}" } }
  sink "file" { config = { path = "${AGENT_DIR}/token" } }
}
template {
  source      = "${AGENT_DIR}/ca.tpl"
  destination = "${AGENT_DIR}/.ca.json"
  command     = "CHAIN_FILE='${CHAIN_PATH}' RELOAD_TLS_LABEL='tls=true' ${POST}"
}
HCL

sudo -u "${PROXY_USER}" tee "${AGENT_DIR}/ca.tpl" >/dev/null <<TPL
{{ with secret "${PKI_MOUNT}/cert/ca" }}
{ "issuing_ca": {{ toJSON .Data.certificate }} }
{{ end }}
TPL

info "install post script (overwrite)"
sudo /usr/bin/install -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" -D "$POST_SRC" "$POST"

info "systemd user unit (overwrite)"
sudo -u "${PROXY_USER}" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
sudo -u "${PROXY_USER}" tee "${UNIT}" >/dev/null <<UNIT
[Unit]
Description=Vault Agent (${APPN}) - refresh proxy CA chain
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
WorkingDirectory=${AGENT_DIR}
Environment=VAULT_ADDR=${VAULT_ADDR}
ExecStartPre=/bin/sh -lc 'echo "🚀 [unit][proxy ${APPN}] starting at \$(date -Is)"'
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

PROXY_UID="$(id -u "${PROXY_USER}")"
loginctl enable-linger "${PROXY_USER}" >/dev/null || true
${SYSTEMD_BIN} start "user@${PROXY_UID}.service" >/dev/null || true
XDG_RUNTIME_DIR="/run/user/${PROXY_UID}"
mkdir -p "$XDG_RUNTIME_DIR" && chown "${PROXY_USER}:${PROXY_USER}" "$XDG_RUNTIME_DIR" || true
sudo -u "${PROXY_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user daemon-reload
sudo -u "${PROXY_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user enable --now "vault-agent-${APPN}.service"

# ---- Wait loop: first write can take a moment ----
TARGET="${CHAIN_PATH}"; OK=""
for i in {1..40}; do  # ~20s total
  if sudo -u "${PROXY_USER}" test -s "${TARGET}"; then
    SIZE="$(sudo -u "${PROXY_USER}" wc -c < "${TARGET}" || echo 0)"
    ok "CA chain present: ${BOLD}${TARGET}${RESET} (${SIZE} bytes)"
    OK=1; break
  fi
  sleep 0.5
done
[[ -n "${OK}" ]] || warn "CA chain not found yet: ${TARGET}  → agent is likely still rendering; check journal"

ok "SUCCESS proxy ${BOLD}${APPN}${RESET} (${ENV_NAME})"


===== ./set-vault-env-auto.sh.bk =====
#!/usr/bin/env sh
# set-vault-env-auto.sh
# Auto-detect & export: VAULT_ADDR, VAULT_CACERT, VAULT_CLIENT_CERT, VAULT_CLIENT_KEY, VAULT_TOKEN (+VAULT_ADMIN_TOKEN)
# Options override auto-detection. Must be *sourced*.

# -------- helpers (POSIX) --------
usage() {
  cat <<'EOF'
Usage: source ./set-vault-env-auto.sh [options]
Options (override auto-detection):
  --addr URL              e.g. https://127.0.0.1:22300
  --cacert PATH           e.g. $HOME/vault/offline-root/test/root-ca.pem
  --client-cert PATH      e.g. $HOME/vault/tls-admin/admin.crt
  --client-key PATH       e.g. $HOME/vault/tls-admin/admin.key
  --token-file PATH       file with token (JSON, "Key  Value" table, or plaintext)
  --token STRING          token string directly
  -q, --quiet             less output
Tip: must be *sourced* (not executed) so exports persist in your shell.
EOF
}

msg()  { [ -n "$QUIET" ] || printf '🟩 %s\n' "$*"; }
warn() { [ -n "$QUIET" ] || printf '🟨 %s\n' "$*" >&2; }
mask() {
  s=$1; n=${#s}
  if [ "$n" -le 10 ]; then printf '%s' "$s"; else
    printf '%s…%s' "$(printf '%s' "$s" | cut -c1-6)" "$(printf '%s' "$s" | tail -c 5)"
  fi
}

pick_first_readable() {
  for f in "$@"; do
    [ -n "$f" ] && [ -r "$f" ] && { printf '%s' "$f"; return 0; }
  done
  printf ''
}

token_from_table() { awk 'BEGIN{FS="[ \t]+"} $1=="token"{print $2; exit}' "$1"; }
token_from_json() {
  command -v jq >/dev/null 2>&1 || return 1
  jq -r '(.auth.client_token // .root_token // .data.token // .token // empty)' "$1"
}

# -------- parse args (override detection) --------
ADDR_OPT=""; CACERT_OPT=""; CCERT_OPT=""; CKEY_OPT=""; TOKEN_FILE_OPT=""; TOKEN_OPT=""
while [ $# -gt 0 ]; do
  case "$1" in
    --addr)        ADDR_OPT=$2; shift 2;;
    --cacert)      CACERT_OPT=$2; shift 2;;
    --client-cert) CCERT_OPT=$2; shift 2;;
    --client-key)  CKEY_OPT=$2; shift 2;;
    --token-file)  TOKEN_FILE_OPT=$2; shift 2;;
    --token)       TOKEN_OPT=$2; shift 2;;
    -q|--quiet)    QUIET=1; shift;;
    -h|--help)     usage; return 0 2>/dev/null || exit 0;;
    *)             warn "Unknown arg: $1"; usage; return 2 2>/dev/null || exit 2;;
  esac
done

# -------- detect values (options > auto)  --------
# 1) VAULT_ADDR
VAULT_ADDR=${ADDR_OPT:-https://127.0.0.1:22300}

# 2) VAULT_CACERT (Server-Trust) – Default: OFFLINE ROOT
if [ -n "$CACERT_OPT" ]; then
  VAULT_CACERT=$CACERT_OPT
else
  VAULT_CACERT=$(pick_first_readable \
    "$HOME/vault/tls-test/ca_chain.pem" \
    "/home/vault/tls-test/ca_chain.pem")
fi

# 3) Client cert/key (prefer admin, fallback agent)
if [ -n "$CCERT_OPT" ]; then VAULT_CLIENT_CERT=$CCERT_OPT; else
  if [ -r "$HOME/vault/tls-admin/admin.crt" ]; then
    VAULT_CLIENT_CERT="$HOME/vault/tls-admin/admin.crt"
  elif [ -r "/vault/mtls/agent.crt" ]; then
    VAULT_CLIENT_CERT="/vault/mtls/agent.crt"
  else
    VAULT_CLIENT_CERT=""
  fi
fi

if [ -n "$CKEY_OPT" ]; then VAULT_CLIENT_KEY=$CKEY_OPT; else
  if [ -r "$HOME/vault/tls-admin/admin.key" ]; then
    VAULT_CLIENT_KEY="$HOME/vault/tls-admin/admin.key"
  elif [ -r "/vault/mtls/agent.key" ]; then
    VAULT_CLIENT_KEY="/vault/mtls/agent.key"
  else
    VAULT_CLIENT_KEY=""
  fi
fi

# 4) Token (options > files > existing env)
if [ -n "$TOKEN_OPT" ]; then
  VAULT_TOKEN=$TOKEN_OPT
else
  TOKEN_FILE_CAND=$TOKEN_FILE_OPT
  if [ -z "$TOKEN_FILE_CAND" ]; then
    for f in "$HOME/vault/secrets/new-admin-token2.txt" "$HOME/vault/secrets/vault-init.json" "$HOME/.vault-token"; do
      [ -r "$f" ] && { TOKEN_FILE_CAND=$f; break; }
    done
  fi
  if [ -n "$TOKEN_FILE_CAND" ] && [ -r "$TOKEN_FILE_CAND" ]; then
    case "$TOKEN_FILE_CAND" in
      *.json)
        VAULT_TOKEN=$(token_from_json "$TOKEN_FILE_CAND")
        [ -n "$VAULT_TOKEN" ] || VAULT_TOKEN=$(grep -Eo '"(root_token|token)"[[:space:]]*:[[:space:]]*"[^"]+"' "$TOKEN_FILE_CAND" | head -n1 | sed -E 's/.*"([^"]+)".*/\1/')
        ;;
      *)
        VAULT_TOKEN=$(token_from_table "$TOKEN_FILE_CAND")
        [ -n "$VAULT_TOKEN" ] || VAULT_TOKEN=$(grep -E -m1 '^[[:alnum:]][[:alnum:]\.\-=_/]*$' "$TOKEN_FILE_CAND" 2>/dev/null || printf '')
        ;;
    esac
  else
    # fall back to existing env only if nothing else found
    VAULT_TOKEN=${VAULT_TOKEN:-}
  fi
fi
[ -n "$VAULT_TOKEN" ] && VAULT_ADMIN_TOKEN="$VAULT_TOKEN"

# -------- export --------
export VAULT_ADDR VAULT_CACERT VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_TOKEN VAULT_ADMIN_TOKEN

# -------- output --------
msg "env gesetzt:"
printf '  VAULT_ADDR         = %s\n' "${VAULT_ADDR}"
printf '  VAULT_CACERT       = %s\n' "${VAULT_CACERT:-<unset>}"
printf '  VAULT_CLIENT_CERT  = %s\n' "${VAULT_CLIENT_CERT:-<unset>}"
printf '  VAULT_CLIENT_KEY   = %s\n' "${VAULT_CLIENT_KEY:-<unset>}"
printf '  VAULT_TOKEN        = %s\n' "$(mask "${VAULT_TOKEN:-}")"
printf '  VAULT_ADMIN_TOKEN  = %s\n' "$(mask "${VAULT_ADMIN_TOKEN:-}")"

[ -n "${VAULT_CACERT:-}" ]      && [ ! -r "$VAULT_CACERT" ]      && warn "VAULT_CACERT not readable."
[ -n "${VAULT_CLIENT_CERT:-}" ] && [ ! -r "$VAULT_CLIENT_CERT" ] && warn "VAULT_CLIENT_CERT not readable."
[ -n "${VAULT_CLIENT_KEY:-}" ]  && [ ! -r "$VAULT_CLIENT_KEY" ]  && warn "VAULT_CLIENT_KEY not readable."
[ -z "${VAULT_TOKEN:-}" ] && warn "No token found. Use --token-file or --token to provide one."

# (print test hints)
if [ -z "$QUIET" ]; then
  printf '\nTests:\n'
  printf '  vault status\n'
  printf '  curl -sSk --cert "$VAULT_CLIENT_CERT" --key "$VAULT_CLIENT_KEY" '
  printf '--cacert "$VAULT_CACERT" "$VAULT_ADDR/v1/sys/health" | jq .\n'
fi


===== ./set-vault-env-auto.sh =====
#!/usr/bin/env sh
# set-vault-env-auto.sh
# Auto-detect & export: VAULT_ADDR, VAULT_CACERT, VAULT_CLIENT_CERT, VAULT_CLIENT_KEY, VAULT_TOKEN (+VAULT_ADMIN_TOKEN)
# Supports sudo reads from /home/vault/tls-<env>/… and optional copy into your $HOME.
# Usage: source ./set-vault-env-auto.sh [--env test|prod] [--addr URL] [--cacert PATH] [--client-cert PATH] [--client-key PATH] [--token-file PATH] [--token STRING] [--sudo-copy-ca] [-q]

# ---------- helpers ----------
usage() {
  cat <<'EOF'
Usage: source ./set-vault-env-auto.sh [options]
Options:
  --env NAME            test|prod (default: test) → prefers /home/vault/tls-<env>/*
  --addr URL            e.g. https://127.0.0.1:22300 (default: https://127.0.0.1:22300)
  --cacert PATH         explicit CA file
  --client-cert PATH    admin/agent client cert
  --client-key  PATH    admin/agent client key
  --token-file PATH     file with token (JSON or "Key  Value" table or plaintext)
  --token STRING        token string directly
  --sudo-copy-ca        copy /home/vault/tls-<env>/ca_chain.pem → $HOME/vault/ca/ca.pem (via sudo)
  -q, --quiet           less output
Tip: must be *sourced* so exports persist in your shell.
EOF
}

msg()  { [ -n "$QUIET" ] || printf '🟩 %s\n' "$*"; }
warn() { [ -n "$QUIET" ] || printf '🟨 %s\n' "$*" >&2; }
mask() {
  s=$1; n=${#s}; if [ "$n" -le 10 ]; then printf '%s' "$s"; else
    printf '%s…%s' "$(printf '%s' "$s" | cut -c1-6)" "$(printf '%s' "$s" | tail -c 5)"; fi
}

pick_first_readable() {
  for f in "$@"; do [ -n "$f" ] && [ -r "$f" ] && { printf '%s' "$f"; return 0; }; done
  printf ''
}
pick_first_readable_sudo() {
  for f in "$@"; do
    [ -n "$f" ] || continue
    if command -v sudo >/dev/null 2>&1 && sudo test -r "$f" 2>/dev/null; then
      printf '%s' "$f"; return 0
    fi
  done
  printf ''
}

token_from_table() { awk 'BEGIN{FS="[ \t]+"} $1=="token"{print $2; exit}' "$1"; }
token_from_json() { command -v jq >/dev/null 2>&1 || return 1; jq -r '(.auth.client_token // .root_token // .data.token // .token // empty)' "$1"; }

# ---------- parse args ----------
ENV_NAME="test"; ADDR_OPT=""; CACERT_OPT=""; CCERT_OPT=""; CKEY_OPT=""; TOKEN_FILE_OPT=""; TOKEN_OPT=""
SUDO_COPY_CA=""
while [ $# -gt 0 ]; do
  case "$1" in
    --env)          ENV_NAME=$2; shift 2;;
    --addr)         ADDR_OPT=$2; shift 2;;
    --cacert)       CACERT_OPT=$2; shift 2;;
    --client-cert)  CCERT_OPT=$2; shift 2;;
    --client-key)   CKEY_OPT=$2; shift 2;;
    --token-file)   TOKEN_FILE_OPT=$2; shift 2;;
    --token)        TOKEN_OPT=$2; shift 2;;
    --sudo-copy-ca) SUDO_COPY_CA=1; shift;;
    -q|--quiet)     QUIET=1; shift;;
    -h|--help)      usage; return 0 2>/dev/null || exit 0;;
    *)              warn "Unknown arg: $1"; usage; return 2 2>/dev/null || exit 2;;
  esac
done

# ---------- derive common paths ----------
TLS_DIR_VAULT="/home/vault/tls-${ENV_NAME}"
TLS_DIR_USER="$HOME/vault/tls-${ENV_NAME}"
USER_CA_DIR="$HOME/vault/ca"
USER_CA="$USER_CA_DIR/ca.pem"
OFFLINE_ROOT="$HOME/vault/offline-root/${ENV_NAME}"

# ---------- 1) VAULT_ADDR ----------
# default https; override with --addr if needed
VAULT_ADDR=${ADDR_OPT:-https://127.0.0.1:22300}

# ---------- 2) VAULT_CACERT (prefer CHAIN) ----------
if [ -n "$CACERT_OPT" ]; then
  VAULT_CACERT=$CACERT_OPT
else
  # try user's own CA, then sudo-read from /home/vault, then offline root as last resort
  VAULT_CACERT=$(pick_first_readable \
      "$USER_CA" \
      "$TLS_DIR_USER/ca_chain.pem" \
      "$OFFLINE_ROOT/root-ca.pem")
  if [ -z "$VAULT_CACERT" ]; then
    VAULT_CACERT=$(pick_first_readable_sudo \
      "$TLS_DIR_VAULT/ca_chain.pem" \
      "$TLS_DIR_VAULT/root_ca.pem")
    # Optionally copy into user's $HOME (so future runs don't need sudo)
    if [ -n "$VAULT_CACERT" ] && [ -n "$SUDO_COPY_CA" ]; then
      if command -v sudo >/dev/null 2>&1; then
        # choose chain if available; otherwise whatever we found
        SRC=$(pick_first_readable_sudo "$TLS_DIR_VAULT/ca_chain.pem")
        [ -n "$SRC" ] || SRC="$VAULT_CACERT"
        sudo install -d -m 0755 -o "$USER" -g "$USER" "$USER_CA_DIR" 2>/dev/null || true
        sudo install -m 0644 -o "$USER" -g "$USER" "$SRC" "$USER_CA" 2>/dev/null && VAULT_CACERT="$USER_CA"
      fi
    fi
  fi
fi

# ---------- 3) VAULT_CLIENT_CERT/KEY ----------
if [ -n "$CCERT_OPT" ]; then VAULT_CLIENT_CERT=$CCERT_OPT; else
  if [ -r "$HOME/vault/tls-admin/admin.crt" ]; then
    VAULT_CLIENT_CERT="$HOME/vault/tls-admin/admin.crt"
  elif [ -r "$HOME/vault/mtls/agent.crt" ]; then
    VAULT_CLIENT_CERT="$HOME/vault/mtls/agent.crt"
  else
    VAULT_CLIENT_CERT=""
  fi
fi
if [ -n "$CKEY_OPT" ]; then VAULT_CLIENT_KEY=$CKEY_OPT; else
  if [ -r "$HOME/vault/tls-admin/admin.key" ]; then
    VAULT_CLIENT_KEY="$HOME/vault/tls-admin/admin.key"
  elif [ -r "$HOME/vault/mtls/agent.key" ]; then
    VAULT_CLIENT_KEY="$HOME/vault/mtls/agent.key"
  else
    VAULT_CLIENT_KEY=""
  fi
fi

# ---------- 4) VAULT_TOKEN (+VAULT_ADMIN_TOKEN) ----------
if [ -n "$TOKEN_OPT" ]; then
  VAULT_TOKEN=$TOKEN_OPT
else
  TOKEN_FILE_CAND=$TOKEN_FILE_OPT
  if [ -z "$TOKEN_FILE_CAND" ]; then
    for f in "$HOME/vault/secrets/new-admin-token2.txt" "$HOME/vault/secrets/vault-init.json" "$HOME/.vault-token"; do
      [ -r "$f" ] && { TOKEN_FILE_CAND=$f; break; }
    done
  fi
  if [ -n "$TOKEN_FILE_CAND" ] && [ -r "$TOKEN_FILE_CAND" ]; then
    case "$TOKEN_FILE_CAND" in
      *.json)
        VAULT_TOKEN=$(token_from_json "$TOKEN_FILE_CAND")
        [ -n "$VAULT_TOKEN" ] || VAULT_TOKEN=$(grep -Eo '"(root_token|token)"[[:space:]]*:[[:space:]]*"[^"]+"' "$TOKEN_FILE_CAND" | head -n1 | sed -E 's/.*"([^"]+)".*/\1/')
        ;;
      *)
        VAULT_TOKEN=$(token_from_table "$TOKEN_FILE_CAND")
        [ -n "$VAULT_TOKEN" ] || VAULT_TOKEN=$(grep -E -m1 '^[[:alnum:]][[:alnum:]\.\-=_/]*$' "$TOKEN_FILE_CAND" 2>/dev/null || printf '')
        ;;
    esac
  else
    VAULT_TOKEN=${VAULT_TOKEN:-}
  fi
fi
[ -n "$VAULT_TOKEN" ] && VAULT_ADMIN_TOKEN="$VAULT_TOKEN"

# ---------- export ----------
export VAULT_ADDR VAULT_CACERT VAULT_CLIENT_CERT VAULT_CLIENT_KEY VAULT_TOKEN VAULT_ADMIN_TOKEN

# ---------- output ----------
msg "env gesetzt:"
printf '  VAULT_ADDR         = %s\n' "${VAULT_ADDR}"
printf '  VAULT_CACERT       = %s\n' "${VAULT_CACERT:-<unset>}"
printf '  VAULT_CLIENT_CERT  = %s\n' "${VAULT_CLIENT_CERT:-<unset>}"
printf '  VAULT_CLIENT_KEY   = %s\n' "${VAULT_CLIENT_KEY:-<unset>}"
printf '  VAULT_TOKEN        = %s\n' "$(mask "${VAULT_TOKEN:-}")"
printf '  VAULT_ADMIN_TOKEN  = %s\n' "$(mask "${VAULT_ADMIN_TOKEN:-}")"

[ -n "${VAULT_CACERT:-}" ]      && [ ! -r "$VAULT_CACERT" ]      && warn "VAULT_CACERT not readable (maybe sudo-only path; consider --sudo-copy-ca)."
[ -n "${VAULT_CLIENT_CERT:-}" ] && [ ! -r "$VAULT_CLIENT_CERT" ] && warn "VAULT_CLIENT_CERT not readable."
[ -n "${VAULT_CLIENT_KEY:-}" ]  && [ ! -r "$VAULT_CLIENT_KEY" ]  && warn "VAULT_CLIENT_KEY not readable."
[ -z "${VAULT_TOKEN:-}" ] && warn "No token found. Use --token-file or --token."

# ---------- tests ----------
if [ -z "$QUIET" ]; then
  printf '\nTests:\n'
  if [ -n "$VAULT_CACERT" ]; then
    printf '  vault status\n'
    printf '  curl -sS --cert "$VAULT_CLIENT_CERT" --key "$VAULT_CLIENT_KEY" --cacert "$VAULT_CACERT" "$VAULT_ADDR/v1/sys/health" | jq .\n'
  else
    printf '  (no CA set) vault status -ca-path <path-to-ca>  # or set --sudo-copy-ca to pull from /home/vault\n'
  fi
fi


===== ./setup-vault-agent-proxy-config2.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
#
# setup-vault-agent-proxy-config2.sh
#
# Purpose:
#   Create a *user* systemd Vault Agent that periodically fetches the ISSUING CA
#   (Intermediate) from Vault and writes a full chain file for NGINX:
#     chain = <Intermediate> + <Root>
#   - Copies Root CA from /home/vault/tls-<env>/root_ca.pem to the proxy user
#   - Uses a dedicated AppRole for this CA-fetcher (separate from leaf issuer)
#   - Renders to ~/.vault-agent-<app>-ca/.ca.json and calls your post-hook
#
# Usage:
#   VAULT_ADMIN_TOKEN=hvs.XXXX \
#   sudo -E ./setup-vault-agent-proxy-config2.sh --env test --config ./config/apps.yaml --app proxytest
#
# Config (apps.yaml):
#   environments.<env>.{vault_addr,pki_mount,proxy.user,proxy.chain_path,proxy.reload}
#   apps[].{name,proxy_user,proxy_chain_path,proxy_reload}  # optional per-app overrides
#
# Requirements: vault, python3, jq

# ---------- pretty logs ----------
if [[ -t 1 ]]; then
  BOLD=$'\e[1m'; RESET=$'\e[0m'; BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'
else BOLD=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""; fi
ts(){ date +"%Y-%m-%d %H:%M:%S"; }
info(){ echo -e "🟦 ${BLUE}${BOLD}[$(ts)]${RESET} $*"; }
ok(){   echo -e "🟩 ${GREEN}${BOLD}[$(ts)]${RESET} $*"; }
warn(){ echo -e "🟨 ${YELLOW}${BOLD}[$(ts)]${RESET} $*"; }
err(){  echo -e "🟥 ${RED}${BOLD}[$(ts)]${RESET} $*" >&2; }

# ---------- defaults ----------
: "${LOG_LEVEL:=info}"
: "${VAULT_BIN:=/usr/bin/vault}"
: "${SYSTEMD_BIN:=/usr/bin/systemctl}"
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
: "${DEFAULT_ENV:=test}"

[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN missing"; exit 2; }

# ---------- args ----------
ENV_NAME="$DEFAULT_ENV"; CFG="$DEFAULT_CONFIG"; APPN=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --env)    ENV_NAME="$2"; shift 2;;
    --config) CFG="$2";      shift 2;;
    --app)    APPN="$2";     shift 2;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) err "unknown arg: $1"; exit 2;;
  esac
done
[[ -n "$APPN" ]] || { err "--app is required"; exit 2; }

need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
need python3; need jq; need "$VAULT_BIN"

SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
POST_SRC="${SCRIPT_DIR}/scripts/vault-agent-post.sh"
[[ -r "$POST_SRC" ]] || { err "post script missing: $POST_SRC"; exit 4; }

# ---------- load YAML ----------
CFG_ABS="$(readlink -f "$CFG")"
CFGJSON="$(python3 - <<PY
import yaml, json
with open("$CFG_ABS","r",encoding="utf-8") as f:
    print(json.dumps(yaml.safe_load(f)))
PY
)"
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')"
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" ]] || { err "incomplete env config"; exit 2; }

PROXY_USER_DEF="$(jqenv '.proxy.user')"
CHAIN_PATH_DEF="$(jqenv '.proxy.chain_path')"
RELOAD_DEF="$(jqenv '.proxy.reload')"

PROXY_USER_APP="$(jqapp '.proxy_user')"
CHAIN_PATH_APP="$(jqapp '.proxy_chain_path')"
RELOAD_APP="$(jqapp '.proxy_reload')"

PROXY_USER="$PROXY_USER_DEF"; [[ "$PROXY_USER_APP" != "null" ]] && PROXY_USER="$PROXY_USER_APP"
CHAIN_PATH="$CHAIN_PATH_DEF"; [[ "$CHAIN_PATH_APP" != "null" ]] && CHAIN_PATH="$CHAIN_PATH_APP"
RELOAD_LABEL="${RELOAD_APP:-$RELOAD_DEF}"
[[ "$RELOAD_LABEL" == "null" || -z "$RELOAD_LABEL" ]] && RELOAD_LABEL="tls=true"

[[ -n "$PROXY_USER" && -n "$CHAIN_PATH" ]] || { err "missing required fields (proxy.user / proxy.chain_path)"; exit 2; }

export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"

ROLE_NAME="${APPN}-pki-ca"
POLICY_BOOT="pki-ca-bootstrap-${APPN}"
POLICY_RUN="pki-ca-read-${APPN}"

HOME_DIR="/home/${PROXY_USER}"
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}-ca"
RID="${AGENT_DIR}/role_id"
SID="${AGENT_DIR}/secret_id"
TOKEN_FILE="${AGENT_DIR}/token"
POST="${AGENT_DIR}/bin/vault-agent-post.sh"
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}-ca.service"

# Paths for trust material
OUTDIR="${HOME_DIR}/vault/mtls"          # where a client cert would live if you enforce client mTLS
CA_DIR="${HOME_DIR}/vault/ca"
CA_FILE="${CA_DIR}/ca.pem"               # Root CA (will be copied here)
ROOT_SRC="/home/vault/tls-${ENV_NAME}/root_ca.pem"

info "env=${BOLD}${ENV_NAME}${RESET} VAULT=${VAULT_ADDR} PKI=${PKI_MOUNT}"
info "proxy for app=${BOLD}${APPN}${RESET} user=${BOLD}${PROXY_USER}${RESET}"
info "chain_path=${BOLD}${CHAIN_PATH}${RESET}"
id -u "${PROXY_USER}" >/dev/null 2>&1 || { err "linux user ${PROXY_USER} missing"; exit 3; }

# ---------- ensure directories ----------
sudo install -d -m 0700 -o "${PROXY_USER}" -g "${PROXY_USER}" "${AGENT_DIR}"
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "${AGENT_DIR}/bin"
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "$(dirname "${CHAIN_PATH}")"
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "${CA_DIR}"

# ---------- copy Root CA from vault user to proxy user ----------
if [[ -r "$ROOT_SRC" ]]; then
  sudo install -D -m 0644 -o "${PROXY_USER}" -g "${PROXY_USER}" "$ROOT_SRC" "$CA_FILE"
  ok "copied root CA to ${CA_FILE}"
else
  warn "Root source not found: $ROOT_SRC → copy it later to ${CA_FILE}"
fi

# ---------- policies ----------
TMP="$(mktemp)"
cat >"$TMP" <<EOF
path "auth/approle/role/${ROLE_NAME}/role-id"   { capabilities=["read"] }
path "auth/approle/role/${ROLE_NAME}/secret-id" { capabilities=["create","update"] }
EOF
${VAULT_BIN} policy write "${POLICY_BOOT}" "$TMP" >/dev/null

cat >"$TMP" <<EOF
path "${PKI_MOUNT}/cert/ca"  { capabilities=["read"] }
path "auth/token/renew-self" { capabilities=["update"] }
EOF
${VAULT_BIN} policy write "${POLICY_RUN}" "$TMP" >/dev/null
rm -f "$TMP"

# ---------- AppRole for CA fetcher ----------
${VAULT_BIN} auth enable approle >/dev/null 2>&1 || true
${VAULT_BIN} 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_BIN} read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
SID_VAL="$(${VAULT_BIN} write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
sudo bash -c "umask 077; printf '%s' '${RID_VAL}' > '${RID}'; chown ${PROXY_USER}:${PROXY_USER} '${RID}'; chmod 600 '${RID}'"
sudo bash -c "umask 077; printf '%s' '${SID_VAL}' > '${SID}'; chown ${PROXY_USER}:${PROXY_USER} '${SID}'; chmod 600 '${SID}'"

# ---------- Agent HCL ----------
# If you enforce client mTLS to Vault, place a client cert/key under ${OUTDIR}/agent.crt|key
TLS_EXTRA=""
if sudo -u "${PROXY_USER}" test -s "${OUTDIR}/agent.crt" && sudo -u "${PROXY_USER}" test -s "${OUTDIR}/agent.key"; then
  TLS_EXTRA=$'\n  tls_server_name = "vault.test.local"\n  client_cert = "'"${OUTDIR}/agent.crt"\"$'\n  client_key  = "'"${OUTDIR}/agent.key"\"$'\n'
else
  TLS_EXTRA=$'\n  tls_server_name = "vault.test.local"\n'
  warn "no client mTLS files in ${OUTDIR} (proceeding with CA-only TLS)."
fi

sudo -u "${PROXY_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 = 0600
    }
  }
}

template {
  source      = "${AGENT_DIR}/ca.tpl"
  destination = "${AGENT_DIR}/.ca.json"
command = "CHAIN_FILE='${CHAIN_PATH}' ROOT_FILE='${CA_FILE}' RELOAD_TLS_LABEL='${RELOAD_LABEL}' ${AGENT_DIR}/bin/vault-agent-post-chain.sh"
}
HCL

sudo -u "${PROXY_USER}" tee "${AGENT_DIR}/ca.tpl" >/dev/null <<TPL
{{ with secret "${PKI_MOUNT}/cert/ca" }}
{ "issuing_ca": {{ toJSON .Data.certificate }} }
{{ end }}
TPL

# ---------- install post-hook ----------
sudo /usr/bin/install -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" -D "$POST_SRC" "${POST}"

# ---------- systemd user unit ----------
sudo -u "${PROXY_USER}" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
sudo -u "${PROXY_USER}" tee "${UNIT}" >/dev/null <<UNIT
[Unit]
Description=Vault Agent (${APPN}-ca) - refresh proxy CA chain
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

# ---------- enable user manager & start ----------
PROXY_UID="$(id -u "${PROXY_USER}")"
loginctl enable-linger "${PROXY_USER}" >/dev/null || true
${SYSTEMD_BIN} start "user@${PROXY_UID}.service" >/dev/null || true
XDG_RUNTIME_DIR="/run/user/${PROXY_UID}"
mkdir -p "$XDG_RUNTIME_DIR" && chown "${PROXY_USER}:${PROXY_USER}" "$XDG_RUNTIME_DIR" || true

sudo -u "${PROXY_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user daemon-reload
sudo -u "${PROXY_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user enable --now "vault-agent-${APPN}-ca.service"

# ---------- quick result ----------
if sudo -u "${PROXY_USER}" test -s "${CHAIN_PATH}"; then
  ok "CA chain present: ${CHAIN_PATH}"
else
  warn "CA chain not found yet: ${CHAIN_PATH} → check: journalctl --user -u vault-agent-${APPN}-ca.service"
fi


===== ./02_intermediate_in_vault_sign_with_root.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
# 02_intermediate_in_vault_sign_with_root.sh

# ===== Pretty logging =====
if [[ -t 1 ]]; then
  B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'
else B=""; R=""; G=""; Y=""; E=""; fi
ok(){   echo -e "🟩 ${G}${B}$*${R}"; }
warn(){ echo -e "🟨 ${Y}${B}$*${R}"; }
die(){  echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }

# ===== Defaults / Args =====
CFG="${CFG:-./config/apps.yaml}"
ENV_NAME="test"
API_ADDR="http://127.0.0.1:22300"   # switch to https after step 04
INT_TTL="43800h"                     # 5 years
INT_CN="PrivSec Intermediate CA"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --config) CFG="$2"; shift 2;;
    --env)    ENV_NAME="$2"; shift 2;;
    --api)    API_ADDR="$2"; shift 2;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) die "Unknown arg: $1";;
  esac
done

# ===== Tooling =====
need(){ command -v "$1" >/dev/null || die "missing: $1"; }
need vault; need jq; need python3; need openssl

# ===== Auth =====
if [[ -z "${VAULT_TOKEN:-}" && -n "${VAULT_ADMIN_TOKEN:-}" ]]; then
  export VAULT_TOKEN="$VAULT_ADMIN_TOKEN"
fi
: "${VAULT_TOKEN:?Set VAULT_TOKEN or VAULT_ADMIN_TOKEN}"

# ===== Load config YAML -> JSON =====
CFG_ABS="$(readlink -f "$CFG")" || die "cannot resolve $CFG"
CFGJSON="$(python3 - "$CFG_ABS" <<'PY'
import yaml, json, sys
p = sys.argv[1]
with open(p, "r", encoding="utf-8") as f:
    print(json.dumps(yaml.safe_load(f)))
PY
)" || die "failed to parse YAML"

jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }

VAULT_ADDR="$(jqenv '.vault_addr')"
INT_MOUNT="$(jqenv '.pki_mount')"
[[ "$VAULT_ADDR" != "null" && "$INT_MOUNT" != "null" ]] || die "incomplete config: vault_addr/pki_mount"
export VAULT_ADDR

# ===== Canonical OFFLINE ROOT paths =====
: "${ENV_NAME:?use --env test|prod}"
ME_HOME="$(cd ~ && pwd)"
ROOT_DIR="${ME_HOME}/vault/offline-root/${ENV_NAME}"
ROOT_KEY="${ROOT_DIR}/root-ca.key"
ROOT_CRT="${ROOT_DIR}/root-ca.pem"
[[ -r "$ROOT_KEY" ]] || die "Root KEY not found: $ROOT_KEY"
[[ -r "$ROOT_CRT" ]] || die "Root PEM not found: $ROOT_CRT"
chmod 0400 "$ROOT_KEY" 2>/dev/null || true

ok "Using Vault @ ${VAULT_ADDR} (mount: ${INT_MOUNT})"
ok "Using OFFLINE ROOT @ ${ROOT_DIR}"

# ===== Enable/tune Intermediate PKI mount =====
vault secrets enable -path="${INT_MOUNT}" pki >/dev/null 2>&1 || true
vault secrets tune -max-lease-ttl="${INT_TTL}" "${INT_MOUNT}" >/dev/null

# ===== Generate CSR inside Vault =====
CSR="$(vault write -format=json "${INT_MOUNT}/intermediate/generate/internal" \
      common_name="${INT_CN} (${ENV_NAME})" ttl="${INT_TTL}" \
      | jq -r .data.csr)"
[[ -n "$CSR" && "$CSR" != "null" ]] || die "failed to generate intermediate CSR in Vault"

# ===== Sign CSR with OFFLINE ROOT (v3_intermediate_ca) =====
OPENSSL_INT_CONF="$(mktemp)"
cat > "$OPENSSL_INT_CONF" <<'CNF'
[ v3_intermediate_ca ]
basicConstraints       = critical,CA:true,pathlen:0
keyUsage               = critical,keyCertSign,cRLSign
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
CNF

SERIAL_FILE="${ROOT_DIR}/root-ca.srl"
if [[ -n "${ROOT_CA_PASSPHRASE:-}" ]]; then
  SIGNED="$(openssl x509 -req \
    -in <(printf '%s\n' "$CSR") \
    -CA "$ROOT_CRT" -CAkey "$ROOT_KEY" -passin env:ROOT_CA_PASSPHRASE \
    -CAcreateserial -CAserial "$SERIAL_FILE" \
    -days 1825 -sha256 -extfile "$OPENSSL_INT_CONF" -extensions v3_intermediate_ca)"
else
  SIGNED="$(openssl x509 -req \
    -in <(printf '%s\n' "$CSR") \
    -CA "$ROOT_CRT" -CAkey "$ROOT_KEY" \
    -CAcreateserial -CAserial "$SERIAL_FILE" \
    -days 1825 -sha256 -extfile "$OPENSSL_INT_CONF" -extensions v3_intermediate_ca)"
fi
rm -f "$OPENSSL_INT_CONF"
[[ -n "$SIGNED" ]] || die "OpenSSL did not produce a signed intermediate"

# ===== Upload signed Intermediate to Vault =====
printf '%s\n' "$SIGNED" | vault write "${INT_MOUNT}/intermediate/set-signed" certificate=- >/dev/null

# ===== Configure PKI URLs (HTTP now; update to HTTPS after step 04) =====
vault write "${INT_MOUNT}/config/urls" \
  issuing_certificates="${API_ADDR}/v1/${INT_MOUNT}/ca" \
  crl_distribution_points="${API_ADDR}/v1/${INT_MOUNT}/crl" >/dev/null

# ===== Copy public root CA cert to Vault host (public only) =====
TLSDIR="/home/vault/tls-${ENV_NAME}"
sudo install -d -m 0755 -o vault -g vault "${TLSDIR}"
sudo install -m 0644 "$ROOT_CRT" "${TLSDIR}/root_ca.pem"

ok "Intermediate ready at mount ${INT_MOUNT}."
ok "Public root copied to ${TLSDIR}/root_ca.pem (private key remains OFFLINE at ${ROOT_DIR}/root-ca.key)."
ok "After enabling Vault HTTPS, re-run URL config with --api https://… to switch issuing/CRL URLs."


===== ./04_enable_https_in_compose.sh =====
#!/usr/bin/env bash
set -Eeuo pipefail
# 04_enable_https_in_compose.sh
#
# Purpose:
#   Replace Vault container config with HTTPS (using files in /home/vault/tls-<env>).
#   Writes:
#     - /home/vault/config/config.hcl (HTTPS listener)
#     - /home/vault/docker-compose.tls.yml
#
# Usage:
#   ./04_enable_https_in_compose.sh --env test --cn vault.int.privsec.ch --port 22300 [--rootless]
#
# After:
#   cd /home/vault
#   podman-compose down
#   podman-compose -f docker-compose.tls.yml up -d
#   curl --cacert tls-test/ca_chain.pem https://127.0.0.1:22300/v1/sys/health

if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= G= Y= E=; fi
ok(){ echo -e "🟩 ${G}${B}$*${R}"; }; die(){ echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }

ENV_NAME="test"; CN="vault.int.local"; PORT="22300"; ROOTLESS=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --env) ENV_NAME="$2"; shift 2;;
    --cn) CN="$2"; shift 2;;
    --port) PORT="$2"; shift 2;;
    --rootless) ROOTLESS=1; shift 1;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) die "Unknown arg: $1";;
  esac
done

SERVER_DIR="/home/vault"
TLS_DIR="${SERVER_DIR}/tls-${ENV_NAME}"
for f in server.key server.crt fullchain.crt ca_chain.pem; do
  [[ -s "${TLS_DIR}/${f}" ]] || die "Missing ${TLS_DIR}/${f} (run script #03 first)"
done

# Write HTTPS config.hcl
CFG="${SERVER_DIR}/config/config.hcl"
sudo install -d -m 0755 -o vault -g vault "$(dirname "$CFG")"
TMP="$(mktemp)"; cat >"$TMP" <<HCL
ui = true
disable_mlock = ${ROOTLESS:+true}${ROOTLESS:-false}

api_addr     = "https://${CN}:${PORT}"
cluster_addr = "https://${CN}:8201"

storage "file" {
  path = "/vault/file"
}

listener "tcp" {
  address         = "0.0.0.0:8200"
  tls_disable     = 0
  tls_min_version = "tls12"
  tls_cert_file   = "/vault/tls/server.crt"
  tls_key_file    = "/vault/tls/server.key"
  # We are NOT enabling client cert auth here (no tls_require_and_verify_client_cert)
}
HCL
sudo install -m 0644 -o vault -g vault "$TMP" "$CFG"; rm -f "$TMP"
ok "Wrote ${CFG}"

# Write docker-compose.tls.yml
DC="${SERVER_DIR}/docker-compose.tls.yml"
TMP="$(mktemp)"
if [[ -n "$ROOTLESS" ]]; then
  cat >"$TMP" <<YML
services:
  vault:
    image: docker.io/hashicorp/vault:1.17.6
    container_name: vault
    command: ["server","-config=/vault/config"]
    ports:
      - "127.0.0.1:${PORT}:8200"
    environment:
      VAULT_DISABLE_MLOCK: "true"
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=32m
    volumes:
      - ./config:/vault/config:ro
      - ./tls-${ENV_NAME}:/vault/tls:ro
      - ./file:/vault/file:rw
    restart: unless-stopped
YML
else
  cat >"$TMP" <<YML
services:
  vault:
    image: docker.io/hashicorp/vault:1.17.6
    container_name: vault
    command: ["server","-config=/vault/config"]
    ports:
      - "127.0.0.1:${PORT}:8200"
    environment:
      VAULT_DISABLE_MLOCK: "false"
    cap_add:
      - IPC_LOCK
    ulimits:
      memlock:
        soft: -1
        hard: -1
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=32m
    volumes:
      - ./config:/vault/config:ro
      - ./tls-${ENV_NAME}:/vault/tls:ro
      - ./file:/vault/file:rw
    restart: unless-stopped
YML
fi
sudo install -m 0644 -o vault -g vault "$TMP" "$DC"; rm -f "$TMP"
ok "Wrote ${DC}

Next:
  cd /home/vault
  podman-compose down
  podman-compose -f docker-compose.tls.yml up -d
  curl --cacert tls-${ENV_NAME}/ca_chain.pem https://127.0.0.1:${PORT}/v1/sys/health
"


===== ./03_issue_vault_server_cert.sh.bk =====
#!/usr/bin/env bash
set -Eeuo pipefail
# 03_issue_vault_server_cert.sh
#
# Purpose:
#   Issue a Vault server cert from the environment's Intermediate in Vault,
#   and write files into /home/vault/tls-<env>.
#
# It also composes:
#   - fullchain.crt     = server cert + intermediate
#   - ca_chain.pem      = intermediate + root (root from your offline-root location)
#
# Usage:
#   VAULT_TOKEN=hvs.XXX ./03_issue_vault_server_cert.sh \
#     --env test --config /home/<deinUser>/vault/config/apps.yaml \
#     --cn vault.int.privsec.ch \
#     --dns vault.int.privsec.ch,localhost,host.containers.internal \
#     --ips 127.0.0.1,::1 \
#     --ttl 720h
#
# IMPORTANT:
#   Provide offline root path with --root-dir if you put it somewhere else.

if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; E=$'\e[31m'; else B= R= G= E=; fi
ok(){ echo -e "🟩 ${G}${B}$*${R}"; }; die(){ echo -e "🟥 ${E}${B}$*${R}" >&2; exit 1; }

: "${VAULT_TOKEN:?Set VAULT_TOKEN}"
CFG="./config/apps.yaml"; ENV_NAME="test"; CN="vault.local"; DNS="localhost"; IPS="127.0.0.1,::1"; TTL="720h"
ROOT_DIR_OVERRIDE=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --config) CFG="$2"; shift 2;;
    --env)    ENV_NAME="$2"; shift 2;;
    --cn)     CN="$2"; shift 2;;
    --dns)    DNS="$2"; shift 2;;
    --ips)    IPS="$2"; shift 2;;
    --ttl)    TTL="$2"; shift 2;;
    --root-dir) ROOT_DIR_OVERRIDE="$2"; shift 2;;
    -h|--help) sed -n '1,200p' "$0"; exit 0;;
    *) die "Unknown arg: $1";;
  esac
done

need(){ command -v "$1" >/dev/null || { die "missing: $1"; }; }
need vault; need jq; need python3; need sudo

CFG_ABS="$(readlink -f "$CFG")"
CFGJSON="$(python3 - <<PY
import yaml, json; print(json.dumps(yaml.safe_load(open("$CFG_ABS","r",encoding="utf-8"))))
PY
)"
jqenv(){ echo "$CFGJSON" | jq -r ".environments.\"$ENV_NAME\"$1"; }

VAULT_ADDR="$(jqenv '.vault_addr')"
INT_MOUNT="$(jqenv '.pki_mount')"
[[ "$VAULT_ADDR" != "null" && "$INT_MOUNT" != "null" ]] || die "incomplete config"
export VAULT_ADDR

ME_HOME="$(cd ~ && pwd)"
ROOT_DIR="${ROOT_DIR_OVERRIDE:-${ME_HOME}/vault/offline-root/${ENV_NAME}}"
ROOT_CRT="${ROOT_DIR}/root-ca.pem"
[[ -s "$ROOT_CRT" ]] || die "root-ca.pem not found at $ROOT_CRT (run script #02)"

TLSDIR="/home/vault/tls-${ENV_NAME}"
sudo install -d -m 0755 -o vault -g vault "${TLSDIR}"

# Ensure PKI role exists (liberal domains based on CN base)
BASE="$(echo "${CN}" | sed 's/^[^.]*\.//')"
vault write "${INT_MOUNT}/roles/vault-server" \
  key_type="ec" allow_ip_sans=true \
  allowed_domains="${BASE}" allow_subdomains=true allow_bare_domains=true \
  server_flag=true client_flag=false max_ttl="2160h" >/dev/null 2>&1 || true

ARGS=( "common_name=${CN}" "format=pem_bundle" "ttl=${TTL}" )
[[ -n "$DNS" ]] && ARGS+=( "alt_names=${DNS}" )
[[ -n "$IPS" ]] && ARGS+=( "ip_sans=${IPS}" )
RESP="$(vault write -format=json "${INT_MOUNT}/issue/vault-server" "${ARGS[@]}")"

CERT="$(echo "$RESP" | jq -r .data.certificate)"
KEY="$(echo "$RESP"  | jq -r .data.private_key)"

# fetch intermediate (public) from mount and compose chains
INT_CRT="$(vault read -field=certificate "${INT_MOUNT}/cert/ca")"

# Write files
echo "${KEY}"  | sudo install -m 0600 -o vault -g vault /dev/stdin "${TLSDIR}/server.key"
echo "${CERT}" | sudo install -m 0644 -o vault -g vault /dev/stdin "${TLSDIR}/server.crt"
{ echo "${CERT}"; echo "${INT_CRT}"; } \
  | sudo install -m 0644 -o vault -g vault /dev/stdin "${TLSDIR}/fullchain.crt"
{ echo "${INT_CRT}"; cat "${ROOT_CRT}"; } \
  | sudo install -m 0644 -o vault -g vault /dev/stdin "${TLSDIR}/ca_chain.pem"

ok "Wrote:
 - ${TLSDIR}/server.key        (0600)
 - ${TLSDIR}/server.crt        (0644)
 - ${TLSDIR}/fullchain.crt     (server + intermediate)
 - ${TLSDIR}/ca_chain.pem      (intermediate + root)"


===== ./config/apps.yaml =====
environments:
  test:
    vault_addr: "https://127.0.0.1:22300"
    kv_mount: "kv"            # für nctest (KV)
    pki_mount: "pki-test"     # für PKI (apptest)
    proxy:
      user: "proxytest"
      listen_port: 7701
      chain_path: "/home/proxytest/nginx/ca/current-ca-chain.pem"
      reload: "podman:proxytest"
    app:
      user: "apptest"
      sidecar_host_port: 22288
      sidecar_container: "app88-sidecar-test"

  prod:
    vault_addr: "http://127.0.0.1:22300"
    kv_mount: "kv"            # ggf. eigener Mount, falls gewünscht
    pki_mount: "pki-prod"
    proxy:
      user: "proxyprod"
      listen_port: 8701
      chain_path: "/home/proxyprod/nginx/ca/current-ca-chain.pem"
      reload: "podman:proxyprod"
    app:
      user: "appprod"
      sidecar_host_port: 32288
      sidecar_container: "app88-sidecar-prod"

apps:
  # KV/Container (Nextcloud/MariaDB): Secrets unter kv/data/nctest
  - name: "nctest"
    user: "nctest"                 # per-App Override (wichtig!)
    kv_subpath: "nctest"
    seed:
      mariadb_root_password: "CHANGE_ME_ROOT"
      mysql_password: "CHANGE_ME_APP"
    internal_cn: "nctest.int.privsec.ch"
    external_host_test: "nctest.test.privsec.ch"
    external_host_prod: "nctest.prod.privsec.ch"
    issue_ttl: "24h"

  # PKI/Leaf (CN+SAN), läuft als Env.app.user (apptest/prod)
  - name: "apptest"
    user: "apptest"
    internal_cn: "apptest.int.privsec.ch"
    external_host_test: "apptest.test.privsec.ch"
    external_host_prod: "apptest.prod.privsec.ch"
    issue_ttl: "24h"
    # (optional per-App Overrides)
    # sidecar_container: "my-own-sidecar"
    # proxy_user: "customproxy"
    # proxy_chain_path: "/path/to/chain.pem"
    # proxy_reload: "systemctl-user:nginx.service"
  - name: "proxytest"
    user: "proxytest"                         # WICHTIG: wird vom mTLS-Client-Script gelesen
    internal_cn: "proxytest.int.privsec.ch"   # CN für das Client-mTLS des Agents
    external_host_test: "proxy.test.privsec.ch"
    external_host_prod: "proxy.prod.privsec.ch"
    issue_ttl: "24h"

