feat: add connections, a saved SSH connection manager#6483
Draft
gustavosbarreto wants to merge 12 commits into
Draft
feat: add connections, a saved SSH connection manager#6483gustavosbarreto wants to merge 12 commits into
gustavosbarreto wants to merge 12 commits into
Conversation
6e8e7d8 to
811282a
Compare
Connections are saved SSH targets, kept separate from devices. A connection targets either a direct host (dial host:port directly, with no agent) or a device (an agent-registered machine reached through the gateway). It adds a connection-manager entry point alongside the fleet. Direct connections are dialed by the ssh service over a lightweight /ws/connect bridge, independent of the device session machinery. Device connections reuse the existing terminal flow. Reachability is probed server-side over TCP: shown per row in the list and checked at save time, where an unreachable direct host surfaces a hint to install the agent (for hosts behind NAT or a firewall) with a save-anyway override.
Saved SSH connection manager design and product strategy notes.
Web connect to external hosts now supports SSH key auth via the zero-knowledge vault: the server proxies the signing challenge over the websocket, so the private key never leaves the browser. /ws/connect gains public-key auth (reusing the device Signer); the browser sends the public key since external hosts are not registered in ShellHub. Persist a per-connection auth preference (method + key fingerprint, never the secret). The key is referenced by fingerprint, so it resolves against whichever vault is unlocked (local or remote). Migration 009 adds auth_method and key_fingerprint to connections. Unify ConnectDrawer into a single drawer for create, edit and connect, replacing the separate connection form. Footer is Save plus Connect; a "Save changes" checkbox appears when connecting a saved entry with edits, so persisting is always explicit. The devices list opens a saved connection when one already exists for the device. Rename the connection kind "direct" to "external" (stored value and UI label). The external target renders as a plain cyan ssh endpoint with an external-link icon, visually distinct from the boxed device chip.
A connection is now the shared TARGET; personal auth (username + key) is no
longer stored on the shared row. This fixes the multi-user smell where one
member's saved key/username leaked onto a connection everyone sees.
Ownership and visibility:
- connections gain owner_id and visibility ("personal" | "namespace").
Personal connections are private to their owner; namespace connections are
shared with the whole namespace. List filtering uses a new VisibleTo query
option (shared OR owned-by-me), resolved from the X-ID header.
- Replace the single unique(namespace_id,label) index with partial indexes:
team labels unique per namespace, personal labels unique per owner.
Per-user auth:
- New connection_user_prefs table keyed by (connection_id, user_id) holds the
username, auth method and key fingerprint (never the secret). The drawer
reads/writes the caller's own prefs, so each member keeps their own identity.
- Connections keep an optional shared default_username hint that prefs override.
Authorization:
- Personal connections are writable by their owner regardless of role (observers
included, now granted the connection permissions). Namespace connections still
require operator+, enforced in the service since ownership is row-level data.
Frontend:
- ConnectDrawer gains a Personal/Team toggle (Team gated by role) and reads or
saves per-user prefs instead of auth on the connection. Edit mode only touches
the shared record (label/visibility/target/default username), not auth.
- Connections page groups rows into "Your connections" and "Team connections".
Migrations are edited in place (007 adds owner_id/visibility, renames username
to default_username; 009 now creates connection_user_prefs) since the feature is
unreleased. Mocks and the authorizer role test are updated accordingly.
Rework the Connections feature: - Personal-only in CE: a connection belongs to its owner (owner_id) and the auth lives on the row; drop the shared visibility / per-user-prefs model. - SSRF egress guard via code.dny.dev/ssrf on the reachability probe and the /ws/connect dial; public-only by default. A local-dev allowance for private ranges is included, marked DEV and to be removed before shipping. - Team connections UI (Enterprise/Cloud) merged into one list, gated by edition; the backend lives in the cloud repo. - Known hosts (TOFU) for external targets: scan and confirm the host key in the browser, persist it (ssh_known_hosts, scoped personal/team), verify on connect and abort on mismatch. The external target chip opens a host-key modal. Error messages distinguish blocked vs unreachable vs key mismatch. Mocks regenerated; tests added (owner scoping, connectionDirty, egress).
Fold create save into the Connect action via a "Save connection" checkbox (unchecked connects one-off without persisting), let team edit set the caller's own auth prefs, route "Set up auth" to the edit drawer, and prompt a vault unlock when connecting with a key whose vault is locked.
AcceptKnownHost derives key_type and fingerprint from the parsed public key instead of trusting the client, and any namespace-scoped write now requires operator+, so a member can't plant a host key the team trusts. Device connections validate device_uid against the caller's namespace on create/update, and ConnectionStatus scopes DeviceResolve to the namespace, closing a cross-tenant liveness leak. KnownHostUpsert is a single ON CONFLICT, and a non-UUID id resolves to not-found.
An empty known_host_key is rejected instead of silently falling back to InsecureIgnoreHostKey, closing a browser-forced MITM downgrade. A host-key mismatch is tracked out of band so it surfaces as a real mismatch rather than a generic auth failure. Connect tokens are consumed on first use (one-shot) to stop replay, and the dial is bound to the request context so a dropped client cancels it.
Add the connections, known-hosts, and team-connection endpoints to the OpenAPI spec and move the hand-written clients onto the generated SDK (typed params, no raw URLs or scattered casts). The connections page paginates (100/page) over the generated list endpoints. Also invalidate the host-key cache after a TOFU accept, keep direct connects out of the recents palette, surface a permission error when editing a team target without operator+, and refresh status after an edit.
The POST decode error, token failure, and stdio pipe failures returned the raw Go error to the browser, which can carry internal detail. Log them and return a generic message or session sentinel instead.
Mark the connection, team-connection, known-host and prefs schema fields as required so the generated types are non-optional, then drop the hand-written types/connection.ts and the boundary casts in favor of the generated @/client types end to end.
The All/Personal/Team filter added noise; the list now always shows every connection (personal and team) with the scope badges still present.
811282a to
642aaf5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
DO NOT MERGE — MVP / work in progress, opened for review and visibility only.
Connections are saved SSH targets, kept separate from devices. A connection targets either a direct host (dial
host:portdirectly, with no agent) or a device (an agent-registered machine reached through the gateway). It adds a connection-manager entry point alongside the fleet.Direct connections are dialed by the ssh service over a lightweight
/ws/connectbridge, independent of the device session machinery. Device connections reuse the existing terminal flow.Reachability is probed server-side over TCP: shown per row in the list and checked at save time, where an unreachable direct host surfaces a hint to install the agent (for hosts behind NAT or a firewall) with a save-anyway override.
What changed
connectionroutes/service/store across pg, with migrations007_connectionsand008_connection_device_target, new permissions, request/response models./ws/connectbridge inssh/web/connect.gothat dials direct hosts independently of the device session machinery.Screenshots
Connections list — direct hosts and devices side by side, each with a live status dot.
Add a connection — direct host — dial a host by address, no agent required.
Unreachable host → install-the-agent funnel — save-time TCP probe; if the host can't be reached, it nudges toward installing the agent (NAT/firewall), with a save-anyway override.
Add a connection — device — pick an agent-registered machine via a server-side searched picker.
Connect — device connections reuse the rich terminal/auth flow.
Screenshots are hotlinked from branch
docs/connections-screenshots(images only, deletable once this PR closes).