#!/bin/bash set -euo pipefail ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) env_file=${1:-} expected_domain=${2:-} usage() { echo "Usage: $0 ENV_FILE EXPECTED_DOMAIN" >&2 } if [[ -z "$env_file" || -z "$expected_domain" ]]; then usage exit 1 fi if [[ ! -f "$env_file" ]]; then echo "Deployment environment does not exist: $env_file" >&2 exit 1 fi if [[ "$(stat -c '%a' "$env_file")" != 600 ]]; then echo "Deployment environment must have mode 0600: $env_file" >&2 exit 1 fi if [[ "$(stat -c '%u' "$env_file")" != "$(id -u)" ]]; then echo "Deployment environment must be owned by the current operator." >&2 exit 1 fi read_value() { local key=$1 awk -v key="$key" ' index($0, key "=") == 1 { print substr($0, length(key) + 2) found = 1 } END { if (!found) exit 1 } ' "$env_file" } require_value() { local key=$1 local value if ! value=$(read_value "$key") || [[ -z "$value" ]]; then echo "$key is missing or empty in $env_file." >&2 exit 1 fi printf '%s' "$value" } reject_marker() { local key=$1 local value=$2 case "$value" in *REPLACE* | *GENERATE* | *example.com*) echo "$key still contains a template value." >&2 exit 1 ;; esac } phx_host=$(require_value PHX_HOST) phx_scheme=$(require_value PHX_SCHEME) phx_url_port=$(require_value PHX_URL_PORT) base_url=$(require_value WNH_BASE_URL) debug_base_url=$(require_value WNH_DEBUG_BASE_URL) http_bind_address=$(require_value HTTP_BIND_ADDRESS) trusted_proxy_ips=$(require_value TRAEFIK_TRUSTED_IPS) postgres_password=$(require_value POSTGRES_PASSWORD) database_url=$(require_value DATABASE_URL) secret_key_base=$(require_value SECRET_KEY_BASE) handover_secret=$(require_value HANDOVER_SECRET) release_cookie=$(require_value RELEASE_COOKIE) metrics_token=$(require_value METRICS_TOKEN) smtp_relay=$(require_value SMTP_RELAY) smtp_port=$(require_value SMTP_PORT) smtp_auth=$(require_value SMTP_AUTH) smtp_tls=$(require_value SMTP_TLS) smtp_ssl=$(require_value SMTP_SSL) email_from_address=$(require_value EMAIL_FROM_ADDRESS) [[ "$phx_host" == "$expected_domain" ]] || { echo "PHX_HOST does not match EXPECTED_DOMAIN." >&2 exit 1 } [[ "$phx_scheme" == https && "$phx_url_port" == 443 ]] || { echo "Production PHX_SCHEME/PHX_URL_PORT must describe the public HTTPS origin." >&2 exit 1 } [[ "$base_url" == "https://$expected_domain" ]] || { echo "WNH_BASE_URL does not match the public HTTPS origin." >&2 exit 1 } [[ "$debug_base_url" == "$base_url" ]] || { echo "WNH_DEBUG_BASE_URL and WNH_BASE_URL must use the same deployment origin." >&2 exit 1 } for pair in \ "HTTP_BIND_ADDRESS:$http_bind_address" \ "TRAEFIK_TRUSTED_IPS:$trusted_proxy_ips" \ "POSTGRES_PASSWORD:$postgres_password" \ "DATABASE_URL:$database_url" \ "SECRET_KEY_BASE:$secret_key_base" \ "HANDOVER_SECRET:$handover_secret" \ "RELEASE_COOKIE:$release_cookie" \ "METRICS_TOKEN:$metrics_token" \ "SMTP_RELAY:$smtp_relay" \ "EMAIL_FROM_ADDRESS:$email_from_address" do reject_marker "${pair%%:*}" "${pair#*:}" done [[ "$smtp_relay" != mailpit ]] || { echo "SMTP_RELAY still targets local Mailpit; public registration needs a transactional relay." >&2 exit 1 } [[ "$smtp_port" =~ ^[0-9]+$ ]] || { echo "SMTP_PORT must be numeric." >&2 exit 1 } [[ "$smtp_auth" =~ ^(always|never|if_available)$ ]] || { echo "SMTP_AUTH has an unsupported value." >&2 exit 1 } [[ "$smtp_tls" =~ ^(always|never|if_available)$ ]] || { echo "SMTP_TLS has an unsupported value." >&2 exit 1 } [[ "$smtp_ssl" =~ ^(true|false|0|1)$ ]] || { echo "SMTP_SSL has an unsupported value." >&2 exit 1 } [[ "$email_from_address" == *@* ]] || { echo "EMAIL_FROM_ADDRESS is not an email address." >&2 exit 1 } secrets=( "$postgres_password" "$secret_key_base" "$handover_secret" "$release_cookie" "$metrics_token" ) for ((left = 0; left < ${#secrets[@]}; left++)); do for ((right = left + 1; right < ${#secrets[@]}; right++)); do if [[ "${secrets[$left]}" == "${secrets[$right]}" ]]; then echo "Deployment secrets must be independent." >&2 exit 1 fi done done expected_database_url="ecto://postgres:$postgres_password@db/who_need_help" [[ "$database_url" == "$expected_database_url" ]] || { echo "DATABASE_URL does not match the generated PostgreSQL role/password/database." >&2 exit 1 } docker compose \ --env-file "$env_file" \ --file "$ROOT/compose.yaml" \ --file "$ROOT/compose.production.yaml" \ config --quiet echo "Production environment structure passed validation without printing secrets." echo "This does not test DNS, TLS, SMTP reachability/delivery, proxy source IPs, or server capacity."