301 lines
11 KiB
Bash
Executable file
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"
|
|
|