Skip to content

[go] allow opt-in Tailscale ACLs for admin/view access - #503

Open
patrickod wants to merge 2 commits into
mainfrom
patrickod/tailscale-acls
Open

[go] allow opt-in Tailscale ACLs for admin/view access#503
patrickod wants to merge 2 commits into
mainfrom
patrickod/tailscale-acls

Conversation

@patrickod

Copy link
Copy Markdown
Collaborator

Extend the Tailscale serve integration to optionally consume Tailscale capability grants and use these to create admin and view roles for accessing the web UI with the latter being restricted from changing device settings.

LAN access is unaffected and retains admin privileges to all endpoints.

Copilot AI review requested due to automatic review settings May 8, 2026 19:49
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

🤖 Version Bump Advisory

Warnings

Radar version unchanged - consider bumping version in Makefile

New Features

ℹ️ New API handlers (2 handler(s)) - MINOR bump suggested


📖 See CHANGELOG.md for detailed guidelines.

This is an automated advisory. Review the detected changes and update versions accordingly.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-in authorisation layer for Tailscale-served HTTP access, using Tailscale capability grants to distinguish view (read-only) vs admin access, while keeping LAN/loopback behaviour unchanged by default.

Changes:

  • Add a Tailscale peer-capability lookup surface (with short-TTL caching) to resolve a peer’s view/admin grants.
  • Add an API-layer auth gate + route classifier that applies to the entire HTTP mux (default-deny; view allowlists).
  • Wire the feature behind a new -ts-cap-enforcement=off|on flag and document the operational model.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/tailscale/peercaps.go Implements peer capability lookup and short-TTL caches for peer identity and local prefixes.
internal/tailscale/peercaps_test.go Unit tests for capability parsing and caching behaviours.
internal/tailscale/manager.go Extends LocalClient with WhoIs and adds caches to the Manager.
internal/tailscale/manager_test.go Updates test fake client to implement WhoIs.
internal/api/server.go Adds route classification (view/admin/allowlist) and wraps the server handler with the auth gate.
internal/api/auth.go New middleware implementing trust model, enforcement modes, and 403 JSON responses.
internal/api/auth_test.go Unit + integration tests covering source classification, grant matrix, failure modes, and default-deny semantics.
docs/platform/operations/tailscale-remote-access.md Updates ops documentation for capability grants, enforcement modes, and troubleshooting.
cmd/radar/radar.go Adds -ts-cap-enforcement flag and wires SetAuthGate into server startup.

Comment thread internal/api/server.go
Comment thread internal/api/server.go
Comment thread internal/api/auth.go Outdated
Comment thread internal/api/auth.go Outdated
Comment thread internal/tailscale/peercaps.go Outdated
Comment thread docs/platform/operations/tailscale-remote-access.md Outdated
@codecov

codecov Bot commented May 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.96992% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/api/auth.go 86.36% 7 Missing and 5 partials ⚠️
internal/cmd/server/radar.go 0.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@patrickod
patrickod force-pushed the patrickod/tailscale-acls branch from 234f34f to 3639adc Compare May 8, 2026 20:07
patrickod added a commit that referenced this pull request May 15, 2026
Five fixes from the CoPilot review on #503:

1. Allowlist bare /app and view-gate bare /api/reports.  The auth
   wrapper runs before the ServeMux trailing-slash redirect, so a
   request to /app or /api/reports would hit default-deny CapAdmin
   and 403 view-only peers instead of redirecting to the
   trailing-slash sibling.

2. Narrow isPeerNotFound to match only "no match for IP".  The
   previous substring set ("not found", "404") caught generic
   transport errors and reported them as ErrPeerNotFound, which made
   the api layer fail closed (403) on a transient lookup blip
   instead of failing open as documented.

3. Remove the ineffective LocalTailnetPrefixes fallback in
   isTailnetIP.  The method returned /32 and /128 prefixes for
   *this* node's own Tailscale IPs, so the fallback loop could only
   ever match the node's own address, never a remote peer.  The
   method, the prefix cache, and the related fakePeerAuth wiring are
   removed; isTailnetIP and classifySource are simplified to drop
   the now-unused ctx/gate parameters.

4. Log "auth: capability enforcement armed (mode=on)" at SetAuthGate
   when EnforcementOn is the effective mode.  The troubleshooting
   doc instructed operators to grep journald for this line but no
   such line was emitted.

Test updates: route-classifier tests now cover bare /app and
/api/reports, peercaps tests assert that transient errors containing
"not found"/"404" are not misclassified, and the now-defunct
TestLocalTailnetPrefixes_Cached is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@patrickod
patrickod force-pushed the patrickod/tailscale-acls branch from 3639adc to aaca216 Compare May 15, 2026 19:06
@patrickod
patrickod requested a review from Copilot May 15, 2026 19:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

patrickod and others added 2 commits August 12, 2026 01:27
Extend the Tailscale serve integration to optionally consume Tailscale
capability grants and use these to create admin and view roles for
accessing the web UI with the latter being restricted from changing
device settings.

LAN access is unaffected and retains admin privileges to all endpoints.
Five fixes from the CoPilot review on #503:

