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):
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.
Related
- AI crawler policy — the per-class AI bot maps,
per-vendor rate limits, guard chain and the per-site
policy.txtcontrol file, all keyed on the real client IP recovered here. - Abuse guard — the IDS/abuse layer:
$is_bannedgeo, flood chains,scan_nginxscorer, 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.