244 lines
9.2 KiB
Bash
Executable file
244 lines
9.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
#
|
|
# setup-vault-agent-proxy-config2.sh
|
|
#
|
|
# Purpose:
|
|
# Create a *user* systemd Vault Agent that periodically fetches the ISSUING CA
|
|
# (Intermediate) from Vault and writes a full chain file for NGINX:
|
|
# chain = <Intermediate> + <Root>
|
|
# - Copies Root CA from /home/vault/tls-<env>/root_ca.pem to the proxy user
|
|
# - Uses a dedicated AppRole for this CA-fetcher (separate from leaf issuer)
|
|
# - Renders to ~/.vault-agent-<app>-ca/.ca.json and calls your post-hook
|
|
#
|
|
# Usage:
|
|
# VAULT_ADMIN_TOKEN=hvs.XXXX \
|
|
# sudo -E ./setup-vault-agent-proxy-config2.sh --env test --config ./config/apps.yaml --app proxytest
|
|
#
|
|
# Config (apps.yaml):
|
|
# environments.<env>.{vault_addr,pki_mount,proxy.user,proxy.chain_path,proxy.reload}
|
|
# apps[].{name,proxy_user,proxy_chain_path,proxy_reload} # optional per-app overrides
|
|
#
|
|
# Requirements: vault, python3, jq
|
|
|
|
# ---------- pretty logs ----------
|
|
if [[ -t 1 ]]; then
|
|
BOLD=$'\e[1m'; RESET=$'\e[0m'; BLUE=$'\e[34m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'
|
|
else BOLD=""; 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 ----------
|
|
: "${LOG_LEVEL:=info}"
|
|
: "${VAULT_BIN:=/usr/bin/vault}"
|
|
: "${SYSTEMD_BIN:=/usr/bin/systemctl}"
|
|
: "${DEFAULT_CONFIG:=./config/apps.yaml}"
|
|
: "${DEFAULT_ENV:=test}"
|
|
|
|
[[ -n "${VAULT_ADMIN_TOKEN:-}" ]] || { err "VAULT_ADMIN_TOKEN missing"; exit 2; }
|
|
|
|
# ---------- args ----------
|
|
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) sed -n '1,200p' "$0"; 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 "$VAULT_BIN"
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
|
|
POST_SRC="${SCRIPT_DIR}/scripts/vault-agent-post.sh"
|
|
[[ -r "$POST_SRC" ]] || { err "post script missing: $POST_SRC"; exit 4; }
|
|
|
|
# ---------- load YAML ----------
|
|
CFG_ABS="$(readlink -f "$CFG")"
|
|
CFGJSON="$(python3 - <<PY
|
|
import yaml, json
|
|
with open("$CFG_ABS","r",encoding="utf-8") as f:
|
|
print(json.dumps(yaml.safe_load(f)))
|
|
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')"
|
|
[[ "$VAULT_ADDR" != "null" && "$PKI_MOUNT" != "null" ]] || { err "incomplete env config"; exit 2; }
|
|
|
|
PROXY_USER_DEF="$(jqenv '.proxy.user')"
|
|
CHAIN_PATH_DEF="$(jqenv '.proxy.chain_path')"
|
|
RELOAD_DEF="$(jqenv '.proxy.reload')"
|
|
|
|
PROXY_USER_APP="$(jqapp '.proxy_user')"
|
|
CHAIN_PATH_APP="$(jqapp '.proxy_chain_path')"
|
|
RELOAD_APP="$(jqapp '.proxy_reload')"
|
|
|
|
PROXY_USER="$PROXY_USER_DEF"; [[ "$PROXY_USER_APP" != "null" ]] && PROXY_USER="$PROXY_USER_APP"
|
|
CHAIN_PATH="$CHAIN_PATH_DEF"; [[ "$CHAIN_PATH_APP" != "null" ]] && CHAIN_PATH="$CHAIN_PATH_APP"
|
|
RELOAD_LABEL="${RELOAD_APP:-$RELOAD_DEF}"
|
|
[[ "$RELOAD_LABEL" == "null" || -z "$RELOAD_LABEL" ]] && RELOAD_LABEL="tls=true"
|
|
|
|
[[ -n "$PROXY_USER" && -n "$CHAIN_PATH" ]] || { err "missing required fields (proxy.user / proxy.chain_path)"; exit 2; }
|
|
|
|
export VAULT_ADDR VAULT_TOKEN="${VAULT_ADMIN_TOKEN}"
|
|
|
|
ROLE_NAME="${APPN}-pki-ca"
|
|
POLICY_BOOT="pki-ca-bootstrap-${APPN}"
|
|
POLICY_RUN="pki-ca-read-${APPN}"
|
|
|
|
HOME_DIR="/home/${PROXY_USER}"
|
|
AGENT_DIR="${HOME_DIR}/.vault-agent-${APPN}-ca"
|
|
RID="${AGENT_DIR}/role_id"
|
|
SID="${AGENT_DIR}/secret_id"
|
|
TOKEN_FILE="${AGENT_DIR}/token"
|
|
POST="${AGENT_DIR}/bin/vault-agent-post.sh"
|
|
UNIT="${HOME_DIR}/.config/systemd/user/vault-agent-${APPN}-ca.service"
|
|
|
|
# Paths for trust material
|
|
OUTDIR="${HOME_DIR}/vault/mtls" # where a client cert would live if you enforce client mTLS
|
|
CA_DIR="${HOME_DIR}/vault/ca"
|
|
CA_FILE="${CA_DIR}/ca.pem" # Root CA (will be copied here)
|
|
ROOT_SRC="/home/vault/tls-${ENV_NAME}/root_ca.pem"
|
|
|
|
info "env=${BOLD}${ENV_NAME}${RESET} VAULT=${VAULT_ADDR} PKI=${PKI_MOUNT}"
|
|
info "proxy for app=${BOLD}${APPN}${RESET} user=${BOLD}${PROXY_USER}${RESET}"
|
|
info "chain_path=${BOLD}${CHAIN_PATH}${RESET}"
|
|
id -u "${PROXY_USER}" >/dev/null 2>&1 || { err "linux user ${PROXY_USER} missing"; exit 3; }
|
|
|
|
# ---------- ensure directories ----------
|
|
sudo install -d -m 0700 -o "${PROXY_USER}" -g "${PROXY_USER}" "${AGENT_DIR}"
|
|
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "${AGENT_DIR}/bin"
|
|
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "$(dirname "${CHAIN_PATH}")"
|
|
sudo install -d -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" "${CA_DIR}"
|
|
|
|
# ---------- copy Root CA from vault user to proxy user ----------
|
|
if [[ -r "$ROOT_SRC" ]]; then
|
|
sudo install -D -m 0644 -o "${PROXY_USER}" -g "${PROXY_USER}" "$ROOT_SRC" "$CA_FILE"
|
|
ok "copied root CA to ${CA_FILE}"
|
|
else
|
|
warn "Root source not found: $ROOT_SRC → copy it later to ${CA_FILE}"
|
|
fi
|
|
|
|
# ---------- policies ----------
|
|
TMP="$(mktemp)"
|
|
cat >"$TMP" <<EOF
|
|
path "auth/approle/role/${ROLE_NAME}/role-id" { capabilities=["read"] }
|
|
path "auth/approle/role/${ROLE_NAME}/secret-id" { capabilities=["create","update"] }
|
|
EOF
|
|
${VAULT_BIN} policy write "${POLICY_BOOT}" "$TMP" >/dev/null
|
|
|
|
cat >"$TMP" <<EOF
|
|
path "${PKI_MOUNT}/cert/ca" { capabilities=["read"] }
|
|
path "auth/token/renew-self" { capabilities=["update"] }
|
|
EOF
|
|
${VAULT_BIN} policy write "${POLICY_RUN}" "$TMP" >/dev/null
|
|
rm -f "$TMP"
|
|
|
|
# ---------- AppRole for CA fetcher ----------
|
|
${VAULT_BIN} auth enable approle >/dev/null 2>&1 || true
|
|
${VAULT_BIN} write "auth/approle/role/${ROLE_NAME}" \
|
|
policies="${POLICY_RUN}" bind_secret_id=true \
|
|
token_type=service token_period=24h secret_id_ttl=0 secret_id_num_uses=0 >/dev/null
|
|
|
|
RID_VAL="$(${VAULT_BIN} read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
|
|
SID_VAL="$(${VAULT_BIN} write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
|
|
sudo bash -c "umask 077; printf '%s' '${RID_VAL}' > '${RID}'; chown ${PROXY_USER}:${PROXY_USER} '${RID}'; chmod 600 '${RID}'"
|
|
sudo bash -c "umask 077; printf '%s' '${SID_VAL}' > '${SID}'; chown ${PROXY_USER}:${PROXY_USER} '${SID}'; chmod 600 '${SID}'"
|
|
|
|
# ---------- Agent HCL ----------
|
|
# If you enforce client mTLS to Vault, place a client cert/key under ${OUTDIR}/agent.crt|key
|
|
TLS_EXTRA=""
|
|
if sudo -u "${PROXY_USER}" test -s "${OUTDIR}/agent.crt" && sudo -u "${PROXY_USER}" test -s "${OUTDIR}/agent.key"; then
|
|
TLS_EXTRA=$'\n tls_server_name = "vault.test.local"\n client_cert = "'"${OUTDIR}/agent.crt"\"$'\n client_key = "'"${OUTDIR}/agent.key"\"$'\n'
|
|
else
|
|
TLS_EXTRA=$'\n tls_server_name = "vault.test.local"\n'
|
|
warn "no client mTLS files in ${OUTDIR} (proceeding with CA-only TLS)."
|
|
fi
|
|
|
|
sudo -u "${PROXY_USER}" tee "${AGENT_DIR}/vault-agent.hcl" >/dev/null <<HCL
|
|
pid_file = "${AGENT_DIR}/pidfile"
|
|
|
|
vault {
|
|
address = "${VAULT_ADDR}"
|
|
ca_cert = "${CA_FILE}"${TLS_EXTRA}
|
|
}
|
|
|
|
auto_auth {
|
|
method "approle" {
|
|
config = {
|
|
role_id_file_path = "${RID}"
|
|
secret_id_file_path = "${SID}"
|
|
remove_secret_id_file_after_reading = false
|
|
}
|
|
}
|
|
sink "file" {
|
|
config = {
|
|
path = "${TOKEN_FILE}"
|
|
mode = 0600
|
|
}
|
|
}
|
|
}
|
|
|
|
template {
|
|
source = "${AGENT_DIR}/ca.tpl"
|
|
destination = "${AGENT_DIR}/.ca.json"
|
|
command = "CHAIN_FILE='${CHAIN_PATH}' ROOT_FILE='${CA_FILE}' RELOAD_TLS_LABEL='${RELOAD_LABEL}' ${AGENT_DIR}/bin/vault-agent-post-chain.sh"
|
|
}
|
|
HCL
|
|
|
|
sudo -u "${PROXY_USER}" tee "${AGENT_DIR}/ca.tpl" >/dev/null <<TPL
|
|
{{ with secret "${PKI_MOUNT}/cert/ca" }}
|
|
{ "issuing_ca": {{ toJSON .Data.certificate }} }
|
|
{{ end }}
|
|
TPL
|
|
|
|
# ---------- install post-hook ----------
|
|
sudo /usr/bin/install -m 0755 -o "${PROXY_USER}" -g "${PROXY_USER}" -D "$POST_SRC" "${POST}"
|
|
|
|
# ---------- systemd user unit ----------
|
|
sudo -u "${PROXY_USER}" install -d -m 0755 "${HOME_DIR}/.config/systemd/user"
|
|
sudo -u "${PROXY_USER}" tee "${UNIT}" >/dev/null <<UNIT
|
|
[Unit]
|
|
Description=Vault Agent (${APPN}-ca) - refresh proxy CA chain
|
|
Wants=network-online.target
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
WorkingDirectory=${AGENT_DIR}
|
|
Environment=VAULT_ADDR=${VAULT_ADDR}
|
|
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
|
|
|
|
# ---------- enable user manager & start ----------
|
|
PROXY_UID="$(id -u "${PROXY_USER}")"
|
|
loginctl enable-linger "${PROXY_USER}" >/dev/null || true
|
|
${SYSTEMD_BIN} start "user@${PROXY_UID}.service" >/dev/null || true
|
|
XDG_RUNTIME_DIR="/run/user/${PROXY_UID}"
|
|
mkdir -p "$XDG_RUNTIME_DIR" && chown "${PROXY_USER}:${PROXY_USER}" "$XDG_RUNTIME_DIR" || true
|
|
|
|
sudo -u "${PROXY_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user daemon-reload
|
|
sudo -u "${PROXY_USER}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" ${SYSTEMD_BIN} --user enable --now "vault-agent-${APPN}-ca.service"
|
|
|
|
# ---------- quick result ----------
|
|
if sudo -u "${PROXY_USER}" test -s "${CHAIN_PATH}"; then
|
|
ok "CA chain present: ${CHAIN_PATH}"
|
|
else
|
|
warn "CA chain not found yet: ${CHAIN_PATH} → check: journalctl --user -u vault-agent-${APPN}-ca.service"
|
|
fi
|
|
|