fix: validate agent-supplied AllowedIPs in coordinator by f0ssel · Pull Request #26144 · coder/coder · GitHub
Skip to content

fix: validate agent-supplied AllowedIPs in coordinator#26144

Merged
f0ssel merged 3 commits into
mainfrom
garrett/plat-264-agent-supplied-allowedips-not-validated-by-coordinator-sec
Jun 11, 2026
Merged

fix: validate agent-supplied AllowedIPs in coordinator#26144
f0ssel merged 3 commits into
mainfrom
garrett/plat-264-agent-supplied-allowedips-not-validated-by-coordinator-sec

Conversation

@f0ssel

@f0ssel f0ssel commented Jun 8, 2026

Copy link
Copy Markdown
Member

AgentCoordinateeAuth.Authorize validated every prefix in upd.Node.Addresses (each must be a /128 derived from the authenticating agent's own UUID) but applied no equivalent check to upd.Node.AllowedIps. Because AllowedIPs are installed verbatim into the WireGuard peer config (tailnet/configmaps.go) and WireGuard routing is driven by AllowedIPs, a malicious agent could advertise a victim agent's /128 and become an eligible route for that IP. With ServerTailnet tunneling to many agents and routing by destination IP, this could let an attacker intercept sessions intended for the victim workspace.

This applies the same UUID-derivation validation to AllowedIps that already guards Addresses, extracted into a shared authorizeNodePrefixes helper. The check is the single chokepoint used by both the in-memory coordinator (tailnet/coordinator.go) and the Postgres coordinator (enterprise/tailnet/connio.go), so one fix covers both. Legitimate agents are unaffected: an agent's AllowedIPs is a clone of its Addresses (tailnet/node.go), which are already UUID-derived /128s.

Fixes PLAT-264 (SEC-89): https://linear.app/codercom/issue/PLAT-264

Implementation notes and decision log

Root cause

Asymmetric validation in tailnet/tunnel.go: Addresses were bound to the agent's UUID, but AllowedIps were trusted as-is and propagated into the WireGuard peer config, which drives routing.

Why the fix is safe for legitimate agents

  • tailnet/node.go builds the node with AllowedIPs: slices.Clone(u.addresses), identical to Addresses.
  • agent/agent.go sets those addresses to TailscaleServicePrefix.PrefixFromUUID(agentID) and CoderServicePrefix.PrefixFromUUID(agentID) (both /128, UUID-derived).
  • The existing Addresses check already accepts exactly those prefixes plus the legacy workspace agent IP, so identical validation of AllowedIPs passes for real traffic and only rejects forged prefixes.

Coverage: one method, both coordinators

AgentCoordinateeAuth.Authorize is the shared auth path. A failed Authorize is wrapped as AuthorizationError{Wrapped: err} and closes the agent's response stream.

Tests

  • tailnet/tunnel_internal_test.go: fast unit tests on Authorize (valid AllowedIPs accepted; foreign /128 rejected with InvalidNodeAddressError; wrong-bits rejected with InvalidAddressBitsError).
  • tailnet/coordinator_test.go: in-memory coordinator closes the agent stream on a forged AllowedIp.
  • enterprise/tailnet/pgcoord_test.go: same regression for the Postgres coordinator.

Verified the regression tests fail when the new AllowedIps check is disabled, then pass with it enabled. Local validation: targeted tests (in-memory, internal, and Postgres-backed enterprise), plus make pre-commit (gen/fmt/lint/build) passing.

Generated by Coder Agents on behalf of @f0ssel.

AgentCoordinateeAuth.Authorize validated upd.Node.Addresses but applied no check to upd.Node.AllowedIps. Because AllowedIPs are installed verbatim into the WireGuard peer config and drive routing, a malicious agent could advertise a victim agent's /128 and have traffic routed to it, intercepting sessions intended for the victim workspace.

Apply the same UUID-derivation validation to AllowedIps that is already used for Addresses, via a shared authorizeNodePrefixes helper. This covers both the in-memory and Postgres coordinators, which share this auth path.

Fixes PLAT-264 (SEC-89).
@linear-code

linear-code Bot commented Jun 8, 2026

Copy link
Copy Markdown

@f0ssel

f0ssel commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Chat: Review in progress | View chat
Requested: 2026-06-09 17:17 UTC by @f0ssel

deep-review v0.7.1 | Round 1 | f242ef0..58e6baf

Last posted: Round 1, 4 findings (1 P3, 1 P4, 2 Nit), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P4 Open tailnet/tunnel.go:132 Client-side handleClientNodeRequests has same AllowedIPs validation gap R1 Netero, Hisoka Yes
CRF-2 P3 Open tailnet/tunnel.go:78 Errors from authorizeNodePrefixes don't identify which field (Addresses vs AllowedIPs) R1 Chopper P3, Leorio P3, Pariston Nit Yes
CRF-3 Nit Open tailnet/tunnel.go:92 Loop variable addrStr inconsistent with parameter name prefixes R1 Gon Yes
CRF-4 Nit Dropped by orchestrator (Leorio and Zoro praised the comment; security threat model context is non-local and valuable) tailnet/tunnel.go:74 Security comment slightly verbose R1 Gon No
CRF-5 Nit Dropped by orchestrator (Leorio praised; legacy IP exception is non-obvious security context) tailnet/tunnel.go:89 Doc comment on authorizeNodePrefixes restates body R1 Gon No
CRF-6 Nit Dropped by orchestrator (marginal value on well-executed security fix) tailnet/tunnel_internal_test.go:92 Test comment restates what variable names show R1 Gon No
CRF-7 Nit Dropped by orchestrator (marginal, same pattern as CRF-6) tailnet/coordinator_test.go:119 Test comment restates test name and setup R1 Gon No
CRF-8 Nit Dropped by orchestrator (marginal, same pattern as CRF-6) enterprise/tailnet/pgcoord_test.go:166 Test comment restates test name and setup R1 Gon No
CRF-9 Nit Open enterprise/tailnet/pgcoord_test.go:177 Comment says "immediately" but assertion is AssertEventuallyResponsesClosed R1 Gon Yes

Round log

Round 1

Panel. 0 P0-P1, 0 P2, 1 P3, 1 P4, 2 Nit posted. 5 Nit dropped. Reviewed against f242ef0..58e6baf.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot 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.

Well-targeted security fix. Extracting authorizeNodePrefixes and applying it to both Addresses and AllowedIPs closes the routing-hijack gap at the single chokepoint shared by both coordinators. The refactoring is mechanical, the test coverage hits three layers (unit, in-memory coordinator, PG coordinator), and the PR description traces every claim to a file and line.

As Bisky put it: "Three layers of testing, each proving a different slice of the security behavior, and not a costume gem in the bunch."

Pariston explored four framings of the vulnerability (asymmetric validation, server-side override, whitelist construction, test coverage gap) and confirmed the chosen framing is at the right causal level. Meruem verified no new concurrency hazards. Kurapika, Kite, and Knov found no security gaps in the fix itself. Hisoka probed edge cases including IPv4 injection (blocked by the /128 check) and the legacy workspace agent IP (pre-existing, no agents emit it anymore).

Severity summary: 1 P3, 1 P4, 2 Nit.


tailnet/tunnel.go:132

P4 [CRF-1] handleClientNodeRequests has the same AllowedIPs validation gap this PR fixes for agents. It validates Addresses (bit count only, no UUID derivation) but skips AllowedIps entirely. configmaps.go:619 installs AllowedIPs for all peers unconditionally, so the field reaches WireGuard routing for clients too.

Exploitability is lower for clients (ServerTailnet routes to agents, not clients), and the client path intentionally applies a weaker policy (bit count only, no UUID binding). But the complete absence of AllowedIPs validation is the same class of bug. Worth a follow-up. (Netero, Hisoka)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread tailnet/tunnel.go
Comment thread tailnet/tunnel.go Outdated
Comment thread enterprise/tailnet/pgcoord_test.go Outdated
Wrap the errors returned by authorizeNodePrefixes so an operator can tell
whether a rejected prefix came from Addresses or AllowedIps instead of
having to compare the address against both fields manually. Rename the
loop variable to prefixStr to match the parameter, and correct a test
comment that said "immediately" while asserting eventual closure.

Addresses review feedback on #26144.

f0ssel commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

@f0ssel f0ssel requested a review from spikecurtis June 11, 2026 14:27
@f0ssel f0ssel marked this pull request as ready for review June 11, 2026 14:27
@f0ssel f0ssel requested review from jdomeracki-coder and sreya June 11, 2026 15:02
Comment thread tailnet/tunnel_internal_test.go Outdated
The AgentCoordinateeAuth.Authorize AllowedIPs cases are already covered at the coordinator level (in-memory AgentWithoutClients_InvalidAllowedIP and PG TestPGCoordinatorSingle_AgentInvalidAllowedIP), matching the existing InvalidAddress and InvalidBits coverage. The unit test duplicated that matrix, slowing the suite and creating two places to maintain.
@f0ssel f0ssel requested a review from spikecurtis June 11, 2026 19:17
@f0ssel f0ssel merged commit 9b351c3 into main Jun 11, 2026
28 checks passed
@f0ssel f0ssel deleted the garrett/plat-264-agent-supplied-allowedips-not-validated-by-coordinator-sec branch June 11, 2026 19:27
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants