vault-ops/infra/archiv/setup-vault-agent-app-config2.sh
2025-10-06 07:25:33 +02:00

242 lines
9.4 KiB
Bash
Executable file

#!/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