177 lines
7.3 KiB
Bash
Executable file
177 lines
7.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
# ========= Pretty logging =========
|
|
if [[ -t 1 ]]; then
|
|
BOLD=$'\e[1m'; DIM=$'\e[2m'; RESET=$'\e[0m'
|
|
BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'
|
|
else
|
|
BOLD=""; DIM=""; 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 / Args =========
|
|
: "${LOG_LEVEL:=info}"
|
|
: "${VAULT_BIN:=/usr/bin/vault}"
|
|
: "${SYSTEMD_BIN:=/usr/bin/systemctl}"
|
|
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
|
|
: "${DEFAULT_ENV:=test}"
|
|
|
|
if [[ -z "${VAULT_ADMIN_TOKEN:-}" ]]; then
|
|
err "VAULT_ADMIN_TOKEN missing. Example:
|
|
sudo env VAULT_ADMIN_TOKEN=hvs.XXX $0 --env test --config ./config/apps.yaml --app apptest"
|
|
exit 2
|
|
fi
|
|
|
|
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) echo "usage: $0 [--env test|prod] [--config ./config/apps.yaml] --app <name>"; exit 0;;
|
|
*) err "unknown arg: $1"; exit 2;;
|
|
esac
|
|
done
|
|
[[ -n "$APPN" ]] || { err "--app is required"; exit 2; }
|
|
|
|
need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
|
|
need python3; need jq; need openssl; need "$VAULT_BIN"
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
|
|
POST_SRC="${SCRIPT_DIR}/scripts/vault-agent-post.sh"
|
|
[[ -r "$POST_SRC" ]] || { err "POST_SRC not readable: $POST_SRC"; exit 4; }
|
|
|
|
# ========= Load config (YAML → JSON) =========
|
|
CFG_ABS="$(readlink -f "$CFG")"
|
|
CFGJSON="$(python3 - <<PY
|
|
import yaml, json, sys
|
|
with open("$CFG_ABS","r",encoding="utf-8") as f:
|
|
data=yaml.safe_load(f)
|
|
print(json.dumps(data))
|
|
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="$(jqenv '.app.user')"
|
|
COMMON_NAME="$(jqapp '.internal_cn')"
|
|
ISSUE_TTL="$(jqapp '.issue_ttl')"
|
|
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" && "$APP_USER" != "null" && "$COMMON_NAME" != "null" && "$ISSUE_TTL" != "null" ]] || { err "incomplete config"; exit 2; }
|
|
|
|
export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"
|
|
|
|
ROLE_NAME="${APPN}-pki-issue"
|
|
POLICY_NAME="pki-issue-${APPN}"
|
|
PKI_ROLE="nginx-${APPN}"
|
|
|
|
HOME_DIR="/home/${APP_USER}"
|
|
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}"
|
|
TLS_DIR="${HOME_DIR}/tls"
|
|
ROLE_ID_FILE="${AGENT_DIR}/role_id"
|
|
SECRET_ID_FILE="${AGENT_DIR}/secret_id"
|
|
POST="${AGENT_DIR}/bin/vault-agent-post.sh"
|
|
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}.service"
|
|
|
|
info "using config: ${BOLD}${CFG_ABS}${RESET} env=${BOLD}${ENV_NAME}${RESET}"
|
|
info "APP=${BOLD}${APPN}${RESET} USER=${BOLD}${APP_USER}${RESET} VAULT=${VAULT_ADDR} PKI=${PKI_MOUNT}"
|
|
|
|
id -u "${APP_USER}" >/dev/null 2>&1 || { err "user ${APP_USER} missing"; exit 3; }
|
|
sudo install -d -m 0755 -o "${APP_USER}" -g "${APP_USER}" "${HOME_DIR}"
|
|
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"
|
|
|
|
info "upsert policy ${POLICY_NAME}"
|
|
POL="$(mktemp)"; cat >"$POL" <<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}" "$POL" >/dev/null; rm -f "$POL"
|
|
|
|
info "upsert PKI role ${PKI_ROLE}"
|
|
base_domain="${COMMON_NAME#*.}"
|
|
${VAULT_BIN} write "${PKI_MOUNT}/roles/${PKI_ROLE}" \
|
|
allowed_domains="${base_domain}" allow_subdomains=true allow_bare_domains=true \
|
|
allow_wildcard_certificates=false max_ttl="720h" >/dev/null || true
|
|
|
|
info "upsert 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
|
|
|
|
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")"
|
|
info "RoleID: ${BOLD}${ROLE_ID}${RESET}"
|
|
info "SecretID: ${BOLD}${SECRET_ID:0:6}********${RESET}"
|
|
|
|
sudo bash -c "umask 077; printf '%s\n' '${ROLE_ID}' > '${ROLE_ID_FILE}'; chown ${APP_USER}:${APP_USER} '${ROLE_ID_FILE}'; chmod 600 '${ROLE_ID_FILE}'"
|
|
sudo bash -c "umask 077; printf '%s\n' '${SECRET_ID}' > '${SECRET_ID_FILE}'; chown ${APP_USER}:${APP_USER} '${SECRET_ID_FILE}'; chmod 600 '${SECRET_ID_FILE}'"
|
|
|
|
info "write agent hcl/tpl (embed values, no env{} in template)"
|
|
sudo -u "${APP_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<HCL
|
|
pid_file = "${AGENT_DIR}/pidfile"
|
|
auto_auth {
|
|
method "approle" { config = { role_id_file_path="${ROLE_ID_FILE}" secret_id_file_path="${SECRET_ID_FILE}" } }
|
|
sink "file" { config = { path = "${AGENT_DIR}/token" } }
|
|
}
|
|
template {
|
|
source = "${AGENT_DIR}/cert.tpl"
|
|
destination = "${AGENT_DIR}/.issue.json"
|
|
command = "OUTDIR='${TLS_DIR}' RELOAD_TLS_LABEL='tls=true' ${POST}"
|
|
}
|
|
HCL
|
|
|
|
sudo -u "${APP_USER}" tee "${AGENT_DIR}/cert.tpl" >/dev/null <<TPL
|
|
{{ 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 }}
|
|
TPL
|
|
|
|
info "install post script (overwrite)"
|
|
sudo /usr/bin/install -m 0755 -o "${APP_USER}" -g "${APP_USER}" -D "$POST_SRC" "$POST"
|
|
|
|
info "systemd user unit (overwrite)"
|
|
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}
|
|
ExecStartPre=/bin/sh -lc 'echo "🚀 [unit][${APPN}] starting at \$(date -Is)"'
|
|
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
|
|
|
|
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
|
|
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"
|
|
|
|
CERT="${TLS_DIR}/${APPN}.fullchain.pem"
|
|
if sudo -u "${APP_USER}" test -s "${CERT}"; then
|
|
SIZE="$(sudo -u "${APP_USER}" wc -c < "${CERT}" || echo 0)"
|
|
ok "TLS fullchain present: ${BOLD}${CERT}${RESET} (${SIZE} bytes)"
|
|
openssl x509 -in "${CERT}" -noout -subject -issuer -enddate | sed "s/^/ 🔎 /" || true
|
|
else
|
|
err "TLS fullchain not found: ${CERT}"
|
|
fi
|
|
ok "SUCCESS app ${BOLD}${APPN}${RESET} (${ENV_NAME})"
|
|
|