design: pretty-URL tailnet-direct path for the Authelia-gated MCP fleet (split-DNS + direct-to-pod tailnet Caddy) #474

Open
opened 2026-07-07 05:18:09 +00:00 by coilysiren · 2 comments
Owner

Intent (Kai)

Give Kai's tailnet devices (mcporter / Claude Code / Codex local harnesses) the pretty <svc>.coilysiren.me MCP URLs, reached tailnet-direct and gate-free, instead of today's ugly unauthenticated NodePort (http://kai-server:30931/mcp). The Authelia gate exists for the hosted claude.ai connector (which runs server-side, off-tailnet, and cannot reach the tailnet - deploy#28); a trusted tailnet peer does not need per-request OIDC. This is the pretty-URL upgrade of deploy#39's tailnet-direct decision.

This is a design brief - post a proposal for Kai to approve before any implementation.

Current state (grounded)

  • <svc>.coilysiren.me resolves to the public IP and flows public -> Traefik -> Authelia, even from the tailnet - no split-DNS override exists.
  • The tailnet-direct path is the NodePort (kai-server:30931/mcp, ungated - the tailnet is the trust boundary), chosen in deploy#39.
  • A tailnet Caddy already exists (http://kai-server front door, caddy/Caddyfile + caddy/sites/*.caddy generated by scripts/generate-caddy-shortcuts.py from each sibling repo's config.yml tailnet.shortcut). But its shortcuts reverse_proxy to Traefik (192.168.0.194:80) with the <svc>.coilysiren.me Host header, so for a gated MCP they would hit the same Authelia middleware - they do not bypass it today.

The three pieces

  1. Tailscale split-DNS - override the MCP hostnames (per-host, or a scoped set) to resolve to kai-server's tailnet IP for tailnet clients only. Public DNS keeps the public A record, so the hosted claude.ai connector still hits the gate. Home: the Tailscale DNS control-plane, candidate for terraform/tailscale/.
  2. Tailnet Caddy site blocks for each pretty host that reverse_proxy straight to the pod / NodePort / Service ClusterIP (NOT to Traefik), bypassing the Authelia ForwardAuth middleware, served with a DNS-01 cert for *.coilysiren.me so it is HTTPS-clean. This is a new "direct-to-pod" shortcut variant alongside the existing "to-Traefik" one in generate-caddy-shortcuts.py.
  3. Per-pod --allowed-hosts (Playwright MCP host-header guard, and the equivalent on the other upstream images) must include the pretty host. Playwright is already getting playwright.coilysiren.me; the fleet needs the same.

Design questions to resolve in the proposal

  • Split-DNS scope - per-host overrides for exactly the MCP hostnames, vs a *.coilysiren.me wildcard. A wildcard would also capture non-MCP hosts (forgejo, galaxy-gen, tangled) and yank them onto the tailnet path - lean per-host, or wildcard-with-exceptions, and say which.
  • Public path must stay unaffected - the override has to be tailnet-scoped (Tailscale split nameserver / restricted search domain), never a change to the public A record. Acceptance must prove the off-tailnet hosted connector still resolves + gates.
  • Direct target - Caddy -> pod: NodePort vs Service ClusterIP vs Traefik-with-a-middleware-skip. Pick the cleanest that provably skips ForwardAuth.
  • Cert - Caddy DNS-01 for *.coilysiren.me on the tailnet host (issuance + DNS-provider creds); confirm it does not collide with Traefik's public cert for the same names.
  • Scope of services - the gated fleet (node-stats, reddit, steam, playwright, lunch-money) plus the deploy#30 tailnet-upstream fleet.
  • Shortcut mechanism - is "direct-to-pod" a new tailnet.shortcut type in the generator, or a separate hand-written sites-manual/*.caddy set?

Security posture

The tailnet becomes an ungated path to these MCPs - but that is already true via the NodePort, so this opens no new exposure, it only makes the existing ungated reach pretty. Public / hosted-connector reach stays Authelia-gated. The single load-bearing invariant: keep the split-DNS override tailnet-scoped so the public path is never redirected.

Relationships

  • deploy#39 (closed) - chose the NodePort tailnet-direct path; this prettifies it.
  • deploy#30 (open) - the personal-MCP fleet pattern this extends.
  • deploy#28 (closed) - why the gate exists (hosted claude.ai cannot reach the tailnet).
  • Per-pod --allowed-hosts follow-up on the playwright bundle (deploy#47).

Ask

Concept + design capture. An advisor design proposal (split-DNS scope, direct target, cert, service list, generator change) can follow when Kai wants - no implementation or infra mutation yet.

## Intent (Kai) Give Kai's **tailnet devices** (mcporter / Claude Code / Codex local harnesses) the **pretty** `<svc>.coilysiren.me` MCP URLs, reached **tailnet-direct and gate-free**, instead of today's ugly unauthenticated NodePort (`http://kai-server:30931/mcp`). The Authelia gate exists for the **hosted claude.ai connector** (which runs server-side, off-tailnet, and cannot reach the tailnet - deploy#28); a trusted tailnet peer does not need per-request OIDC. This is the pretty-URL upgrade of deploy#39's tailnet-direct decision. This is a **design brief** - post a proposal for Kai to approve before any implementation. ## Current state (grounded) - `<svc>.coilysiren.me` resolves to the **public IP** and flows public -> Traefik -> Authelia, **even from the tailnet** - no split-DNS override exists. - The tailnet-direct path is the **NodePort** (`kai-server:30931/mcp`, ungated - the tailnet is the trust boundary), chosen in deploy#39. - A **tailnet Caddy** already exists (`http://kai-server` front door, `caddy/Caddyfile` + `caddy/sites/*.caddy` generated by `scripts/generate-caddy-shortcuts.py` from each sibling repo's `config.yml` `tailnet.shortcut`). But its shortcuts `reverse_proxy` to **Traefik** (`192.168.0.194:80`) with the `<svc>.coilysiren.me` Host header, so for a **gated** MCP they would hit the **same Authelia middleware** - they do not bypass it today. ## The three pieces 1. **Tailscale split-DNS** - override the MCP hostnames (per-host, or a scoped set) to resolve to kai-server's **tailnet IP** for **tailnet clients only**. Public DNS keeps the public A record, so the hosted claude.ai connector still hits the gate. Home: the Tailscale DNS control-plane, candidate for `terraform/tailscale/`. 2. **Tailnet Caddy site blocks** for each pretty host that `reverse_proxy` **straight to the pod / NodePort / Service ClusterIP** (NOT to Traefik), bypassing the Authelia ForwardAuth middleware, served with a **DNS-01 cert for `*.coilysiren.me`** so it is HTTPS-clean. This is a **new "direct-to-pod" shortcut variant** alongside the existing "to-Traefik" one in `generate-caddy-shortcuts.py`. 3. **Per-pod `--allowed-hosts`** (Playwright MCP host-header guard, and the equivalent on the other upstream images) must include the pretty host. Playwright is already getting `playwright.coilysiren.me`; the fleet needs the same. ## Design questions to resolve in the proposal - **Split-DNS scope** - per-host overrides for exactly the MCP hostnames, vs a `*.coilysiren.me` wildcard. A wildcard would also capture non-MCP hosts (forgejo, galaxy-gen, tangled) and yank them onto the tailnet path - lean per-host, or wildcard-with-exceptions, and say which. - **Public path must stay unaffected** - the override has to be **tailnet-scoped** (Tailscale split nameserver / restricted search domain), never a change to the public A record. Acceptance must prove the off-tailnet hosted connector still resolves + gates. - **Direct target** - Caddy -> pod: NodePort vs Service ClusterIP vs Traefik-with-a-middleware-skip. Pick the cleanest that provably skips ForwardAuth. - **Cert** - Caddy DNS-01 for `*.coilysiren.me` on the tailnet host (issuance + DNS-provider creds); confirm it does not collide with Traefik's public cert for the same names. - **Scope of services** - the gated fleet (node-stats, reddit, steam, playwright, lunch-money) plus the deploy#30 tailnet-upstream fleet. - **Shortcut mechanism** - is "direct-to-pod" a new `tailnet.shortcut` type in the generator, or a separate hand-written `sites-manual/*.caddy` set? ## Security posture The tailnet becomes an **ungated** path to these MCPs - but that is **already true** via the NodePort, so this opens **no new exposure**, it only makes the existing ungated reach pretty. Public / hosted-connector reach stays Authelia-gated. The single load-bearing invariant: keep the split-DNS override **tailnet-scoped** so the public path is never redirected. ## Relationships - deploy#39 (closed) - chose the NodePort tailnet-direct path; this prettifies it. - deploy#30 (open) - the personal-MCP fleet pattern this extends. - deploy#28 (closed) - why the gate exists (hosted claude.ai cannot reach the tailnet). - Per-pod `--allowed-hosts` follow-up on the playwright bundle (deploy#47). ## Ask Concept + design capture. An advisor design proposal (split-DNS scope, direct target, cert, service list, generator change) can follow when Kai wants - no implementation or infra mutation yet.
Member

Concrete follow-up from deploy#133.

http://kai-server/skillsmp/mcp still returns 404 on the live host, while http://kai-server:30081/mcp returns 200 with serverInfo: skillsmp. The deploy repo documents the tailnet shortcut as a host-Caddy path, so this looks like the missing host-side deploy/reload step for the existing sites-manual/skillsmp.caddy shortcut.

The current deploy issue is coilyco-bridge/deploy#133. Please re-check the kai-server Caddy install path / reload path for the skillsmp shortcut.

Concrete follow-up from deploy#133. `http://kai-server/skillsmp/mcp` still returns 404 on the live host, while `http://kai-server:30081/mcp` returns 200 with `serverInfo: skillsmp`. The deploy repo documents the tailnet shortcut as a host-Caddy path, so this looks like the missing host-side deploy/reload step for the existing `sites-manual/skillsmp.caddy` shortcut. The current deploy issue is `coilyco-bridge/deploy#133`. Please re-check the kai-server Caddy install path / reload path for the skillsmp shortcut.
Member

🔎 ward agent advisor

ward agent advisor --harness codex ran a one-shot standard research pass on this question:

Read the issue title, body, and comment thread below as the research brief. Identify the decisions, options, open questions, and risks, then give a clear advisory recommendation that can be posted back on the issue.

Additional framing from the dispatcher:
infrastructure#474


pre-commit installed at .git/hooks/pre-commit
pre-commit installed at .git/hooks/commit-msg


Researched and posted automatically by ward agent advisor --harness codex (ward#179). This is one-shot research, not a carried change - verify before acting on it.

— Codex, via ward agent

### 🔎 ward agent advisor `ward agent advisor --harness codex` ran a one-shot **standard** research pass on this question: > Read the issue title, body, and comment thread below as the research brief. Identify the decisions, options, open questions, and risks, then give a clear advisory recommendation that can be posted back on the issue. > > Additional framing from the dispatcher: > infrastructure#474 --- pre-commit installed at .git/hooks/pre-commit pre-commit installed at .git/hooks/commit-msg --- Researched and posted automatically by `ward agent advisor --harness codex` (ward#179). This is one-shot research, not a carried change - verify before acting on it. <!-- ward-agent-reply --> <!-- ward-agent-signature --> — Codex, via `ward agent`
Sign in to join this conversation.
No description provided.