Skip to content

feat(security): encrypt host TLS private keys at rest - #65

Merged
malickyeu merged 1 commit into
mainfrom
feat/encrypt-host-certs
Jun 16, 2026
Merged

feat(security): encrypt host TLS private keys at rest#65
malickyeu merged 1 commit into
mainfrom
feat/encrypt-host-certs

Conversation

@malickyeu

Copy link
Copy Markdown
Contributor

Summary

A TCP host's client private key (hosts.tls_key) was stored in plaintext in the SQLite DB. This encrypts it with the same AES-256-GCM cipher that already protects registry and SMTP secrets.

  • CreateHost encrypts tls_key before insert; HostByID/ListHosts decrypt it transparently, so every consumer (the docker TLS client in manager.go, ComposeHostEnv for remote deploy) keeps getting the plaintext key. The CA and client certificate are public, so they're stored as-is.
  • Decryption is best-effort, so a legacy plaintext row keeps working; EncryptPlaintextHostKeys (run once at startup after the cipher is set) migrates any plaintext key to ciphertext — idempotent.

Folds into 1.5.0 (not yet tagged) — caught during the Remote Projects review, fixing it now while we know about it.

Type of change

  • New feature / security hardening

Checklist

  • go test -short ./... and go vet ./... pass
  • gofmt gate is clean
  • Added/updated tests for the change
  • CHANGELOG.md entry (under 1.5.0 → Security)

Notes for reviewers

Follows the already-audited registry/LDAP secret-encryption pattern (key-only — certs are public). Tests in internal/store/hosts_test.go: encryption at rest (raw column ≠ plaintext key; CA stays plaintext), transparent decrypt via HostByID/ListHosts, and the idempotent legacy-row migration. Verified no host reader bypasses the decrypting accessors.

A TCP host's client private key (hosts.tls_key) was stored in plaintext in
the SQLite DB. It's now encrypted with the same AES-256-GCM cipher that already
protects registry and SMTP secrets:

- CreateHost encrypts tls_key before insert; HostByID/ListHosts decrypt it
  transparently, so every consumer (the docker TLS client, ComposeHostEnv) keeps
  getting the plaintext key. The CA and client certificate are public, so they
  stay as-is.
- Decryption is best-effort, so a legacy plaintext row keeps working, and
  EncryptPlaintextHostKeys (run once at startup, after the cipher is set)
  migrates any plaintext key to ciphertext — idempotent.

Folds into the 1.5.0 release (not yet tagged). Tests cover encryption at rest
(raw column isn't the plaintext key; CA stays plaintext), transparent decrypt on
read, and the idempotent legacy-row migration.
Copilot AI review requested due to automatic review settings June 16, 2026 12:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@malickyeu
malickyeu merged commit 291111c into main Jun 16, 2026
3 checks passed
@malickyeu
malickyeu deleted the feat/encrypt-host-certs branch June 16, 2026 12:12
malickyeu added a commit that referenced this pull request Jun 16, 2026
The 1.5.0 round updated CHANGELOG + per-feature docs/ pages but left the
headline lists stale:

- README ✨ Features: add image vulnerability scanning, schema-aware Compose
  autocomplete + private-registry tag suggestions, remote-host project deploy,
  alert-rule import/export, LDAP group→section, --make-certs, one-tap in-app
  update, and host TLS keys encrypted at rest.
- NEXT.md: record host TLS-key encryption under Shipped (v1.5) and drop it from
  the RBAC "Next" bullet (it shipped in #65, not a TODO).
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