4ba9da6721
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 24s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m49s
Refuse a client whose IP is in a curated CIDR feed with 403 in the same
abuseGuard, before the fail2ban ban. Prod-only (keys by real client IP), off by
default.
- ratelimit.Blocklist: a sorted-range IPv4 matcher (binary search) with an
allowlist checked first; ParseDROP reads the feed; ApplyRefresh keeps the
last-good feed on a transient fetch failure and drops it fail-open once stale
(better to under-block than block a legitimate client on a frozen feed). A
separate static CIDR set, not the per-IP fail2ban store.
- gateway: a refresher goroutine re-fetches every few hours (bounded fetch + size
cap); config GATEWAY_BLOCKLIST_{ENABLED,URL,ALLOW,REFRESH,MAX_STALENESS}.
IPv6 is not matched (a v6 client is still covered by fail2ban / the honeypot).
- observability: gateway_blocklist_blocked_total + entries/age gauges; a Grafana
alert warns before the feed is dropped; service-overview panels.
- deploy: compose env + write-prod-env.sh + prod-deploy/rollback wiring (opt-in
via PROD_ vars).
- docs (ARCHITECTURE, deploy/README); unit tests (match, allowlist, IPv6 skip,
parser, fault-tolerance) + a 403 abuse-guard integration test.
98 lines
4.8 KiB
Bash
Executable File
98 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Render the prod main-host runtime env.sh from the workflow job environment.
|
|
#
|
|
# Sourced identically by prod-deploy (deploy-main) and prod-rollback
|
|
# (rollback-main) so the two paths cannot drift: a rollback recreates the
|
|
# containers, so it must re-render the SAME runtime env a full deploy does —
|
|
# otherwise transactional email, VK login and Grafana alerts silently go dark
|
|
# after a rollback until the next full deploy.
|
|
#
|
|
# Usage: APP_VERSION=<tag> bash deploy/write-prod-env.sh <out-path>
|
|
#
|
|
# Every other value comes from the caller's environment (the job `env:` block,
|
|
# vars.* / secrets.*). Mirrors the compose interpolation contract in
|
|
# deploy/.env.example; keep in sync with deploy/docker-compose{,.prod}.yml.
|
|
out="${1:?usage: write-prod-env.sh <out-path>}"
|
|
|
|
# Derive the public URLs from the one canonical origin instead of storing each
|
|
# under its own variable. The path suffixes are structural (SPA routes / the
|
|
# Caddy /_gm sub-path), identical on every contour.
|
|
base="${PUBLIC_BASE_URL%/}"
|
|
TELEGRAM_MINIAPP_URL="$base/telegram/"
|
|
GRAFANA_ROOT_URL="$base/_gm/grafana/"
|
|
VITE_VK_ID_REDIRECT_URL="$base/app/"
|
|
|
|
# Grafana needs a BARE from-address (it rejects the "Name" <addr> form the backend
|
|
# go-mail accepts, and validates it even when SMTP is disabled — a bad value
|
|
# crash-loops Grafana). Split the display-format SERVICE From into address + name;
|
|
# the backend keeps the full form.
|
|
svc_from="${SMTP_RELAY_SERVICE_FROM:-}"
|
|
GRAFANA_SMTP_FROM_NAME=''
|
|
case "$svc_from" in
|
|
*"<"*">"*)
|
|
GRAFANA_SMTP_FROM_ADDRESS="$(printf '%s' "$svc_from" | sed -E 's/.*<([^>]+)>.*/\1/')"
|
|
GRAFANA_SMTP_FROM_NAME="$(printf '%s' "$svc_from" | sed -E 's/[[:space:]]*<[^>]*>.*$//; s/^"//; s/"$//')" ;;
|
|
*) GRAFANA_SMTP_FROM_ADDRESS="$svc_from" ;;
|
|
esac
|
|
|
|
cat > "$out" <<EOF
|
|
export REGISTRY='$REGISTRY'
|
|
export SCRABBLE_CONFIG_DIR='/opt/scrabble'
|
|
export POSTGRES_DB='${POSTGRES_DB:-scrabble}'
|
|
export POSTGRES_USER='${POSTGRES_USER:-scrabble}'
|
|
export POSTGRES_PASSWORD='$POSTGRES_PASSWORD'
|
|
export GM_BASICAUTH_USER='${GM_BASICAUTH_USER:-gm}'
|
|
export GM_BASICAUTH_HASH='$GM_BASICAUTH_HASH'
|
|
export GRAFANA_ADMIN_PASSWORD='$GRAFANA_ADMIN_PASSWORD'
|
|
export GRAFANA_ROOT_URL='$GRAFANA_ROOT_URL'
|
|
export CADDY_SITE_ADDRESS='$CADDY_SITE_ADDRESS'
|
|
export LOG_LEVEL='${LOG_LEVEL:-info}'
|
|
export DICT_VERSION='$DICT_VERSION'
|
|
export APP_VERSION='$APP_VERSION'
|
|
export TELEGRAM_BOT_TOKEN='$TELEGRAM_BOT_TOKEN'
|
|
export TELEGRAM_MINIAPP_URL='$TELEGRAM_MINIAPP_URL'
|
|
export GATEWAY_VK_APP_SECRET='$GATEWAY_VK_APP_SECRET'
|
|
export ROBOKASSA_MERCHANT_LOGIN='$ROBOKASSA_MERCHANT_LOGIN'
|
|
export ROBOKASSA_PASSWORD1='$ROBOKASSA_PASSWORD1'
|
|
export ROBOKASSA_PASSWORD2='$ROBOKASSA_PASSWORD2'
|
|
export ROBOKASSA_TEST='$ROBOKASSA_TEST'
|
|
export VITE_VK_APP_ID='$VITE_VK_APP_ID'
|
|
export VITE_VK_ID_REDIRECT_URL='$VITE_VK_ID_REDIRECT_URL'
|
|
export GATEWAY_VK_ID_CLIENT_SECRET='$GATEWAY_VK_ID_CLIENT_SECRET'
|
|
export EXPORT_SIGN_KEY='$EXPORT_SIGN_KEY'
|
|
export SMTP_RELAY_HOST='$SMTP_RELAY_HOST'
|
|
export SMTP_RELAY_PORT='$SMTP_RELAY_PORT'
|
|
export SMTP_RELAY_TLS='$SMTP_RELAY_TLS'
|
|
export SMTP_RELAY_USER='$SMTP_RELAY_USER'
|
|
export SMTP_RELAY_PASS='$SMTP_RELAY_PASS'
|
|
export SMTP_RELAY_FROM='$SMTP_RELAY_FROM'
|
|
export SMTP_RELAY_ADMIN_FROM='$SMTP_RELAY_ADMIN_FROM'
|
|
export ADMIN_EMAIL='$ADMIN_EMAIL'
|
|
export SMTP_RELAY_SERVICE_FROM='$SMTP_RELAY_SERVICE_FROM'
|
|
export GRAFANA_SMTP_FROM_ADDRESS='$GRAFANA_SMTP_FROM_ADDRESS'
|
|
export GRAFANA_SMTP_FROM_NAME='$GRAFANA_SMTP_FROM_NAME'
|
|
export SERVICE_EMAIL='$SERVICE_EMAIL'
|
|
export GRAFANA_SMTP_PORT='$GRAFANA_SMTP_PORT'
|
|
export GF_SMTP_ENABLED='$GF_SMTP_ENABLED'
|
|
export PUBLIC_BASE_URL='$PUBLIC_BASE_URL'
|
|
export GATEWAY_HONEYTOKEN='$GATEWAY_HONEYTOKEN'
|
|
export GATEWAY_ABUSE_BAN_ENABLED='true'
|
|
# Community IP blocklist (Spamhaus DROP): opt-in — the operator enables it and sets the feed URL +
|
|
# allowlist via PROD_GATEWAY_BLOCKLIST_* vars once the feed is verified. Unset ⇒ off (safe).
|
|
export GATEWAY_BLOCKLIST_ENABLED='${GATEWAY_BLOCKLIST_ENABLED:-false}'
|
|
export GATEWAY_BLOCKLIST_URL='$GATEWAY_BLOCKLIST_URL'
|
|
export GATEWAY_BLOCKLIST_ALLOW='$GATEWAY_BLOCKLIST_ALLOW'
|
|
# Continuous WAL archiving (pgBackRest -> S3) for point-in-time recovery. The artifact
|
|
# ships disarmed: PGBACKREST_ARCHIVE_MODE defaults off, so archiving stays inert until the
|
|
# operator sets the S3 repository values + secrets, creates the stanza and flips the switch
|
|
# on (deploy/README.md). Empty repository values are harmless while the mode is off.
|
|
export PGBACKREST_ARCHIVE_MODE='${PGBACKREST_ARCHIVE_MODE:-off}'
|
|
export PGBACKREST_S3_ENDPOINT='$PGBACKREST_S3_ENDPOINT'
|
|
export PGBACKREST_S3_PORT='${PGBACKREST_S3_PORT:-443}'
|
|
export PGBACKREST_S3_BUCKET='$PGBACKREST_S3_BUCKET'
|
|
export PGBACKREST_S3_REGION='$PGBACKREST_S3_REGION'
|
|
export PGBACKREST_S3_KEY='$PGBACKREST_S3_KEY'
|
|
export PGBACKREST_S3_KEY_SECRET='$PGBACKREST_S3_KEY_SECRET'
|
|
export PGBACKREST_CIPHER_PASS='$PGBACKREST_CIPHER_PASS'
|
|
EOF
|