Skip to content

Add SSH keys and config to credential sharing - #139

Merged
ralyodio merged 3 commits into
masterfrom
feat/ssh-secrets
Aug 14, 2026
Merged

Add SSH keys and config to credential sharing#139
ralyodio merged 3 commits into
masterfrom
feat/ssh-secrets

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Stores SSH key pairs and ~/.ssh/config in the same end-to-end-encrypted team vaults as .env secrets, plus an agent path that uses a key without writing it to disk — the trade Proton Pass makes with its SSH agent.

Surface

logicsrc secrets ssh push profullstack            # ~/.ssh → vault ssh--anthony
logicsrc secrets ssh push --dry-run               # or --include authorized_keys
logicsrc secrets ssh list profullstack            # paths, kinds, modes — never key bodies
logicsrc secrets ssh pull profullstack            # restore, permissions and all
logicsrc secrets ssh agent profullstack --lifetime 3600

Key material is addressed by person, not project: the vault is ssh--<username>, which teams vaults lists as project ssh, env <username>. One teammate's keys never land in another's restore, and sharing stays a deliberate teams grant.

Design notes

  • New ssh provider rather than bespoke CLI code, so diff, dry-run, approval, rollback and audit come from the existing engine. logicsrc secrets inspect --provider ssh --path ~/.ssh and plan --from ssh --to ssh work too.
  • Files are selected by sniffing contents, not filenames: PRIVATE KEY blocks, ssh-*/ecdsa-*/sk-* public keys, plus config, config.d/*, allowed_signers. known_hosts and authorized_keys are host-specific and access-granting, so they need --include.
  • Each file is one secret carrying a JSON envelope of path, mode and body. The engine only hands write() the secrets that changed, so a separate manifest secret would be missing whenever a key's contents change but the file list doesn't — self-describing values keep every restore total.
  • Both directions hold back anything that would overwrite a file that already differs, and report what they skipped; --force opts in. A restore onto a machine that has its own keys is otherwise a way to lose them.
  • Restores chmod each file back to its recorded mode (writeFileSync's mode applies only on create, so an existing world-readable key would stay world-readable) and create the directory 0700. The adapter declares delete: false.
  • push warns about passphrase-less private keys before they go up.

Verification

  • 16 new provider tests + 7 new CLI tests; full suites green (credential-sharing 68, cli 54).
  • End-to-end through the real engine: secrets plan/sync --from ssh --to ssh between two directories restored byte-identical files at 0600/0644, ssh-keygen -y still derives the public key, and known_hosts/authorized_keys were correctly left behind.

🤖 Generated with Claude Code

Private keys have lived as plaintext-on-disk files guarded only by a
passphrase. This puts them in the same end-to-end-encrypted vaults as
.env secrets, and adds an agent path so a machine can use a key without
ever writing one to its disk.

- `ssh` provider: ~/.ssh as a value bag. Files are picked by sniffing
  contents (PRIVATE KEY blocks, ssh-*/ecdsa-*/sk-* public keys) plus
  config, config.d/* and allowed_signers. known_hosts and
  authorized_keys are host-specific and access-granting, so they need
  an explicit --include.
- Each file is one secret carrying a JSON envelope of path, mode and
  body. The engine only hands write() the secrets that CHANGED, so a
  separate manifest secret would be absent whenever a key's contents
  change but the file list doesn't — self-describing values keep every
  restore total.
- `logicsrc secrets ssh push|pull|list|agent`, addressed by PERSON not
  project: the vault is ssh--<username>, which teams vaults reads as
  project ssh, env <username>. One teammate's keys never land in
  another's restore; sharing stays a deliberate teams grant.
- Both directions hold back anything that would overwrite a file that
  already differs, and say what they skipped. --force opts in. A
  restore onto a machine with its own keys is otherwise a way to lose
  them.
- Restores chmod each file back to its recorded mode; writeFileSync's
  mode applies only on create, so an existing world-readable key would
  otherwise stay world-readable. The adapter declares delete:false.
- push warns about passphrase-less private keys before they go up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread plugins/credential-sharing/src/providers/ssh.test.ts Dismissed
Comment thread plugins/credential-sharing/src/providers/ssh.ts Dismissed
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

72 finding(s)

HIGH/CRITICAL: 8 | MEDIUM: 40 | LOW: 24

Severity Rule Location
HIGH secret-private-key plugins/credential-sharing/src/providers/ssh.ts:129
HIGH js-host-header-trust apps/commandboard-web/server.js:27
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH secret-generic-credential plugins/agentgit/src/index.ts:12
HIGH secret-generic-credential plugins/c0mpute/src/index.ts:12
HIGH secret-generic-credential plugins/coinpay/src/index.ts:12
HIGH secret-generic-credential plugins/sh1pt/src/index.ts:11
HIGH secret-generic-credential plugins/ugig/src/index.ts:11
MEDIUM js-unescaped-html-sink apps/commandboard-web/src/main.ts:19
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/app/[[...slug]]/page.tsx:73
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/app/blog/[slug]/page.tsx:120
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/app/docs/[slug]/page.tsx:55
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/app/layout.tsx:83
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/app/pricing/page.tsx:46
MEDIUM js-unescaped-html-sink apps/logicsrc-web/src/components/site-shell.tsx:46
MEDIUM redos-nested-quantifier packages/opencontext/src/ids.ts:10
MEDIUM redos-nested-quantifier packages/opencontext/src/ids.ts:11
MEDIUM redos-nested-quantifier packages/opencontext/src/validate.ts:104
MEDIUM js-timing-unsafe-mac-compare packages/opencontext/src/validate.ts:208
MEDIUM redos-nested-quantifier packages/openontology/src/ids.ts:20
MEDIUM redos-nested-quantifier packages/openprd/src/validate.ts:41
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-account-audit-event.schema.json:23
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-account-grant.schema.json:23
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-account-grant.schema.json:35
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-account-provider.schema.json:20
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-connected-account.schema.json:30
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-credential-audit-event.schema.json:14
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-bundle.schema.json:206
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-decision.schema.json:12
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-decision.schema.json:131
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-decision.schema.json:137
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-decision.schema.json:193
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-decision.schema.json:196
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-decision.schema.json:200
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-decision.schema.json:225
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-diagnostic.schema.json:47
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-manifest.schema.json:333
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-object.schema.json:197
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-object.schema.json:203
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-object.schema.json:306
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-provenance.schema.json:16
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-provenance.schema.json:36
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-provenance.schema.json:70
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-opencontext-role.schema.json:89
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-openontology-manifest.schema.json:28
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-openontology-manifest.schema.json:121
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-openontology-namespace.schema.json:14
MEDIUM redos-nested-quantifier packages/schemas/schemas/logicsrc-plugin.schema.json:21
LOW secret-generic-credential apps/logicsrc-web/contract/logicsrc-web.contract.test.ts:392
LOW secret-generic-credential apps/logicsrc-web/contract/logicsrc-web.contract.test.ts:410

…and 22 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

Commander's usage line shows only the first alias, so `logicsrc secrets`
— the spelling people actually type — was invisible in its own help.
The examples carry it, alongside the flows worth copying: link/up/down,
the ssh backup round trip, and a plan → dry-run → approve sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio marked this pull request as ready for review August 13, 2026 23:47
The marketing-drift contract failed the build because `ssh` shipped in the
provider registry with no entry in MARKETING_PROOF -- which is the test
working: it exists so a provider cannot ship while the pages people
actually land on still describe the tool without it.

The proof regex is `/~\/\.ssh|SSH key/` rather than a bare `/SSH/` on
purpose. The provider grid renders every registry `name`, and this one is
"Local SSH directory", so `/SSH/` would already be satisfied by the
generated grid and the provider could ship with no copy written about it
at all -- passing the test while failing its intent. Requiring the path or
the phrase means a human wrote a sentence.

That sentence is the new block in the credential-sharing band: ~/.ssh is a
directory of files whose permission bits are load-bearing, not a set of
KEY=VALUE lines, which is the part that makes this provider different from
the other six. README already named ~/.ssh keys, so it needed no change.

apps/logicsrc-web: 75/75 contract tests pass (was 74 passed, 1 failed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit b1805d0 into master Aug 14, 2026
6 checks passed
@ralyodio
ralyodio deleted the feat/ssh-secrets branch August 14, 2026 00:42
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.

2 participants