Files
ipx/web/Caddyfile
rays 3e58447f12 ipx-web: pass the tunnel's Access headers when it arrives as ::ffff: (#209)
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>
2026-10-12 14:17:22 +00:00

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
}
}