Skip to content

PMM-14979 Detect and reject a mismatched encryption key - #5735

Draft
ademidoff wants to merge 2 commits into
PMM-15188from
PMM-14979-share-enc-key-between-nodes
Draft

PMM-14979 Detect and reject a mismatched encryption key#5735
ademidoff wants to merge 2 commits into
PMM-15188from
PMM-14979-share-enc-key-between-nodes

Conversation

@ademidoff

@ademidoff ademidoff commented Aug 5, 2026

Copy link
Copy Markdown
Member

Ticket number: PMM-14979

Feature build: Percona-Lab/pmm-submodules#4510

Important

Based on #5587 (PMM-15188) to avoid conflicts: both change managed/utils/encryption/encryption.go and managed/models/database.go. Retarget to main once #5587 merges. Review the diff against that base.

Problem

Reported on the forum against a 3-node Docker HA cluster: after pmm-agents reconnected, QAN and postgres_exporter data vanished for several PostgreSQL services, and agents logged

pq: password authentication failed for user "AQ+rKT/93psPSlwWLR8Qb0zsQqLDdhfXYNB9EBYk+507mw1Y"

That "username" is the encrypted value from the agents table. Followers only logged decryption: aead_factory: decryption failed at warning level.

In HA all nodes share one PostgreSQL database, but each reads its encryption key from a local file, and New() in managed/utils/encryption/encryption.go generates one whenever the file is absent. migrateDB encrypts on whichever node migrates first and records the columns in settings.EncryptedItems, so the other nodes skip encryption and keep an unrelated key.

The failure was silent because Decrypt returned the input ciphertext next to the error, and agentEncryption in managed/models/encryption_helpers.go logged a warning and assigned that value anyway. The ciphertext then flowed through Agent.DSN (managed/models/agent_model.go:543) into SetState.

A worse variant was reachable: if a follower won the migration race on a fresh database, the leader's later writes used a different key, leaving one table encrypted under two keys, which pmm-encryption-rotation cannot repair.

Changes

Fail closed rather than degrade

  • Decrypt and Encrypt return an empty string on failure, so a caller that ignores the error cannot pass ciphertext off as plaintext or persist a secret unencrypted. The ciphertext is no longer interpolated into the base64 error message, which was putting secret material in logs.
  • EncryptAgent/DecryptAgent return an error; all 20 call sites in managed/models/agent_helpers.go, managed/services/agents/service_info_broker.go and managed/services/realtimeanalytics/service.go propagate it. Errors name the agent and column.

Detect a mismatched key

  • Settings.EncryptionKeyFingerprint plus encryption.Fingerprint(). Recorded in dbEncryption in the same transaction that marks the columns encrypted, so a concurrently starting node cannot see encrypted data with no fingerprint and adopt its own key. Cleared on the decrypt path, which keeps key rotation (decrypt -> new key -> encrypt) correct.
  • models.VerifyEncryptionKey compares the local key at startup. Under HA a mismatch is fatal: the remedy is copying one file, and continuing means handing out unusable credentials. Standalone PMM logs it and sets pmm_managed_encryption_key_mismatch, so an installation whose key went missing does not stop booting on upgrade. Databases with no fingerprint adopt the local key only if it decrypts what is already stored.

Prevent it at the source

  • pmm-managed-init refuses to start an HA node with no key file instead of letting one be generated per node. build/docker/server/entrypoint.sh runs under set -o errexit, so the non-zero exit does abort startup.

Documentation

  • documentation/docs/admin/security/data_encryption.md described the key as 32 raw bytes; the code writes a base64-encoded Tink keyset, so anyone following it to pre-provision a shared key would have hit a startup failure. Corrected, with an HA section.
  • Added key handling to documentation/docs/install-pmm/install-HA-clustered.md (which had no mention of encryption) and a warning to the Docker PMM_HA_* table in documentation/docs/install-pmm/install-pmm-server/deployment-options/docker/preview_env_var.md.

Related

Testing

Added: encrypt/decrypt round trip and a regression test that a value this node cannot decrypt surfaces as an error instead of being returned (managed/models/encryption_helpers_test.go); five fingerprint cases via sqlmock, including the reported follower scenario (managed/models/encryption_key_test.go); the HA preflight (managed/cmd/pmm-managed-init/main_test.go).

go build ./... and go vet ./... clean, golangci-lint reports 0 new issues against the base.

Not verified locally: testdb-backed suites need /srv/.postgres_password, so they fail with pq: password authentication failed here. Failure counts are identical on this branch and the base, but TestDefaultAgentEncryptionColumnsRoundTrip and the realtimeanalytics suite are unexercised against these changes and need CI.

In HA every PMM Server node shares one PostgreSQL database but reads its
encryption key from a local file. A node that generated its own key could
not decrypt the credentials stored by the others, and that failure was
silent: Decrypt returned the input ciphertext alongside the error, and
agentEncryption logged a warning and assigned that value anyway. The
ciphertext then travelled through Agent.DSN into SetState, so pmm-agent
received it in place of a username and reported

    pq: password authentication failed for user "AQ+rKT/93psPS..."

while QAN and postgres_exporter data disappeared for the affected
services.

Fail closed instead of degrading:

- Decrypt and Encrypt now return an empty string on failure, so a caller
  that ignores the error cannot pass ciphertext off as plaintext, nor
  persist a secret unencrypted. The ciphertext is also no longer included
  in the base64 error message, which put secret material in the log.
- EncryptAgent and DecryptAgent return an error, and the call sites in
  models, service_info_broker and realtimeanalytics propagate it. The
  error names the agent and the column, so a bad row can be identified.
- Settings gains EncryptionKeyFingerprint, recorded in the same
  transaction that marks the columns encrypted. A node compares its own
  key against it at startup: under HA a mismatch is fatal, since the
  remedy is copying one file and continuing would hand out unusable
  credentials and write rows the other nodes cannot read. Standalone PMM
  logs the problem and exposes pmm_managed_encryption_key_mismatch so an
  installation whose key went missing does not stop booting on upgrade.
  Databases with no fingerprint recorded adopt the local key only if it
  decrypts what is already stored.
- pmm-managed-init refuses to start an HA node that has no key, rather
  than letting one be generated per node.

The documentation described the key file as 32 raw bytes, but the code
writes a base64-encoded Tink keyset, so anyone following it to
pre-provision a shared key would have hit a startup failure. It now
documents the real format, how to generate a key, and that HA nodes must
share one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant