# AppSecGW 1.6.8 — sample env file for docker-compose. # Copy to `.env` (in the same dir as docker-compose.yml), edit values, # then `docker compose up -d`. Keys are NOT committed to git. # ── Required ──────────────────────────────────────────────────────────── UPSTREAM=https://your-internal-app.example.com # ── 1.6.8 — Redis sidecar password (mesh-sync + cross-replica bans) ──── # Required by docker-compose. Generate one with: # python3 -c "import secrets; print(secrets.token_urlsafe(32))" REDIS_PASSWORD=CHANGE_ME_GENERATE_A_STRONG_RANDOM_TOKEN # ── 1.6.8 — CrowdSec LAPI bouncer key ────────────────────────────────── # Generated AFTER first `docker compose up` (LAPI must be running first): # BKEY=$(docker exec crowdsec cscli bouncers add appsecgw -o raw) # echo "CROWDSEC_LAPI_KEY=$BKEY" >> .env # docker compose up -d --force-recreate appsec-antibot-gw # CROWDSEC_LAPI_URL defaults to http://crowdsec:8080 (the bundled sidecar). CROWDSEC_LAPI_KEY= CROWDSEC_LAPI_URL=http://crowdsec:8080 # ── Strongly recommended (production) ─────────────────────────────────── ALLOWED_HOSTS=www.example.com,example.com ADMIN_ALLOWED_IPS=YOUR.HOME.IP/32,YOUR.OFFICE.CIDR/24,127.0.0.1/32,172.17.0.1/32,172.18.0.0/16 # Set this to the CIDRs of the reverse proxy / CDN immediately upstream # (e.g. cloudflared, nginx). Closes the XFF spoofing bypass. TRUSTED_PROXIES=127.0.0.1/32,172.17.0.0/16,172.18.0.0/16 # ADMIN_KEY auto-generates if blank — read it from /data/.admin_key after first boot. ADMIN_KEY= # ── External integrations (all OPT-IN, leave blank to skip) ───────────── # 1. Cloudflare Turnstile — real-browser challenge # Replace TEST keys with the real ones from https://dash.cloudflare.com/turnstile TURNSTILE_SITEKEY= TURNSTILE_SECRET= # Set TURNSTILE_ENABLED=1 to flip on (off-by-default since 1.5.4). TURNSTILE_ENABLED=0 # 2. AbuseIPDB — IP reputation, +50/+15 risk on hit (auto-on when key set) ABUSEIPDB_KEY= # 3. CrowdSec LAPI — already configured above (see top of file). The # bundled compose ships a CrowdSec sidecar; override the URL only if # pointing at an external LAPI. # 4. MaxMind GeoLite2 — ASN + City lookups + GeoMap dashboard. # NOTE: GeoLite2-ASN.mmdb + GeoLite2-City.mmdb are SHIPPED INSIDE the # image (auto-copied to /data on first boot) — no license key needed # for an initial install. Set MAXMIND_LICENSE_KEY only if you want the # `_maxmind_auto_fetch` loop to download fresh weekly snapshots. # Get a free key at https://www.maxmind.com/en/geolite2/signup MAXMIND_LICENSE_KEY= # 5. Anubis-mode — strict in-process PoW gate (raises difficulty by BOOST) ANUBIS_ENABLED=0 ANUBIS_DIFFICULTY_BOOST=1 # 6. Redis — already configured above (REDIS_PASSWORD). The bundled # sidecar URL is auto-built in compose. Override here only if pointing # at an external Redis (e.g. managed Redis Cloud / Upstash): # REDIS_URL=rediss://:token@your-managed-redis:6380 # ── 1.6.0 Tier A — Akamai-Kona / Cloudflare-WAF parity ───────────────── # Country-level geo block (uses GeoLite2-City — set MAXMIND_LICENSE_KEY first # OR rely on the image-bundled mmdb seed). # COUNTRY_BLOCK_ENABLED=1 turns the detector on. Empty list → no-op. # Allowlist takes precedence over denylist. COUNTRY_BLOCK_ENABLED=0 COUNTRY_DENYLIST= # COUNTRY_DENYLIST=RU,CN,KP # examples # COUNTRY_ALLOWLIST=PT,ES,US,DE,FR # whitelist mode COUNTRY_ALLOWLIST= # AI-crawler granular toggles. Default ON for all groups (preserves 1.5.5 # blocking). Set any of these to 0 to allow that vendor's crawler through. AI_UA_OPENAI_ENABLED=1 AI_UA_ANTHROPIC_ENABLED=1 AI_UA_GOOGLE_ENABLED=1 AI_UA_PERPLEXITY_ENABLED=1 AI_UA_META_ENABLED=1 AI_UA_OTHER_ENABLED=1 # Tor exit-node block — auto-fetches torbulkexitlist on startup, refreshed # weekly. O(1) set membership; +50 risk → instant ban (hard tier). TOR_BLOCK_ENABLED=0 # TOR_FEED_URL=https://check.torproject.org/torbulkexitlist # TOR_REFRESH_SECS=604800 # 7 days # Datacenter / commercial-VPN block — adds a heavier `datacenter-vpn` tag # (+30 risk) on top of the existing `asn-hosting` flag (+5). Both rely on # GeoLite2-ASN matching against HOSTING_ASN_KEYWORDS. DC_VPN_BLOCK_ENABLED=0 # Per-endpoint policy engine — JSON array of {path, policy, rps?, burst?}. # Policies: "bypass" | "challenge" | "strict" | "default". # `path` uses fnmatch globs (* and ?). First match wins. # 1.6.1 — also accepts optional rps/burst per route for endpoint rate-limit. # Example: bypass JS challenge for /api/* but force strict mode on /admin. # ENDPOINT_POLICIES=[{"path":"/login","policy":"challenge","rps":5,"burst":10},{"path":"/api/v1/*","policy":"bypass"},{"path":"/admin","policy":"strict"}] ENDPOINT_POLICIES= # ── 1.6.1 Tier B — operator-defined rules + JWT + managed rulesets ───── # Custom rules engine (Cloudflare-Custom-Rules parity). First-match-wins; # evaluated *before* standard detectors. Conditions (all must match): # path:, method:, ua_contains:, # header.:, query.:, # ip_cidr:, country: # Actions: "allow" (forward, skip detectors), "block" (silent decoy +50 risk), # "challenge" (force JS challenge), "tag" (log only) # Example: bypass detectors for an internal IP block + block /wp-login.php hits: # CUSTOM_RULES=[{"if":{"ip_cidr":"10.0.0.0/8"},"then":"allow"},{"if":{"path":"/wp-login.php"},"then":"block"}] CUSTOM_RULES= # Managed body-pattern rule groups. Each group is independently # toggleable. Default ON when BODY_PATTERN_MATCH=1; toggle a group off # if it's noisy on your specific upstream. BODY_GROUP_SQLI_ENABLED=1 BODY_GROUP_XSS_ENABLED=1 BODY_GROUP_LFI_ENABLED=1 BODY_GROUP_RCE_ENABLED=1 BODY_GROUP_SSRF_ENABLED=1 BODY_GROUP_CMD_ENABLED=1 # JWT/Bearer signature validation. When JWT_VALIDATE_PATHS is set, any # request whose path matches one of the globs MUST carry # `Authorization: Bearer ` whose HS256 signature verifies against # JWT_HMAC_SECRET. Optional issuer / audience claim enforcement. # Empty JWT_VALIDATE_PATHS = JWT gate disabled (default). JWT_VALIDATE_PATHS= # JWT_VALIDATE_PATHS=/api/v1/*,/admin/* JWT_HMAC_SECRET= JWT_REQUIRED_ISSUER= JWT_REQUIRED_AUDIENCE= JWT_LEEWAY_SECS=30 # ── 1.6.2 Tier C — response-side DLP + webhook event filtering ───────── # Outbound DLP — scan upstream response bodies for sensitive data # leaving the gateway. Off by default. Costs ~0.5 ms per response on a # typical 100 KiB JSON body. Bound large responses with DLP_MAX_BYTES. # Hits are logged as `dlp-` events; risk score is NOT bumped on # the requester (upstream leakage is the operator's problem, not the # client's). When WEBHOOK_URL is set, every DLP hit also fires a # `dlp_leak` webhook event. DLP_ENABLED=0 DLP_REDACT=0 DLP_MAX_BYTES=262144 DLP_GROUP_CC_ENABLED=1 DLP_GROUP_AWS_ENABLED=1 DLP_GROUP_JWT_ENABLED=1 DLP_GROUP_PRIVATE_KEY_ENABLED=1 DLP_GROUP_API_KEY_ENABLED=1 DLP_GROUP_PII_EMAIL_ENABLED=0 # off by default (noisy on most upstreams) DLP_GROUP_PII_SSN_ENABLED=1 # Webhook event filter — comma-separated reason whitelist (with fnmatch # globs). Empty = every event fires the webhook (legacy 1.5.0 behaviour). # Examples: # WEBHOOK_EVENT_FILTER=canary-echo,custom-rule-block # only these # WEBHOOK_EVENT_FILTER=dlp-*,body-rce,body-cmd # DLP family + ban-tier body groups WEBHOOK_EVENT_FILTER= # ── Event-store backend (single-DB mode) ─────────────────────────────── # The active backend is AUTO-DERIVED from POSTGRES_DSN: # POSTGRES_DSN set → Postgres-only. # POSTGRES_DSN unset → SQLite-only (zero-deps single file). # The legacy DB_BACKEND env is IGNORED whenever POSTGRES_DSN is set, so the # bundled docker-compose (which always sets POSTGRES_DSN) runs Postgres-only. # Leaving DB_BACKEND here would be a no-op; it is kept only for SQLite-only # deployments that do NOT set POSTGRES_DSN. SQLite is otherwise just an # in-process emergency fallback if Postgres becomes unreachable, and the # gateway auto-recovers back to Postgres once it is healthy (no restart). # DB_BACKEND=sqlite # only honored when POSTGRES_DSN is unset # TimescaleDB sidecar credentials (used by docker-compose). The Timescale # container is always-running and the gateway connects to it as POSTGRES_DSN # — which makes this a Postgres-only deployment. POSTGRES_USER=appsec POSTGRES_PASSWORD=changeme-pls-rotate POSTGRES_DB=appsec # POSTGRES_DSN is auto-built in docker-compose from POSTGRES_USER / # POSTGRES_PASSWORD / POSTGRES_DB and pointed at the appsec-timescaledb # service. Set it explicitly only if running the gateway WITHOUT compose # (e.g. against an external/managed Postgres) — its presence is what selects # Postgres-only mode: # POSTGRES_DSN=postgresql://appsec:secret@timescaledb:5432/appsec # ── Cookie gate ──────────────────────────────────────────────────────── # 1 forces every non-static path through the chal-cookie gate. Subresources # (SPA assets, S2S APIs) bypass via JS_CHAL_OPEN_PATHS below. JS_CHALLENGE=1 JS_CHAL_OPEN_PATHS=/content/,/static/,/assets/,/api/ # ── Risk-score sliders (also live-tunable at /__controls) ────────────── SOFT_CHALLENGE_SCORE=4 RISK_BAN_THRESHOLD=50 TURNSTILE_RISK_THRESHOLD=0 # ── Logging ──────────────────────────────────────────────────────────── LOG_LEVEL=info LOG_FORMAT=json