Skip to content

Latest commit

 

History

History
399 lines (298 loc) · 31.6 KB

File metadata and controls

399 lines (298 loc) · 31.6 KB

Environment Variables Reference

Read this in: English · Русский

This document is the authoritative reference for every environment variable consumed by AstroCTFb. All values originate from .env (operator-managed) or HashiCorp Vault (auto-populated by setup.sh on first start). Build-time variables for the frontend (VITE_*) are baked into the SPA bundle by docker build and require an image rebuild to change.


Table of Contents


How values reach each service

┌─────────┐   .env file   ┌─────────────────┐
│ Operator│──────────────▶│ docker compose  │──env──▶ Postgres / Redis / Grafana / SeaweedFS / HAProxy
└─────────┘               └─────────────────┘
     │
     │ via setup.sh
     ▼
┌────────────┐  init-vault.sh  ┌──────────┐  Vault API   ┌─────────┐
│ .vault-keys│────────────────▶│  Vault   │─────────────▶│ Backend │
└────────────┘                 └──────────┘              └─────────┘

Frontend SPA: VITE_* baked at `docker build` time -> JS bundle -> no runtime injection.

The backend pulls all sensitive secrets from Vault at startup (8 KV-v2 paths under secret/ctf-platform/). Infrastructure containers (Postgres, Redis, SeaweedFS, Grafana) consume credentials directly from .env because they need them before Vault is reachable. HAProxy and certbot read a small subset for routing and TLS.


REQUIRED - fill before first start

Without these, setup.sh start will fail or the platform will start in a broken state. There are no sensible defaults - they describe your deployment.

