A reverse proxy gives you a place to apply request policy before traffic reaches WordPress. To use it well, you need to understand which router matches, which middleware runs, and whether callers can bypass the proxy.
We’ll learn those mechanics with a disposable HTTP backend before connecting them to a real application.
Follow one request
An entrypoint is a listening address. A router selects requests using rules such as hostname and path. Middleware can examine or modify the request before a service sends it to a backend.
For WordPress, a public route and an administrative route may share a backend while applying different policies. The diagram shows the common path. Policy belongs on the route the request actually selects.
Build the smallest observable backend
In an empty working directory:
mkdir -p demo-site/wp-admin
printf 'public-demo\n' > demo-site/index.html
printf 'admin-demo\n' > demo-site/wp-admin/index.html
python3 -m http.server 8081 --bind 127.0.0.1 --directory demo-site
This is a static fixture, not WordPress. It makes routing visible without adding a database or login flow.
In the same working directory, create traefik.yml:
entryPoints:
web:
address: "127.0.0.1:8080"
providers:
file:
filename: dynamic.yml
accessLog: {}
Create dynamic.yml:
http:
routers:
public:
entryPoints: [web]
rule: "Host(`wp.example.test`)"
priority: 10
service: demo
admin:
entryPoints: [web]
rule: "Host(`wp.example.test`) && (Path(`/wp-admin`) || PathPrefix(`/wp-admin/`) || Path(`/wp-login.php`))"
priority: 100
middlewares: [local-admin]
service: demo
middlewares:
local-admin:
ipAllowList:
sourceRange: ["127.0.0.1/32"]
services:
demo:
loadBalancer:
servers:
- url: "http://127.0.0.1:8081"
Start traefik --configFile=traefik.yml from that directory. Both listeners are loopback-only, and this exercise does not expose a dashboard or mount a container runtime socket.
Explicit priorities make the overlapping route decision visible. Review the Traefik routing rules when adapting the example.
Predict, request, inspect
curl --noproxy '*' -H 'Host: wp.example.test' http://127.0.0.1:8080/
curl --noproxy '*' -H 'Host: wp.example.test' http://127.0.0.1:8080/wp-admin/
Expect public-demo and admin-demo. The second request matches both routers, but the higher-priority admin router wins. Its IP allowlist permits the local client.
Now change the admin allowlist to ["192.0.2.0/24"], a documentation range that excludes this loopback client. Restart the lab proxy so the change is unambiguous. Repeat both requests. The public request should work; the admin request should return 403. Check the backend log: the rejected request should not arrive there.
Pause and predict
The admin router has middleware, but its priority is lower than the public router's. Does its existence protect matching requests?
Need a hint?
Middleware runs on the router that wins selection.
Show the reasoning
Transfer the model to WordPress
Real deployment adds TLS, application authentication, persistent state, and a backend network that callers cannot reach around the proxy. An IP allowlist is not a user identity check. WordPress still needs its own authorization and updates.
Test the full workflow. Some themes and plugins use /wp-admin/admin-ajax.php for public features. A blanket restriction on that path can break legitimate behavior. An exception needs its own reviewed route and test, rather than weakening the entire admin policy.
Rate limits are another separate control. A token-bucket limit can constrain request bursts, but it doesn’t provide general denial-of-service protection. Choose its grouping and thresholds from workload behavior; see the RateLimit reference.
Try a bypass check
Apply the idea
The proxy blocks an administrative request, but the same caller can reach the backend directly. Is the boundary complete?
Need a hint?
Draw every route from the caller to the application.
Show the reasoning
Stop both foreground processes and remove the demo directory when finished. Next, learn which address a proxy policy actually evaluates.