vault-ops/infra/archiv/all_files.txt
2025-10-06 07:25:33 +02:00

3466 lines
126 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

===== ./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"