Restricting a whole site to an IP allow-list (ip_access)
The /admin* URL protection is path-scoped. For a
stronger control — locking an entire site (an intranet, a staging site, an
admin-only area) to a set of IP addresses — BOA ships ip_access, a single
global generator that emits per-site Nginx allow/deny fragments. Everything not
on the list gets a 403 (Nginx deny all;).
This is the operator-side reference for the feature hosted customers drive from
their own account as the site IP lock.
For the path-scoped variant limited to /user + /admin, see
user_admin_access.
The generator and its contexts
It is a single global generator deployed at /var/xdrago/ip_access.sh,
replacing the older per-Octopus nginx_ip_access_<oct>.sh copies. It runs one
shared routine over two kinds of context:
- the master (sqladmin proxy):
/var/aegir/control/ip/access.txt→/var/aegir/config/includes/ip_access/. On each run the generator auto-seeds this control file with the recordsqladmin.com 192.168.1.1when it is absent, producing asqladmin.conffragment. That fragment is what actually guards the SQL-admin proxies — it is pulled in by the Adminer/phpMyAdmin-style vhosts (nginx_sql_adminer.conf,nginx_sql_buddy.conf,nginx_sql_cgp.conf,nginx_sql_chive.conf) and byssl_proxy.confviainclude .../ip_access/sqladmin*;. The seed IP192.168.1.1is a private placeholder, so on a real box only the anti-lockout IPs (loopback, the server's own IP, established SSH clients) can reach the SQL admin tools until an operator adds real public IPs to that control file; - every Octopus instance:
/data/disk/<oct>/static/control/ip/access.txt→/data/disk/<oct>/config/includes/ip_access/. Only real instances are processed (identified by the BOA-canonicaltools/drushmarker, so pseudo-dirs likearch,all,legacy,global,static,customare skipped).
The per-site vhost pulls the fragment via
include $server->include_path/ip_access/<uri>.conf*, so the restriction applies
to that site only (the .conf* suffix anchors the glob to this site's own
fragment; a bare <uri>* would also match a longer site whose name extends it,
e.g. example.com vs example.com.au).
A site with no record has no fragment and is unrestricted. (The sqladmin* master
include above is deliberately a broader prefix glob — it matches the seeded
sqladmin.com.conf.)
Control file format
One site per line — the site name followed by space-separated allowed addresses, each an IPv4 or IPv6 address with an optional CIDR prefix:
# /data/disk/o1/static/control/ip/access.txt
intranet.example.com 203.0.113.10 203.0.113.0/24 2001:db8::/32
staging.example.com 198.51.100.42 2001:db8:1::1
#comments and blank lines are ignored.- Each address is validated against a strict subset of what the Nginx access module accepts; a malformed address or an invalid site name is skipped with a logged warning while the rest of the line still applies — one bad token never breaks the box-wide Nginx configtest.
Anti-lockout
Every generated fragment always allows, in addition to the listed IPs, so a typo can never strand the site or the operator:
127.0.0.1and::1— loopback;- the server's own IPv4, read from
/root/.found_correct_ipv4.cnf(BOA tracks no server IPv6); - every established inbound SSH client IP (IPv4 or IPv6), harvested from
netstat -tn(peers on anESTABLISHED:22connection — the peer address is taken by stripping the trailing:port, and each is validated before it reaches anallowline). BOA usesnetstathere, notwho --ips, becausewho --ipsis unavailable on Excalibur and newer.
An admin working over SSH is therefore added to every site's allow-list automatically. The SSH-client set is part of the change-gate, so a newly logged-in admin triggers a regenerate on the next pass.
Generator behaviour
- Change-gate — a context regenerates only when its control file's mtime advanced or the host's SSH-client set changed. No change → no write, no reload.
- Prune-on-removal — deleting a site's line removes its fragment on the next run, lifting the restriction (the site is open again).
- Per-context safety — back up current fragments, regenerate atomically, run
service nginx configtest, thenreload; on a failed configtest or reload, restore the last-good backup and reload (or drop the just-written fragments if there is no last-good yet). - Serialisation — the whole run holds the shared
/run/boa_nginx_config.lock(flock -w 30, then skip and retry next tick) so it never overlaps the other Nginx-config writers (ai_policy,nginx_deny,cloudflare_realip). - Schedule —
*/2cron, so changes take effect within about two minutes.
IPv4, IPv6 and CIDR
ip_access is a pure Nginx allow/deny layer — it keys on the recovered
client IP at the web tier and does not touch CSF — so it accepts both address
families and subnets: each entry may be an IPv4 or IPv6 address, singly or as a
CIDR range (203.0.113.0/24, 2001:db8::/32, …).
The validator is a strict subset of what the Nginx access module accepts, so a validated entry can never break the box-wide configtest.
This is independent of CSF: adding an IPv6 rule here restricts the site at Nginx but does not add a host-firewall rule (CSF remains a separate layer, and BOA tracks only the server's own IPv4 for anti-lockout).
The restriction is whole-site; for a path-scoped variant limited to /user +
/admin, see user_admin_access.
Behind Cloudflare (realip)
The allow/deny rules key on $remote_addr. With Cloudflare realip active,
$remote_addr is the real visitor IP, so enter the visitor's real public IP
(what e.g. https://ifconfig.co shows) — which is what an operator naturally
does.
In the brief pre-cron window before realip activates, a CF-proxied site would see the CF edge instead; install-time realip activation closes that window on a normal box.
Verify
# inspect a site's generated allow/deny fragment
cat /data/disk/o1/config/includes/ip_access/intranet.example.com.conf
# from a non-allowed IP -> 403; from a listed IP (or over the
# server / SSH) -> 200
curl -sS -o /dev/null -w '%{http_code}\n' https://intranet.example.com/
service nginx configtest
The script is aegir/tools/system/ip_access.sh, deployed as
/var/xdrago/ip_access.sh.
Related
/admin*URL protection — the path-scoped default admin guard and its INI opt-out.- Login/admin IP allow-list (
user_admin_access) — the sibling generator that locks only the/user+/adminsurface. - Site IP lock — the
hosted-customer view of this same
access.txtcontrol file. - Nginx internals — the vhost generator that pulls
the per-site
ip_accessfragments. - Control files & INI —
static/control/ip/access.txtcatalogued among the per-site control files.