265 lines
9.4 KiB
Bash
Executable file
265 lines
9.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
# =========================
|
||
# Config you may tweak
|
||
# =========================
|
||
: "${VAULT_ADDR:=http://127.0.0.1:22300}" # where Vault API is reachable
|
||
: "${PKI_MOUNT:=pki-test}" # PKI mount path
|
||
: "${ISSUE_TTL:=720h}" # default cert TTL
|
||
: "${LOG_LEVEL:=info}" # vault agent -log-level
|
||
: "${VAULT_BIN:=/usr/bin/vault}" # vault binary
|
||
: "${SYSTEMD_BIN:=/usr/bin/systemctl}" # systemctl binary
|
||
|
||
# =========================
|
||
# Required admin token
|
||
# =========================
|
||
if [[ -z "${VAULT_ADMIN_TOKEN:-}" ]]; then
|
||
echo "[ERROR] VAULT_ADMIN_TOKEN missing. Run as: VAULT_ADMIN_TOKEN='hvs.XXXX' $0 <app> <user> <fqdn>" >&2
|
||
exit 2
|
||
fi
|
||
export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"
|
||
|
||
# =========================
|
||
# Inputs
|
||
# =========================
|
||
APP_NAME="${1:?usage: $0 <app-name> <target-user> <fqdn>}"
|
||
TARGET_USER="${2:?usage: $0 <app-name> <target-user> <fqdn>}"
|
||
COMMON_NAME="${3:?usage: $0 <app-name> <target-user> <fqdn>}"
|
||
|
||
ROLE_NAME="${APP_NAME}-pki-issue"
|
||
POLICY_NAME="pki-issue-${APP_NAME}"
|
||
PKI_ROLE="nginx-${APP_NAME}"
|
||
|
||
HOME_DIR="/home/${TARGET_USER}"
|
||
AGENT_DIR="${HOME_DIR}/.vault-agent-${APP_NAME}"
|
||
TLS_DIR="${HOME_DIR}/tls"
|
||
USER_UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APP_NAME}.service"
|
||
SYS_UNIT="/etc/systemd/system/vault-agent-${APP_NAME}.service"
|
||
|
||
_ts(){ date +"[%Y-%m-%d %H:%M:%S]"; }
|
||
log(){ echo "$(_ts) [INFO] $*"; }
|
||
warn(){ echo "$(_ts) [WARN] $*" >&2; }
|
||
err(){ echo "$(_ts) [ERROR] $*" >&2; }
|
||
|
||
log "Starting on host $(hostname) for APP=${APP_NAME}, USER=${TARGET_USER}, CN=${COMMON_NAME}"
|
||
log "VAULT_ADDR=${VAULT_ADDR}"
|
||
log "PKI_MOUNT=${PKI_MOUNT} PKI_ROLE=${PKI_ROLE}"
|
||
|
||
# Ensure user exists and has home
|
||
if ! id -u "${TARGET_USER}" >/dev/null 2>&1; then
|
||
err "User ${TARGET_USER} does not exist"; exit 3
|
||
fi
|
||
install -d -m 0755 -o "${TARGET_USER}" -g "${TARGET_USER}" "${HOME_DIR}"
|
||
|
||
# ======================================
|
||
# 1) Policy allowing issue + approle ops
|
||
# ======================================
|
||
log "Writing policy: ${POLICY_NAME}"
|
||
POLICY_FILE="$(mktemp)"
|
||
cat >"${POLICY_FILE}" <<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}" "${POLICY_FILE}" >/dev/null
|
||
rm -f "${POLICY_FILE}"
|
||
|
||
# ======================================
|
||
# 2) AppRole (unlimited secret-id)
|
||
# ======================================
|
||
log "Creating 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
|
||
|
||
# ======================================
|
||
# 3) PKI role (CN/domain policy)
|
||
# ======================================
|
||
log "Upserting PKI role: ${PKI_ROLE}"
|
||
${VAULT_BIN} write "${PKI_MOUNT}/roles/${PKI_ROLE}" \
|
||
allowed_domains="int.privsec.ch" \
|
||
allow_subdomains=true \
|
||
allow_bare_domains=false \
|
||
allow_wildcard_certificates=false \
|
||
max_ttl="720h" >/dev/null || true
|
||
|
||
# ======================================
|
||
# 4) Fetch AppRole creds and place for user
|
||
# ======================================
|
||
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")"
|
||
log "RoleID: ${ROLE_ID}"
|
||
log "SecretID: ${SECRET_ID:0:6}********"
|
||
|
||
sudo -u "${TARGET_USER}" install -d -m 0700 "${AGENT_DIR}"
|
||
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 config (pid, auto_auth, sink, template)
|
||
# ======================================
|
||
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" {
|
||
config = {
|
||
role_id_file_path = "${AGENT_DIR}/role_id"
|
||
secret_id_file_path = "${AGENT_DIR}/secret_id"
|
||
}
|
||
}
|
||
sink "file" {
|
||
config = { path = "${AGENT_DIR}/token" }
|
||
}
|
||
}
|
||
|
||
template {
|
||
source = "${AGENT_DIR}/cert.tpl"
|
||
destination = "${AGENT_DIR}/.issue.json"
|
||
command = "${AGENT_DIR}/bin/vault-agent-post.sh"
|
||
error_on_missing_key = true
|
||
}
|
||
EOF
|
||
|
||
# ======================================
|
||
# 6) Template that emits JSON (easy for jq)
|
||
# ======================================
|
||
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=${COMMON_NAME}" "alt_names=${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 }}
|
||
EOF
|
||
|
||
# ======================================
|
||
# 7) Post-script: minimal – schreibt key/fullchain nach ~/tls
|
||
# ======================================
|
||
log "Writing post-script (minimal)"
|
||
sudo -u "${TARGET_USER}" install -d -m 0755 "${AGENT_DIR}/bin" "${TLS_DIR}"
|
||
|
||
# Immer überschreiben (tee) – super simpel.
|
||
sudo -u "${TARGET_USER}" tee "${AGENT_DIR}/bin/vault-agent-post.sh" >/dev/null <<'EOS'
|
||
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
# Eingaben/Orte
|
||
JSON="${1:-./.issue.json}" # Agent rendert hierhin; WorkingDirectory ist AGENT_DIR
|
||
APP="${APP_NAME:-__APP__}" # wird vom Setup-Script ersetzt
|
||
OUT="${HOME}/tls"
|
||
|
||
echo "[post] Processing ${JSON} -> ${OUT}/${APP}.*"
|
||
|
||
# Temp-Ordner, sichere Rechte
|
||
umask 077
|
||
mkdir -p "${OUT}"
|
||
tmp="$(mktemp -d "${OUT}/.staging.XXXX")"
|
||
|
||
# Abbruch wenn jq fehlt
|
||
command -v jq >/dev/null || { echo "[post] ERROR: jq not found"; exit 1; }
|
||
|
||
# Key schreiben
|
||
jq -r '.private_key' "${JSON}" > "${tmp}/${APP}.key"
|
||
|
||
# Fullchain schreiben (cert + ca_chain (array|string) oder 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}" > "${tmp}/${APP}.fullchain.pem"
|
||
|
||
# Final installieren
|
||
install -m 600 "${tmp}/${APP}.key" "${OUT}/${APP}.key"
|
||
install -m 644 "${tmp}/${APP}.fullchain.pem" "${OUT}/${APP}.fullchain.pem"
|
||
rm -rf "${tmp}"
|
||
|
||
echo "[post] wrote ${OUT}/${APP}.key and ${APP}.fullchain.pem"
|
||
EOS
|
||
|
||
# APP-Placeholder auf echten Namen setzen und ausführbar machen
|
||
sudo -u "${TARGET_USER}" sed -i "s|__APP__|${APP_NAME}|g" "${AGENT_DIR}/bin/vault-agent-post.sh"
|
||
sudo -u "${TARGET_USER}" chmod +x "${AGENT_DIR}/bin/vault-agent-post.sh"
|
||
|
||
# Sofortige Ausführung beim nächsten Render erzwingen
|
||
sudo -u "${TARGET_USER}" touch "${AGENT_DIR}/cert.tpl"
|
||
|
||
# ======================================
|
||
# 8) user systemd unit (preferred)
|
||
# ======================================
|
||
log "Writing user unit"
|
||
sudo -u "${TARGET_USER}" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
|
||
sudo -u "${TARGET_USER}" tee "${USER_UNIT}" >/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}
|
||
ExecStart=${VAULT_BIN} agent -log-level=${LOG_LEVEL} -config=${AGENT_DIR}/vault-agent.hcl
|
||
Restart=on-failure
|
||
RestartSec=5s
|
||
|
||
[Install]
|
||
WantedBy=default.target
|
||
EOF
|
||
|
||
# ======================================
|
||
# 9) Start preferred USER unit (no fallback here)
|
||
# ======================================
|
||
log "Enable linger and start user@ for ${TARGET_USER}"
|
||
APP_UID="$(id -u "${TARGET_USER}")"
|
||
loginctl enable-linger "${TARGET_USER}" >/dev/null || true
|
||
# ensure the per-user instance exists (Debian/Ubuntu needs this sometimes)
|
||
${SYSTEMD_BIN} start "user@${APP_UID}.service" >/dev/null || true
|
||
|
||
XDG_RUNTIME_DIR="/run/user/${APP_UID}"
|
||
export XDG_RUNTIME_DIR
|
||
|
||
# Some distros require the runtime dir to exist when called from sudo/root
|
||
mkdir -p "${XDG_RUNTIME_DIR}" && chown "${TARGET_USER}:${TARGET_USER}" "${XDG_RUNTIME_DIR}" || true
|
||
|
||
log "Reloading user units and starting agent"
|
||
sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user daemon-reload
|
||
sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user enable --now "vault-agent-${APP_NAME}.service"
|
||
|
||
# ======================================
|
||
# 10) Show live status & verify outputs
|
||
# ======================================
|
||
log "User-unit status:"
|
||
sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user status "vault-agent-${APP_NAME}.service" --no-pager || true
|
||
|
||
log "Recent logs (last 30 lines):"
|
||
sudo -u "${TARGET_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" journalctl --user -u "vault-agent-${APP_NAME}.service" -n 30 -o cat || true
|
||
|
||
log "Quick verification:"
|
||
CERT="${TLS_DIR}/${APP_NAME}.fullchain.pem"
|
||
KEY="${TLS_DIR}/${APP_NAME}.key"
|
||
if sudo -u "${TARGET_USER}" test -s "${CERT}"; then
|
||
SIZE="$(sudo -u "${TARGET_USER}" wc -c < "${CERT}" || echo 0)"
|
||
log "TLS fullchain present: ${CERT} (${SIZE} bytes)"
|
||
openssl x509 -in "${CERT}" -noout -subject -enddate | sed "s/^/[$(_ts)] /" || true
|
||
else
|
||
warn "TLS fullchain not found: ${CERT}"
|
||
fi
|
||
|
||
echo
|
||
log "SUCCESS: Vault Agent for ${APP_NAME} ready."
|
||
echo "[INFO] TLS: ${KEY} | ${CERT}"
|
||
echo "[INFO] Agent: ${AGENT_DIR} | Service: user-unit"
|
||
|