199 lines
6.9 KiB
Bash
Executable file
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
|