Skip to content

feat(opencode-plugin): pin the SSH gateway host key from the Daytona API by default - #59

Merged
mislavivanda merged 3 commits into
mainfrom
feat/opencode-plugin-auto-pin-host-key
Sep 14, 2026
Merged

mislavivanda merged 3 commits into
mainfrom
feat/opencode-plugin-auto-pin-host-key

Conversation

@mislavivanda

@mislavivanda mislavivanda commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes the last piece of #46 item 3 on the plugin side. Strict host-key verification for sandbox git transfers existed only as an opt-in (DAYTONA_SSH_KNOWN_HOSTS, #50), so most users were still on trust-on-first-use. The Daytona API now publishes the gateway host key on /config (daytona/daytona-ai#1043 — sshGatewayHost, sshGatewayPort, sshGatewayHostKeys), and this PR makes the plugin consume it so strict verification becomes the default.

Fetching the key over the same TLS channel the plugin already trusts for the SSH access token itself adds no new trust root. What it removes is the first-connection window in which any network position could substitute the gateway.

Trust resolution (once per process)

Mode When Trust root
manual DAYTONA_SSH_KNOWN_HOSTS set only that file
pinned (default) otherwise, unless DAYTONA_SSH_AUTO_PIN=false only the published key, written to storage/daytona/gateway_known_hosts (0600, atomic)
inherited nothing to pin (older API, API unreachable, auto-pin off) the SSH client's normal verification — the previous behavior

In both pinned modes: UserKnownHostsFile=<pin>, GlobalKnownHostsFile=/dev/null, StrictHostKeyChecking=yes. The pin file, not the API, is what connections use — an API outage after the first pin never weakens verification.

Fail-closed against change: if the API later publishes a key set that no longer contains the pinned key, transfers are refused with a message pointing at the security policy and the pin file, and no access token is minted. A rotation that publishes old + new together (as the policy commits to) extends the pin without failing anyone.

Also: the gateway endpoint now comes from the API (no more hardcoded ssh.app.daytona.io), with [host]:port entries for non-22 ports; a manual file that disagrees with the published key logs a warning but is honored.

