vault-ops/archiv/setup-vault-agent3.sh
2025-09-22 10:27:22 +02:00

301 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
############################################
# Vault Agent One-Shot Provisioner (all-in)
# - Single sudo prompt (keepalive)
# - Robust logging (file + console)
# - User bus handling (+ fallback to system unit)
# - JSON-safe template (toJSON) + jq post-process
# - TLS => ~/tls/<app>.key + <app>.fullchain.pem
############################################
# -------- args --------
APP_NAME="${1:-test}" # e.g. "test" or "app88"
TARGET_USER="${2:-test}" # e.g. "test" or "apptest"
DOMAIN="${3:-${APP_NAME}.example.com}" # optional CN override
# -------- logging: timestamp + tee to file --------
LOG_FILE="/var/log/vault-agent-setup-${APP_NAME}-${TARGET_USER}.log"
sudo touch "$LOG_FILE" && sudo chown "$(id -u)":"$(id -g)" "$LOG_FILE" || true
exec > >(awk '{ print strftime("[%Y-%m-%d %H:%M:%S]"), $0; fflush() }' | tee -a "$LOG_FILE") 2>&1
if [[ "${DEBUG:-0}" = "1" ]]; then set -x; fi
trap 'echo "[ERROR] line $LINENO: cmd \"${BASH_COMMAND}\" failed"; exit 1' ERR
echo "[INFO] Starting on host $(hostname) for APP=${APP_NAME}, USER=${TARGET_USER}, CN=${DOMAIN}"
# -------- single sudo prompt + keepalive --------
if ! sudo -n true 2>/dev/null; then
echo "[INFO] Need sudo once to set linger / create dirs / write units…"
sudo -v || { echo "[ERROR] sudo failed"; exit 1; }
fi
( while true; do sudo -n true; sleep 60; done ) >/dev/null 2>&1 &
SUDO_KEEPALIVE_PID=$!
trap 'kill $SUDO_KEEPALIVE_PID 2>/dev/null || true' EXIT
# -------- defaults / config --------
VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:22300}"
PKI_MOUNT="${PKI_MOUNT:-pki-test}"
PKI_ROLE="nginx-${APP_NAME}"
ROLE_NAME="${APP_NAME}-pki-issue"
POLICY_NAME="pki-issue-${APP_NAME}"
AGENT_DIR="/home/${TARGET_USER}/.vault-agent-${APP_NAME}"
TLS_DIR="/home/${TARGET_USER}/tls"
USER_UNIT_DIR="/home/${TARGET_USER}/.config/systemd/user"
USER_SERVICE="${USER_UNIT_DIR}/vault-agent-${APP_NAME}.service"
SYSTEM_SERVICE="/etc/systemd/system/vault-agent-${APP_NAME}.service"
echo "[INFO] VAULT_ADDR=${VAULT_ADDR}"
echo "[INFO] PKI_MOUNT=${PKI_MOUNT} PKI_ROLE=${PKI_ROLE}"
# -------- resolve admin token (ENV > ~/vault-init.json > ./vault/admin-token.txt > prompt) --------
if [[ -z "${VAULT_ADMIN_TOKEN:-}" ]]; then
if [[ -f "$HOME/vault-init.json" ]]; then
VAULT_ADMIN_TOKEN="$(jq -r '.root_token' "$HOME/vault-init.json" 2>/dev/null || true)"
fi
fi
if [[ -z "${VAULT_ADMIN_TOKEN:-}" ]]; then
for cand in "./vault/admin-token.txt" "vault/admin-token.txt"; do
[[ -f "$cand" ]] && VAULT_ADMIN_TOKEN="$(tr -d '\r\n' < "$cand")" && break
done
fi
if [[ -z "${VAULT_ADMIN_TOKEN:-}" ]]; then
read -r -s -p "Enter VAULT_ADMIN_TOKEN: " VAULT_ADMIN_TOKEN; echo
fi
[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { echo "[ERROR] VAULT_ADMIN_TOKEN empty"; exit 2; }
export VAULT_ADDR
export VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"
# -------- dependency checks --------
for bin in vault jq systemctl openssl; do
command -v "$bin" >/dev/null 2>&1 || { echo "[ERROR] missing dependency: $bin"; exit 2; }
done
# -------- helpers --------
log(){ echo "[INFO] $*"; }
warn(){ echo "[WARN] $*"; }
# -------- 0) ensure target home bits --------
log "Preparing home for ${TARGET_USER}"
sudo -u "${TARGET_USER}" bash -c "umask 077; mkdir -p '${AGENT_DIR}/bin' '${TLS_DIR}' '${USER_UNIT_DIR}'"
# -------- 1) policy --------
log "Writing policy: ${POLICY_NAME}"
tmp_policy="$(mktemp)"
cat >"$tmp_policy" <<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 policy write "${POLICY_NAME}" "$tmp_policy"
rm -f "$tmp_policy"
# -------- 2) approle --------
log "Creating AppRole: ${ROLE_NAME}"
vault 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
# -------- 3) pki role (idempotent upsert) --------
log "Upserting PKI role: ${PKI_ROLE}"
vault write "${PKI_MOUNT}/roles/${PKI_ROLE}" \
allowed_domains="${DOMAIN}" \
allow_subdomains=true \
allow_bare_domains=true \
key_type="rsa" key_bits=2048 \
max_ttl="720h" >/dev/null
# -------- 4) credentials to app home --------
ROLE_ID="$(vault read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
SECRET_ID="$(vault write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
log "RoleID: ${ROLE_ID}"
log "SecretID: ${SECRET_ID:0:6}********"
sudo -u "${TARGET_USER}" bash -c "
umask 077
printf '%s\n' '${ROLE_ID}' > '${AGENT_DIR}/role_id'
printf '%s\n' '${SECRET_ID}' > '${AGENT_DIR}/secret_id'
chmod 600 '${AGENT_DIR}/role_id' '${AGENT_DIR}/secret_id'
"
# -------- 5) agent hcl --------
log "Writing agent config"
sudo -u "${TARGET_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<EOF
pid_file = "${AGENT_DIR}/pidfile"
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "${AGENT_DIR}/role_id"
secret_id_file_path = "${AGENT_DIR}/secret_id"
}
}
sink "file" {
config = { path = "${AGENT_DIR}/token" }
}
}
# render JSON, post-script writes key+fullchain into ~/tls/
template {
source = "${AGENT_DIR}/cert.tpl"
destination = "${AGENT_DIR}/.issue.json"
command = ["${AGENT_DIR}/bin/vault-agent-post.sh"]
}
EOF
# -------- 6) template (JSON-safe) --------
log "Writing certificate template"
sudo -u "${TARGET_USER}" tee "${AGENT_DIR}/cert.tpl" >/dev/null <<EOF
{{ with secret "${PKI_MOUNT}/issue/${PKI_ROLE}" "common_name=${DOMAIN}" "ttl=5m" }}
{
"certificate": {{ toJSON .Data.certificate }},
"issuing_ca": {{ toJSON .Data.issuing_ca }},
"ca_chain": {{ toJSON .Data.ca_chain }},
"private_key": {{ toJSON .Data.private_key }}
}
{{ end }}
EOF
# -------- 7) post-script (jq -> TLS files + optional nginx reload) --------
log "Writing post-script"
sudo -u "${TARGET_USER}" tee "${AGENT_DIR}/bin/vault-agent-post.sh" >/dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
APP_NAME_ENV="${APP_NAME:-}"
[[ -n "$APP_NAME_ENV" ]] || APP_NAME_ENV="$(basename "$HOME")"
AGENT_DIR="$HOME/.vault-agent-${APP_NAME_ENV}"
JSON="$AGENT_DIR/.issue.json"
OUTDIR="$HOME/tls"
echo "[post] Processing $JSON -> $OUTDIR/${APP_NAME_ENV}.*"
mkdir -p "$OUTDIR"; umask 077
tmp="$(mktemp -d "$OUTDIR/.staging.XXXX")"
# extract key
jq -r .private_key "$JSON" > "$tmp/${APP_NAME_ENV}.key"
# cert + chain, robust to array/string/null
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_ENV}.fullchain.pem"
install -m 600 "$tmp/${APP_NAME_ENV}.key" "$OUTDIR/${APP_NAME_ENV}.key"
install -m 644 "$tmp/${APP_NAME_ENV}.fullchain.pem" "$OUTDIR/${APP_NAME_ENV}.fullchain.pem"
rm -rf "$tmp"
echo "[post] wrote $OUTDIR/${APP_NAME_ENV}.key and ${APP_NAME_ENV}.fullchain.pem"
# optional nginx reload (without sudo; adjust sudoers if needed)
if systemctl is-active --quiet nginx; then
systemctl reload nginx || true
echo "[post] nginx reloaded"
fi
EOF
sudo -u "${TARGET_USER}" chmod +x "${AGENT_DIR}/bin/vault-agent-post.sh"
# -------- 8) user unit (with better logging + APP_NAME env) --------
log "Writing user unit"
sudo -u "${TARGET_USER}" tee "${USER_SERVICE}" >/dev/null <<EOF
[Unit]
Description=Vault Agent (${APP_NAME}) - issue & rotate TLS certs
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
WorkingDirectory=${AGENT_DIR}
Environment=VAULT_ADDR=${VAULT_ADDR}
Environment=VAULT_LOG_LEVEL=info
Environment=APP_NAME=${APP_NAME}
ExecStart=/usr/bin/vault -log-level=info agent -config=${AGENT_DIR}/vault-agent.hcl
SyslogIdentifier=vault-agent-${APP_NAME}
StandardOutput=journal
StandardError=inherit
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=default.target
EOF
# -------- 9) start user unit (or fallback to system unit) --------
log "Enable linger and user bus for ${TARGET_USER}"
sudo loginctl enable-linger "${TARGET_USER}" >/dev/null 2>&1 || true
TARGET_UID="$(id -u "${TARGET_USER}")"
sudo mkdir -p "/run/user/${TARGET_UID}"
sudo chown "${TARGET_USER}:${TARGET_USER}" "/run/user/${TARGET_UID}"
sudo chmod 700 "/run/user/${TARGET_UID}"
export XDG_RUNTIME_DIR="/run/user/${TARGET_UID}"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${TARGET_UID}/bus"
log "Trying user service first"
if sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS}" \
systemctl --user daemon-reload 2>/dev/null; then
sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS}" \
systemctl --user enable --now "vault-agent-${APP_NAME}.service" || true
sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS}" \
systemctl --user status "vault-agent-${APP_NAME}.service" --no-pager || true
else
warn "user bus unavailable → using system-wide service (User=${TARGET_USER})"
sudo tee "${SYSTEM_SERVICE}" >/dev/null <<EOF
[Unit]
Description=Vault Agent (${APP_NAME}) - system service
Wants=network-online.target
After=network-online.target
[Service]
User=${TARGET_USER}
WorkingDirectory=${AGENT_DIR}
Environment=VAULT_ADDR=${VAULT_ADDR}
Environment=VAULT_LOG_LEVEL=info
Environment=APP_NAME=${APP_NAME}
ExecStart=/usr/bin/vault -log-level=info agent -config=${AGENT_DIR}/vault-agent.hcl
SyslogIdentifier=vault-agent-${APP_NAME}
StandardOutput=journal
StandardError=inherit
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now "vault-agent-${APP_NAME}.service"
systemctl status "vault-agent-${APP_NAME}.service" --no-pager || true
fi
# -------- 10) quick verification (no redirection before sudo) --------
echo
log "Quick verification:"
# service logs (prefer user unit)
if sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS}" \
systemctl --user is-active --quiet "vault-agent-${APP_NAME}.service" 2>/dev/null; then
sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS}" \
journalctl --user -u "vault-agent-${APP_NAME}.service" -n 30 -o cat || true
else
journalctl -u "vault-agent-${APP_NAME}.service" -n 30 -o cat || true
fi
# TLS file present?
if sudo -u "${TARGET_USER}" test -f "${TLS_DIR}/${APP_NAME}.fullchain.pem"; then
SIZE=$(sudo -u "${TARGET_USER}" stat -c %s "${TLS_DIR}/${APP_NAME}.fullchain.pem" 2>/dev/null || echo 0)
echo "[INFO] TLS fullchain present: ${TLS_DIR}/${APP_NAME}.fullchain.pem (${SIZE} bytes)"
# show subject & expiry
sudo -u "${TARGET_USER}" openssl x509 -in "${TLS_DIR}/${APP_NAME}.fullchain.pem" -noout -subject -enddate || true
else
echo "[WARN] TLS fullchain not found: ${TLS_DIR}/${APP_NAME}.fullchain.pem"
fi
echo
log "SUCCESS: Vault Agent for ${APP_NAME} ready."
echo "TLS: ${TLS_DIR}/${APP_NAME}.key | ${TLS_DIR}/${APP_NAME}.fullchain.pem"
echo "Agent: ${AGENT_DIR} | Service: user-unit (preferred) or system-unit fallback"