Fix tailnet access to LAN-gated apps, and Caddy access logging - #59
Merged
Merged
Conversation
mathewcsims
force-pushed
the
fix-tailnet-subnet-access-and-caddy-logging
branch
from
August 7, 2026 09:41
5e35396 to
2d0bbb4
Compare
Two faults found while debugging docs.mathewcsims.uk being unreachable
from an Android client on mobile data. Neither was in this repo's config,
and both had been latent for a while.
The tailnet fault: a subnet route is a separate ACL destination from the
tailnet IPs of the devices behind it. Babel advertised 10.0.1.0/24, the
route was approved, and the client had "use subnet routes" on — all three
necessary, none sufficient, because no grant named the CIDR. Traffic was
dropped by policy at the receiving node, which is why every server-side
check came back clean: Caddy, DNS records, the NextDNS rewrite, the
tailnet's own resolver and HedgeDoc were all correct throughout.
The symptoms actively misdirect. With an exit node the connection times
out, because Tailscale keeps RFC1918 destinations off the exit path, so
the packet leaves via the local interface and dies. Without one it is
refused, because the browser falls back to the public A record and hits
handle { abort } from the internet. Neither leaves a trace on the Pi — a
full packet capture of the client showed no connection attempt toward the
LAN address at all.
Adds scripts/tailscale-acl.sh to manage the policy file, in the same shape
as dns-nextdns.sh: the API key is used only inside a Python process via
urllib so it never appears in argv, /acl/validate runs before anything is
applied, and If-Match makes a concurrent console edit a 412 rather than a
silent clobber.
The logging fault: Caddy emits access logs only for site blocks carrying
the log directive. The global log block configures the default logger's
sink, not access logging, so no request has ever been logged here. That
also means the caddy-abuse fail2ban jail has been watching a file it was
never fed — its lifetime counter stood at exactly 3, matching the one
synthetic and two hand-fed lines used to verify it nine days ago, and
nothing since. Fixed with an (access_log) snippet imported by all 25 site
blocks; re-verified that a probe with a unique path now lands in the log
and that fail2ban-regex matches the emitted format.
SETUP.md's "Accessing LAN-only apps over Tailscale" section said two
things were required and claimed the arrangement was confirmed working
end to end. It now documents three, and records that no LAN-gated app had
ever actually been reachable from a genuinely off-LAN tailnet device —
testing from a device on the physical LAN passes regardless, which is how
it went unnoticed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mathewcsims
force-pushed
the
fix-tailnet-subnet-access-and-caddy-logging
branch
from
August 7, 2026 11:30
2d0bbb4 to
dd6eebe
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while debugging
docs.mathewcsims.ukbeing unreachable from an Android client on mobile data. Two separate latent faults, neither in this repo's configuration.Tailnet access
A subnet route is a separate ACL destination from the tailnet IPs of the devices behind it. Babel advertised
10.0.1.0/24, the route was approved in the console, and the client had "use subnet routes" enabled — all three necessary, none sufficient, because no grant ever named the CIDR. Traffic was dropped by policy at the receiving node.That is why every server-side check came back clean. Caddy's
@lanmatcher, the DigitalOcean and NextDNS records, the tailnet's own resolver and HedgeDoc itself were all verified correct and none of them was the problem.The symptoms actively misdirect:
ERR_CONNECTION_REFUSEDhandle { abort }Neither leaves a trace on the Pi — a full packet capture of the client's traffic showed no connection attempt toward the LAN address at all.
The grant itself is applied to the live tailnet already (verified present in babel's enforced packet filter, where the
10.xlist was previously empty). This PR addsscripts/tailscale-acl.shso the policy is managed the same way as everything else here: key used only inside a Python process via urllib so it never reaches argv,/acl/validatebefore applying, andIf-Matchso a concurrent console edit is a 412 rather than a silent clobber.Caddy access logging
Caddy emits access logs only for site blocks carrying the
logdirective. The globallogblock configures the default logger's sink, which is not the same thing — so no request has ever been logged.The consequence is that the
caddy-abusefail2ban jail has been watching a file it was never fed. Its lifetime counter stood at exactly 3: the one synthetic and two hand-fed lines used to verify it nine days ago, and nothing since.Fixed with an
(access_log)snippet imported by all 25 site blocks. Verified after reload: a probe with a unique path lands inlogs/access.log, andfail2ban-regexmatches the emitted format (1 matched, 0 missed).Docs
SETUP.md's "Accessing LAN-only apps over Tailscale" said two things were required and claimed the arrangement was "confirmed working end-to-end". It now documents three, and records plainly that no LAN-gated app had ever been reachable from a genuinely off-LAN tailnet device. Testing from a device sitting on the physical LAN passes regardless, which is how it went unnoticed.Verification
tailscale debug netmappacket filter (wasNONE)docs.mathewcsims.ukloads over the tailnetcaddy validate; applied withcaddy reload, no downtimefail2ban-regexconfirmed against the real emitted formattcpdump, installed on the Pi for the diagnosis, has been removed again🤖 Generated with Claude Code