#!/usr/bin/env bash # Read-only identity bootstrap. The caller captures the database identity in # memory and must never forward this script's stdout to an ordinary log. set -euo pipefail fail() { printf '[production-identity-bootstrap][FAIL] code=%s\n' "$1" >&2 exit 1 } for key in IDENTITY_BOOTSTRAP_MODE REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE \ COMPOSE_PROJECT_NAME EXPECTED_PRODUCTION_ORIGIN_SHA256; do [[ -n "${!key:-}" ]] || fail "${key}_MISSING" done case "$IDENTITY_BOOTSTRAP_MODE" in origin|database) ;; *) fail "IDENTITY_BOOTSTRAP_MODE_INVALID" ;; esac [[ "$EXPECTED_PRODUCTION_ORIGIN_SHA256" =~ ^[0-9a-f]{64}$ ]] \ || fail "PROD_TARGET_ORIGIN_HASH_INVALID" cd "$REMOTE_DIR" [[ -f "$REMOTE_ENV_FILE" && ! -L "$REMOTE_ENV_FILE" && -r "$REMOTE_ENV_FILE" ]] \ || fail "PROD_TARGET_ENV_UNREADABLE" dotenv_value() { local key="$1" local value value="$(awk -v wanted="$key" ' $0 !~ /^[[:space:]]*#/ && $0 ~ "^[[:space:]]*" wanted "=" { sub("^[[:space:]]*" wanted "=", "") print exit } ' "$REMOTE_ENV_FILE")" || return 1 value="${value%$'\r'}" case "$value" in \"*\") value="${value:1:${#value}-2}" ;; \'*\') value="${value:1:${#value}-2}" ;; esac [[ -n "$value" ]] || return 1 printf '%s' "$value" } hash_text() { if command -v sha256sum >/dev/null 2>&1; then printf '%s' "$1" | sha256sum | awk '{print $1}' return 0 fi if command -v shasum >/dev/null 2>&1; then printf '%s' "$1" | shasum -a 256 | awk '{print $1}' return 0 fi return 1 } reject_non_production_identity() { local value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" [[ "$value" != *"<"* && "$value" != *">"* ]] || return 1 [[ "$value" != *"localhost"* && "$value" != *"127.0.0.1"* \ && "$value" != *"::1"* && "$value" != *"hyb_oa_regression"* \ && "$value" != *"staging"* && "$value" != *"stage"* \ && "$value" != *"regression"* && "$value" != *"shadow"* \ && "$value" != *"/test"* ]] } actual_origin="$(dotenv_value APP_ORIGIN)" || fail "PROD_TARGET_ORIGIN_MISSING" [[ "$(hash_text "$actual_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \ || fail "PROD_TARGET_ORIGIN_MISMATCH" if docker compose version >/dev/null 2>&1; then compose_cmd=(docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE") elif command -v docker-compose >/dev/null 2>&1; then compose_cmd=(docker-compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE") else fail "PROD_TARGET_COMPOSE_UNAVAILABLE" fi mapfile -t server_ids < <( docker ps \ --filter "label=com.docker.compose.project=$COMPOSE_PROJECT_NAME" \ --filter "label=com.docker.compose.service=server" \ --format '{{.ID}}' 2>/dev/null || true ) [[ "${#server_ids[@]}" -eq 1 ]] || fail "PROD_TARGET_RUNTIME_NOT_UNIQUE" runtime_origin="$( docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "${server_ids[0]}" \ | sed -n 's/^APP_ORIGIN=//p' | tail -n 1 )" [[ "$(hash_text "$runtime_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \ || fail "PROD_TARGET_RUNTIME_ORIGIN_MISMATCH" if [[ "$IDENTITY_BOOTSTRAP_MODE" == "origin" ]]; then printf '[production-identity-bootstrap][PASS] origin_identity\n' exit 0 fi # Only the comparison baseline crosses the SSH channel. The database name # itself remains remote and cannot be reconstructed from task evidence. actual_db_name="$(dotenv_value POSTGRES_DB)" || fail "PROD_TARGET_DB_NAME_MISSING" reject_non_production_identity "$actual_db_name" \ || fail "PROD_TARGET_DB_NON_PRODUCTION_IDENTITY" printf '[production-identity-bootstrap][DB_SHA256] %s\n' "$(hash_text "$actual_db_name")"