Restrict production wp-admin/wp-login to an IP allowlist (opt-in)
New WP_ADMIN_ALLOWED_IPS env var (space-separated CIDR ranges, matching Caddy's remote_ip syntax directly). Defaults to 0.0.0.0/0 ::/0 (today's open behavior) when unset, resolved at the Compose level rather than in the Caddyfile so an unset var can never leave `remote_ip` with zero arguments and fail Caddy's startup. admin-ajax.php stays exempt since WooCommerce routes logged-out storefront AJAX through it. Production only — staging is already LAN-only. Verified live: a scratch Caddy instance on a Docker network, with a sibling container's actual observed source IP used to prove both the blocked (403 on wp-admin/wp-login, everything else unaffected) and allowed (matching CIDR lets it through) cases, plus `docker compose config` to confirm the unset-var default resolves correctly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MqhgnCRjjVLFnZ3Jpn93Vg
This commit is contained in:
1 parent
3143128798
commit
8b149a672d
4 files changed
+39
No files matched your search
@@ -61,3 +61,10 @@ MAILERLITE_API_KEY=
|
|||||||
# --- Hardcover (AO3-style tag browsing — genre/mood/content-warning/tag sync) ---
|
# --- Hardcover (AO3-style tag browsing — genre/mood/content-warning/tag sync) ---
|
||||||
# Get a token: hardcover.app account settings -> Hardcover API -> New API Key
|
# Get a token: hardcover.app account settings -> Hardcover API -> New API Key
|
||||||
HARDCOVER_API_TOKEN=
|
HARDCOVER_API_TOKEN=
|
||||||
|
|
||||||
|
# --- Admin access restriction (production only) ---
|
||||||
|
# Space-separated IPs/CIDR ranges allowed to reach /wp-admin and
|
||||||
|
# /wp-login.php (matches Caddy's `remote_ip` syntax directly — no quotes,
|
||||||
|
# space-separated even though most other values here are one token).
|
||||||
|
# Leave blank to leave wp-admin reachable from anywhere (today's behavior).
|
||||||
|
WP_ADMIN_ALLOWED_IPS=
|
||||||
@@ -123,6 +123,19 @@ is blocked on it. Go-live (switching the gateway from sandbox to Helcim's
|
|||||||
live environment) is a deliberate manual step on the settings screen, not
|
live environment) is a deliberate manual step on the settings screen, not
|
||||||
something this repo automates.
|
something this repo automates.
|
||||||
|
|
||||||
|
### Admin access restriction (production)
|
||||||
|
|
||||||
|
`/wp-admin` and `/wp-login.php` can be restricted to specific IPs/CIDR
|
||||||
|
ranges in production via `WP_ADMIN_ALLOWED_IPS` in `.env.production` —
|
||||||
|
space-separated, matching Caddy's own `remote_ip` syntax directly (e.g.
|
||||||
|
`203.0.113.5 198.51.100.0/24`). Leave it blank and wp-admin stays
|
||||||
|
reachable from anywhere, same as today — this is opt-in, not on by
|
||||||
|
default, and only applies in production (staging is already LAN-only).
|
||||||
|
`/wp-admin/admin-ajax.php` is always exempt regardless of the IP list,
|
||||||
|
since WordPress/WooCommerce route some logged-out storefront AJAX
|
||||||
|
through it. Applying a changed value just needs `make deploy ENV=production`
|
||||||
|
(or restarting the `caddy` container) — no code change required.
|
||||||
|
|
||||||
## Deploy pipeline
|
## Deploy pipeline
|
||||||
|
|
||||||
The Git server (Gitea) and the staging/production Docker hosts are separate
|
The Git server (Gitea) and the staging/production Docker hosts are separate
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ services:
|
|||||||
- "443:443"
|
- "443:443"
|
||||||
environment:
|
environment:
|
||||||
SITE_DOMAIN: ${SITE_DOMAIN}
|
SITE_DOMAIN: ${SITE_DOMAIN}
|
||||||
|
# Default keeps wp-admin open to everyone (today's behavior) when
|
||||||
|
# unset — resolved here, not in the Caddyfile, since Compose would
|
||||||
|
# otherwise pass an *empty* string rather than an unset var, and
|
||||||
|
# Caddy's own {$VAR:default} placeholder only applies to a var
|
||||||
|
# that's truly absent, not an empty one.
|
||||||
|
WP_ADMIN_ALLOWED_IPS: ${WP_ADMIN_ALLOWED_IPS:-0.0.0.0/0 ::/0}
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/caddy/Caddyfile.production:/etc/caddy/Caddyfile:ro
|
- ./docker/caddy/Caddyfile.production:/etc/caddy/Caddyfile:ro
|
||||||
- wp_core:/var/www/html:ro
|
- wp_core:/var/www/html:ro
|
||||||
|
|||||||
@@ -18,6 +18,19 @@
|
|||||||
@dev_only path /wp-content/plugins/bookstore-core/tests/* /wp-content/plugins/bookstore-core/vendor/* /wp-content/plugins/bookstore-core/composer.json /wp-content/plugins/bookstore-core/composer.lock /wp-content/plugins/bookstore-core/phpunit.xml*
|
@dev_only path /wp-content/plugins/bookstore-core/tests/* /wp-content/plugins/bookstore-core/vendor/* /wp-content/plugins/bookstore-core/composer.json /wp-content/plugins/bookstore-core/composer.lock /wp-content/plugins/bookstore-core/phpunit.xml*
|
||||||
respond @dev_only 404
|
respond @dev_only 404
|
||||||
|
|
||||||
|
# WP_ADMIN_ALLOWED_IPS (.env.production) — defaults to 0.0.0.0/0 ::/0
|
||||||
|
# (unrestricted) in docker-compose.production.yml when unset. Excludes
|
||||||
|
# admin-ajax.php: WordPress/WooCommerce route logged-out-visitor AJAX
|
||||||
|
# (cart fragments, nonces, some add-to-cart flows) through that file
|
||||||
|
# even though it lives under /wp-admin/ — blocking it by IP would break
|
||||||
|
# the storefront for every visitor, not just admin access.
|
||||||
|
@blocked_admin {
|
||||||
|
path /wp-admin* /wp-login.php
|
||||||
|
not path /wp-admin/admin-ajax.php
|
||||||
|
not remote_ip {$WP_ADMIN_ALLOWED_IPS}
|
||||||
|
}
|
||||||
|
respond @blocked_admin 403
|
||||||
|
|
||||||
php_fastcgi wordpress:9000
|
php_fastcgi wordpress:9000
|
||||||
file_server
|
file_server
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user