Caddy listens dual-stack, so the ipx_default gateway connects as ::ffff:192.168.64.1, and remote_ip 192.168.64.1 does not match it: every Cf-Access-* header was stripped, the tunnel's too. The browser suite binds Caddy to 127.0.0.1, IPv4 only, so it never saw it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
2.8 KiB
Caddyfile
78 lines
2.8 KiB
Caddyfile
# ipx-web (#203): the frontend as static files, and everything else passed to ipx, on one origin,
|
|
# so the session cookie, live updates and Cloudflare Access work as they did when ipx served it.
|
|
# IPX_UPSTREAM where ipx is, host:port (ipx:8099)
|
|
# WEB_BIND where to listen (:8080)
|
|
# TUNNEL_PEER the one IPv4 address whose Cf-Access-* headers are passed on: where cloudflared's
|
|
# requests come from. Nobody else's are, so nobody else can claim to be signed in
|
|
# by Cloudflare. Unset passes none.
|
|
{
|
|
admin off
|
|
auto_https off
|
|
}
|
|
|
|
http://{$WEB_BIND::8080} {
|
|
root * {$WEB_ROOT:/srv}
|
|
|
|
# Before anything else, so no page or API call sees them: only ipx sets these. Both forms of
|
|
# the peer: Caddy listens dual-stack, so an IPv4 peer arrives as ::ffff:a.b.c.d, which the
|
|
# plain address does not match, and the tunnel's headers were stripped too (#209).
|
|
@forged not remote_ip {$TUNNEL_PEER:255.255.255.255} ::ffff:{$TUNNEL_PEER:255.255.255.255}
|
|
request_header @forged -Cf-Access-Authenticated-User-Email
|
|
request_header @forged -Cf-Access-Jwt-Assertion
|
|
|
|
@api path /api/* /media/*
|
|
handle @api {
|
|
# Live updates are a stream: flushed as they come, never buffered.
|
|
reverse_proxy {$IPX_UPSTREAM:ipx:8099} {
|
|
flush_interval -1
|
|
}
|
|
}
|
|
|
|
# Signing in with the shared token in the address (?token=): ipx sets the session cookie and
|
|
# sends the browser on to the page without it.
|
|
@tokened {
|
|
path / /admin
|
|
query token=*
|
|
}
|
|
handle @tokened {
|
|
rewrite * /api/signin?{query}
|
|
reverse_proxy {$IPX_UPSTREAM:ipx:8099}
|
|
}
|
|
|
|
# The app and the admin page go to whoever ipx says may have them: a browser not signed in is
|
|
# sent to /login, anyone but an admin asking for /admin to the app.
|
|
@page path / /index.html /admin /admin.html /admin.js
|
|
handle @page {
|
|
forward_auth {$IPX_UPSTREAM:ipx:8099} {
|
|
uri /api/page-check
|
|
}
|
|
rewrite / /index.html
|
|
rewrite /admin /admin.html
|
|
header Cache-Control no-cache
|
|
file_server
|
|
}
|
|
|
|
# The sign-in form, except for someone Cloudflare Access vouches for, who is sent to the app.
|
|
@login path /login /login.html
|
|
handle @login {
|
|
forward_auth {$IPX_UPSTREAM:ipx:8099} {
|
|
uri /api/login-check
|
|
}
|
|
rewrite * /login.html
|
|
header Cache-Control no-cache
|
|
file_server
|
|
}
|
|
|
|
# Scripts and the stylesheet are named by a hash of their contents (web/build.mjs), so a browser
|
|
# keeps them a year; the icons a day, under names that do not change.
|
|
@hashed path /app.js /admin.js /login.js /app.css /hls.js /boot.js
|
|
header @hashed Cache-Control "public, max-age=31536000, immutable"
|
|
# The same PNG as /favicon.png, for a browser that asks for /favicon.ico on its own.
|
|
header /favicon.ico Content-Type image/png
|
|
@icons path *.png *.svg *.ico *.woff2
|
|
header @icons Cache-Control "public, max-age=86400"
|
|
handle {
|
|
file_server
|
|
}
|
|
}
|