fix(ssh): ensure identityContent ends with trailing newline (#511) #25

Merged
stack72 merged 1 commit from worktree-511 into main 2026-06-03 00:50:15 +00:00
Owner

Summary

OpenSSH rejects PEM key files that lack a trailing newline with Load key: invalid format. This happens when vault-stored keys are stripped of their final newline — for example, swamp vault put reading from stdin strips the trailing \n from piped content.

materializeTempKeys() now defensively ensures the written content ends with a newline before writing to the temp file.

What changed

  • ssh/extensions/models/_lib/operations.ts — 2-line fix: check if identityContent ends with \n, append one if not, before writing to the temp file
  • ssh/extensions/models/ssh.ts — version bump to 2026.06.03.1 with upgrades entry
  • ssh/manifest.yaml — version bump to 2026.06.03.1
  • ssh/extensions/models/ssh_test.ts — new test verifying a PEM key without trailing newline gets one appended

Why this is correct

The PEM format (RFC 7468) requires a trailing newline after the -----END ...----- line. OpenSSH enforces this — a key file missing the final \n fails with invalid format even though the key content is otherwise valid. This defensive fix handles secrets stored without a trailing newline regardless of how they were stored.

Existing keys that already have a trailing newline are unaffected — the fix is a no-op in that case.

Test plan

  • New test: exec: identityContent without trailing newline gets one appended
  • All 39 existing tests pass (no regressions)
  • deno check, deno lint, deno fmt all clean
## Summary OpenSSH rejects PEM key files that lack a trailing newline with `Load key: invalid format`. This happens when vault-stored keys are stripped of their final newline — for example, `swamp vault put` reading from stdin strips the trailing `\n` from piped content. `materializeTempKeys()` now defensively ensures the written content ends with a newline before writing to the temp file. ## What changed - **`ssh/extensions/models/_lib/operations.ts`** — 2-line fix: check if `identityContent` ends with `\n`, append one if not, before writing to the temp file - **`ssh/extensions/models/ssh.ts`** — version bump to `2026.06.03.1` with upgrades entry - **`ssh/manifest.yaml`** — version bump to `2026.06.03.1` - **`ssh/extensions/models/ssh_test.ts`** — new test verifying a PEM key without trailing newline gets one appended ## Why this is correct The PEM format (RFC 7468) requires a trailing newline after the `-----END ...-----` line. OpenSSH enforces this — a key file missing the final `\n` fails with `invalid format` even though the key content is otherwise valid. This defensive fix handles secrets stored without a trailing newline regardless of how they were stored. Existing keys that already have a trailing newline are unaffected — the fix is a no-op in that case. ## Test plan - [x] New test: `exec: identityContent without trailing newline gets one appended` - [x] All 39 existing tests pass (no regressions) - [x] `deno check`, `deno lint`, `deno fmt` all clean
fix(ssh): ensure identityContent ends with trailing newline (#511)
All checks were successful
CI / workflows/s3-bootstrap - test (pull_request) Has been skipped
CI / workflows/gcs-bootstrap - lockfile up to date (pull_request) Has been skipped
CI / workflows/s3-bootstrap - lockfile up to date (pull_request) Has been skipped
CI / cve/dirtyfrag - check (pull_request) Has been skipped
CI / cve/dirtyfrag - fmt (pull_request) Has been skipped
CI / cve/dirtyfrag - lint (pull_request) Has been skipped
CI / cve/dirtyfrag - test (pull_request) Has been skipped
CI / cve/mini-shai-hulud - check (pull_request) Has been skipped
CI / cve/mini-shai-hulud - fmt (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lint (pull_request) Has been skipped
CI / cve/mini-shai-hulud - test (pull_request) Has been skipped
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / aws models - sample check (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / gcp models - sample check (pull_request) Has been skipped
CI / ssh - lockfile up to date (pull_request) Successful in 1m1s
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 4m30s
CI / cloudflare models - sample check (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / CI Security Review (pull_request) Has been skipped
CI / Claude Code Review (pull_request) Successful in 2m51s
CI / Merge Gate (pull_request) Successful in 24s
CI / codegen - check (pull_request) Has been skipped
CI / Adversarial Code Review (pull_request) Has been skipped
2aa5f291f8
OpenSSH rejects PEM key files that lack a trailing newline with "Load
key: invalid format". This happens when vault-stored keys are stripped
of their final newline (e.g. by `swamp vault put` reading from stdin).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Code Review

Blocking Issues

None.

Suggestions

  1. Missing comment before sanitizeResources: false in new test (ssh_test.ts:677): Every other sanitizeResources: false declaration in this file is preceded by the comment // Deno.makeTempFile's internal handle may outlive the test callback. (lines 388, 423, 455, 490, 521, 584, 637). The new "exec: identityContent without trailing newline gets one appended" test omits it. Not a rules violation (CLAUDE.md's sanitizeResources rule targets connection-pooled SDK clients), but inconsistent with the file's own pattern and makes the rationale less obvious to a future reader.

The fix itself (ic.endsWith("\n") ? ic : ic + "\n" in materializeTempKeys) is correct and minimal. The version bump in both ssh.ts (line 93) and manifest.yaml (line 3) are consistent at 2026.06.03.1. The upgrade entry accurately describes the fix. Existing tests continue to verify that a key with a trailing newline is not double-newlined, and the new test directly covers the no-trailing-newline path. Temp file cleanup in a finally block is intact for the exec/script/copy/open paths. No credential leaks, no any types, all exports are named.

## Code Review ### Blocking Issues None. ### Suggestions 1. **Missing comment before `sanitizeResources: false` in new test** (`ssh_test.ts:677`): Every other `sanitizeResources: false` declaration in this file is preceded by the comment `// Deno.makeTempFile's internal handle may outlive the test callback.` (lines 388, 423, 455, 490, 521, 584, 637). The new `"exec: identityContent without trailing newline gets one appended"` test omits it. Not a rules violation (CLAUDE.md's `sanitizeResources` rule targets connection-pooled SDK clients), but inconsistent with the file's own pattern and makes the rationale less obvious to a future reader. --- The fix itself (`ic.endsWith("\n") ? ic : ic + "\n"` in `materializeTempKeys`) is correct and minimal. The version bump in both `ssh.ts` (line 93) and `manifest.yaml` (line 3) are consistent at `2026.06.03.1`. The upgrade entry accurately describes the fix. Existing tests continue to verify that a key with a trailing newline is not double-newlined, and the new test directly covers the no-trailing-newline path. Temp file cleanup in a `finally` block is intact for the exec/script/copy/open paths. No credential leaks, no `any` types, all exports are named.
stack72 deleted branch worktree-511 2026-06-03 00:50:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
swamp-club/swamp-extensions!25
No description provided.