261 lines
9.1 KiB
Bash
Executable file
261 lines
9.1 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:=5m}" # 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="${COMMON_NAME}" \
|
|
allow_subdomains=true \
|
|
allow_bare_domains=true \
|
|
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"
|
|
}
|
|
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}" "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: write key/fullchain to ~/tls
|
|
# ======================================
|
|
log "Writing post-script"
|
|
sudo -u "${TARGET_USER}" install -d -m 0755 "${AGENT_DIR}/bin" "${TLS_DIR}"
|
|
sudo -u "${TARGET_USER}" tee "${AGENT_DIR}/bin/vault-agent-post.sh" >/dev/null <<'EOS'
|
|
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
JSON="${1:-./.issue.json}"
|
|
APP="${APP_NAME:-__APP__}"
|
|
OUT="${HOME}/tls"
|
|
|
|
# APP placeholder is replaced at install-time by the main script
|
|
if [[ "${APP}" == "__APP__" ]]; then
|
|
APP="$(basename "${PWD}")" || APP="app"
|
|
fi
|
|
|
|
echo "[post] Processing ${JSON} -> ${OUT}/${APP}.*"
|
|
|
|
tmp="$(mktemp -d "${OUT}/.staging.XXXX")"
|
|
umask 077
|
|
|
|
# Make sure jq exists
|
|
command -v jq >/dev/null || { echo "[post] ERROR: jq not found"; exit 1; }
|
|
|
|
# Private key
|
|
jq -r '.private_key' "${JSON}" > "${tmp}/${APP}.key"
|
|
|
|
# Full chain (certificate + CA chain)
|
|
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"
|
|
|
|
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
|
|
# replace placeholder with real app name
|
|
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"
|
|
|
|
# ======================================
|
|
# 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"
|
|
|