Reach the tailnet-shared LLM gateway #86
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner#86
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
already encrypted and the gateway speaks plain HTTP, so an HTTP-level proxy would
only add a place to get paths and headers wrong. The pod's own
mitmdumpreverseproxy keeps its job; only its target hostname changes.
0.0.0.0. That is whyAfter=/Requires=tailscaled.serviceis load-bearing, and whyRestart=alwaysmatters: a bind that races
tailscaledat boot retries until it succeeds.an edit.
nainawith no capability and no root after installation. The relay isunauthenticated and the tailnet ACL is allow-all, so the gateway's API key stays
the only authentication on this path — a dumb forwarder is the right shape.
Half 2 — the port opening:
--add-interface=tailscale0 --zone=trusted, which opensevery port on the host to the tailnet.
--permanentsurvives reboots and reloads; a host upgrade that resets zoneassignments would not, so the drift check below belongs in the runbook.
4000on the host is free (measured: no listener inss -lnt).What Terraform can and cannot own
Recommendation: no Terraform root for this relay. The evaluation, in short:
dmacvicar/libvirtmanages domains, volumes, networks and pools — no host-processsurface. There is no
null_resource,remote-exec,hashicorp/localor SSH useanywhere in this tree.
dlarochette/systemd(0.10.x, MIT, single maintainer),whose own README assumes root SSH and writes system units only. There is no
firewalld provider, so half this change — the half that actually makes it work —
stays a hand-run command regardless.
naina-serverdoes not offer today:sudo -nfailsfor
naina(password required) and Fedora's root login is key-only with no root keyauthorized here. That means a root key on a tailnet device, or a passwordless-sudoers
or polkit rule — a new privileged credential path, in a repo whose rules keep
credentials out of state.
planthatnotices drift, for the price of a young third-party provider on the inference
critical path.
AGENTS.md's rule is "no manualvirsh/qemu-img" — it does not require Terraformfor host OS packages, so a documented system unit does not violate it.
Out of scope
separate issue; this issue ends at a reachable port on the host.
lain-llmand how it is shared.tailscale serveare allrejected, with reasons, so they do not come back:
tailscale serve's target must bea service on the local machine, and it would be gated by the same firewalld zone.
Done when
And the rows this change writes down:
OPERATIONS.md§1 — a new row naming the unit,naina-serveras the host, systemdas what restarts it, and the drift check above as how it is verified. This is the
first row where a running cluster component depends on a host process; the row
should say so.
docs/tailscale.md— a new section, as a sibling of §6, covering the relay, thefirewall rule, the allowed-reachability decision, and the names the cluster may use.
OPERATIONS.md's domain index — the existingdocs/tailscale.mdrow extended tomention the relay.
ARCHITECTURE.mdis not touched by this issue.Naina:
Overview
A cluster workload must reach the OpenAI-compatible gateway
lain-llm.tailc23d6.ts.net:4000, which is machine-shared tovandomej@and therefore invisible to every tagged device the Tailscale operator can mint. This issue documents the hand-run fix: asocatTCP relay onnaina-server— the untagged libvirt host that already sees the gateway — plus a firewalld opening for port 4000. unverified Documentation only; the relay and the firewall rule are applied by hand on the host, andARCHITECTURE.mdis untouched.Technical Approach
Add
docs/tailscale.md§6b, a sibling of §6 — "Reach the shared gateway from the cluster". Lead with the reason, because it is why no ACL edit helps:Record the symptoms and their bound:
State the names the cluster may use:
naina-server.faun-snapper.ts.net:4000(own tailnet). The cluster-side consumer is #87.Document the relay unit —
/etc/systemd/system/llm-relay.service, applied by hand:Four deltas from the unit in the issue body, each load-bearing:
-djournalctl -u llm-relaycheck shows nothing without it.2StartLimitIntervalSec=0RestartSec=2a boot bind race burns the budget and the relay stays dead untilsystemctl reset-failed+AF_NETLINKgetaddrinfo, which opens aNETLINK_ROUTEsocket4; without it the failure looks like a DNS problemmax-children=64forkis the cheapest way for any device to exhaust the host's process limitAdd:
Requires=tailscaled.servicedoes not guarantee100.72.140.122is bindable (tailscaledis "ready" when LocalAPI is up, not when the address exists), soRestart=alwaysis the mechanism that wins the race.Document the firewall half, and the reachability semantics
tailscale0is in no zone, so it inherits the defaultpublic— which is why--get-active-zoneslists it under nothing.5publichastarget: default→ reject6, which is why the probe returnedno route to host(EHOSTUNREACH)7 rather than hanging — indistinguishable from an ACL denial.Zones are one-to-many:
wlp1s0f0is also inpublic, so the port opens on both interfaces. The boundbind=100.72.140.122is the compensating control; say so. Scoped alternative if that is not enough:Reject
--add-interface=tailscale0 --zone=trusted:trustedaccepts every port, NetworkManager can reassign the zone (tailscale#3416), and tailscale#15968 reports the interface going DOWN.Document the drift check — also the §1 row's verification:
It must assert both which zone
tailscale0lands in and that the zone allows 4000 —--list-ports --zone=publicalone silently passes ifDefaultZoneever changes.Add the
OPERATIONS.md§1 row8 — the first row where a running cluster component depends on a host process:Note in
OPERATIONS.md§89 — no renumbering of its steps:Extend the domain index row10
Implementation Details
Files
docs/tailscale.mdOPERATIONS.mdNothing under
gitops/,control-plane/,worker/,data/orcloudflare/;ARCHITECTURE.mduntouched.Constraints
AGENTS.md's rule is "no manualvirsh/qemu-img" — host OS packages and system units are not in that set, so a documented unit does not violate it.12tests/test_docs.py::test_runbook_carries_no_volatile_identity_literals— the new prose must carry no 32+ hex literal, UUID,talos-*name orpvc-*name.13dlarochette/systemdneeds root SSHnaina-serverdoes not offer unverified. Frame as documented, not automated.Out-of-scope notes
ExternalNameService) is #87.tailscale serveis rejected: its target must be a service on the local machine, and it hits the same firewalld zone.systemd-socket-proxydon aFreeBind=yes.socketremoves the boot race entirely;socatstays because the issue chose it and it is one command.Acceptance Criteria
docs/tailscale.md§6b exists as a sibling of §6 and covers the machine-share reason, the in-cluster symptoms, the unit, the firewall rule, the reachability/trusteddecision, the names the cluster may use, and the drift check.-d,StartLimitIntervalSec=0,max-childrenandAF_NETLINK, and is otherwise behaviourally identical to the unit in the issue body.OPERATIONS.md§1 has one new row namingllm-relay.service,naina-server, systemd, the drift check, and that it is the first running cluster component depending on a host process.OPERATIONS.md§8 carries the host-side note, and the domain index row fordocs/tailscale.mdmentions the relay.pytest tests/test_docs.pypasses.Testing Plan
pytest tests/test_docs.py— the runbook guards.naina-server, after the documented install:systemctl is-active llm-relayisactive, andjournalctl -u llm-relay -n 20shows one line per connection (-dis what makes this check meaningful).tailscale nc naina-server 4000reachesCONNECTED(wasno route to host).tailscale0and that zone lists4000/tcp.activeafterwards — theRestart=alwaysrace.sudo ausearch -m avc -ts recentshows no denials forllm-relay, andsystemd-analyze security llm-relay.serviceis run once and recorded.Related Issues/PRs
docs/tailscale.md§2/§3 — no admin-console or ACL change is part of this work.AGENTS.mdHard Rules — the "no manualvirsh/qemu-img" rule, read as not covering host OS packages.Research Findings
socatis an official Fedora package with first-class aarch64 builds (socat-1.8.1.1-*), so plaindnf install socatis correct on Fedora Asahi. CVE-2024-54661 affects only versions below1.8.0.3, so the runbook recordsrpm -q socat.public) is what an unassigned interface inherits, which is whytailscale0never appears in--get-active-zones. Zones are one-to-many, so the opening is not scoped to the tailnet interface and the relay'sbind=is the compensating control.--add-interface=tailscale0 --zone=trustedis worse than one port:trustedaccepts every host port, NetworkManager can reassign the zone (tailscale#3416), and tailscale#15968 reports the interface going DOWN.Restart=alwaysthe unit relies on.tailscaledsignals "ready" before its tailnet address is bindable (tailscale#11504, closed as documented behaviour), so the100.72.140.122bind only ever succeeds by retry.RestrictAddressFamiliesmust includeAF_NETLINK: glibc'sgetaddrinfo/getifaddrsopens aNETLINK_ROUTEsocket, and omitting it fails as something that looks like a DNS error.systemd-socket-proxydon aFreeBind=yessocket was the considered alternative; it removes the boot race, butsocatremains the simpler, chosen design.Pre-Mortem
The docs-only change is unfalsifiable in the pipeline, so a broken relay ships as "done" —
pytest tests/test_docs.pyis the only automated check and it verifies nothing about the relay, while the plan's real done-when criteria (tailscale nc ... CONNECTED, the reboot survival test,ausearch) are hand-run on a host no agent can reach. A runbook whose unit was never installed passes review, and issue #87's cluster half is then built against a gateway that is still unreachable.The load-bearing unit deltas were inferred from research rather than a running relay —
-d,StartLimitIntervalSec=0,max-children=64,AF_NETLINKand the belief thatRequires=tailscaled.serviceplusRestart=alwayswins the bind race are all documented as fact but never validated onnaina-server. If any inference is wrong, the relay dies silently after a reboot and the runbook confidently instructs the next operator to reproduce the failure.The hand-run host state drifts from the committed docs and nothing observes it — the relay is neither Terraform- nor Flux-managed, so
bind=100.72.140.122, thepublic-zone default and theUser=nainaaccount are unversioned host facts that a rebuild, a tailnet address change, or aDefaultZonechange can invalidate. The plan's entire value rests on those literals matching the host, and the mismatch only surfaces when the cluster-side consumer is tested.