who_need_help/scripts/validate-production-env.sh

176 lines
4.6 KiB
Bash
Executable File

#!/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."