Skip to content

Hybrid: harden linking, sign-ins, the relay, names and same-site requests - #309

Merged
nambok merged 1 commit into
mainfrom
fix/hybrid-security
Oct 26, 2026
Merged

nambok merged 1 commit into
mainfrom
fix/hybrid-security

Conversation

@nambok

@nambok nambok commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

A security review of the hybrid self-host features (registration, the event relay, the token and sign-in brokers, CoderHelm addresses and DNS, the public endpoints, deliveries). This PR fixes what it found, each with a regression test, and adds the hybrid threat model to the security docs. Details of the findings went to the maintainers directly.

Changes

  • Brokered sign-ins (routes/hybrid.rs, mcp_remote/runtime.rs): a Notion, Confluence or Google Drive sign-in started through the hosted broker is finished in a holding partition and moves into the install's partition only when the install redeems the code at the token endpoint with its secret and PKCE verifier (Runtime::adopt_sign_in, resealed under the install's KMS context). The public authorize endpoint is rate limited per address.
  • GitHub linking (routes/hybrid.rs, hybrid/broker.rs): after the GitHub sign-in, a confirmation page (no-store, frame-ancestors 'none') names the install's address; only POST /hybrid/github/confirm with its one-time token links. Only installations of CoderHelm's own app count. A link stores a ceiling, the repositories the linker can push to (from GET /user/installations/{id}/repositories), and every token is minted with at most those repositories, so GitHub enforces it. set_link_repos refuses repositories outside the ceiling. Return addresses are compared host by host. Links made before this keep their behaviour until linked again.
  • Requests to installs (hybrid/relay.rs): relay forwards and ownership proofs use a client that never follows redirects and read at most 16 KB of an answer.
  • Same-site requests (middleware/cors.rs): a change (not GET, HEAD or OPTIONS) sent with cookies from a same-site origin that isn't a trusted dashboard origin is refused with 403 (Sec-Fetch-Site, and on the hosted side the origin for browsers without it).
  • Names (hybrid/names.rs): names containing coderhelm (digit look-alikes included) or a part such as app, api, login, billing or support are reserved. Names already claimed are unchanged.
  • DNS permissions (infra/lib/hybrid.ts): a second deny statement for anything under CoderHelm's own hosts, _domainkey, _dmarc, _mta-sts, _smtp, _spf, _acme-challenge, and names three labels deep (IAM's * crosses dots, so the allow alone matched them).
  • Relay signature and replay store (hybrid/sign.rs, routes/hybrid_install.rs): the relay header adds v2, which also covers the source and event headers; installs check it when present and still accept v1 alone from an older hosted side. The intake answers 503 (and the relay retries) when it can't record a delivery id, instead of handling the event.
  • Revocation (routes/hybrid.rs): revoking an install also removes its GitHub links and keeps its CloudFront address from registering again; linking checks the install is active.
  • Docs: docs/self-host/security.md gains a hybrid threat model (assets, actors, each attack and its control, what CoderHelm can and cannot see, known limits). docs/self-host/hybrid.md is corrected: addresses under coderhelm.com are the same site as app and api to a browser.

Follow-ups outside this repo: the dashboard and the docs install guide don't yet have words for the new link outcomes (no_write_access, declined, install_off).

Verification

  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test (workspace).
  • New tests, each failing before its fix where the code already existed: the_relay_never_follows_a_redirect, a_large_error_answer_is_cut, a_customer_subdomain_cant_send_changes_with_the_session, without_sec_fetch_site_the_hosted_side_goes_by_the_origin, look_alike_names_are_refused, the_route_of_a_relayed_event_is_signed, the_intake_never_takes_a_copy_it_couldnt_record, a_sign_in_is_adopted_from_a_holding_partition, a_provider_sign_in_is_held_apart_from_the_install, a_link_never_reaches_past_what_its_linker_can_push_to, writable_repositories_come_from_the_persons_own_permissions, the_return_address_is_one_of_the_installs_own_hosts, the_link_confirmation_names_the_install_and_cant_be_framed, a_revoked_cloudfront_is_one_key_however_it_is_written, and the updated linking_needs_the_person_to_see_the_installation.
  • infra: npm test, with the Route 53 test extended to check the deeper names are fenced and the install's own records stay writable; the generated managed policies stay under IAM's size limit.
  • Nothing was run against production; no AWS writes and no real secrets were used.

…ests

A security review of the hybrid self-host features, with a regression test
for each fix:

- Brokered sign-ins (Notion, Confluence, Google Drive) are finished in a
  holding partition and become the install's only when it redeems the code
  with its secret and PKCE verifier. Starting one is rate limited.
- Linking CoderHelm's GitHub App needs a confirmation on a page that names
  the install's address and can't be framed; only installations of
  CoderHelm's own app count; a link reaches only the repositories the person
  who linked it can push to (sent to GitHub as the token's repository list);
  return addresses are compared host by host.
- Requests to an install's address (relay forwards, ownership proofs) never
  follow redirects and read at most 16 KB of the answer.
- The API refuses changes sent with cookies from another origin on the same
  site (customer {name}.coderhelm.com pages share a site with app and api).
- Names that read as CoderHelm's own (coderhelm with digit look-alikes, or a
  part such as app, login, billing or support) are reserved.
- The Route 53 permissions also deny anything under CoderHelm's own hosts,
  mail and certificate-authority records, and names three labels deep.
- Relayed events also sign the source and event headers (v2); the intake
  refuses an event when it can't record its delivery id.
- Revoking an install removes its GitHub links and keeps its CloudFront
  address from registering again.
- docs/self-host/security.md gains the hybrid threat model.
@nambok
nambok merged commit 4fb3dc7 into main Oct 26, 2026
7 checks passed
@nambok
nambok deleted the fix/hybrid-security branch October 26, 2026 18:10
nambok added a commit that referenced this pull request Nov 4, 2026
…461)

Pins dashboardRef to CoderHelm/coderhelm dcc848d (#309): the Sign in
with GitHub card shows the included mode from #459, and the e2e covers
the button on a registered hybrid install.
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.

1 participant