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 threadplugins/credential-sharing/src/providers/ssh.test.ts Dismissed
Comment threadplugins/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

SeverityRuleLocation
HIGHsecret-private-keyplugins/credential-sharing/src/providers/ssh.ts:129
HIGHjs-host-header-trustapps/commandboard-web/server.js:27
HIGHjs-ssrf-outbound-requestapps/pwa/public/sw.js:45
HIGHsecret-generic-credentialplugins/agentgit/src/index.ts:12
HIGHsecret-generic-credentialplugins/c0mpute/src/index.ts:12
HIGHsecret-generic-credentialplugins/coinpay/src/index.ts:12
HIGHsecret-generic-credentialplugins/sh1pt/src/index.ts:11
HIGHsecret-generic-credentialplugins/ugig/src/index.ts:11
MEDIUMjs-unescaped-html-sinkapps/commandboard-web/src/main.ts:19
MEDIUMjs-unescaped-html-sinkapps/logicsrc-web/src/app/[[...slug]]/page.tsx:73
MEDIUMjs-unescaped-html-sinkapps/logicsrc-web/src/app/blog/[slug]/page.tsx:120
MEDIUMjs-unescaped-html-sinkapps/logicsrc-web/src/app/docs/[slug]/page.tsx:55
MEDIUMjs-unescaped-html-sinkapps/logicsrc-web/src/app/layout.tsx:83
MEDIUMjs-unescaped-html-sinkapps/logicsrc-web/src/app/pricing/page.tsx:46
MEDIUMjs-unescaped-html-sinkapps/logicsrc-web/src/components/site-shell.tsx:46
MEDIUMredos-nested-quantifierpackages/opencontext/src/ids.ts:10
MEDIUMredos-nested-quantifierpackages/opencontext/src/ids.ts:11
MEDIUMredos-nested-quantifierpackages/opencontext/src/validate.ts:104
MEDIUMjs-timing-unsafe-mac-comparepackages/opencontext/src/validate.ts:208
MEDIUMredos-nested-quantifierpackages/openontology/src/ids.ts:20
MEDIUMredos-nested-quantifierpackages/openprd/src/validate.ts:41
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-account-audit-event.schema.json:23
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-account-grant.schema.json:23
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-account-grant.schema.json:35
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-account-provider.schema.json:20
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-connected-account.schema.json:30
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-credential-audit-event.schema.json:14
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-bundle.schema.json:206
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-decision.schema.json:12
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-decision.schema.json:131
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-decision.schema.json:137
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-decision.schema.json:193
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-decision.schema.json:196
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-decision.schema.json:200
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-decision.schema.json:225
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-diagnostic.schema.json:47
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-manifest.schema.json:333
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-object.schema.json:197
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-object.schema.json:203
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-object.schema.json:306
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-provenance.schema.json:16
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-provenance.schema.json:36
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-provenance.schema.json:70
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-opencontext-role.schema.json:89
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-openontology-manifest.schema.json:28
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-openontology-manifest.schema.json:121
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-openontology-namespace.schema.json:14
MEDIUMredos-nested-quantifierpackages/schemas/schemas/logicsrc-plugin.schema.json:21
LOWsecret-generic-credentialapps/logicsrc-web/contract/logicsrc-web.contract.test.ts:392
LOWsecret-generic-credentialapps/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 masterAug 14, 2026
6 checks passed
@ralyodio
ralyodio deleted the feat/ssh-secrets branch August 14, 2026 00:42
Sign up for freeto 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

@ralyodio@github-advanced-security