Files
platform-core/scripts/bootstrap/apply-host-nginx-observability.sh
T

1133 lines
42 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
set +x
umask 077
readonly HOST_NGINX_PRODUCTION_PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
PATH=$HOST_NGINX_PRODUCTION_PATH
export PATH
hash -r
readonly ROOT="$(cd -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P)"
declare -Ar HOST_NGINX_PRODUCTION_COMMANDS=(
[awk]=/usr/bin/awk [bash]=/usr/bin/bash [cat]=/usr/bin/cat
[curl]=/usr/bin/curl [date]=/usr/bin/date [dig]=/usr/bin/dig
[dirname]=/usr/bin/dirname [grep]=/usr/bin/grep [head]=/usr/bin/head
[id]=/usr/bin/id [install]=/usr/bin/install [jq]=/usr/bin/jq
[kubectl]=/usr/local/bin/kubectl [mktemp]=/usr/bin/mktemp
[openssl]=/usr/bin/openssl [python3]=/usr/bin/python3
[readlink]=/usr/bin/readlink [rm]=/usr/bin/rm [sed]=/usr/bin/sed
[sha256sum]=/usr/bin/sha256sum [sleep]=/usr/bin/sleep
[sort]=/usr/bin/sort [ss]=/usr/bin/ss [stat]=/usr/bin/stat
[tail]=/usr/bin/tail [test]=/usr/bin/test [tr]=/usr/bin/tr
)
readonly HOST_NGINX_SOURCE="$ROOT/infrastructure/networking/host-nginx"
readonly METRICS_CANDIDATE="$HOST_NGINX_SOURCE/learn-services-metrics-guard.conf"
readonly DENY_CANDIDATE="$HOST_NGINX_SOURCE/learn-services-grafana-deny-guard.conf"
readonly FULL_CANDIDATE="$HOST_NGINX_SOURCE/learn-services-observability.conf"
readonly ADMIN_CANDIDATE="$HOST_NGINX_SOURCE/learn-services-admin.conf"
readonly SOURCE_TEST="$ROOT/scripts/validate/test-host-nginx-observability-source.sh"
readonly ACTIVE="${PLATFORM_HOST_NGINX_ACTIVE:-/etc/nginx/sites-available/learn-services}"
readonly ENABLED="${PLATFORM_HOST_NGINX_ENABLED:-/etc/nginx/sites-enabled/learn-services}"
readonly ROLLBACK_BASE="${PLATFORM_HOST_NGINX_ROLLBACK_BASE:-/var/lib/hyeonworks/platform-rollbacks}"
readonly GRAFANA_HOST=grafana.learn.hyeonworks.com
readonly GITEA_HOST=git.learn.hyeonworks.com
readonly KEYCLOAK_HOST=id.learn.hyeonworks.com
readonly STORAGE_HOST=storage-admin.learn.hyeonworks.com
readonly DB_HOST=db-admin.learn.hyeonworks.com
readonly KEYCLOAK_ISSUER="https://$KEYCLOAK_HOST/realms/hyeonworks"
readonly LAN_IP=192.168.0.107
readonly TAIL_IP=100.92.240.34
readonly CERT_NAME="$GRAFANA_HOST"
readonly CERT_DIR="${PLATFORM_HOST_NGINX_CERT_DIR:-/etc/letsencrypt/live/$CERT_NAME}"
readonly CREDENTIALS="${PLATFORM_HOST_NGINX_CREDENTIALS:-/home/donghyeon/.secrets/certbot/cloudflare.ini}"
readonly NGINX_BIN="${PLATFORM_HOST_NGINX_NGINX_BIN:-/usr/sbin/nginx}"
readonly SYSTEMCTL_BIN="${PLATFORM_HOST_NGINX_SYSTEMCTL_BIN:-/usr/bin/systemctl}"
readonly SUDO_BIN="${PLATFORM_HOST_NGINX_SUDO_BIN:-/usr/bin/sudo}"
readonly TEST_BIN="${PLATFORM_HOST_NGINX_TEST_BIN:-/usr/bin/test}"
readonly CERTBOT_BIN="${PLATFORM_HOST_NGINX_CERTBOT_BIN:-/snap/bin/certbot}"
readonly PREFLIGHT_COMMAND="${PLATFORM_HOST_NGINX_PREFLIGHT_COMMAND:-}"
readonly POSTCHECK_COMMAND="${PLATFORM_HOST_NGINX_POSTCHECK_COMMAND:-}"
readonly TEST_MODE="${PLATFORM_HOST_NGINX_TEST_MODE:-0}"
readonly RULES_ACCEPTANCE_SCHEMA=platform-observability-rules-alerts-v2
readonly SOURCE_PROOF_SCHEMA=platform-blackbox-source-v1
readonly STAGE_LEDGER_HEADER=$'stage\tprevious_sha256\tcandidate_sha256\tpayload_file\tpayload_sha256\tapplied_at_utc'
execute=false
mode=full
mode_count=0
certbot_email=''
verified_output_dir=''
rollback_armed=false
rollback_payload=''
rollback_previous_sha=''
rollback_stage=''
timestamp_backup=''
fail() {
printf 'ERROR: %s\n' "$*" >&2
return 1
}
usage() {
cat <<'USAGE'
Usage:
bash scripts/bootstrap/apply-host-nginx-observability.sh
bash scripts/bootstrap/apply-host-nginx-observability.sh --execute --metrics-guard-only
bash scripts/bootstrap/apply-host-nginx-observability.sh \
--execute --certificate-only --certbot-email you@example.com
bash scripts/bootstrap/apply-host-nginx-observability.sh --execute --grafana-deny-guard-only
bash scripts/bootstrap/apply-host-nginx-observability.sh \
--execute --verified-output-dir /tmp/platform-observability-metrics.XXXXXX
Dry-run prints the exact stage hashes and does not call sudo, Nginx, systemd,
Certbot, Kubernetes, or network probes. Execute modes are interactive. The full
cutover additionally requires source proof, rules-alerts acceptance, and the two
accepted metric inventories for the same rollback ID.
USAGE
}
sha_of() {
sha256sum -- "$1" | awk '{print $1}'
}
is_sha256() {
[[ "$1" =~ ^[0-9a-f]{64}$ ]]
}
require_regular_source() {
[[ -f "$1" && ! -L "$1" ]] || fail "unsafe or missing source: $1"
}
root_cat() {
"$SUDO_BIN" -n cat -- "$1"
}
root_stat() {
"$SUDO_BIN" -n stat "$@"
}
expected_owner_mode() {
local path=$1 mode=$2 metadata
metadata="$(root_stat -c '%U:%G %a' -- "$path")" || return 1
if [[ "$TEST_MODE" == 1 ]]; then
[[ "${metadata##* }" == "$mode" ]]
else
[[ "$metadata" == "root:root $mode" ]]
fi
}
ensure_root_directory_no_follow() {
local path=$1 metadata type owner mode
if "$SUDO_BIN" -n "$TEST_BIN" -L "$path"; then
fail "root directory path is a symlink: $path"
return 1
fi
if ! "$SUDO_BIN" -n "$TEST_BIN" -e "$path"; then
if ! "$SUDO_BIN" install -d -o root -g root -m 0700 "$path"; then
fail "cannot create root directory: $path"
return 1
fi
fi
if ! metadata="$("$SUDO_BIN" -n stat -c '%F|%U:%G|%a' -- "$path")"; then
fail "cannot lstat root directory: $path"
return 1
fi
IFS='|' read -r type owner mode <<<"$metadata"
if [[ "$type" != directory || "$mode" != 700 ]]; then
fail "root directory type or mode is unsafe: $path"
return 1
fi
if [[ "$TEST_MODE" == 0 && "$owner" != root:root ]]; then
fail "root directory owner is unsafe: $path"
return 1
fi
return 0
}
validate_root_file_no_follow() {
local path=$1 expected_mode=$2 metadata type owner mode links
if "$SUDO_BIN" -n "$TEST_BIN" -L "$path"; then
fail "root file path is a symlink: $path"
return 1
fi
if ! metadata="$("$SUDO_BIN" -n stat -c '%F|%U:%G|%a|%h' -- "$path")"; then
fail "cannot lstat root file: $path"
return 1
fi
IFS='|' read -r type owner mode links <<<"$metadata"
if [[ "$type" != 'regular file' || "$mode" != "$expected_mode" || "$links" != 1 ]]; then
fail "root file type, mode, or link count is unsafe: $path"
return 1
fi
if [[ "$TEST_MODE" == 0 && "$owner" != root:root ]]; then
fail "root file owner is unsafe: $path"
return 1
fi
return 0
}
validate_test_command_boundary() {
local label=$1 command_path=$2 fixture_bin=$3 resolved metadata type owner mode mode_value
if [[ "$command_path" == /usr/bin/sudo ]]; then
fail 'test command boundary is unsafe: real /usr/bin/sudo is forbidden'
return 1
fi
if ! resolved="$(readlink -f -- "$command_path" 2>/dev/null)" || [[ "$resolved" != "$command_path" ]]; then
fail "test command boundary is unsafe: $label is not canonical"
return 1
fi
if [[ "$(dirname -- "$resolved")" != "$fixture_bin" ]]; then
fail "test command boundary is unsafe: $label escapes the fixture bin"
return 1
fi
if [[ ! -f "$command_path" || -L "$command_path" || ! -x "$command_path" ]]; then
fail "test command boundary is unsafe: $label is not a regular executable"
return 1
fi
if ! metadata="$(stat -c '%F|%u|%a' -- "$command_path")"; then
fail "test command boundary is unsafe: cannot stat $label"
return 1
fi
IFS='|' read -r type owner mode <<<"$metadata"
mode_value=$((8#$mode))
if [[ "$type" != 'regular file' || "$owner" != "$(id -u)" ||
$((mode_value & 0022)) != 0 || $((mode_value & 07000)) != 0 ]]; then
fail "test command boundary is unsafe: $label owner or mode"
return 1
fi
return 0
}
validate_test_command_fixture_directory() {
local path=$1 label=$2 metadata type owner mode mode_value
if [[ ! -d "$path" || -L "$path" ]]; then
fail "test command fixture $label is not a regular directory"
return 1
fi
if ! metadata="$(stat -c '%F|%u|%a' -- "$path")"; then
fail "cannot stat test command fixture $label"
return 1
fi
IFS='|' read -r type owner mode <<<"$metadata"
mode_value=$((8#$mode))
if [[ "$type" != directory || "$owner" != "$(id -u)" ||
$((mode_value & 0022)) != 0 || $((mode_value & 07000)) != 0 ]]; then
fail "test command fixture $label owner or mode is unsafe"
return 1
fi
return 0
}
production_executable_metadata_is_safe() {
local label=$1 lexical=$2 canonical=$3 type=$4 uid=$5 gid=$6 mode=$7 links=$8 policy=$9
local mode_value
[[ "$type" == 'regular file' && "$uid" == 0 && "$gid" == 0 && "$links" == 1 ]] || return 1
[[ "$mode" =~ ^[0-7]{3,4}$ ]] || return 1
mode_value=$((8#$mode))
(( (mode_value & 0111) != 0 )) || return 1
(( (mode_value & 0022) == 0 )) || return 1
case "$policy" in
sudo)
[[ "$label" == sudo && "$lexical" == /usr/bin/sudo && "$canonical" == /usr/bin/sudo ]] || return 1
(( mode_value == 04755 )) || return 1
;;
ordinary)
(( (mode_value & 06000) == 0 )) || return 1
;;
*) return 1 ;;
esac
}
validate_production_executable_boundary() {
local label=$1 lexical=$2 policy=$3 canonical metadata type uid gid mode links
[[ "$lexical" == /* ]] || {
fail "production command path is not absolute: $label"
return 1
}
if ! canonical="$(/usr/bin/readlink -f -- "$lexical" 2>/dev/null)"; then
fail "production command target does not resolve: $label"
return 1
fi
if ! metadata="$(/usr/bin/stat -Lc '%F|%u|%g|%a|%h' -- "$lexical" 2>/dev/null)"; then
fail "production command metadata is unreadable: $label"
return 1
fi
IFS='|' read -r type uid gid mode links <<<"$metadata"
if ! production_executable_metadata_is_safe "$label" "$lexical" "$canonical" \
"$type" "$uid" "$gid" "$mode" "$links" "$policy"; then
fail "production command boundary is unsafe: $label"
return 1
fi
}
validate_production_path_directory() {
local path=$1 canonical metadata type uid gid mode mode_value
if ! canonical="$(/usr/bin/readlink -f -- "$path" 2>/dev/null)" ||
! metadata="$(/usr/bin/stat -Lc '%F|%u|%g|%a' -- "$path" 2>/dev/null)"; then
fail "production PATH directory does not resolve: $path"
return 1
fi
IFS='|' read -r type uid gid mode <<<"$metadata"
mode_value=$((8#$mode))
if [[ "$canonical" != /* || "$type" != directory || "$uid" != 0 || "$gid" != 0 ||
$((mode_value & 0022)) != 0 ]]; then
fail "production PATH directory is unsafe: $path"
return 1
fi
}
validate_production_command_boundaries() {
local name expected resolved path_directory
local -a production_path_directories=()
[[ "$PATH" == "$HOST_NGINX_PRODUCTION_PATH" ]] || {
fail 'production PATH is not exact'
return 1
}
IFS=: read -r -a production_path_directories <<<"$HOST_NGINX_PRODUCTION_PATH"
for path_directory in "${production_path_directories[@]}"; do
validate_production_path_directory "$path_directory" || return 1
done
for name in "${!HOST_NGINX_PRODUCTION_COMMANDS[@]}"; do
expected=${HOST_NGINX_PRODUCTION_COMMANDS[$name]}
if [[ "$name" == test ]]; then
resolved=$expected
elif ! resolved="$(command -v -- "$name")" || [[ "$resolved" != "$expected" ]]; then
fail "production command resolution drifted: $name"
return 1
fi
validate_production_executable_boundary "$name" "$expected" ordinary || return 1
done
validate_production_executable_boundary nginx "$NGINX_BIN" ordinary || return 1
validate_production_executable_boundary systemctl "$SYSTEMCTL_BIN" ordinary || return 1
validate_production_executable_boundary certbot "$CERTBOT_BIN" ordinary || return 1
validate_production_executable_boundary sudo "$SUDO_BIN" sudo || return 1
}
validate_test_mode() {
local override_name active_fixture_root rollback_fixture_root fixture_bin fixture_root
if [[ "$TEST_MODE" != 0 && "$TEST_MODE" != 1 ]]; then
fail 'invalid test-mode flag'
return 1
fi
if [[ "$TEST_MODE" == 0 ]]; then
for override_name in ${!PLATFORM_HOST_NGINX_@}; do
fail "production rejects Host Nginx override: $override_name"
return 1
done
if [[ "$ACTIVE" != /etc/nginx/sites-available/learn-services ||
"$ENABLED" != /etc/nginx/sites-enabled/learn-services ||
"$ROLLBACK_BASE" != /var/lib/hyeonworks/platform-rollbacks ||
"$CERT_DIR" != /etc/letsencrypt/live/grafana.learn.hyeonworks.com ||
"$CREDENTIALS" != /home/donghyeon/.secrets/certbot/cloudflare.ini ||
"$NGINX_BIN" != /usr/sbin/nginx || "$SYSTEMCTL_BIN" != /usr/bin/systemctl ||
"$SUDO_BIN" != /usr/bin/sudo || "$TEST_BIN" != /usr/bin/test || "$CERTBOT_BIN" != /snap/bin/certbot ||
-n "$PREFLIGHT_COMMAND" || -n "$POSTCHECK_COMMAND" ]]; then
fail 'production Host Nginx constants are not exact'
return 1
fi
return 0
fi
case "$ACTIVE:$ROLLBACK_BASE" in
/tmp/platform-host-nginx-test.*:/tmp/platform-host-nginx-test.*) ;;
*)
fail 'test mode is restricted to an isolated /tmp/platform-host-nginx-test.* tree'
return 1
;;
esac
if [[ "$SUDO_BIN" == /usr/bin/sudo ]]; then
fail 'test command boundary is unsafe: real /usr/bin/sudo is forbidden'
return 1
fi
if ! active_fixture_root="$(readlink -f -- "$(dirname -- "$(dirname -- "$ACTIVE")")" 2>/dev/null)" ||
! rollback_fixture_root="$(readlink -f -- "$(dirname -- "$ROLLBACK_BASE")" 2>/dev/null)"; then
fail 'test fixture root does not resolve'
return 1
fi
fixture_root=$active_fixture_root
fixture_bin="$fixture_root/bin"
if [[ "$fixture_root" != /tmp/platform-host-nginx-test.* ||
"$ACTIVE" != "$fixture_root/active/learn-services" ||
"$ENABLED" != "$fixture_root/enabled/learn-services" ||
"$ROLLBACK_BASE" != "$fixture_root/rollbacks" ||
"$rollback_fixture_root" != "$fixture_root" ||
"$(readlink -f -- "$fixture_root" 2>/dev/null)" != "$fixture_root" ]]; then
fail 'test command fixture bin is not tied to the active and rollback fixture tree'
return 1
fi
if ! validate_test_command_fixture_directory "$fixture_root" root ||
! validate_test_command_fixture_directory "$fixture_bin" bin; then
return 1
fi
validate_test_command_boundary sudo "$SUDO_BIN" "$fixture_bin" || return 1
validate_test_command_boundary test "$TEST_BIN" "$fixture_bin" || return 1
validate_test_command_boundary nginx "$NGINX_BIN" "$fixture_bin" || return 1
validate_test_command_boundary systemctl "$SYSTEMCTL_BIN" "$fixture_bin" || return 1
validate_test_command_boundary certbot "$CERTBOT_BIN" "$fixture_bin" || return 1
validate_test_command_boundary preflight "$PREFLIGHT_COMMAND" "$fixture_bin" || return 1
validate_test_command_boundary postcheck "$POSTCHECK_COMMAND" "$fixture_bin" || return 1
PATH="$fixture_bin:$HOST_NGINX_PRODUCTION_PATH"
export PATH
hash -r
return 0
}
parse_args() {
while (( $# > 0 )); do
case "$1" in
--execute)
execute=true
shift
;;
--metrics-guard-only)
mode=metrics
mode_count=$((mode_count + 1))
shift
;;
--certificate-only)
mode=certificate
mode_count=$((mode_count + 1))
shift
;;
--grafana-deny-guard-only)
mode=deny
mode_count=$((mode_count + 1))
shift
;;
--certbot-email)
(( $# >= 2 )) || fail '--certbot-email requires a value'
certbot_email=$2
shift 2
;;
--verified-output-dir)
(( $# >= 2 )) || fail '--verified-output-dir requires a value'
verified_output_dir=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
fail "unsupported argument: $1"
;;
esac
done
(( mode_count <= 1 )) || fail 'execute modes are mutually exclusive'
[[ -z "$certbot_email" || "$mode" == certificate ]] || fail '--certbot-email is certificate-only'
[[ -z "$verified_output_dir" || "$mode" == full ]] || fail '--verified-output-dir is full-mode only'
}
source_contract() {
require_regular_source "$ADMIN_CANDIDATE"
require_regular_source "$METRICS_CANDIDATE"
require_regular_source "$DENY_CANDIDATE"
require_regular_source "$FULL_CANDIDATE"
require_regular_source "$SOURCE_TEST"
bash "$SOURCE_TEST" >/dev/null
python3 - "$ADMIN_CANDIDATE" "$METRICS_CANDIDATE" <<'PY'
import pathlib
import sys
prior, guard = map(lambda value: pathlib.Path(value).read_text(), sys.argv[1:])
needle = " location / {\n"
addition = " location = /metrics {\n return 404;\n }\n\n"
if prior.count(needle) < 1 or guard != prior.replace(needle, addition + needle, 1):
raise SystemExit("metrics guard differs from the admin source outside exact /metrics")
PY
}
print_plan() {
local active_sha=unreadable
[[ -f "$ACTIVE" && ! -L "$ACTIVE" && -r "$ACTIVE" ]] && active_sha="$(sha_of "$ACTIVE")"
printf 'HOST_NGINX_ACTIVE_SHA256=%s\n' "$active_sha"
printf 'HOST_NGINX_ADMIN_SHA256=%s\n' "$(sha_of "$ADMIN_CANDIDATE")"
printf 'HOST_NGINX_METRICS_GUARD_SHA256=%s\n' "$(sha_of "$METRICS_CANDIDATE")"
printf 'HOST_NGINX_GRAFANA_DENY_GUARD_SHA256=%s\n' "$(sha_of "$DENY_CANDIDATE")"
printf 'HOST_NGINX_FULL_SHA256=%s\n' "$(sha_of "$FULL_CANDIDATE")"
printf 'HOST_NGINX_CERTIFICATE_EXPECTED_SAN=%s\n' "$GRAFANA_HOST"
printf 'HOST_NGINX_CERTIFICATE_SAN=NOT_CHECKED_DRY_RUN\n'
printf 'HOST_NGINX_GRAFANA_PUBLIC_DNS=NOT_CHECKED_DRY_RUN\n'
printf 'HOST_NGINX_OBSERVABILITY_DRY_RUN=PASS\n'
}
validate_host_files() {
"$SUDO_BIN" -n "$TEST_BIN" -f "$ACTIVE" || fail "active site is missing: $ACTIVE"
! "$SUDO_BIN" -n "$TEST_BIN" -L "$ACTIVE" || fail "active site is a symlink: $ACTIVE"
"$SUDO_BIN" -n "$TEST_BIN" -L "$ENABLED" || fail "enabled site is not a symlink: $ENABLED"
[[ "$("$SUDO_BIN" -n readlink -f -- "$ENABLED")" == "$("$SUDO_BIN" -n readlink -f -- "$ACTIVE")" ]] ||
fail 'enabled site target changed'
expected_owner_mode "$ACTIVE" 644 || fail 'active site must be root:root 0644'
}
validate_rollback_root() {
local id=${PLATFORM_OBSERVABILITY_ROLLBACK_ID:-}
[[ "$id" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] || fail 'PLATFORM_OBSERVABILITY_ROLLBACK_ID format is invalid'
ROLLBACK_ROOT="$ROLLBACK_BASE/observability-$id"
"$SUDO_BIN" -n "$TEST_BIN" -d "$ROLLBACK_ROOT" || fail "rollback root is missing: $ROLLBACK_ROOT"
! "$SUDO_BIN" -n "$TEST_BIN" -L "$ROLLBACK_ROOT" || fail "rollback root is a symlink: $ROLLBACK_ROOT"
expected_owner_mode "$ROLLBACK_ROOT" 700 || fail 'rollback root must be root:root 0700'
readonly ROLLBACK_ROOT
}
declare -A PARSED_ENV=()
read_exact_env() {
local file=$1
shift
local line key value allowed expected=$# count=0
PARSED_ENV=()
if ! "$SUDO_BIN" -n "$TEST_BIN" -f "$file"; then
fail "evidence file is missing: $file"
return 1
fi
if "$SUDO_BIN" -n "$TEST_BIN" -L "$file"; then
fail "evidence file is a symlink: $file"
return 1
fi
if ! expected_owner_mode "$file" 600; then
fail "evidence file must be root:root 0600: $file"
return 1
fi
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" != *=* || "$line" == *$'\t'* || "$line" == *$'\r'* ]]; then
fail "malformed evidence line: $file"
return 1
fi
key=${line%%=*}
value=${line#*=}
if [[ -z "$key" || -z "$value" || -n "${PARSED_ENV[$key]+x}" ]]; then
fail "duplicate or empty evidence field: $file"
return 1
fi
allowed=false
for expected_key in "$@"; do
[[ "$key" == "$expected_key" ]] && allowed=true
done
if [[ "$allowed" != true ]]; then
fail "unexpected evidence field $key: $file"
return 1
fi
PARSED_ENV[$key]=$value
count=$((count + 1))
done < <(root_cat "$file")
if (( count != expected )); then
fail "evidence field count is not exact: $file"
return 1
fi
for expected_key in "$@"; do
if [[ -z "${PARSED_ENV[$expected_key]:-}" ]]; then
fail "missing evidence field $expected_key: $file"
return 1
fi
done
return 0
}
validate_slack_acceptance_tuple() {
local tuple
tuple="${PARSED_ENV[slack_deployment_gate]}|${PARSED_ENV[slack_gate_approval_ref]}|${PARSED_ENV[slack_gate_accepted_by_uid]}"
case "$tuple" in
'RECOVERY|strict-recovery-evidence-v1|not-applicable'|\
'RISK_ACCEPTED|2026-08-14-observability-slack-recovery-risk-acceptance-design|1000')
return 0
;;
*)
fail 'rules-alerts Slack deployment gate tuple mismatch'
return 1
;;
esac
}
validate_recent_utc() {
local value=$1 label=$2 then now age
if [[ ! "$value" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then
fail "$label timestamp format is invalid"
return 1
fi
if ! then="$(date -u -d "$value" +%s 2>/dev/null)"; then
fail "$label timestamp is invalid"
return 1
fi
if ! now="$(date -u +%s)"; then
fail "cannot read current UTC time for $label"
return 1
fi
age=$((now - then))
if (( age < -300 || age > 86400 )); then
fail "$label evidence is stale or from the future"
return 1
fi
return 0
}
validate_utc() {
local value=$1 label=$2
if [[ ! "$value" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then
fail "$label timestamp format is invalid"
return 1
fi
if ! date -u -d "$value" +%s >/dev/null 2>&1; then
fail "$label timestamp is invalid"
return 1
fi
return 0
}
inventory_sha() {
local root=$1 phase=$2 directory checksum actual declared filename extra directory_mode json_mode checksum_mode
directory="$root/$phase"
checksum="$directory/inventory.sha256"
if [[ ! -d "$directory" || -L "$directory" ]]; then
fail "inventory phase is missing or unsafe: $phase"
return 1
fi
if [[ ! -f "$directory/inventory.json" || -L "$directory/inventory.json" ]]; then
fail "inventory JSON is unsafe: $phase"
return 1
fi
if [[ ! -f "$checksum" || -L "$checksum" ]]; then
fail "inventory checksum is unsafe: $phase"
return 1
fi
if ! directory_mode="$(stat -c %a -- "$directory")"; then
fail "cannot stat inventory directory: $phase"
return 1
fi
if [[ "$directory_mode" != 700 ]]; then
fail "inventory directory mode is not 0700: $phase"
return 1
fi
if ! json_mode="$(stat -c %a -- "$directory/inventory.json")" ||
! checksum_mode="$(stat -c %a -- "$checksum")"; then
fail "cannot stat inventory files: $phase"
return 1
fi
if [[ "$json_mode" != 600 || "$checksum_mode" != 600 ]]; then
fail "inventory file mode is not 0600: $phase"
return 1
fi
if ! read -r declared filename extra <"$checksum"; then
fail "cannot read inventory checksum: $phase"
return 1
fi
if [[ "$filename" != inventory.json || -n "${extra:-}" ]]; then
fail "inventory checksum shape is invalid: $phase"
return 1
fi
if ! is_sha256 "$declared"; then
fail "inventory checksum is invalid: $phase"
return 1
fi
if ! actual="$(sha_of "$directory/inventory.json")"; then
fail "cannot hash inventory JSON: $phase"
return 1
fi
if [[ "$actual" != "$declared" ]]; then
fail "inventory checksum mismatch: $phase"
return 1
fi
printf '%s' "$actual" || return 1
return 0
}
validate_full_evidence() {
local deny_sha=$1 acceptance proof initial_sha post_sha canonical_metric_root
local metric_root_mode
if [[ -z "$verified_output_dir" ]]; then
fail 'full execute requires --verified-output-dir'
return 1
fi
if ! canonical_metric_root="$(readlink -f -- "$verified_output_dir" 2>/dev/null)"; then
fail 'verified output dir does not resolve'
return 1
fi
if [[ "$canonical_metric_root" != "$verified_output_dir" || ! -d "$verified_output_dir" || -L "$verified_output_dir" ]]; then
fail 'verified output dir must be a canonical regular directory'
return 1
fi
if ! metric_root_mode="$(stat -c %a -- "$verified_output_dir")"; then
fail 'cannot stat verified output root'
return 1
fi
if [[ "$metric_root_mode" != 700 ]]; then
fail 'verified output root mode must be 0700'
return 1
fi
if ! initial_sha="$(inventory_sha "$verified_output_dir" target-initial)"; then
return 1
fi
if ! post_sha="$(inventory_sha "$verified_output_dir" post-substrate)"; then
return 1
fi
proof="$ROLLBACK_ROOT/blackbox-source-proof.env"
if ! read_exact_env "$proof" schema rollback_id nginx_sha256 tested_at_utc \
grafana_remote_addr grafana_status storage_admin_remote_addr storage_admin_status \
db_admin_remote_addr db_admin_status; then
return 1
fi
if [[ "${PARSED_ENV[schema]}" != "$SOURCE_PROOF_SCHEMA" ]]; then
fail 'source-proof schema mismatch'
return 1
fi
if [[ "${PARSED_ENV[rollback_id]}" != "$PLATFORM_OBSERVABILITY_ROLLBACK_ID" ]]; then
fail 'source-proof rollback ID mismatch'
return 1
fi
if [[ "${PARSED_ENV[nginx_sha256]}" != "$deny_sha" ]]; then
fail 'source-proof Nginx hash mismatch'
return 1
fi
if [[ "${PARSED_ENV[grafana_status]}" != 403 || "${PARSED_ENV[storage_admin_status]}" != 403 ||
"${PARSED_ENV[db_admin_status]}" != 403 ]]; then
fail 'source-proof statuses are not exact 403'
return 1
fi
if ! python3 - "${PARSED_ENV[grafana_remote_addr]}" "${PARSED_ENV[storage_admin_remote_addr]}" \
"${PARSED_ENV[db_admin_remote_addr]}" <<'PY'
import ipaddress
import sys
for value in sys.argv[1:]:
ipaddress.ip_address(value)
PY
then
fail 'source-proof remote address is invalid'
return 1
fi
if ! validate_recent_utc "${PARSED_ENV[tested_at_utc]}" source-proof; then
return 1
fi
acceptance="$ROLLBACK_ROOT/access-rules-alerts/acceptance.env"
if ! read_exact_env "$acceptance" \
schema rollback_id target_initial_sha256 post_substrate_sha256 \
slack_deployment_gate slack_gate_approval_ref slack_gate_accepted_by_uid \
accepted_at_utc state; then
return 1
fi
if [[ "${PARSED_ENV[schema]}" != "$RULES_ACCEPTANCE_SCHEMA" ]]; then
fail 'rules-alerts acceptance schema mismatch'
return 1
fi
if [[ "${PARSED_ENV[rollback_id]}" != "$PLATFORM_OBSERVABILITY_ROLLBACK_ID" ]]; then
fail 'rules-alerts rollback ID mismatch'
return 1
fi
if [[ "${PARSED_ENV[target_initial_sha256]}" != "$initial_sha" ]]; then
fail 'target-initial acceptance hash mismatch'
return 1
fi
if [[ "${PARSED_ENV[post_substrate_sha256]}" != "$post_sha" ]]; then
fail 'post-substrate acceptance hash mismatch'
return 1
fi
if ! validate_slack_acceptance_tuple; then
return 1
fi
if [[ "${PARSED_ENV[state]}" != accepted ]]; then
fail 'rules-alerts state is not accepted'
return 1
fi
if ! validate_utc "${PARSED_ENV[accepted_at_utc]}" rules-alerts; then
return 1
fi
return 0
}
validate_exact_certificate() {
local san_output san_set
if ! "$SUDO_BIN" -n "$TEST_BIN" -s "$CERT_DIR/fullchain.pem"; then
fail 'Grafana certificate is missing'
return 1
fi
if ! "$SUDO_BIN" -n "$TEST_BIN" -s "$CERT_DIR/privkey.pem"; then
fail 'Grafana private key is missing'
return 1
fi
if ! "$SUDO_BIN" -n openssl x509 -in "$CERT_DIR/fullchain.pem" -noout -checkend 0 >/dev/null; then
fail 'Grafana certificate is expired'
return 1
fi
if ! san_output="$("$SUDO_BIN" -n openssl x509 -in "$CERT_DIR/fullchain.pem" -noout -ext subjectAltName)"; then
fail 'cannot read Grafana certificate SAN'
return 1
fi
if ! san_set="$(printf '%s\n' "$san_output" | grep -oE 'DNS:[^,[:space:]]+' | sed 's/^DNS://' | sort -u)"; then
fail 'cannot parse Grafana certificate SAN'
return 1
fi
if [[ "$san_set" != "$GRAFANA_HOST" ]]; then
fail 'Grafana certificate SAN set is not exact'
return 1
fi
return 0
}
validate_cloudflare_credentials() {
local contract
"$SUDO_BIN" -n "$TEST_BIN" -f "$CREDENTIALS" || fail "Cloudflare credential is missing: $CREDENTIALS"
expected_owner_mode "$CREDENTIALS" 600 || fail 'Cloudflare credential must be root:root 0600'
contract="$("$SUDO_BIN" -n awk -F= '
BEGIN { count=0; valid=0; unexpected=0 }
/^[[:space:]]*($|#)/ { next }
/^[[:space:]]*dns_cloudflare_api_token[[:space:]]*=/ {
count++; value=$0; sub(/^[^=]*=[[:space:]]*/, "", value); sub(/[[:space:]]+$/, "", value)
if (length(value) >= 20) valid++; next
}
{ unexpected++ }
END { printf "%d:%d:%d", count, valid, unexpected }
' "$CREDENTIALS")"
[[ "$contract" == 1:1:0 ]] || fail 'Cloudflare credential must contain one nonempty API token key only'
}
certificate_mode() {
if validate_exact_certificate 2>/dev/null; then
printf 'HOST_NGINX_GRAFANA_CERTIFICATE=READY\n'
return 0
fi
[[ "$certbot_email" == *@* ]] || fail 'missing exact certificate; --certbot-email is required for issuance'
validate_cloudflare_credentials
[[ -x "$CERTBOT_BIN" ]] || fail "supported Certbot is missing: $CERTBOT_BIN"
"$CERTBOT_BIN" plugins 2>/dev/null | grep -q dns-cloudflare || fail 'Certbot dns-cloudflare plugin is missing'
confirm "issue the exact Grafana certificate"
"$SUDO_BIN" "$CERTBOT_BIN" certonly --dns-cloudflare \
--dns-cloudflare-credentials "$CREDENTIALS" --dns-cloudflare-propagation-seconds 60 \
--cert-name "$CERT_NAME" --domains "$GRAFANA_HOST" --non-interactive --agree-tos \
--email "$certbot_email"
validate_exact_certificate
printf 'HOST_NGINX_GRAFANA_CERTIFICATE=READY\n'
}
status_code() {
local code
if code="$(curl --disable --noproxy '*' --silent --show-error --output /dev/null \
--connect-timeout 2 --max-time 5 --write-out '%{http_code}' "$@" 2>/dev/null)"; then
printf '%s' "$code"
else
printf 000
fi
}
external_preflight() {
local stage=$1 code host
"$SYSTEMCTL_BIN" is-active --quiet nginx || fail 'Nginx is not active'
"$SUDO_BIN" -n "$NGINX_BIN" -t
if [[ "$TEST_MODE" == 1 ]]; then
"$PREFLIGHT_COMMAND" "$stage"
return
fi
case "$stage" in
metrics) return 0 ;;
deny)
validate_exact_certificate
[[ -z "$(ss -H -ltn '( sport = :1 )')" ]] || fail 'TCP 127.0.0.1:1 guard listener is not absent'
return 0
;;
full) ;;
*) fail "unknown transition stage: $stage" ;;
esac
validate_exact_certificate
[[ "$(dig +time=2 +tries=1 +short "@$LAN_IP" A "$GRAFANA_HOST" | tail -n1)" == "$LAN_IP" ]] || fail 'LAN private DNS answer mismatch'
[[ "$(dig +time=2 +tries=1 +short "@$TAIL_IP" A "$GRAFANA_HOST" | tail -n1)" == "$TAIL_IP" ]] || fail 'Tailscale private DNS answer mismatch'
[[ -z "$(dig +time=2 +tries=1 +short @1.1.1.1 A "$GRAFANA_HOST" | tr -d '[:space:]')" ]] || fail 'Grafana public A exists'
[[ -z "$(dig +time=2 +tries=1 +short @1.1.1.1 AAAA "$GRAFANA_HOST" | tr -d '[:space:]')" ]] || fail 'Grafana public AAAA exists'
kubectl -n observability exec deployment/grafana -c grafana --request-timeout=10s -- \
getent ahostsv4 "$GRAFANA_HOST" | awk -v ip="$LAN_IP" '$1 == ip { found=1 } END { exit !found }' ||
fail 'Grafana Pod private DNS answer mismatch'
kubectl -n observability get ingress grafana -o json --request-timeout=10s | jq -e --arg host "$GRAFANA_HOST" '
.spec.ingressClassName == "traefik" and (.spec.tls // [] | length == 0) and
([.spec.rules[] | select(.host == $host) | .http.paths[] |
select(.path == "/" and .pathType == "Prefix" and
.backend.service.name == "grafana" and .backend.service.port.number == 80)] | length == 1)
' >/dev/null || fail 'Grafana Ingress contract is not ready'
kubectl -n observability get endpointslice -l kubernetes.io/service-name=grafana -o json --request-timeout=10s |
jq -e '[.items[].endpoints[] | select(.conditions.ready == true) | .addresses[]] | length > 0' >/dev/null ||
fail 'Grafana EndpointSlice has no ready address'
[[ "$(status_code --header "Host: $GRAFANA_HOST" http://127.0.0.1:30080/api/health)" == 200 ]] || fail 'Grafana internal health is not 200'
[[ "$(status_code --resolve "$GITEA_HOST:443:127.0.0.1" "https://$GITEA_HOST/api/healthz")" == 200 ]] || fail 'Gitea regression failed'
[[ "$(status_code --resolve "$KEYCLOAK_HOST:443:127.0.0.1" "https://$KEYCLOAK_HOST/realms/hyeonworks/.well-known/openid-configuration")" == 200 ]] || fail 'Keycloak regression failed'
for host in "$STORAGE_HOST" "$DB_HOST"; do
code="$(status_code --interface "$LAN_IP" --resolve "$host:443:$LAN_IP" "https://$host/")"
[[ "$code" == 200 || "$code" == 302 || "$code" == 303 ]] || fail "admin endpoint regression failed: $host=$code"
done
[[ "$(status_code http://$LAN_IP:30080/)" == 000 ]] || fail 'LAN NodePort 30080 is reachable'
[[ "$(status_code --insecure https://$LAN_IP:30443/)" == 000 ]] || fail 'LAN NodePort 30443 is reachable'
}
stage_postcheck_once() {
local stage=$1
local code host
[[ "$(status_code --resolve "$GITEA_HOST:443:127.0.0.1" "https://$GITEA_HOST/metrics")" == 404 ]] || return 1
[[ "$(status_code --resolve "$GITEA_HOST:443:127.0.0.1" "https://$GITEA_HOST/api/healthz")" == 200 ]] || return 1
[[ "$(status_code --resolve "$KEYCLOAK_HOST:443:127.0.0.1" "https://$KEYCLOAK_HOST/realms/hyeonworks/.well-known/openid-configuration")" == 200 ]] || return 1
for host in "$STORAGE_HOST" "$DB_HOST"; do
code="$(status_code --interface "$LAN_IP" --resolve "$host:443:$LAN_IP" "https://$host/")"
[[ "$code" == 200 || "$code" == 302 || "$code" == 303 ]] || return 1
[[ "$(status_code --resolve "$host:443:127.0.0.1" "https://$host/")" == 403 ]] || return 1
done
[[ "$(status_code http://$LAN_IP:30080/)" == 000 ]] || return 1
[[ "$(status_code --insecure https://$LAN_IP:30443/)" == 000 ]] || return 1
[[ "$stage" == metrics ]] && return 0
[[ "$(status_code --resolve "$GRAFANA_HOST:443:127.0.0.1" "https://$GRAFANA_HOST/")" == 403 ]] || return 1
[[ "$(status_code --resolve "$GRAFANA_HOST:443:127.0.0.1" "https://$GRAFANA_HOST/metrics")" == 404 ]] || return 1
code="$(status_code --interface "$LAN_IP" --resolve "$GRAFANA_HOST:443:$LAN_IP" "https://$GRAFANA_HOST/")"
if [[ "$stage" == deny ]]; then
[[ "$code" == 502 ]] || return 1
else
[[ "$stage" == full && ( "$code" == 200 || "$code" == 302 ) ]] || return 1
fi
}
external_postcheck() {
local stage=$1 stable=0 attempt
"$SYSTEMCTL_BIN" is-active --quiet nginx || return 1
for ((attempt=1; attempt<=15; attempt++)); do
if { [[ "$TEST_MODE" == 1 ]] && "$POSTCHECK_COMMAND" "$stage"; } ||
{ [[ "$TEST_MODE" == 0 ]] && stage_postcheck_once "$stage"; }; then
stable=$((stable + 1))
(( stable >= 3 )) && break
else
stable=0
fi
[[ "$TEST_MODE" == 0 && attempt -lt 15 ]] && sleep 1
done
(( stable >= 3 )) || return 1
[[ "$TEST_MODE" == 1 ]] && return 0
if curl --disable --noproxy '*' --insecure --silent --output /dev/null --connect-timeout 2 --max-time 5 \
--resolve unconfigured.invalid:443:127.0.0.1 https://unconfigured.invalid/ 2>/dev/null; then
return 1
fi
}
confirm() {
local description=$1 answer
printf 'Type APPLY to %s: ' "$description"
if [[ "$TEST_MODE" == 1 ]]; then
answer=${PLATFORM_HOST_NGINX_CONFIRMATION:-}
printf '%s\n' "$answer"
else
[[ -t 0 ]] || fail '--execute requires an interactive terminal'
read -r answer
fi
[[ "$answer" == APPLY ]] || fail 'cancelled'
}
validate_full_first_cutover_residue_absent() {
local host_root child
host_root="$ROLLBACK_ROOT/host-nginx"
if ! "$SUDO_BIN" -n "$TEST_BIN" ! -e "$host_root" ||
! "$SUDO_BIN" -n "$TEST_BIN" ! -L "$host_root"; then
fail 'full first-cutover Host Nginx evidence root already exists or is unsafe'
return 1
fi
for child in \
"$host_root/stages.tsv" \
"$host_root/payloads" \
"$host_root/payloads/full-prior-0001.conf"; do
if ! "$SUDO_BIN" -n "$TEST_BIN" ! -e "$child" ||
! "$SUDO_BIN" -n "$TEST_BIN" ! -L "$child"; then
fail 'full first-cutover Host Nginx evidence residue exists or is unsafe'
return 1
fi
done
if ! "$SUDO_BIN" -n "$TEST_BIN" ! -e "$host_root" ||
! "$SUDO_BIN" -n "$TEST_BIN" ! -L "$host_root"; then
fail 'full first-cutover Host Nginx evidence root changed during validation'
return 1
fi
}
prepare_stage_ledger() {
local stage=$1 previous_sha=$2 candidate_sha=$3 host_root payload_root ledger attempt payload_rel payload tmp
host_root="$ROLLBACK_ROOT/host-nginx"
payload_root="$host_root/payloads"
ledger="$host_root/stages.tsv"
if ! ensure_root_directory_no_follow "$host_root"; then
return 1
fi
if ! ensure_root_directory_no_follow "$payload_root"; then
return 1
fi
if "$SUDO_BIN" -n "$TEST_BIN" -L "$ledger"; then
fail 'Host Nginx ledger must not be a symlink'
return 1
elif "$SUDO_BIN" -n "$TEST_BIN" -e "$ledger"; then
if ! validate_root_file_no_follow "$ledger" 600; then
return 1
fi
if [[ "$(root_cat "$ledger" | head -n1)" != "$STAGE_LEDGER_HEADER" ]]; then
fail 'Host Nginx ledger header mismatch'
return 1
fi
else
tmp="$(mktemp /tmp/platform-host-nginx-ledger.XXXXXX)"
printf '%s\n' "$STAGE_LEDGER_HEADER" >"$tmp"
if ! "$SUDO_BIN" install -o root -g root -m 0600 "$tmp" "$ledger"; then
rm -f -- "$tmp"
return 1
fi
rm -f -- "$tmp" || return 1
if ! validate_root_file_no_follow "$ledger" 600; then
return 1
fi
fi
attempt=$(( $(root_cat "$ledger" | awk -F '\t' -v stage="$stage" 'NR > 1 && $1 == stage { count++ } END { print count+1 }') ))
printf -v payload_rel 'payloads/%s-prior-%04d.conf' "$stage" "$attempt"
payload="$host_root/$payload_rel"
if "$SUDO_BIN" -n "$TEST_BIN" -L "$payload" || "$SUDO_BIN" -n "$TEST_BIN" -e "$payload"; then
fail "ledger payload already exists or is a symlink: $payload_rel"
return 1
fi
"$SUDO_BIN" install -o root -g root -m 0600 "$ACTIVE" "$payload"
if ! validate_root_file_no_follow "$payload" 600; then
return 1
fi
payload_sha="$(root_cat "$payload" | sha256sum | awk '{print $1}')"
[[ "$payload_sha" == "$previous_sha" ]] || fail 'ledger payload hash mismatch'
tmp="$(mktemp /tmp/platform-host-nginx-ledger.XXXXXX)"
root_cat "$ledger" >"$tmp"
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$stage" "$previous_sha" "$candidate_sha" \
"$payload_rel" "$payload_sha" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$tmp"
if ! "$SUDO_BIN" install -o root -g root -m 0600 "$tmp" "$ledger"; then
rm -f -- "$tmp"
return 1
fi
rm -f -- "$tmp" || return 1
rollback_payload=$payload
rollback_previous_sha=$previous_sha
rollback_stage=$stage
return 0
}
rollback() {
local rc=0 actual
set +e
"$SUDO_BIN" -n "$TEST_BIN" -f "$rollback_payload" || rc=1
"$SUDO_BIN" -n "$TEST_BIN" -L "$rollback_payload" && rc=1
expected_owner_mode "$rollback_payload" 600 || rc=1
actual="$(root_cat "$rollback_payload" 2>/dev/null | sha256sum | awk '{print $1}')"
[[ "$actual" == "$rollback_previous_sha" ]] || rc=1
if (( rc == 0 )); then
"$SUDO_BIN" install -o root -g root -m 0644 "$rollback_payload" "$ACTIVE" || rc=1
[[ "$(sha_of "$ACTIVE")" == "$rollback_previous_sha" ]] || rc=1
"$SUDO_BIN" "$NGINX_BIN" -t || rc=1
"$SUDO_BIN" "$SYSTEMCTL_BIN" reload nginx || rc=1
"$SUDO_BIN" "$SYSTEMCTL_BIN" is-active --quiet nginx || rc=1
fi
if (( rc == 0 )); then
rollback_armed=false
printf 'HOST_NGINX_OBSERVABILITY_ROLLBACK=PASS\n' >&2
else
printf 'HOST_NGINX_OBSERVABILITY_ROLLBACK=FAIL\nMANUAL_RECOVERY_REQUIRED=YES\n' >&2
fi
set -e
return "$rc"
}
on_exit() {
local rc=$?
trap - EXIT INT TERM
if (( rc != 0 )) && [[ "$rollback_armed" == true ]]; then
rollback || true
fi
exit "$rc"
}
transition() {
local stage=$1 expected=$2 candidate=$3 description=$4 previous_sha candidate_sha active_sha
previous_sha="$(sha_of "$expected")"
candidate_sha="$(sha_of "$candidate")"
active_sha="$(sha_of "$ACTIVE")"
if [[ "$stage" == full ]]; then
if ! validate_full_evidence "$(sha_of "$DENY_CANDIDATE")"; then
return 1
fi
fi
if [[ "$stage" == full && "$active_sha" == "$candidate_sha" ]]; then
fail 'full cutover is already active without first-cutover provenance'
return 1
fi
if [[ "$active_sha" == "$candidate_sha" ]]; then
external_preflight "$stage"
external_postcheck "$stage" || fail "already-active $stage stage is unhealthy"
printf 'HOST_NGINX_%s_STAGE=ALREADY_ACTIVE\n' "${stage^^}"
return 0
fi
[[ "$active_sha" == "$previous_sha" ]] || fail "$stage active hash gate failed: $active_sha"
if [[ "$stage" == full ]]; then
validate_full_first_cutover_residue_absent || return 1
fi
external_preflight "$stage"
[[ "$(sha_of "$ACTIVE")" == "$previous_sha" ]] || fail "$stage active config changed during preflight"
printf 'HOST_NGINX_STAGE=%s\nHOST_NGINX_PREVIOUS_SHA256=%s\nHOST_NGINX_CANDIDATE_SHA256=%s\n' \
"$stage" "$previous_sha" "$candidate_sha"
confirm "$description"
[[ "$(sha_of "$ACTIVE")" == "$previous_sha" ]] || fail "$stage active config changed before commit"
[[ "$(sha_of "$candidate")" == "$candidate_sha" ]] || fail "$stage candidate changed before commit"
if ! prepare_stage_ledger "$stage" "$previous_sha" "$candidate_sha"; then
fail 'fresh rollback ID + source proof + complete Task 6 required'
return 1
fi
timestamp_backup="${ACTIVE}.before-observability-${stage}-$(date -u +%Y%m%dT%H%M%SZ)"
if "$SUDO_BIN" -n "$TEST_BIN" -e "$timestamp_backup" || "$SUDO_BIN" -n "$TEST_BIN" -L "$timestamp_backup"; then
fail "timestamp backup already exists or is unsafe: $timestamp_backup"
fail 'fresh rollback ID + source proof + complete Task 6 required'
return 1
fi
if ! "$SUDO_BIN" install -o root -g root -m 0644 "$ACTIVE" "$timestamp_backup"; then
fail 'fresh rollback ID + source proof + complete Task 6 required'
return 1
fi
if [[ "$(sha_of "$timestamp_backup")" != "$previous_sha" ]]; then
fail 'timestamp backup hash mismatch'
fail 'fresh rollback ID + source proof + complete Task 6 required'
return 1
fi
rollback_armed=true
"$SUDO_BIN" install -o root -g root -m 0644 "$candidate" "$ACTIVE"
[[ "$(sha_of "$ACTIVE")" == "$candidate_sha" ]] || fail 'installed active hash mismatch'
"$SUDO_BIN" "$SYSTEMCTL_BIN" daemon-reload
"$SUDO_BIN" "$NGINX_BIN" -t
"$SUDO_BIN" "$SYSTEMCTL_BIN" reload nginx
"$SUDO_BIN" "$SYSTEMCTL_BIN" is-active --quiet nginx
external_postcheck "$stage" || fail "$stage bounded postcheck failed"
rollback_armed=false
printf 'HOST_NGINX_%s_STAGE=PASS\nHOST_NGINX_TIMESTAMP_BACKUP=%s\n' "${stage^^}" "$timestamp_backup"
}
main() {
parse_args "$@"
validate_test_mode
if [[ "$TEST_MODE" == 0 ]]; then
validate_production_command_boundaries
fi
source_contract
if [[ "$execute" == false ]]; then
print_plan
return 0
fi
for cmd in awk cat curl date dig grep head install jq kubectl mktemp openssl python3 readlink sed sha256sum sort ss stat; do
command -v "$cmd" >/dev/null 2>&1 || fail "required command is missing: $cmd"
done
[[ -x "$NGINX_BIN" && -x "$SYSTEMCTL_BIN" && -x "$SUDO_BIN" ]] || fail 'required host command boundary is missing'
"$SUDO_BIN" -v
validate_host_files
validate_rollback_root
trap on_exit EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
case "$mode" in
metrics) transition metrics "$ADMIN_CANDIDATE" "$METRICS_CANDIDATE" 'install only the Gitea metrics guard' ;;
certificate) certificate_mode ;;
deny) transition deny "$METRICS_CANDIDATE" "$DENY_CANDIDATE" 'install only the Grafana deny guard' ;;
full) transition full "$DENY_CANDIDATE" "$FULL_CANDIDATE" 'replace only the Grafana deny upstream with Traefik' ;;
*) fail "invalid mode: $mode" ;;
esac
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi