feat(agent): wipe spent enrollment material; install bundle via stdin (VA-75) #79

Merged
David merged 1 commit from feat/VA-75-enroll-wipe-stdin into main 2026-06-28 21:51:39 +02:00
Owner

Summary

Security hardening of the VA-67 enrollment flow: the agent wipes the single-use bootstrap material after it enrolls, and the installer accepts the bundle on stdin so the Option 2 one-liner can pipe it without leaving a private key in a file.

What changed

  • Wipe spent enrollment material. bootstrap::wipe_enrollment_files() (new) deletes the files named by VERVAIN_ENROLL_CERT_FILE / _KEY_FILE / _ENROLLMENT_CA_FILE / _SERVER_CA_FILE. It runs at the end of net::enroll(), once the issued cert + server CA + domain + group id are persisted. The steady-state connection needs none of the bootstrap material (the issued client cert lives in the store/TPM; the server CA returned by enroll is in the DB), so the single-use enrollment private key no longer lingers on disk.
  • PEMs moved to the data dir. For the wipe to work under the system unit's ProtectSystem=strict (config dir read-only), the installer now writes the bootstrap PEMs into the data dir (/var/lib/vervain-agent, ReadWritePaths/StateDirectory, writable) and points the unit's VERVAIN_*_FILE env there. Mirrored on Windows (%ProgramData%\vervain-agent\data, setx /M). Mode 600 preserved.
  • install --from - (stdin). The installers take the bundle JSON content; main reads it from the file or, when --from -, from stdin. Enables curl .../enroll-bundle?token=... | sudo vervain-agent install --system --from -.
  • Uninstall best-effort removes the bootstrap PEMs from the data dir (covers an uninstall before first enroll); node identity + enrolled cert are preserved.

Why

The enrollment key + cert are single-use and short-TTL, but leaving the private key on disk after it is spent is needless exposure. Wiping it bounds the credential's lifetime to the moment of enrollment. Keeping it in the writable data dir (not the read-only config dir) is what lets the sandboxed service actually delete it.

Acceptance criteria

  • Installer writes the bootstrap PEMs under the data dir (mode 600); the unit env points there.
  • After the first successful enrollment the four bootstrap PEM files are deleted; later runs use the stored identity + server CA and never re-read the bundle.
  • install --system --from - reads the bundle from stdin (no bundle file on disk).
  • just pre-commit green (incl. --features tpm swtpm test and the Windows cross-compile).

Testing

just pre-commit (full check.Dockerfile): fmt, clippy --all-targets -D warnings, build, lib tests (incl. a new bootstrap::wipe_one unit test), Windows cross-compile, agent-gui, and the --features tpm swtpm test.

Completes the agent half of the dual-port onboarding. Server + UI halves tracked in VS-69 (self-contained bundle + one-time bundle-fetch endpoint + delete legacy .vagent minting) and VAPP-38 (SPA "+ New Device" → enroll-tokens). Hardens VA-67.

🤖 Generated with Claude Code

## Summary Security hardening of the VA-67 enrollment flow: the agent wipes the single-use bootstrap material after it enrolls, and the installer accepts the bundle on stdin so the Option 2 one-liner can pipe it without leaving a private key in a file. ## What changed - **Wipe spent enrollment material.** `bootstrap::wipe_enrollment_files()` (new) deletes the files named by `VERVAIN_ENROLL_CERT_FILE` / `_KEY_FILE` / `_ENROLLMENT_CA_FILE` / `_SERVER_CA_FILE`. It runs at the end of `net::enroll()`, once the issued cert + server CA + domain + group id are persisted. The steady-state connection needs none of the bootstrap material (the issued client cert lives in the store/TPM; the server CA returned by enroll is in the DB), so the single-use enrollment **private key** no longer lingers on disk. - **PEMs moved to the data dir.** For the wipe to work under the system unit's `ProtectSystem=strict` (config dir read-only), the installer now writes the bootstrap PEMs into the **data** dir (`/var/lib/vervain-agent`, `ReadWritePaths`/`StateDirectory`, writable) and points the unit's `VERVAIN_*_FILE` env there. Mirrored on Windows (`%ProgramData%\vervain-agent\data`, `setx /M`). Mode 600 preserved. - **`install --from -` (stdin).** The installers take the bundle JSON content; `main` reads it from the file or, when `--from -`, from stdin. Enables `curl .../enroll-bundle?token=... | sudo vervain-agent install --system --from -`. - **Uninstall** best-effort removes the bootstrap PEMs from the data dir (covers an uninstall before first enroll); node identity + enrolled cert are preserved. ## Why The enrollment key + cert are single-use and short-TTL, but leaving the private key on disk after it is spent is needless exposure. Wiping it bounds the credential's lifetime to the moment of enrollment. Keeping it in the writable data dir (not the read-only config dir) is what lets the sandboxed service actually delete it. ## Acceptance criteria - [x] Installer writes the bootstrap PEMs under the data dir (mode 600); the unit env points there. - [x] After the first successful enrollment the four bootstrap PEM files are deleted; later runs use the stored identity + server CA and never re-read the bundle. - [x] `install --system --from -` reads the bundle from stdin (no bundle file on disk). - [x] `just pre-commit` green (incl. `--features tpm` swtpm test and the Windows cross-compile). ## Testing `just pre-commit` (full `check.Dockerfile`): fmt, `clippy --all-targets -D warnings`, build, lib tests (incl. a new `bootstrap::wipe_one` unit test), Windows cross-compile, agent-gui, and the `--features tpm` swtpm test. ## Related Completes the agent half of the dual-port onboarding. Server + UI halves tracked in VS-69 (self-contained bundle + one-time bundle-fetch endpoint + delete legacy `.vagent` minting) and VAPP-38 (SPA "+ New Device" → enroll-tokens). Hardens VA-67. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(agent): wipe spent enrollment material; install bundle via stdin
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 8m24s
Create release / Create release from merged PR (pull_request) Has been skipped
4f907826fa
After a successful enrollment the agent now deletes the one-time bootstrap PEMs (enrollment cert + key, enrollment sub-CA, server CA) from disk via `bootstrap::wipe_enrollment_files()`, called at the end of `net::enroll()`. The single-use enrollment private key must not linger once spent; everything the steady-state mTLS connection needs (issued client cert, server CA) is already in the encrypted store/TPM.

For the wipe to work under the system unit's `ProtectSystem=strict` (which mounts the config dir read-only), the installer now writes the bootstrap PEMs into the DATA dir (writable, `ReadWritePaths`/`StateDirectory`) instead of the config dir, and points the unit's `VERVAIN_*_FILE` env there. Mirrored on the Windows installer (`%ProgramData%\vervain-agent\data`, `setx /M`).

`install --from -` reads the bundle JSON from stdin so the Option 2 one-liner can pipe it without writing a separate file: `curl ... | sudo vervain-agent install --system --from -`. The installers now take the bundle content; `main` reads it from the file or stdin.

Uninstall also best-effort removes the bootstrap PEMs from the data dir (catches an uninstall before first enroll); the rest of the data dir (node identity, enrolled cert) is preserved.

#VA-75

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
David merged commit 74cd41ed6e into main 2026-06-28 21:51:39 +02:00
David deleted branch feat/VA-75-enroll-wipe-stdin 2026-06-28 21:51:39 +02: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
psa-systems/vervain-agent!79
No description provided.