1. Allowlist bare /app and view-gate bare /api/reports.  The auth
   wrapper runs before the ServeMux trailing-slash redirect, so a
   request to /app or /api/reports would hit default-deny CapAdmin
   and 403 view-only peers instead of redirecting to the
   trailing-slash sibling.

2. Narrow isPeerNotFound to match only "no match for IP".  The
   previous substring set ("not found", "404") caught generic
   transport errors and reported them as ErrPeerNotFound, which made
   the api layer fail closed (403) on a transient lookup blip
   instead of failing open as documented.

3. Remove the ineffective LocalTailnetPrefixes fallback in
   isTailnetIP.  The method returned /32 and /128 prefixes for
   *this* node's own Tailscale IPs, so the fallback loop could only
   ever match the node's own address, never a remote peer.  The
   method, the prefix cache, and the related fakePeerAuth wiring are
   removed; isTailnetIP and classifySource are simplified to drop
   the now-unused ctx/gate parameters.

4. Log "auth: capability enforcement armed (mode=on)" at SetAuthGate
   when EnforcementOn is the effective mode.  The troubleshooting
   doc instructed operators to grep journald for this line but no
   such line was emitted.

Test updates: route-classifier tests now cover bare /app and
/api/reports, peercaps tests assert that transient errors containing
"not found"/"404" are not misclassified, and the now-defunct
TestLocalTailnetPrefixes_Cached is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ddol
ddol force-pushed the patrickod/tailscale-acls branch from aaca216 to c93272a Compare August 12, 2026 08:33
@ddol
ddol requested a balanced review from Copilot August 12, 2026 08:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

docs/platform/operations/tailscale-remote-access.md:114

  • The description "read-only access to /api/*" overstates what the view grant provides. Because the gate is default-deny, cap/view only reaches the routes explicitly classified as view (in viewRoutes/viewRoutesGetOnly). Several read-only endpoints under /api — e.g. /api/serial/models, /api/serial/devices, and the /api/lidar/* routes — still resolve to CapAdmin and will return 403 for a view-only peer. Consider rewording so operators don't expect view to grant read access to the entire /api surface. (Optional / minor.)
- `velocity.report/cap/view` — read-only access to `/api/*` and
  `/events`.

@ddol

ddol commented Aug 12, 2026

Copy link
Copy Markdown
Member

@patrickod the P0 generated by this review is worrying:

Review findings

  • P0: Funnel can bypass all authorization.
    classifySource treats loopback requests with no XFF, or an XFF outside Tailscale ranges, as non-tailnet and therefore admin. Tailscale Funnel is public and does not provide identity headers, so Funnel configured out-of-band can expose the full API without capability checks.

    Relevant: internal/api/auth.go:145, internal/api/auth.go:200

    This needs fail-closed handling for proxied traffic before enabling this feature.

  • P1: /api/tailscale/status leaks an active login URL without a capability.
    The endpoint is deliberately allowlisted, but its response contains login_url for up to five minutes. During enrollment, an untrusted LAN or tailnet client can retrieve the URL intended to claim the device.

    Relevant: internal/api/server.go:173, internal/tailscale/manager.go:573

    Restrict recovery access to LAN/local callers, or redact login_url unless the caller has admin.

  • P1: Direct 100.64.0.0/10 traffic contradicts the documented LAN bypass.
    Docs say all non-loopback traffic is treated as LAN/admin, but the code treats direct Tailscale-CGNAT addresses as tailnet and applies WhoIs. LANs using RFC6598 space can therefore receive 403s when enforcement is enabled.

    Relevant: internal/api/auth.go:215, docs/platform/operations/tailscale-remote-access.md:151

    Decide whether direct tailnet-IP traffic is supported, then make code, docs, and tests agree.

  • P2: cap/view is substantially narrower than documented.
    Docs say it provides read-only /api/* access, but authorization is a manual allowlist and defaults all other paths to admin. Read-only serial configuration, LiDAR APIs, offline docs, and future read endpoints require admin.

    Relevant: docs/platform/operations/tailscale-remote-access.md:113, internal/api/server.go:142

    Define the viewer role explicitly and test its complete route/method inventory.

  • P2: The feature has no supported production activation path.
    Enforcement defaults to off, and the production systemd unit does not set -ts-cap-enforcement=on. That preserves compatibility, but means normal deployments receive no protection and operators have no documented persistent opt-in workflow.

    Relevant: internal/cmd/server/radar.go:78, image/stage-velocity/03-velocity-config/files/velocity-report.service:38

  • P3: The peer cache can grow for the process lifetime.
    Expired entries are removed only when the same key is queried again; unique historical peer addresses remain indefinitely.

    Relevant: internal/tailscale/peercaps.go:144

Assessment

The viewer/admin split is justified for shared tailnets. It is unnecessary for single-user deployments, so the default-off posture is sensible.

The custom XFF plus WhoIs trust model is the weak point. Tailscale Serve can forward selected app capabilities in its trusted Tailscale-App-Capabilities header; using that mechanism would eliminate the proxy/source-inference problem and per-peer WhoIs calls.

Before enabling this in production, add an integration matrix for: viewer, admin, no-cap, LAN, direct tailnet IP, Funnel, daemon lookup failure, and Tailscale enrollment URL visibility.

Focused Go tests, link validation, and whitespace checks pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants