3466 lines
126 KiB
Text
3466 lines
126 KiB
Text
|
||
===== ./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"
|
||
|