Restricting /user + /admin to an IP allow-list (user_admin_access)
The whole-site ip_access generator locks a whole site.
Often you instead want the public site to stay open to everyone while only the
login and admin surface — Drupal's /user and /admin URIs — is reachable
from a trusted set of addresses. user_admin_access does exactly that: a per-site
Nginx allow-list scoped to those paths, returning 403 to anyone else who
requests them and leaving the rest of the site public.
This is the operator-side reference for the feature hosted customers drive from their own account as the login/admin IP lock.
Like whole-site ip_access, it is a pure Nginx allow/deny layer with no CSF
involvement, so it accepts IPv4 and IPv6, single addresses and CIDR subnets.
The two features are independent and compose — a site may use either, both, or
neither; the only difference is scope (whole-site vs the /user + /admin
surface).
The generator and its fragments
It is a single global generator deployed at /var/xdrago/user_admin_access.sh,
run over every real Octopus instance (the hostmaster front-end has its own
vhost and is out of scope); there is no master/sqladmin context. For each listed
site it writes two Nginx includes into the instance's config/includes/:
user_admin_access_map/<site>.conf(http scope) — ageoclassifying the client$remote_addras allowed, amapflagging a/user|/adminrequest on the clean$uri, and a finalmap$ua_deny_<hash>that is1only when an admin-area request arrives from a non-allowed address;user_admin_access/<site>.conf(server scope) — a singleif ($ua_deny_<hash>) { return 403; }.
BOA enforces Drupal clean URLs, so /user and /admin always arrive as the real
request path in $uri (which Nginx percent-decodes and normalises); the match is
keyed there.
The legacy ?q=admin query form is not a clean path — not served by
default on BOA — and is deliberately not matched, because Nginx's $arg_q cannot
be reliably gated (it is neither percent-decoded nor de-duplicated the way Drupal
reads $_GET['q']); this matches BOA's existing location ^~ /admin guard, which
likewise keys on the clean path.
The per-site vhost pulls the first once at the file head and the second inside
each serving server block (next to ip_access), both via a wildcard include
anchored on the fragment suffix (<uri>.conf*, not a bare <uri>* — otherwise a
longer site whose name extends this one, e.g. example.com vs example.com.au,
would have its fragment pulled in here too).
A site with no fragment is a no-op — the feature is strict opt-in. <hash> is a
short digest of the site name, so each site's variables are unique in the shared
http{}.
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/user_admin.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 Nginx
geoaccepts; 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 geo always allows, in addition to the listed addresses, so a
typo can never lock the operator out of the admin surface:
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— the same sourceip_accessuses.
Generator behaviour
- Change-gate — a context regenerates only when its control file's mtime advanced, the host's SSH-client set changed, or the emitted-directive version bumped. No change → no write, no reload.
- Prune-on-removal — deleting a site's line removes both its fragments on the next run, lifting the restriction (the admin surface becomes open again).
- Safety — per context: back up the current fragments, regenerate, run
service nginx configtest, thenreload; on failure, restore the last-good backup and reload. It holds the shared/run/boa_nginx_config.lockso it never overlaps the other Nginx-config writers (ip_access,ai_policy,nginx_deny,cloudflare_realip). - Schedule —
*/2cron, so changes take effect within about two minutes.
Defence in depth, not the sole control
This gate narrows who can reach the /user and /admin URL paths at the edge;
Drupal's own login and permission checks still apply on top of it.
The match is on the decoded, normalised $uri, so it is encoding- and
multi-slash-safe. Under the hood the map keys on $uri with
~*^/+(?:user|admin)(?:/|$), and the ^/+ anchor deliberately accepts one
or more leading slashes — so a //admin request is still caught even on the
rare vhost that has turned Nginx's default merge_slashes off, and the gate
never relies on Nginx collapsing the slashes for it.
The legacy ?q=admin query form is not gated here (it is not a clean path and not
served by default on BOA, and $arg_q cannot be reliably matched at the Nginx
layer) — Drupal's authentication remains the control for that vector, exactly as
with BOA's existing /admin guard.
Verify
# inspect a site's generated gate
cat /data/disk/o1/config/includes/user_admin_access_map/intranet.example.com.conf
cat /data/disk/o1/config/includes/user_admin_access/intranet.example.com.conf
# from a non-allowed IP: /admin + /user -> 403, everything else -> 200
curl -sS -o /dev/null -w '%{http_code}\n' https://intranet.example.com/admin
curl -sS -o /dev/null -w '%{http_code}\n' https://intranet.example.com/
service nginx configtest
The script is aegir/tools/system/user_admin_access.sh, deployed as
/var/xdrago/user_admin_access.sh; the per-site includes are emitted by the vhost
generator (provision-private/http/Provision/Config/Nginx/vhost.tpl.php and the
SSL template).
Related
- Whole-site IP allow-list (
ip_access) — the sibling generator that locks the entire site rather than just its admin surface. /admin*URL protection — the path-scoped default admin guard and its INI opt-out.- Login/admin IP lock —
the hosted-customer view of this same
user_admin.txtcontrol file. - Control files & INI —
static/control/ip/user_admin.txtcatalogued among the per-site control files.