Variable Type Read by Purpose
DOMAIN string HAProxy entrypoint, certbot, backend Root domain (e.g. ctfleague.ru)
API_DOMAIN string HAProxy, certbot API subdomain (e.g. api.ctfleague.ru)
GRAFANA_DOMAIN string HAProxy, certbot Grafana subdomain
VAULT_DOMAIN string HAProxy, certbot Vault UI subdomain
S3_DOMAIN string HAProxy, certbot, backend (STORAGE_S3_PUBLIC_ENDPOINT) SeaweedFS UI / public S3 subdomain
VAULT_ADMIN_IP IP/CIDR HAProxy entrypoint.sh:19 IP allowed to reach vault.${DOMAIN} UI
ACME_EMAIL email certbot (docker-compose.yml:393) Let's Encrypt registration email
USE_LE_STAGING bool certbot entrypoint true on first deploy -> LE staging cert (rate-limit safe); flip to false after DNS verified
API_BASE_URL URL backend (config.go:199) Where the SPA expects the API (https://api.ctfleague.ru)
FRONTEND_URL URL backend (config.go:195) Public SPA URL - used in email links
CORS_ORIGINS comma-list backend (config.go:158) Allowed Origin headers for CORS preflight
GF_SERVER_ROOT_URL URL docker-compose -> Grafana Public Grafana URL
STORAGE_S3_PUBLIC_ENDPOINT URL backend (config.go:199) Rewrites internal S3 presigned URLs for public access
OAUTH_GITHUB_REDIRECT_URL URL backend (config.go:211) Must match the value registered in GitHub OAuth App
OAUTH_GOOGLE_REDIRECT_URL URL backend (config.go:213) Must match the value registered in Google Cloud Console
POSTGRES_PASSWORD string Postgres container, init-vault.sh Postgres root password - also seeded into Vault
REDIS_PASSWORD string Redis container, init-vault.sh Redis requirepass - also seeded into Vault
GRAFANA_ADMIN_PASSWORD string Grafana container Required (docker-compose.yml:558 enforces with :?)
HAPROXY_STATS_PASSWORD string HAProxy entrypoint Required (HAPROXY_STATS_PASSWORD:?); auth for haproxy:8405/stats
SEAWEED_S3_ACCESS_KEY string init-vault.sh:53, s3.json SeaweedFS S3 access key
SEAWEED_S3_SECRET_KEY string init-vault.sh:54, s3.json SeaweedFS S3 secret key
ADMIN_EMAIL email init-vault.sh:137 -> secret/ctf-platform/admin Default admin user email (used for password-reset flow)

Note: the five *_DOMAIN records must all resolve to the server's IP for certbot to issue a single multi-SAN certificate covering all of them. Failure of even one record causes the whole certificate request to fail (3 retry attempts × 60s, then certbot exits cleanly).


OPTIONAL - auto-generated by Vault

init-vault.sh (deployment/docker/init-vault.sh) uses set-if-absent semantics: if the corresponding env var is non-empty on first start, it is seeded into Vault verbatim. If empty, a secure random value is generated and the path is logged with the marker [auto-generated]. Subsequent restarts do not rotate these values unless you explicitly run ./setup.sh secrets rotate*.

Variable Auto-gen if empty Vault path / field Notes
FLAG_ENCRYPTION_KEY 64 hex chars (32 bytes, AES-256) secret/ctf-platform/app -> flag_encryption_key Validated at startup: must be exactly 64 hex chars
JWT_ACCESS_SECRET 64 alphanumeric chars secret/ctf-platform/jwt -> access_secret Backend signs access tokens with this
JWT_REFRESH_SECRET 64 alphanumeric chars secret/ctf-platform/jwt -> refresh_secret Backend signs refresh tokens
OAUTH_STATE_SECRET 64 alphanumeric chars secret/ctf-platform/oauth -> state_secret HMAC key for OAuth state nonce
ADMIN_USERNAME admin secret/ctf-platform/admin -> username Default seed admin username
ADMIN_PASSWORD 16 alphanumeric chars (printed once) secret/ctf-platform/admin -> password If empty, init-vault.sh prints to stdout - write it down immediately
VAULT_TOKEN filled by setup.sh after vault operator init .env line VAULT_TOKEN= Operator should not edit; backed up in .vault-keys

Golden rule: if you supply your own value, it goes into Vault verbatim. If you leave it empty, you cannot retrieve the random value later without querying Vault directly (e.g. vault kv get secret/ctf-platform/admin).


INTEGRATIONS - leave empty to disable

These activate optional features. Empty values disable the integration cleanly.

Email (Resend)

Variable Default Effect when empty Read by
RESEND_API_KEY "" Vault stores placeholder; outgoing email disabled at runtime init-vault.sh:116 -> backend config.go:183
RESEND_ENABLED true If false, backend skips email sending entirely config.go:192
RESEND_FROM_EMAIL noreply@example.com - config.go:190
RESEND_FROM_NAME CTF Platform - config.go:191
RESEND_VERIFY_TTL_HOURS 24 Email verification token TTL config.go:193
RESEND_RESET_TTL_HOURS 1 Password reset token TTL config.go:194
VERIFY_EMAILS true If false, registrations skip email verification config.go:154

OAuth providers

Variable Vault path / field Effect when empty
OAUTH_GITHUB_CLIENT_ID oauth.github_client_id GitHub login button hidden
OAUTH_GITHUB_CLIENT_SECRET oauth.github_client_secret (must match the value registered in GitHub OAuth App)
OAUTH_GOOGLE_CLIENT_ID oauth.google_client_id Google login button hidden
OAUTH_GOOGLE_CLIENT_SECRET oauth.google_client_secret (must match Google Cloud Console)

Telegram alerts (Alertmanager)

Variable Read by Effect when empty
TELEGRAM_BOT_TOKEN docker-compose.yml:511, alertmanager.yml setup.sh prompts (y/N) on first start; "no" writes a null-receiver stub
TELEGRAM_CHAT_ID docker-compose.yml:512, alertmanager.yml (same)

DEFAULTS - review only if needed

These have sane production defaults. Change only if you know you need to.

Platform identity

Variable Default Read by Notes
APP_NAME CTF Platform backend config.go:147 Used in email From-name and elsewhere
APP_VERSION 1.0.0 backend config.go:148 Cosmetic
JWT_ISSUER ctf-platform backend config.go:181 iss claim in issued JWTs
VITE_APP_NAME CTF Platform frontend Dockerfile ARG Build-time - baked into SPA navbar/title

Logging

Variable Default Notes
LOG_LEVEL info Levels: debug, info, warn, error
STRUCTURED_LOGGER true JSON log format for Loki ingestion
DEBUG_ENABLED false Verbose stack traces

Backend runtime

Variable Default Notes
SECURE_COOKIES true Forced to true if API_BASE_URL starts with https://
BACKEND_PORT 8090 (.env.example) / 8080 (compose) Compose forces port 8080 inside the container
MIGRATIONS_PATH migrations Goose migrations directory
HTTP_SHUTDOWN_TIMEOUT 15 (seconds) Graceful shutdown deadline

Competition rules

Variable Default Notes
COMPETITION_MODE flexible Allowed: solo_only, teams_only, flexible
ALLOW_TEAM_SWITCH true If false, users cannot leave/join teams once joined
MIN_TEAM_SIZE 1 Validated at registration
MAX_TEAM_SIZE 10 -

Vault

Variable Default Notes
VAULT_ADDR http://vault:8200 Internal Docker network address
VAULT_PORT 8200 Informational
VAULT_TOKEN (auto-filled) setup.sh writes the root token after vault operator init

Postgres

Variable Default
POSTGRES_USER admin
POSTGRES_DB board
POSTGRES_HOST postgres
POSTGRES_PORT 5432
POSTGRES_SSL_MODE disable (intra-network only)
POSTGRES_MAX_CONNS 150
POSTGRES_MIN_CONNS 10

Redis

Variable Default
REDIS_HOST redis
REDIS_PORT 6379
REDIS_POOL_SIZE 50
REDIS_MIN_IDLE 10

JWT TTLs

Variable Default Notes
JWT_ACCESS_TTL_MINUTES 15 Short-lived access token
JWT_REFRESH_TTL_HOURS 72 Long-lived refresh token (httpOnly cookie)

Network / proxy

Variable Default Notes
TRUSTED_PROXY_CIDRS 172.16.0.0/12 Docker bridge - backend trusts X-Forwarded-For from these
METRICS_ALLOWED_IPS "" If empty, /metrics rejects all (set CIDRs to allow Prometheus)

Rate limits

Variable Default Notes
RATE_LIMIT_SUBMIT_FLAG 10 Per-user flag submission limit
RATE_LIMIT_SUBMIT_FLAG_DURATION 1 Duration in minutes

Storage

Variable Default Notes
STORAGE_PROVIDER s3 Allowed: filesystem, s3
STORAGE_LOCAL_PATH ./uploads Used when provider=filesystem
STORAGE_S3_ENDPOINT seaweedfs:8333 Internal endpoint
STORAGE_S3_BUCKET ctf -
STORAGE_S3_REGION us-east-1 minio-go expects this
STORAGE_S3_USE_SSL false Internal network
STORAGE_PRESIGNED_EXPIRY_MINUTES 60 Lifetime of presigned download URLs

Grafana / monitoring

Variable Default
GRAFANA_ADMIN_USER admin
GRAFANA_PORT 3000

HAProxy

Variable Default Notes
ADMIN_ALLOWED_IPS RFC-1918 ranges + 127.0.0.1/32 IPs allowed to reach grafana/s3 admin subdomains
HAPROXY_STATS_USER admin Basic-auth user for :8405/stats
HAPROXY_BEHIND_CDN false If true, HAProxy trusts X-Forwarded-For from TRUSTED_CDN_CIDRS
TRUSTED_CDN_CIDRS "" Cloudflare / DDoS-Guard CIDR list (used only if HAPROXY_BEHIND_CDN=true)

SeaweedFS ports

Variable Default
SEAWEED_S3_PORT 8333
SEAWEEDFS_UI_PORT 5000
SEAWEEDFS_UI_IMAGE ctf-platform-seaweedfs-ui

Docker image tags

Variable Default
BACKEND_IMAGE ctf-platform-backend:latest
FRONTEND_IMAGE ctf-platform-frontend:latest

Vault paths overview

The backend fetches all 8 paths in parallel at startup (config.go:loadFromVault, errgroup.WithContext, 30s timeout). If any path is missing or unreachable, the corresponding env value is used as fallback (graceful degradation).

Path Fields Source
secret/ctf-platform/database user, password, dbname POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB (always overwritten)
secret/ctf-platform/redis password REDIS_PASSWORD (always overwritten)
secret/ctf-platform/storage access_key, secret_key SEAWEED_S3_ACCESS_KEY, SEAWEED_S3_SECRET_KEY (always overwritten)
secret/ctf-platform/jwt access_secret, refresh_secret JWT_ACCESS_SECRET, JWT_REFRESH_SECRET (set-if-absent)
secret/ctf-platform/app flag_encryption_key FLAG_ENCRYPTION_KEY (set-if-absent)
secret/ctf-platform/resend api_key RESEND_API_KEY (set-if-absent, defaults to literal placeholder)
secret/ctf-platform/admin username, email, password ADMIN_USERNAME, ADMIN_EMAIL, ADMIN_PASSWORD (set-if-absent)
secret/ctf-platform/oauth state_secret, github_client_id, github_client_secret, google_client_id, google_client_secret OAUTH_* (set-if-absent for state_secret; client IDs/secrets always overwritten)

Inspect a path:

docker exec -e VAULT_ADDR=http://127.0.0.1:8200 \
            -e VAULT_TOKEN=$(grep ^ROOT_TOKEN= .vault-keys | cut -d= -f2) \
  vault vault kv get secret/ctf-platform/admin

Frontend build-time variables

These are consumed by frontend/board/Dockerfile as ARG directives and baked into the SPA bundle at docker build time (Vite reads them via import.meta.env). To change them you must rebuild and redeploy the frontend container.

Variable Default Notes
VITE_APP_NAME CTF Platform Browser tab title and navbar brand
VITE_API_BASE_URL https://api.example.com/api/v1 Production: must point at API subdomain
VITE_WS_URL wss://api.example.com/api/v1/ws WebSocket endpoint
VITE_SSE_URL https://api.example.com/api/v1/sse SSE fallback endpoint

The SPA also has a runtime fallback (shared/config/env.ts): if VITE_API_BASE_URL is empty in dev, it falls back to /api/v1 (same-origin via Nginx proxy). In production, missing VITE_API_BASE_URL causes the app to throw on startup.

SeaweedFS UI (separate frontend)

The frontend/seaweedfs-ui/ Dockerfile consumes its own set of build args, all unrelated to the main SPA:

Variable Default
VITE_API_URL https://api.example.com
VITE_HOST s3.example.com
VITE_FILER_PORT 8888
VITE_MASTER_PORT 9333
VITE_MASTER_PROXY_PATH master
VITE_FILER_PROXY_PATH filer

HAProxy entrypoint environment

deployment/haproxy/entrypoint.sh reads these and generates HAProxy map files / fragment configs:

Variable Used for
DOMAIN, API_DOMAIN, GRAFANA_DOMAIN, VAULT_DOMAIN, S3_DOMAIN self-signed bootstrap cert SAN list, Host ACL routing
ADMIN_ALLOWED_IPS written to /etc/haproxy/maps/admin_ips.txt (gates grafana/s3)
VAULT_ADMIN_IP written to /etc/haproxy/maps/vault_ips.txt (gates vault subdomain)
TRUSTED_CDN_CIDRS written to /etc/haproxy/maps/cdn_cidrs.txt (XFF trust list)
HAPROXY_BEHIND_CDN toggles HTTPS bind + redirect (CDN terminates TLS upstream)
HAPROXY_STATS_USER, HAPROXY_STATS_PASSWORD basic-auth on :8405/stats

Certbot environment

deployment/certbot/Dockerfile + the entrypoint declared inline in docker-compose.yml:395–432 read:

Variable Used for
DOMAIN, API_DOMAIN, GRAFANA_DOMAIN, VAULT_DOMAIN, S3_DOMAIN -d <domain> flags for the multi-SAN cert request
ACME_EMAIL --email registration
USE_LE_STAGING adds --staging flag (rate-limit safe)

After successful issuance the entrypoint invokes renewal-hook.sh once to install the cert into HAProxy via Runtime API, then enters a sleep 12h ; certbot renew loop.


What setup.sh writes back to .env

After the wizard completes (or ./setup.sh start initializes Vault for the first time), the following keys are mutated in-place:

Key Source
DOMAIN wizard step 2 (and all derived URLs)
VAULT_TOKEN written from vault operator init JSON output (setup.sh:752–754)
USE_LE_STAGING wizard step 2 (y/n prompt)
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID wizard step 7 (or do_start prompt if alertmanager.yml missing)
*_DOMAIN, API_BASE_URL, FRONTEND_URL, CORS_ORIGINS, OAUTH_*_REDIRECT_URL, all VITE_* URLs derived from DOMAIN
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB wizard step 3
REDIS_PASSWORD wizard step 4
SEAWEED_S3_ACCESS_KEY, SEAWEED_S3_SECRET_KEY wizard step 6
GRAFANA_ADMIN_PASSWORD, HAPROXY_STATS_PASSWORD wizard steps 7 / auto-gen
OAUTH_GITHUB_CLIENT_ID, OAUTH_GOOGLE_CLIENT_ID wizard step 8 (public IDs only - secrets go to Vault)

The env_set helper (setup.sh:159) preserves comments and structure when updating values, so manual edits to ordering or comments survive subsequent reconfigure runs.


Secret generators (quick reference)

If you want to supply your own values for the OPTIONAL block:

# FLAG_ENCRYPTION_KEY (32 bytes / 64 hex chars, AES-256)
openssl rand -hex 32

# JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, OAUTH_STATE_SECRET (64 alphanumeric)
openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 64

# Generic password (24 chars base64)
openssl rand -base64 18

These match the formats init-vault.sh would auto-generate.


Changing values after deploy

Kind of change How
Non-secret config (rate limits, TTLs, domains) edit .env -> ./setup.sh restart
Admin password / Resend key / OAuth client secret ./setup.sh secrets edit (interactive Vault patch) -> ./setup.sh restart
JWT keys + OAuth state ./setup.sh secrets rotate (logs out everyone) -> ./setup.sh restart
FLAG_ENCRYPTION_KEY ./setup.sh secrets rotate-flag (destroys all encrypted regex flags) -> ./setup.sh restart
SeaweedFS S3 credentials ./setup.sh secrets rotate-s3 (briefly restarts seaweedfs + backend)
VITE_* build args edit .env -> docker compose --env-file .env -f deployment/docker/docker-compose.yml up -d --force-recreate --build frontend
TLS cert (staging -> production) flip USE_LE_STAGING=false -> docker compose ... up -d --force-recreate certbot

For full deployment instructions see DEPLOYMENT.md.