Skip to content

Encrypt Tentacle certificate password and subscription id at rest#449

Merged
ppXD merged 3 commits into
mainfrom
feat/encrypt-tentacle-key-at-rest
Jun 15, 2026
Merged

Encrypt Tentacle certificate password and subscription id at rest#449
ppXD merged 3 commits into
mainfrom
feat/encrypt-tentacle-key-at-rest

Conversation

@ppXD

@ppXD ppXD commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • The Tentacle's at-rest encryption — machine-key AES-256-GCM protecting the certificate password (which guards the PFX holding the agent's private key) and the polling subscription id — was implemented and unit-tested in TentacleCertificateManager, but never wired: the runtime (TentacleApp) and all five CLI commands (register, show-thumbprint, show-config, check-services, new-certificate) constructed the encryptor-less overload, so production agents wrote both secrets in plaintext on disk.
  • Add ProductionTentacleCertificateManager as the single construction site that wires the machine-key encryptor, and route all six production sites through it. Routing every site through one factory keeps the write path (register / new-certificate) and the read paths (show-thumbprint, the runtime load) in agreement on whether the cert password is encrypted — otherwise a writer storing a random encrypted password and a reader built without the encryptor would fall back to the legacy fixed password and fail to open the PFX.
  • Completes the P2 at-rest encryption series (account credentials, certificate, external feed, SSH proxy password already shipped).

Non-breaking

  • If the machine-key encryptor cannot be initialised (key derivation throws on an unusual host), the factory degrades to the legacy plaintext manager rather than blocking Tentacle startup — at-rest encryption is best-effort hardening, never a startup gate.
  • Existing plaintext installs migrate seamlessly on first encryptor-aware load: TentacleCertificateManager already writes the existing password / subscription id back in encrypted form (the migration logic predates this PR). No data migration required.
  • The encryptor-less TentacleCertificateManager(certsPath) constructor stays for tests and back-compat.

Test plan

  • Unit (real AES-256-GCM + real file IO + real X509): ProductionTentacleCertificateManagerTests — the factory produces a manager that encrypts the subscription id (v1: envelope + .encrypted marker) and the cert password at rest, round-trips across reopen, and never returns null.
  • Existing TentacleCertificateManagerEncryptorTests / TentacleCertificateManagerTests still green (21/21 cert-manager tests pass).
  • E2E (Linux, real binary + real /etc/squid-tentacle filesystem): D5h_RegisterWritesEncryptedSecretsAtRest — after a real register, the on-disk subscription-id and tentacle-cert.pfx.pwd are v1:-encrypted. The existing D1h register→show-thumbprint round-trip guards write/read path agreement.
  • Full Tentacle unit suite: 1547 pass; 14 pre-existing LocalScriptServiceIdempotencyTests failures are unrelated (process/state-file tests that also fail on main on this host).

The Tentacle's at-rest encryption (machine-key AES-256-GCM protecting the
certificate password, which guards the PFX private key, and the polling
subscription id) was implemented and unit-tested in TentacleCertificateManager
but never wired: the runtime and all five CLI commands constructed the
encryptor-less overload, so production agents wrote both secrets in plaintext.

Add ProductionTentacleCertificateManager as the single construction site that
wires the machine-key encryptor (degrading to the legacy plaintext manager only
if key derivation fails, so it never blocks startup) and route the runtime +
register/show-thumbprint/show-config/check-services/new-certificate through it.
Routing every site through one factory keeps the write and read paths in
agreement on whether the cert password is encrypted.

Non-breaking: existing plaintext installs migrate on first encryptor-aware load
(TentacleCertificateManager already writes the existing secrets back encrypted),
so no data migration is needed.
@ppXD ppXD added this to the 1.9.3 milestone Jun 15, 2026
ppXD added 2 commits June 15, 2026 13:53
Adversarial review of the at-rest wiring surfaced two issues:

1. Machine-id drift bricked the agent. Wiring a real encryptor in production
   made the PFX password random+encrypted, so a host machine-id change (VM
   clone, container rebuild, /etc/machine-id reset) left the password
   undecryptable: ResolveCertPassword fell back to the wrong legacy password
   and LoadPkcs12FromFile threw an UNCAUGHT CryptographicException -> boot
   crash-loop, and even register crashed. The subscription-id path was
   asymmetric, returning the raw ciphertext AS the identity (corrupt). Now
   both paths treat an undecryptable secret as lost identity: regenerate a
   fresh cert / subscription id (operator re-registers), the same graceful
   recovery on both. Narrowed to CryptographicException so transient IO still
   propagates; legacy plaintext migration is unaffected (it never hits the
   decrypt-failure branch).

2. No cross-platform guard stopped a future production site reverting to the
   encryptor-less constructor (silent plaintext regression, caught only by the
   Linux-skipped E2E). Add a source-scan unit test (Rule 12.5) asserting
   'new TentacleCertificateManager(' appears in src only inside the factory.

Also: strengthen D5h to prove the encrypted password actually decrypts and
opens the PFX (show-thumbprint reopen), document the threat boundary on the
factory, and note the defensive degrade catch is unreachable under the current
MachineIdProvider contract.
…trip

Re-verification of the prior hardening surfaced two low-severity gaps:

- The drift guard used a plain substring match, so a reformatted
  (multi-space) or fully-qualified revert to the bare constructor would
  evade it. Switch to a whitespace/qualifier-tolerant regex (still requires
  'new', so the ctor declaration is never flagged). Red-green confirmed
  against a fully-qualified injection.

- The D5h reopen assertion only checked for any 40-char thumbprint, but the
  new regenerate-on-decrypt-failure behaviour means show-thumbprint exits 0
  with a NEW thumbprint even on a writer/reader password mismatch — the exact
  failure the assertion claimed to catch. Assert it equals the thumbprint
  register persisted, making it a true reopen round-trip.
@ppXD
ppXD merged commit b0e4834 into main Jun 15, 2026
15 checks passed
@ppXD
ppXD deleted the feat/encrypt-tentacle-key-at-rest branch June 15, 2026 06:16
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.

1 participant