Origin Guard & Trusted Proxy#
For apps behind Cloudflare or a reverse proxy, Relayer ships OriginGuard, which rejects requests that bypass the CDN, and TrustedProxy, which recovers the real client IP.
Enable with environment variables#
Relayer::boot() reads these variables and sets the result as the router's guard, which runs before /_defer/{name}, the dev profiler and middleware.php. With none set, nothing is added.
ORIGIN_SECRET=<long random value> # enables OriginGuard
ORIGIN_SECRET_PREVIOUS=<old value> # also accepted during rotation
ORIGIN_GUARD_HEADER=X-Origin-Secret # default
ORIGIN_GUARD_EXCEPT=/healthz,/_health/* # exact match, or `/prefix/*`
ORIGIN_GUARD_MODE=enforce # or report
TRUSTED_PROXIES=fdaa::/16 # enables TrustedProxy (comma-separated CIDRs)
TRUSTED_PROXY_PRESET=fly # fly | cloudflare | xff (default)
CLOUDFLARE_IP_RANGES=173.245.48.0/20,… # adds a Cloudflare layer behind the first oneDo not put origin protection in middleware.php. It wraps route dispatch only, so hitting /_defer/{name} directly would bypass it. To compose your own, AppRouter::setGuard() takes the same fn(Request, $next) and replaces the one built from the environment.
<?php
// public/index.php
use Polidog\Relayer\Http\OriginGuard;
use Polidog\Relayer\Relayer;
use Psr\Log\LoggerInterface;
Relayer::boot(__DIR__ . '/..')
->setGuard(OriginGuard::middleware([
'header' => 'X-Origin-Secret', // default
'secretsFromEnv' => ['ORIGIN_SECRET', 'ORIGIN_SECRET_PREVIOUS'],
'except' => ['/healthz'], // exact match, or `/prefix/*`
'mode' => 'enforce', // or 'report'
'logger' => Relayer::container()->get(LoggerInterface::class), // optional
]))
->run();OriginGuard#
Verifies the secret header the CDN adds when forwarding, and answers 403 when it is missing or wrong.
- Off by default. With no non-empty secret it passes everything, so local runs and CI do not fill with 403s.
secretsFromEnvlists environment variable names. Values are trimmed, so a trailing newline cannot make every request 403. You can also pass values directly assecrets.- Old and new secrets both pass during rotation, compared with
hash_equals. exceptexcludes paths such as platform health checks.mode: 'report'logs instead of rejecting, for a trial run beforeenforce.- Logs hold only the method, path and client IP, never the secret or what the client sent.
- Pass secrets through
fly secrets setor a gitignored.env.local.
TrustedProxy#
Resolves the client IP from a forwarding header, but only when the last hop (REMOTE_ADDR) is a trusted proxy. Read the result with $request->clientIp(). $request->ip() stays REMOTE_ADDR, and clientIp() falls back to ip() when nothing resolves.
use Polidog\Relayer\Http\TrustedProxy;
// Cloudflare in front: CF-Connecting-IP. Ranges are not bundled;
// fetch https://www.cloudflare.com/ips-v4 and /ips-v6 and pass them
TrustedProxy::middleware(['preset' => 'cloudflare', 'proxies' => $cloudflareRanges]);
// Fly.io: Fly-Client-IP, trusted only from Fly's private network
TrustedProxy::middleware(['preset' => 'fly', 'proxies' => ['fdaa::/16']]);
// Your own proxy: walks X-Forwarded-For from the right, skipping your proxies
TrustedProxy::middleware(['preset' => 'xff', 'proxies' => ['10.0.0.0/8']]);
// Any single-IP header (cannot be combined with `preset`)
TrustedProxy::middleware(['header' => 'X-Real-IP', 'proxies' => ['192.0.2.1']]);proxiesis required: single addresses or CIDRs (IPv4 / IPv6). An invalid range never matches.- A header from an untrusted hop or an invalid IP value is ignored.
- Hop verification can only be skipped with an explicit
'trustAnyHop' => true, and only when that proxy is provably the sole path to the process.
Multiple proxies (chain)#
With Cloudflare → Fly, Fly-Client-IP is Cloudflare's connecting IP and the user is in CF-Connecting-IP. List layers outermost last; each applies only when the address resolved so far is one of that layer's proxies.
TrustedProxy::middleware(['chain' => [
['preset' => 'fly', 'proxies' => ['fdaa::/16']],
['preset' => 'cloudflare', 'proxies' => $cloudflareRanges],
]]);Recipe: Cloudflare + Fly.io#
- In Cloudflare → Rules → Transform Rules → Modify Request Header, set
X-Origin-Secretto a long random value for the zone. - Give the app the secret and the trust chain, with no PHP.
fly secrets set \
ORIGIN_SECRET=<that value> \
ORIGIN_GUARD_EXCEPT=/healthz \
TRUSTED_PROXIES=fdaa::/16 \
TRUSTED_PROXY_PRESET=fly \
CLOUDFLARE_IP_RANGES="$(curl -s https://www.cloudflare.com/ips-v4 https://www.cloudflare.com/ips-v6 | paste -sd,)"- Check:
curl -I https://<app>.fly.dev/is403, your custom domain is200, and/healthzis200from both.
To rotate, set the new ORIGIN_SECRET plus ORIGIN_SECRET_PREVIOUS=<old value>, and remove the latter later. For CDN caching, see CDN Cache.
Revisions (1)
- v0.31.0: 新規ページ