Skip to content

fix(credentials): remove inline SSH credential read-fallback (#748, ARC-4)#873

Merged
hellodk merged 5 commits into
masterfrom
fix/748-deprecated-ssh-cred-columns
Jun 24, 2026
Merged

fix(credentials): remove inline SSH credential read-fallback (#748, ARC-4)#873
hellodk merged 5 commits into
masterfrom
fix/748-deprecated-ssh-cred-columns

Conversation

@hellodk

@hellodk hellodk commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Closes #748

Summary

ARC-4: eliminate the second SSH-secret resolution path. Node/group SSH secrets historically lived in inline ssh_* columns; the credential-consolidation epic (#704) moved them into the first-class Credential store (credential_id), and migrations 055/058 copied + NULLed the inline columns. But the service layer still read the inline columns as a fallback, leaving two secret-resolution paths. This PR removes that read path.

  • services/credential_resolver.py — drop the inline node/group branches and the _inline_node_creds / _inline_group_creds helpers. Resolution now flows solely through the Credential store (node/group credential_id) plus the controller/global platform tiers. _primary_group_stmt is eligible by credential_id only. Added NoUsableCredentialError + require_usable_node_credentials(/_sync) so credential-required callers (WebSSH, VNC, password-mode bootstrap) fail loudly instead of silently degrading — the explicit replacement for the old inline fallback.
  • services/ssh_credential_link.pyowner_secret_flags no longer consults the inline columns; secret presence comes from the linked Credential only.
  • services/node_import.py, tool_calling.py, bootstrap_svc.py — audited: node_import only parses import input (and bootstrap_svc converts inline input into a Credential link via upsert_owner_ssh_credential), so import ergonomics are preserved and nothing persists/reads an inline dual path. tool_calling does not touch SSH creds. No changes required.

New failure mode (security-relevant)

Previously, a node with no credential_id but legacy inline data resolved via the inline columns. Now the inline columns are never read, so such a node resolves to no usable secret. Call sites that require a secret should use require_usable_node_credentials(/_sync), which raises NoUsableCredentialError with a clear, actionable message rather than attempting a connection with the wrong/empty secret.

Migration note (head 060 → 061): DEFERRED

The task asked for migration 061 to DROP the inline columns "once no code reads them". That precondition is not yet met: workers/ansible_tasks.py and api/routes/{groups,nodes}.py (owned by sibling PRs, out of this PR's scope) still read the inline columns. Dropping them now would break those modules at runtime and fail the repo-wide mypy gate on files this PR must not touch.

So this PR removes the service-layer read path (the ARC-4 security goal) and defers the physical column drop:

  • The inline columns remain on models/node.py / models/group.py (now unread by the service layer, annotated as deprecated).
  • owner_secret_flags keeps inline_password_enc / inline_key_enc kwargs (accepted-but-ignored) so sibling-owned route callers keep type-checking during the staged removal.
  • Once the sibling reader-removal PRs land, the follow-up is mechanical: drop the four columns on nodes/groups in migration 061 (down_revision = "060", current verified head), remove the SQLAlchemy column defs, and delete the deprecated owner_secret_flags kwargs.

Backward-compat caveats

  • require_usable_node_credentials is additive; existing callers of resolve_node_credentials(/_sync) are unaffected (signatures unchanged, dict shape unchanged, credential_source unchanged).
  • Un-migrated rows that still carry only inline secrets (no credential_id) will now resolve to the group/controller/global tier instead of inline — expected, since the store is authoritative after 055/058.

Test plan

  • uv run pytest tests/unit/test_748_no_inline_ssh_fallback.py -q — new, 12 tests, all pass.
  • Updated test_credential_resolver.py, test_credential_resolver_sync.py, test_playbook_credentials_b279.py to the credential-store-only contract — pass.
  • uv run python -c "import fleet_platform.api.main" — smoke import OK.
  • tests/unit/test_migration_chain_guard_571.py — pass (chain unchanged).
  • Pre-commit gate green (ruff, mypy, vulture, bandit).

Note: a handful of unrelated unit tests fail in the sandbox due to missing optional deps (asyncssh, ansible_runner) — verified identical on base origin/master, so not introduced here.

Made with Cursor

…RC-4)

Node/group SSH secrets historically lived in inline ssh_* columns. The
credential-consolidation epic (#704) moved them into the first-class
Credential store (credential_id); migration 055 copied them and 058 NULLed
them, but the service layer still READ the inline columns as a fallback,
leaving two secret-resolution paths.

This closes the ARC-4 security goal by eliminating the inline read path:

- credential_resolver: drop the inline node/group branches and the inline
  helper functions. Resolution now flows solely through the Credential store
  plus the controller/global platform tiers — there is exactly one place a
  per-row secret can come from. Add NoUsableCredentialError and
  require_usable_node_credentials(/_sync) so credential-required callers fail
  loudly instead of silently degrading (the old inline-fallback behaviour).
- ssh_credential_link.owner_secret_flags: no longer consults the inline
  columns; reports secret presence from the linked Credential only. The
  inline_* kwargs are retained (accepted-but-ignored, deprecated) so sibling-
  owned api/routes callers keep type-checking during the staged removal.

node_import and bootstrap_svc already accept inline creds on import and
immediately convert them into a Credential link, so import ergonomics are
preserved; only the persisted dual-path READ is removed. tool_calling does not
touch SSH credentials.

Adds tests/unit/test_748_no_inline_ssh_fallback.py and updates the resolver
tests to the credential-store-only contract.

Deferred (precondition "once no code reads them" not yet met): the physical
DROP of the inline columns (model removal + migration 061) is intentionally
NOT included. workers/ansible_tasks.py and api/routes/{groups,nodes}.py — owned
by sibling PRs — still read the inline columns, so dropping them now would break
those modules and the repo-wide type gate. The columns stay on the model
(unread by the service layer) until those readers are removed; the drop is the
natural follow-up migration once they land.
@github-actions github-actions Bot added test Test coverage improvement frontend Frontend / React changes labels Jun 24, 2026
@github-actions

Copy link
Copy Markdown

Coverage Report

86.1% on fleet_platform/services/ (gate: 80%)

Gate passed

…ssh-cred-columns

# Conflicts:
#	VERSION
#	frontend/package.json
@github-actions

Copy link
Copy Markdown

Coverage Report

86.0% on fleet_platform/services/ (gate: 80%)

Gate passed

@github-actions

Copy link
Copy Markdown

Coverage Report

86.0% on fleet_platform/services/ (gate: 80%)

Gate passed

@github-actions

Copy link
Copy Markdown

Coverage Report

fleet_platform/services/ 86.0% (gate: 80%)

fleet_platform/agent/ 94.7% (gate: 80%)

Gate passed

@github-actions

Copy link
Copy Markdown

Coverage Report

fleet_platform/services/ 86.0% (gate: 80%)

fleet_platform/agent/ 94.7% (gate: 80%)

Gate passed

@hellodk
hellodk merged commit 7c5f1fd into master Jun 24, 2026
12 checks passed
@hellodk
hellodk deleted the fix/748-deprecated-ssh-cred-columns branch June 24, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

frontend Frontend / React changes test Test coverage improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Audit] Deprecated inline SSH credential columns still live and read as fallback (dual secret paths) (ARC-4)

1 participant