Skip to content

fix(clients): decrypt client secret on restore (apply-to-new-node bug)#92

Merged
amirotin merged 1 commit into
mainfrom
fix/client-secret-decrypt-on-restore
May 28, 2026
Merged

fix(clients): decrypt client secret on restore (apply-to-new-node bug)#92
amirotin merged 1 commit into
mainfrom
fix/client-secret-decrypt-on-restore

Conversation

@amirotin
Copy link
Copy Markdown
Contributor

Bug

Assigning an existing (adopted or created) client to a newly added node fails the rollout with:
apply client failed: bad_request: secret must be exactly 32 hex characters (the error comes from telemt — the agent shipped a non-32-hex secret).

Root cause

The clients Service encrypts secrets at rest (secret_ciphertext / PVS2:) and keeps plaintext in the in-memory mirror only transiently after Save/SaveState/adopt. No read path decryptsService.Restore (startup / mirror reload) loads the ciphertext straight into the mirror and decryptSecret was never called anywhere. After a panel restart, every client's in-memory secret is the PVS2: ciphertext, so the next client-apply job ships ciphertext to telemt → rejected.

Adopt only exposed it: adopted clients matched telemt's existing config on the original nodes (no real apply happened), so the first true apply was the new-node assignment.

Secondary corruption: editing such a client (updateClient → SaveState) re-encrypted the already-ciphertext secret into PVS2:PVS2:… in the DB.

Fix

  • Restore decrypts each secret into the mirror via decryptSecretFully, which peels every layer so double-wrapped rows self-heal on next load.
  • encryptSecret is now idempotent — never re-encrypts an already vault-prefixed value, closing the double-wrap mechanism on all save paths.

Tests

Reproduce the restore-without-decrypt failure, the double-wrap heal, and encrypt idempotency. Full clients + server suites pass; go vet clean.

Operator remediation

Deploy + restart the panel — Restore heals the mirror (incl. any double-wrapped row); the next save rewrites the DB row single-encrypted. No manual secret reset required.

Assigning an (adopted or created) client to a new node failed the
rollout with 'apply client failed: bad_request: secret must be exactly
32 hex characters'. The clients Service stores secrets encrypted at rest
(secret_ciphertext / PVS2:) and keeps plaintext in the in-memory mirror
only transiently after Save/SaveState/adopt. No read path decrypted:
Service.Restore (run at startup / mirror reload) loaded the ciphertext
straight into the mirror, and decryptSecret was never called anywhere.
After a panel restart every client's in-memory secret was the PVS2:
ciphertext, so the next client-apply job shipped that ciphertext to
telemt, which rejected it. Adopt merely exposed it: adopted clients
matched telemt's existing config on the original nodes (no real apply),
so the first true apply was the new-node assignment.

Secondary corruption: editing such a client (updateClient -> SaveState)
re-encrypted the already-ciphertext secret into PVS2:PVS2:… in the DB.

Fix:
- Restore now decrypts each secret into the mirror via decryptSecretFully,
  which peels every layer so double-wrapped rows self-heal on next load.
- encryptSecret is idempotent: it never re-encrypts an already
  vault-prefixed value, closing the double-wrap mechanism on all save
  paths.

Tests: reproduce the restore-without-decrypt failure, the double-wrap
heal, and encrypt idempotency. Full clients + server suites pass.

Operator remediation: deploy + restart the panel — Restore heals the
mirror (incl. any double-wrapped row); the next save rewrites the DB
row single-encrypted.
Copilot AI review requested due to automatic review settings May 28, 2026 21:49
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a bug where client secrets stored encrypted (PVS2:) were loaded into the in-memory mirror as ciphertext on panel restart, causing client-apply jobs to ship ciphertext to telemt and fail validation. Also addresses secondary double-encryption corruption from saves that ran with a stale ciphertext mirror.

Changes:

  • Service.Restore now decrypts each client secret into the mirror via a new decryptSecretFully helper that peels multiple encryption layers to heal double-wrapped rows.
  • encryptSecret is made idempotent — returns vault-prefixed inputs unchanged rather than re-encrypting them.
  • Adds tests for restore decryption, double-wrap self-healing, and encrypt idempotency.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
internal/controlplane/clients/service.go Decrypt secrets in Restore, add decryptSecretFully, make encryptSecret idempotent.
internal/controlplane/clients/encryption_test.go New tests covering restore decrypt, double-wrap heal, and idempotent encrypt.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@amirotin amirotin merged commit a97eaf2 into main May 28, 2026
27 checks passed
@amirotin amirotin deleted the fix/client-secret-decrypt-on-restore branch May 28, 2026 21:57
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