vault-ops/infra/versions/mtls-rotate-v1.0.sh
2026-04-14 11:45:15 +07:00

199 lines
6.9 KiB
Bash
Executable file

#!/usr/bin/env bash
set -Eeuo pipefail
# mtls-rotate.sh — rotates Vault client mTLS certs for your agents
# Uses your set-vault-env-auto2.sh to load VAULT_* for test/prod.
# ---- pretty ----
if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'; else B= R= G= Y= E=; fi
ok(){ echo -e "🟩 ${B}$*${R}"; }
info(){ echo -e "🟦 ${B}$*${R}"; }
warn(){ echo -e "🟨 ${B}$*${R}"; }
err(){ echo -e "🟥 ${B}$*${R}" >&2; }
# ---- defaults/flags ----
ENV_NAME="test"
APPS_LIST="" # comma-separated (e.g., "nctest,tridev,proxytest")
THRESHOLD_DAYS=14
CFG="./config/apps.yaml"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
SET_ENV="${SCRIPT_DIR}/set-vault-env-auto2.sh"
SETUP_MTLS="${SCRIPT_DIR}/setup-vault-agent-mtls-client-config.sh"
RESTART_MODE="auto" # auto|docker|systemd|none
DRY_RUN=0
usage() {
cat <<EOF
Usage: sudo ENV_NAME=<test|prod> $0 [--env test|prod] [--apps a,b,c] [--threshold-days N]
[--config ./config/apps.yaml] [--set-env <path>] [--setup-mtls <path>]
[--restart auto|docker|systemd|none] [--dry-run]
Examples:
$0 --env test
$0 --env prod --apps nctest,espodev,proxyprod --threshold-days 10
Notes:
- Loads Vault env via: . "\$SET_ENV" --env <ENV> --sudo-copy-ca -q
- Issues client certs via: "\$SETUP_MTLS" --env <ENV> --config "\$CFG" --app <app>
EOF
}
need(){ command -v "$1" >/dev/null || { err "missing: $1"; exit 2; }; }
need openssl
need date
need bash
# ---- parse args ----
while [[ $# -gt 0 ]]; do
case "$1" in
--env) ENV_NAME="$2"; shift 2;;
--apps) APPS_LIST="$2"; shift 2;;
--threshold-days) THRESHOLD_DAYS="$2"; shift 2;;
--config) CFG="$2"; shift 2;;
--set-env) SET_ENV="$2"; shift 2;;
--setup-mtls) SETUP_MTLS="$2"; shift 2;;
--restart) RESTART_MODE="$2"; shift 2;;
--dry-run) DRY_RUN=1; shift;;
-h|--help) usage; exit 0;;
*) err "unknown arg: $1"; usage; exit 2;;
esac
done
# ---- source your env loader ----
# ---- Vault-Env laden (entweder aus vault-run-home-safe oder lokalem set-vault-env-auto2.sh) ----
if [[ -z "${VAULT_ENV:-}" || "$VAULT_ENV" != "$ENV_NAME" || -z "${VAULT_TOKEN:-}" ]]; then
[[ -r "$SET_ENV" ]] || { err "set-vault-env-auto2.sh not found at $SET_ENV"; exit 2; }
# shellcheck disable=SC1090
. "$SET_ENV" --env "$ENV_NAME" --sudo-copy-ca -q
fi
: "${VAULT_ADDR:?VAULT_ADDR not set by set-vault-env-auto2.sh or vault-run-home-safe}"
: "${VAULT_ENV:?VAULT_ENV not set by set-vault-env-auto2.sh or vault-run-home-safe}"
: "${VAULT_TOKEN:?VAULT_TOKEN not set (need admin/management token to issue client certs)}"
ok "Vault env: VAULT_ENV=$VAULT_ENV VAULT_ADDR=$VAULT_ADDR"
info "Using CFG=$CFG SETUP_MTLS=$SETUP_MTLS THRESHOLD=${THRESHOLD_DAYS}d RESTART=$RESTART_MODE DRY_RUN=$DRY_RUN"
# ---- helpers ----
to_epoch() { date -u -d "$1" +"%s"; } # GNU date
secs_until_expiry() {
local crt="$1"
local notAfter
notAfter="$(openssl x509 -in "$crt" -noout -enddate 2>/dev/null | sed 's/^notAfter=//')"
[[ -n "$notAfter" ]] || { echo -1; return 0; }
local exp ts_now
exp="$(to_epoch "$notAfter")" || { echo -1; return 0; }
ts_now="$(date -u +%s)"
echo $(( exp - ts_now ))
}
find_apps_auto() {
# any /home/<user>/vault/mtls/agent.crt → <user> is considered an app
awk -F'/' '/\/home\/[^/]+\/vault\/mtls\/agent\.crt$/ {print $3}' < <(find /home -maxdepth 3 -path '*/vault/mtls/agent.crt' -type f 2>/dev/null | sort -u)
}
restart_agent() {
local app="$1"
case "$RESTART_MODE" in
none) return 0;;
docker|auto)
if command -v docker >/dev/null 2>&1; then
# try plain container name
if docker ps --format '{{.Names}}' | grep -qx "vault-agent-${app}"; then
docker restart "vault-agent-${app}" >/dev/null && ok "restarted docker container vault-agent-${app}" && return 0 || true
fi
# try compose in /home/<app>/docker-compose.yml
local dc="/home/${app}/docker-compose.yml"
if [[ -r "$dc" ]]; then
( cd "/home/${app}" && docker compose -f "$dc" restart "vault-agent-${app}" ) >/dev/null && ok "compose restart vault-agent-${app} in /home/${app}" && return 0 || true
fi
# generic compose restart in cwd if present
if [[ -r "./docker-compose.yml" ]]; then
docker compose restart "vault-agent-${app}" >/dev/null && ok "compose restart vault-agent-${app} in $(pwd)" && return 0 || true
fi
fi
[[ "$RESTART_MODE" == "docker" ]] && { warn "docker restart failed for ${app}"; return 0; }
;;&
systemd|auto)
# user-service fallback: vault-agent-<app>.service
if command -v systemctl >/dev/null 2>&1 && id -u "$app" >/dev/null 2>&1; then
sudo -u "$app" systemctl --user restart "vault-agent-${app}.service" >/dev/null && ok "systemd --user restarted vault-agent-${app}.service" && return 0 || true
fi
[[ "$RESTART_MODE" == "systemd" ]] && { warn "systemd user restart failed for ${app}"; return 0; }
;;
esac
warn "no restart path succeeded for ${app} (continuing)"
}
rotate_one() {
local app="$1"
local mdir="/home/${app}/vault/mtls"
local crt="${mdir}/agent.crt"
local key="${mdir}/agent.key"
# inspect current cert
local secs rem_days
if [[ -s "$crt" ]]; then
secs="$(secs_until_expiry "$crt")"
rem_days=$(( secs / 86400 ))
else
secs=-1
rem_days=-999
fi
# derive expected CN and show
local exp_cn="agent-${app}.${VAULT_ENV}.privsec.ch"
if [[ -s "$crt" ]]; then
local act_cn
act_cn="$(openssl x509 -in "$crt" -noout -subject -nameopt RFC2253 2>/dev/null | sed -n 's/^subject=//; s/.*CN=\([^,]*\).*/\1/p')"
info "[$app] CN(now)='$act_cn' CN(exp)='$exp_cn'"
else
info "[$app] no existing agent.crt (will issue new)"
fi
local threshold_secs=$(( THRESHOLD_DAYS * 86400 ))
local need_rotate=0
[[ ! -s "$crt" || ! -s "$key" || $secs -lt $threshold_secs ]] && need_rotate=1
if [[ $need_rotate -eq 0 ]]; then
ok "[$app] OK (remaining ${rem_days}d ≥ ${THRESHOLD_DAYS}d)"
return 0
fi
if [[ $DRY_RUN -eq 1 ]]; then
warn "[$app] DRY-RUN: would rotate (remaining ${rem_days}d)"
return 0
fi
info "[$app] rotating mTLS (remaining ${rem_days}d)…"
VAULT_ADDR="$VAULT_ADDR" VAULT_TOKEN="$VAULT_TOKEN" \
"$SETUP_MTLS" --env "$VAULT_ENV" --config "$CFG" --app "$app" >/tmp/mtls-rotate-"$app".log 2>&1 || {
err "[$app] rotation failed; see /tmp/mtls-rotate-${app}.log"
return 1
}
ok "[$app] new agent.{crt,key} written"
restart_agent "$app"
}
# ---- apps to process ----
declare -a APPS
if [[ -n "$APPS_LIST" ]]; then
IFS=',' read -r -a APPS <<< "$APPS_LIST"
else
mapfile -t APPS < <(find_apps_auto)
if [[ ${#APPS[@]} -eq 0 ]]; then
warn "no apps auto-detected; set --apps a,b,c"
exit 0
fi
fi
info "Will process apps: ${APPS[*]}"
# ---- main loop ----
fail=0
for a in "${APPS[@]}"; do
rotate_one "$a" || fail=1
done
[[ $fail -eq 0 ]] && ok "All done." || err "Done with errors."
exit $fail