RedlineKnowledge base

Network design for the experiment

Topology

Reader's browser, on the tailnet
  ├─ https://yyz.fio.sh          static site (public hosting, unchanged)
  ├─ https://yqa.fio.sh          fio-redline service
  │     DNS: Tailscale A record → GitLab host (100.x.y.z)
  │     TLS: host nginx, same pattern as glab.fio.sh
  │     nginx → http://fio-redline:8787 (Docker)
  │     fio-redline → http://glab.fio.sh-server (Docker bridge glab.fio.sh-bridge)
  └─ https://glab.fio.sh         GitLab, for the OAuth authorize step only
        DNS: existing Tailscale A record

Reader's browser, off the tailnet
  ├─ https://yyz.fio.sh          works; shows build-time history
  └─ https://yqa.fio.sh          DNS resolves to 100.x.y.z; TCP connect fails
                                  → browser fetch rejects with TypeError
                                  → client state offline-network

No Cloudflare Access, no Tunnel, no cloudflared container. The network is the perimeter; GitLab OAuth is the identity.

Constraints that fix the design

  1. Session cookies must be same-site with the page. The service sets its OAuth session cookie with SameSite=Lax, so browsers send it on a cross-origin fetch only when page and service share a registrable domain. yyz.fio.sh and yqa.fio.sh share fio.sh; a *.ts.net MagicDNS name would not. Therefore the service must be published on a fio.sh name that resolves to the Tailscale address. This is the same pattern glab.fio.sh already uses.
  2. The cookie prefix __Host- requires HTTPS. Browsers reject a __Host- cookie without the Secure attribute and a secure origin. The service must be served over TLS by host nginx. For local and e2e runs over http://localhost, the service drops the prefix and the Secure flag when PUBLIC_SERVICE_URL is http: (T0.4); production validation refuses http:.
  3. CORS. SITES_JSON.<site>.allowedOrigins must list every page origin that will call the service: https://yyz.fio.sh for production, and http://localhost:4321 for the harness.
  4. OAuth callback. The GitLab OAuth application’s redirect URI is exactly https://yqa.fio.sh/v1/auth/gitlab/callback. The authorize step happens in the browser against https://glab.fio.sh, which is also tailnet-only, so it fails off-net in the same way the service does. The client only offers the connect link after a successful service response, so an off-net reader is never sent to a dead redirect.
  5. Service to GitLab. The container joins the external Docker network and calls GitLab by its bridge alias, so this edge does not depend on Tailscale. It fails only when GitLab itself is down or the bridge is misconfigured. The service reports that as gitlab-unreachable and the health probe reports gitlab.reachable=false.

The three failure states, by edge

Edge that failedHow the browser sees itClient stateWhat the reader can do
browser → servicefetch rejects (TypeError)offline-networkjoin the tailnet
service → GitLabHTTP 502 with code gitlab-unreachableoffline-gitlabwait; nothing on their side
identityHTTP 401unauthenticatedsign in (not expected in the experiment; reads are anonymous)

Playwright reproduces the first by aborting requests to the service origin, the second by switching the mock GitLab to down, the third by running the service with ALLOW_ANONYMOUS_READ=false.

Harness topology (local and CI)

Chromium (Playwright)
  ├─ http://localhost:4321   fixture Astro site, `astro preview`
  ├─ http://localhost:8787   fio-redline service, IDENTITY_MODE=gitlab-oauth
  └─ http://localhost:9999   mock GitLab (T0.1), also the OAuth authorize origin

All three are localhost, which browsers treat as one site for cookies and as a secure context, so the cookie flow matches production without TLS.

Alternative identity: Tailscale headers

GitLab OAuth is the first choice because it attributes writes to the person’s own GitLab account. It is not a constraint. If the OAuth chain proves unworkable on the tailnet (application cannot be registered, redirect chain fails, cookies rejected), the executor may add IDENTITY_MODE=tailscale:

  • Publish the service with tailscale serve instead of host nginx. Tailscale terminates TLS and injects Tailscale-User-Login, Tailscale-User-Name and Tailscale-User-Profile-Pic on every proxied request; they cannot be forged from the browser because the proxy strips incoming copies.
  • The service trusts those headers only in this mode, performs GitLab writes with the service token, and prefixes every note body with the person’s login so attribution still reaches GitLab. The redline-audit line carries the same login.
  • The same-site cookie constraint disappears because there is no session cookie; the service name may then be a ts.net MagicDNS name, and the CORS allow-list is the only coupling to the site origin.

Record the switch in STATUS.md and add a Playwright spec that drives the service with the headers set by page.setExtraHTTPHeaders.

Operator inputs (T4.3)

ItemValue or owner
DNSyqa.fio.sh A record → GitLab host Tailscale IP, same zone as glab.fio.sh
TLShost nginx vhost yqa.fio.sh, certificate by the same method as glab.fio.sh
nginx upstreamhttp://127.0.0.1:<published port> or the Docker network alias fio-redline:8787
GitLab OAuth appconfidential, scope api, redirect https://yqa.fio.sh/v1/auth/gitlab/callback
GitLab read tokenproject or group token, read_api only
Site registrationSITES_JSON for yyz as in ops/service.env.example, with features.editing=false
Secretsops/secrets/* files as the compose file lists, except the tunnel token
Git history

Loading the page's history…