server: authorize saved vault cards under restrict_hosts#73
Merged
Conversation
A saved vault card connects via `vault_id`/`conn_id` and carries no `connection` name, so the connect handler's gate — `if not conn_name and not is_host_allowed(...)` — treated it as a free-form manual POST. Under `restrict_hosts: true` that gate rejects unconditionally, so vault-card connects were refused outright with "connections to this host are not allowed" (surfaced by the client as the generic "username not authorized" popup) and never reached SSH. A vault card is not a free-form connect — its target is a stored record, and under restrict_hosts it is as legitimate as the named prompt connection whose host:port it matches. - Add `find_prompt_connection_by_host()` — the named `prompt` connection matching a target, or None. - Add `authorize_target()` — the gate decision for connects with no `connection` name. A saved card under restrict_hosts is authorized against the matching prompt connection (fixed-username connections pin the user; open ones run `check_prompt_user`); a manual POST stays rejected. restrict_hosts off → both fall through to the existing deny-list gate (`is_host_allowed`, unchanged). - The connect handler gates via `authorize_target`. Named-connection, manual, and deny-list / scan-pattern behaviour is unchanged. - test_server.py: +7 cases pinning the gate (manual rejected; saved card allowed on a matching prompt connection, rejected on an unconfigured host; allowed_users / denied_users / fixed-username honored). 427 pass.
Owner
Author
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Reviewed:
|
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.
The bug
On a deployment with
restrict_hosts: true, connecting via a savedvault card is rejected — the connect never reaches SSH and the client
shows the generic "username … is not authorized" popup. Connecting to
the same host through the named connection works.
Root cause
The connect handler resolves credentials three ways — named connection
(
connection), saved vault card (vault_id/conn_id), or free-formmanual body — then gates the request:
A vault card carries no
connectionname (it is keyed byvault_id/conn_id), soconn_nameis empty and the request fallsinto the gate. Under
restrict_hosts,is_host_allowed()returnsFalseunconditionally, so the vault connect is refused as if it werea forbidden free-form manual POST. The client's
mapConnectErrormatches the
/not allowed/message and renders thepolicy_denypopup, which reads as a username problem even though the rejection is
about connection routing.
A vault card is not a free-form connect — its target is a stored
record. Under
restrict_hostsit is exactly as legitimate as thenamed
promptconnection whose host:port it matches.The fix (server-side)
find_prompt_connection_by_host(host, port)— the namedpromptconnection matching a target, or
None.ready(fixed-credential)connections are intentionally not matched: they connect with
operator-stored credentials, not a user's saved card.
authorize_target(host, port, username, is_saved)— one decisionfor connects that carry no
connectionname. A saved card underrestrict_hostsis authorized against the matching promptconnection — a fixed-username connection pins the user; an open one
runs
check_prompt_user, soallowed_users/denied_usersstillapply. A manual POST stays rejected. With
restrict_hostsoff, bothfall through to the existing deny-list gate.
authorize_target.is_host_allowed, the named-connection path, the manual path, andthe deny-list / scan-pattern behaviour are unchanged.
A vault card whose host:port matches no configured prompt connection
is still rejected —
restrict_hostsstill pins connections toconfigured destinations.
Test plan
python3 test_server.py— 427 passed (was 420; +7). Frontendtests untouched (server-only change); CI runs both.
authorize_targetcases pin the gate: manual rejected underrestrict_hosts; saved card allowed when it matches a promptconnection; rejected when the host is unconfigured; honoring
allowed_users/denied_users/ a fixed username on the matchedconnection. Verified red→green — each case was watched failing
against both an under-permissive and an over-permissive
implementation.