Skip to content

Powered by Grav

Edge policy

Edge policy

BOA applies a set of edge policies at the Nginx layer before a request reaches PHP-FPM. This page covers the shared machinery: real-client-IP recovery behind Cloudflare, the universal secret/config-path deny, and the shared reload lock the config generators take. These are rendered into the master http{} config by server.tpl.php and enforced per request by the guard chain in Inc/vhost_include.tpl.php.

Two sibling subsystems build on this machinery and have their own pages: the per-class AI bot policy (classes, per-vendor rate limits, the per-site policy.txt flips) is AI crawler policy, and the abuse/IDS layer (the $is_banned geo, the asset/content-chain flood maps, the print/no-referer gate, the .php catch-all, the bot and search-amplification blocks, the $is_cms_probe foreign-CMS admin-probe map (server.tpl.php:493-498), the boa_i18n_* anonymous-localised concurrency maps, and the scan_nginx log scorer) is documented in Abuse guard.

Universal secret / config-path deny

map $uri $is_secret_path (server.tpl.php:448-456) hard-blocks (444) probes for credential and config paths that never exist on a hosted Drupal/Ægir site, regardless of UA. The guard fires at vhost_include.tpl.php:236, before the AI guards.

It matches on the decoded, normalised $uri (not the raw request line) and is anchored to path segments, so a literal .env cannot match an arbitrary substring, and $uri normalisation defends against percent-encoded evasion (e.g. /%2eenv/.env). The roster:

Pattern Covers
.env .git .aws .ssh dotfile / VCS / cloud-credential dirs
secrets.json key.json credentials.json config.json google-services.json loadable-stats.json credential / config JSON
application.ya?ml Spring/Java app config
settings.py Django settings
__/firebase/init.json Firebase init
.next/required-server-files.json Next.js server manifest

config.json and key.json are listed because Drupal/Ægir never serve them at web root, so a request for one is always a probe. A hosted decoupled/headless front-end that legitimately serves such a file would be caught (444); if that ever bites, remove just that token from $is_secret_path in server.tpl.php. This deny spans the edge-policy and abuse-guard subsystems; the abuse framing is in Abuse guard.

Real client IP (Cloudflare realip)

Every guard at this layer keys on $remote_addr. Behind Cloudflare the raw TCP peer is a CF edge, not the visitor — so the master http{} config recovers the real client (server.tpl.php:134-136):

NGINX
real_ip_header    CF-Connecting-IP;
real_ip_recursive on;
include /data/conf/nginx_cloudflare_real_ip.c*;   # set_real_ip_from <CF ranges>

The include is a wildcard glob (.c*) so nginx -t passes before the ranges file exists. With no trusted ranges declared, the CF-Connecting-IP header is ignored and $remote_addr is left unchanged — there is no spoofing risk, and direct (non-CF) traffic never triggers realip.

The trusted ranges are maintained by aegir/tools/system/cloudflare_realip.sh, which fetches Cloudflare's published IPv4 + IPv6 ranges into /data/conf/nginx_cloudflare_real_ip.conf. It is deployed to /var/xdrago/ and runs daily via crontab (04:45) plus once at install directly from BOA.sh.txt — so a fresh or updated box does not wait for the daily cron. It is idempotent: it only rewrites the include and reloads Nginx when the fetched ranges actually change, and validates every CIDR (octet range, prefix length) before writing so a malformed upstream response can never break the host's configtest.

Effect: with realip active, $remote_addr becomes the real visitor and $realip_remote_addr the CF edge — so rate-limit keys, bans and access logs all bite the real client even for CF-proxied sites. Realip-window caveat: until cloudflare_realip.sh populates the ranges file, $remote_addr is unchanged (the CF edge), so enforcement keys on the edge during that window.

PHP REMOTE_ADDR pin

fastcgi_param REMOTE_ADDR is set to $realip_remote_addr — the original TCP peer (the CF edge) — not $remote_addr (server.tpl.php:159). This is deliberate: it keeps the BOA global.inc chain's own reverse-proxy real-client resolution correct (aegir/conf/global/global-main.inc:36-91 — PHP still sees the edge as the proxy and resolves the client itself), while Nginx-level enforcement keys on the realip-rewritten $remote_addr. For PHP this is not a behaviour change: REMOTE_ADDR carries the same raw-peer value it did before the realip module arrived — only the Nginx-side view changed.

A related tool, migration_proxy_realip.sh, extends the same wildcard include with a sibling .cmig member to trust an xmass/xoct migration proxy (the old host forwarding CF-Connecting-IP) during a site migration; see Migration & cloning.

Shared Nginx reload lock

Five BOA generators write into the host Nginx config and run configtest + reload independently:

Generator Cadence Delivery
ai_policy.sh */2 serial-gated _fetch_versioned
ip_access.sh */2 serial-gated _fetch_versioned
nginx_deny.sh */2 serial-gated _fetch_versioned
cloudflare_realip.sh daily 04:45 + at install serial-gated _fetch_versioned
migration_proxy_realip.sh */5 serial-gated _fetch_versioned

To stop their configtest+reload cycles colliding on the same host Nginx, the five generators above — together with user_admin_access.sh and nginx_deny6.sh, seven cron generators in all — wrap their whole run in a shared advisory lock /run/boa_nginx_config.lock (flock -w 30 on fd 9); if the lock is not acquired within 30s the run is skipped and retried on the next tick (ai_policy.sh:26,33-37). Two more paths serialise on the same lock without the whole-run wrap: the certificate mirror migration_proxy_certs.sh takes it only around its conditional reload after certificates actually changed (fd 8, migration_proxy_certs.sh:234-250), and the installer locks its own reload the same way (BOA.sh.txt:3661-3665). Each generator follows the same discipline: change-gate → atomic write → configtest → reload, with rollback to the last-good config on failure.

Source: all five scripts under aegir/tools/system/; fetch + at-install realip run at BOA.sh.txt:1651-1665; cron cadence in aegir/tools/system/cron/crontabs/root:8-14.

  • AI crawler policy — the per-class AI bot maps, per-vendor rate limits, guard chain and the per-site policy.txt control file, all keyed on the real client IP recovered here.
  • Abuse guard — the IDS/abuse layer: $is_banned geo, flood chains, scan_nginx scorer, the CSF ban pipeline. This page is the edge machinery; that section is the abuse detection/enforcement.
  • Config templates — the master Nginx config these directives render into, and the never-remove-map-vars rule.
  • Custom rewrites & location blocks — operator includes that layer over (and can short-circuit) the guard chain.
  • Control files & INI — the per-site control files, including policy.txt.
  • Migration & cloning — the migration-proxy realip extension.

© 2026 BOA Documentation. All rights reserved.