Docs

  • Plugin README: the pinning section now points at the published known_hosts line in the org security policy instead of asking users to verify "out of band" against nothing; documents the three modes and the fail-closed message.
  • Repo README: Security section linking the org-wide policy for vulnerability reporting (the repo-local SECURITY.md was removed in docs(security): defer to org-wide default security policy #53; this makes the inheritance visible to readers).

Verification

Prod /config does not serve the fields yet (pending daytona/daytona-ai#1043 + daytonaio/infrastructure-aws#419), so a local /config double served the real published key, and every transfer ran against the live gateway with a real sandbox — 23 assertions:

  • auto-pinned strict push and pull succeed against ssh.app.daytona.io
  • a wrong pin → live gateway rejected (Host key verification failed) — the pin is the only trust root
  • published key change → transfer refused before any token is minted
  • rotation: old+new published → pin extended; old dropped after overlap → pin follows cleanly
  • API down + existing pin → still pinned; API down + no pin → inherited; older API without fields → inherited; junk keys → treated as unpublished
  • DAYTONA_SSH_AUTO_PIN=false → inherited, nothing written; DAYTONA_SSH_KNOWN_HOSTS → manual wins, disagreement only warns
  • non-22 port → [host]:port entry
  • inherited mode still transfers (no regression for older APIs)
  • tsc + build clean

Activates automatically once the API fields are live; until then behavior is identical to today.


Summary by cubic

Strict SSH host-key verification for sandbox git transfers is now the default: the plugin fetches the gateway host key from the Daytona API and pins it in a plugin-managed known_hosts file over the same TLS channel already trusted for the SSH access token. Previously strict verification only happened when DAYTONA_SSH_KNOWN_HOSTS was set; otherwise transfers used trust-on-first-use.

Behavior

  • In pinned and manual modes the pin file is the only trust root for sandbox transfers: StrictHostKeyChecking=yes, system-wide known hosts ignored, and KnownHostsCommand, UpdateHostKeys, VerifyHostKeyDNS, and HostKeyAlias neutralized so no other ssh_config directive can add or redirect trust; other SSH remotes are unaffected.
  • Key validation and fingerprint computation now happen in-process — a missing or failing ssh-keygen no longer silently downgrades verification, and non-integer API ports are rejected.
  • DAYTONA_SSH_KNOWN_HOSTS still wins (disagreement only logs a warning); DAYTONA_SSH_AUTO_PIN=false or no published key (older API, unreachable) restores the SSH client's normal verification.
  • If the API later publishes a key set without the pinned key, transfers are refused before any token is minted; the message names the benign cause — a client that hasn't synced since before a rotation's overlap window — and points at the security policy and the rm recovery.
  • The gateway endpoint (host, port, [host]:port entries for non-22 ports) comes from the API instead of the hardcoded ssh.app.daytona.io; when the API is down after a non-default pin, it's recovered from the pin file's own host field.

Rollout

  • Until the API serves sshGatewayHostKeys, behavior is unchanged, so this can merge before the API change ships.

Written for commit c6b4f7c. Summary will update on new commits.

Review in cubic

…API by default

Strict host-key verification for sandbox git transfers existed only as an
opt-in (DAYTONA_SSH_KNOWN_HOSTS), so most users were on trust-on-first-use
- the gap reported in #46 item 3. The Daytona API now publishes the
gateway host key on /config (sshGatewayHost, sshGatewayPort,
sshGatewayHostKeys), fetched over the same TLS channel the plugin already
trusts for the SSH access token itself, so consuming it adds no new trust
root while making strict verification the default.

Trust is resolved once per process, in order:
1. DAYTONA_SSH_KNOWN_HOSTS set -> that file is the only trust root (manual).
2. Otherwise, unless DAYTONA_SSH_AUTO_PIN=false, the published key is
   written to a plugin-managed known_hosts file (0600, atomic) and used as
   the only trust root (pinned). The pin file, not the API, is what
   connections use, so an API outage after the first pin never weakens
   verification.
3. If nothing can be pinned (older API, API unreachable, auto-pin off),
   the SSH client's normal verification applies - the previous behavior.

Pinning is fail-closed against change: if the API later publishes a key
set that no longer contains the pinned key, transfers are refused with a
message pointing at the security policy and the pin file, and no access
token is minted. A rotation that publishes old and new keys together (as
the security policy commits to) extends the pin without failing.

The gateway endpoint is taken from the API as well, replacing the
hardcoded hostname and producing [host]:port known_hosts entries for
non-default ports. A manual file that disagrees with the published key
logs a warning but is honored as configured.

Docs: the README section now points at the published known_hosts line in
the org-wide security policy instead of asking users to verify 'out of
band' against nothing, and the repository README links the security
policy for vulnerability reporting.

Verified against the live gateway with a local /config double serving the
real published key: auto-pinned strict push and pull succeed; a wrong pin
is rejected by the gateway (the pin is the only trust root); a published
key change refuses the transfer before any token is minted; rotation
overlap extends the pin and the retired key drops out cleanly; API outage
keeps an existing pin and otherwise degrades to inherited verification;
DAYTONA_SSH_AUTO_PIN=false and DAYTONA_SSH_KNOWN_HOSTS behave as
documented; inherited mode still transfers.

Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 6 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread packages/opencode-plugin/.opencode/plugin/daytona/git/host-git-manager.ts Outdated
Comment thread packages/opencode-plugin/.opencode/plugin/daytona/git/gateway-host-key.ts Outdated
Comment thread packages/opencode-plugin/.opencode/plugin/daytona/git/gateway-host-key.ts Outdated
Comment thread packages/opencode-plugin/README.md
Comment thread packages/opencode-plugin/README.md Outdated
…d explain the dormant-client case

- Rename HostKeyTrust to HostKeyVerification. The type is the OUTPUT of
  resolution - which known_hosts file, if any, is the trust root and where
  it came from - not a trust toggle. No mode disables verification; the
  weakest outcome ('inherited') is the SSH client's own behavior. The old
  name read as a switch a user could flip.
- The key-change refusal now names the benign cause: a machine that has
  not synced since before a rotation's overlap window sees only the new
  key and cannot tell rotation from substitution. The message states this
  and gives the exact recovery (verify against the policy, rm the pin
  file, sync again).

Verified: all four rotation client states (active during overlap ->
extended; after retirement -> follows; dormant past the window -> refused
with the new message, old pin intact, recovers after rm; API down or
publishing nothing -> existing pin kept, never treated as a change), no
code path sets StrictHostKeyChecking=no, and live auto-pinned transfers
plus wrong-pin rejection against the real gateway still pass.

Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
…st-key verification

Review follow-ups on auto-pin:

- Neutralize every ssh_config directive that can add or redirect host-key
  trust when a pin is in force, so the pin file is the only trust root:
  KnownHostsCommand=none (a configured program could supply extra accepted
  keys), UpdateHostKeys=no (a connected server could append further keys
  to the pin file), VerifyHostKeyDNS=no (SSHFP records as a trust source),
  and HostKeyAlias fixed to the pinned host so the lookup cannot be
  redirected. All are first-value-wins options on a command the plugin
  owns.
- Validate published keys and compute fingerprints in-process instead of
  shelling out to ssh-keygen: a missing or failing optional executable
  silently downgraded every API response to inherited verification.
- Require an integer port from the API; a fractional value produced an
  invalid endpoint.
- When the API is unavailable, recover the gateway endpoint from the pin
  file's own host field ([host]:port or bare host) instead of defaulting
  to ssh.app.daytona.io:22, which made every strict transfer against a
  non-default gateway fail while its pin named another endpoint.
- README: manual-pin instructions now cover non-default host/port
  ([host]:port entries, ssh-keyscan -p), and the outage claim is scoped
  to machines that already hold a pin.

Verified live: with a wrong key pinned and a hostile ~/.ssh/config
supplying the real key via KnownHostsCommand plus StrictHostKeyChecking
no, the gateway is still rejected; a correct auto-pin still transfers
with the hardened option set and the pin file stays a single line after
connecting; ssh-keygen removed from PATH no longer prevents pinning and
the in-process fingerprint equals ssh-keygen's; fractional, string,
zero and out-of-range ports fall back to 22; API outage after a
non-default pin recovers host and port from the pin entry.

Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
@mislavivanda
mislavivanda merged commit 7b286ce into main Sep 14, 2026
33 checks passed
@mislavivanda
mislavivanda deleted the feat/opencode-plugin-auto-pin-host-key branch September 14, 2026 14:32
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant