Skip to content

K8SPG-911: Add pg_tde support#1440

Draft
egegunes wants to merge 2 commits intomainfrom
K8SPG-911
Draft

K8SPG-911: Add pg_tde support#1440
egegunes wants to merge 2 commits intomainfrom
K8SPG-911

Conversation

@egegunes
Copy link
Contributor

CHANGE DESCRIPTION

Problem:
Short explanation of the problem.

Cause:
Short explanation of the root cause of the issue if applicable.

Solution:
Short explanation of the solution we are providing with this PR.

CHECKLIST

Jira

  • Is the Jira ticket created and referenced properly?
  • Does the Jira ticket have the proper statuses for documentation (Needs Doc) and QA (Needs QA)?
  • Does the Jira ticket link to the proper milestone (Fix Version field)?

Tests

  • Is an E2E test/test case added for the new feature/change?
  • Are unit tests added where appropriate?

Config/Logging/Testability

  • Are all needed new/changed options added to default YAML files?
  • Are all needed new/changed options added to the Helm Chart?
  • Did we add proper logging messages for operator actions?
  • Did we ensure compatibility with the previous version or cluster upgrade process?
  • Does the change support oldest and newest supported PG version?
  • Does the change support oldest and newest supported Kubernetes version?

Copilot AI review requested due to automatic review settings February 12, 2026 13:58
@egegunes egegunes added this to the v2.9.0 milestone Feb 12, 2026
Copy link

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

This pull request adds pg_tde (Transparent Data Encryption) support for PostgreSQL clusters, integrating with HashiCorp Vault for key management. The implementation follows the existing extension pattern in the codebase and includes API changes, reconciliation logic, and CRD updates.

Changes:

  • Added pg_tde extension support with Vault integration for managing encryption keys
  • Introduced new API types (PGTDESpec, PGTDEVaultSpec, PGTDESecretObjectReference) for configuring pg_tde
  • Refactored extension configuration to use individual extension structs instead of the deprecated builtin structure
  • Added reconciliation logic for configuring pg_tde providers and managing encryption keys

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go Added PGTDESpec, PGTDEVaultSpec, and PGTDESecretObjectReference types for pg_tde configuration
pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go Auto-generated DeepCopy methods for new pg_tde types
pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go Added BuiltInExtensionSpec and pg_tde support; refactored extension defaults handling
pkg/apis/pgv2.percona.com/v2/zz_generated.deepcopy.go Auto-generated DeepCopy methods for BuiltInExtensionSpec
internal/pgtde/postgres.go New package implementing pg_tde enable/disable and Vault provider configuration
internal/postgres/reconcile.go Added pg_tde volume and volume mount configuration for PostgreSQL instance pods
internal/controller/postgrescluster/postgres.go Added reconcilePGTDEProviders function to configure pg_tde Vault providers
internal/controller/postgrescluster/controller.go Integrated pg_tde parameters and provider reconciliation into main reconciliation flow
internal/controller/postgrescluster/instance.go Added TDEInstalledAnnotation to instance pods when pg_tde is enabled
internal/naming/names.go Added constants for pg_tde volume names, mount paths, and provider identifiers
internal/naming/annotations.go Added TDEInstalledAnnotation constant
internal/pgvector/postgres.go Fixed comments to correctly reference pgvector instead of pgAudit
percona/controller/pgbackup/controller.go Added pg.Default() call to ensure extension defaults before backup
deploy/.yaml and config/crd/bases/.yaml Updated CRD definitions with pg_tde specifications
deploy/cr.yaml Added example pg_tde configuration with Vault settings
pkg/apis/pgv2.percona.com/v2/perconapgbackup_types_test.go Replaced custom ptr() function with k8s.io/utils/ptr.To
Comments suppressed due to low confidence (1)

pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go:436

  • Inconsistent usage of extension enabled flags. Line 432 uses cr.Spec.Extensions.PGStatMonitor.Enabled (the new field structure), while lines 433-436 still use cr.Spec.Extensions.BuiltIn.* (the deprecated field structure). All lines should be updated to use the new field structure consistently for maintainability.
	postgresCluster.Spec.Extensions.PGStatMonitor = *cr.Spec.Extensions.PGStatMonitor.Enabled
	postgresCluster.Spec.Extensions.PGStatStatements = *cr.Spec.Extensions.BuiltIn.PGStatStatements
	postgresCluster.Spec.Extensions.PGAudit = *cr.Spec.Extensions.BuiltIn.PGAudit
	postgresCluster.Spec.Extensions.PGVector = *cr.Spec.Extensions.BuiltIn.PGVector
	postgresCluster.Spec.Extensions.PGRepack = *cr.Spec.Extensions.BuiltIn.PGRepack

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

Comment on lines 111 to 115
pgTDECASecret := &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: inCluster.Spec.Extensions.PGTDE.Vault.CASecret.Name,
},
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The SecretProjection is missing the Items field to specify which key from the secret should be mounted. Without Items, all keys in the secret will be mounted. Based on the pattern used elsewhere in the codebase (e.g., internal/patroni/certificates.go), you should add an Items field to specify the exact key to mount from the secret.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +108
stdout, stderr, err := exec.Exec(ctx,
strings.NewReader(strings.Join([]string{
// Quiet NOTICE messages from IF NOT EXISTS statements.
// - https://www.postgresql.org/docs/current/runtime-config-client.html
`SET client_min_messages = WARNING;`,
fmt.Sprintf(`SELECT pg_tde_change_global_key_provider_vault_v2(
'vault-provider', '%s', '%s', '%s', '%s'
);`,
vault.Host,
vault.MountPath,
naming.PGTDEMountPath+"/"+vault.TokenSecret.Key,
naming.PGTDEMountPath+"/"+vault.CASecret.Key,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The function doesn't check if vault.CASecret.Key is empty before using it in the SQL statement. When CASecret.Key is empty, this will result in an invalid file path being passed to the pg_tde function. The function should handle the empty case similar to AddVaultProvider which uses "NULL" when CASecret.Key is empty.

Suggested change
stdout, stderr, err := exec.Exec(ctx,
strings.NewReader(strings.Join([]string{
// Quiet NOTICE messages from IF NOT EXISTS statements.
// - https://www.postgresql.org/docs/current/runtime-config-client.html
`SET client_min_messages = WARNING;`,
fmt.Sprintf(`SELECT pg_tde_change_global_key_provider_vault_v2(
'vault-provider', '%s', '%s', '%s', '%s'
);`,
vault.Host,
vault.MountPath,
naming.PGTDEMountPath+"/"+vault.TokenSecret.Key,
naming.PGTDEMountPath+"/"+vault.CASecret.Key,
// Handle optional CA secret key similarly to AddVaultProvider: use NULL when empty.
caPath := "NULL"
if vault.CASecret.Key != "" {
caPath = fmt.Sprintf("'%s'", naming.PGTDEMountPath+"/"+vault.CASecret.Key)
}
stdout, stderr, err := exec.Exec(ctx,
strings.NewReader(strings.Join([]string{
// Quiet NOTICE messages from IF NOT EXISTS statements.
// - https://www.postgresql.org/docs/current/runtime-config-client.html
`SET client_min_messages = WARNING;`,
fmt.Sprintf(`SELECT pg_tde_change_global_key_provider_vault_v2(
'vault-provider', '%s', '%s', '%s', %s
);`,
vault.Host,
vault.MountPath,
naming.PGTDEMountPath+"/"+vault.TokenSecret.Key,
caPath,

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +104
'vault-provider', '%s', '%s', '%s', '%s'
);`,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The provider name is hard-coded as 'vault-provider' instead of using the naming.PGTDEVaultProvider constant. This inconsistency could lead to configuration issues since AddVaultProvider uses the constant. Both functions should use the same constant for consistency.

Suggested change
'vault-provider', '%s', '%s', '%s', '%s'
);`,
'%s', '%s', '%s', '%s', '%s'
);`,
naming.PGTDEVaultProvider,

Copilot uses AI. Check for mistakes.
Comment on lines +452 to +457
configure := func(ctx context.Context, exec postgres.Executor) error {
if cluster.Status.PGTDERevision == "" {
return pgtde.AddVaultProvider(ctx, exec, cluster.Spec.Extensions.PGTDE.Vault)
}
return pgtde.ChangeVaultProvider(ctx, exec, cluster.Spec.Extensions.PGTDE.Vault)
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

There's no validation to ensure cluster.Spec.Extensions.PGTDE.Vault is not nil before calling AddVaultProvider or ChangeVaultProvider. If pg_tde is enabled but the Vault configuration is not provided, this will result in a nil pointer dereference. Add a nil check for Vault configuration before using it.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +109
fmt.Sprintf(`SELECT pg_tde_change_global_key_provider_vault_v2(
'vault-provider', '%s', '%s', '%s', '%s'
);`,
vault.Host,
vault.MountPath,
naming.PGTDEMountPath+"/"+vault.TokenSecret.Key,
naming.PGTDEMountPath+"/"+vault.CASecret.Key,
),
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The SQL statement construction is vulnerable to SQL injection as user-provided values from vault.Host, vault.MountPath, vault.TokenSecret.Key, and vault.CASecret.Key are directly interpolated into the SQL string using fmt.Sprintf without any sanitization or parameterization. Although these values come from the cluster spec, they should still be properly escaped or parameterized to prevent SQL injection attacks and handle special characters correctly. Consider using proper SQL parameterization or at minimum sanitizing these inputs.

Copilot uses AI. Check for mistakes.
Comment on lines 94 to 98
pgTDETokenSecret := &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: inCluster.Spec.Extensions.PGTDE.Vault.TokenSecret.Name,
},
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The SecretProjection is missing the Items field to specify which key from the secret should be mounted. Without Items, all keys in the secret will be mounted, which may not be the intended behavior. Based on the pattern used elsewhere in the codebase (e.g., internal/patroni/certificates.go), you should add an Items field to specify the exact key to mount from the secret.

Copilot uses AI. Check for mistakes.
crunchyv1beta1 "github.com/percona/percona-postgresql-operator/v2/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
)

// EnableInPostgreSQL installs pgvector triggers into every database.
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The comment incorrectly says "installs pgvector triggers" but this function installs pg_tde extension, not pgvector. This should be updated to accurately describe the function's purpose.

Suggested change
// EnableInPostgreSQL installs pgvector triggers into every database.
// EnableInPostgreSQL installs and updates the pg_tde extension in every database.

Copilot uses AI. Check for mistakes.
Comment on lines 89 to 120
pgTDEVolumeMount := corev1.VolumeMount{
Name: naming.PGTDEVolume,
MountPath: naming.PGTDEMountPath,
ReadOnly: true,
}
pgTDETokenSecret := &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: inCluster.Spec.Extensions.PGTDE.Vault.TokenSecret.Name,
},
}
pgTDEVolume := corev1.Volume{
Name: pgTDEVolumeMount.Name,
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
DefaultMode: initialize.Int32(0o600),
Sources: []corev1.VolumeProjection{
{Secret: pgTDETokenSecret},
},
},
},
}
if inCluster.Spec.Extensions.PGTDE.Vault.CASecret.Name != "" {
pgTDECASecret := &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: inCluster.Spec.Extensions.PGTDE.Vault.CASecret.Name,
},
}
pgTDEVolume.VolumeSource.Projected.Sources = append(
pgTDEVolume.VolumeSource.Projected.Sources, corev1.VolumeProjection{
Secret: pgTDECASecret,
})
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The pg_tde volume and volume mount are created unconditionally at the beginning of the function, even when PGTDE is not enabled. This creates unused volume objects that are only conditionally added to the pod later. Consider moving this logic inside the conditional check at lines 193-195 and 275-277 to avoid creating unnecessary objects.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +70
fmt.Sprintf(`SELECT pg_tde_add_global_key_provider_vault_v2(
'%s', '%s', '%s', '%s', %s
);`,
naming.PGTDEVaultProvider,
vault.Host,
vault.MountPath,
naming.PGTDEMountPath+"/"+vault.TokenSecret.Key,
caSecretPath,
),
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The SQL statement construction is vulnerable to SQL injection as user-provided values from vault.Host, vault.MountPath, vault.TokenSecret.Key, and vault.CASecret.Key are directly interpolated into the SQL string using fmt.Sprintf without any sanitization or parameterization. Although these values come from the cluster spec, they should still be properly escaped or parameterized to prevent SQL injection attacks and handle special characters correctly. Consider using proper SQL parameterization or at minimum sanitizing these inputs.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +123
package pgtde

import (
"context"
"fmt"
"strings"

"github.com/percona/percona-postgresql-operator/v2/internal/logging"
"github.com/percona/percona-postgresql-operator/v2/internal/naming"
"github.com/percona/percona-postgresql-operator/v2/internal/postgres"
crunchyv1beta1 "github.com/percona/percona-postgresql-operator/v2/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
)

// EnableInPostgreSQL installs pgvector triggers into every database.
func EnableInPostgreSQL(ctx context.Context, exec postgres.Executor) error {
log := logging.FromContext(ctx)

stdout, stderr, err := exec.ExecInAllDatabases(ctx,
`SET client_min_messages = WARNING; CREATE EXTENSION IF NOT EXISTS pg_tde; ALTER EXTENSION pg_tde UPDATE;`,
map[string]string{
"ON_ERROR_STOP": "on", // Abort when any one command fails.
"QUIET": "on", // Do not print successful commands to stdout.
})

log.V(1).Info("enabled pg_tde", "stdout", stdout, "stderr", stderr)

return err
}

func DisableInPostgreSQL(ctx context.Context, exec postgres.Executor) error {
log := logging.FromContext(ctx)

stdout, stderr, err := exec.ExecInAllDatabases(ctx,
`SET client_min_messages = WARNING; DROP EXTENSION IF EXISTS pg_tde;`,
map[string]string{
"ON_ERROR_STOP": "on", // Abort when any one command fails.
"QUIET": "on", // Do not print successful commands to stdout.
})

log.V(1).Info("disabled pg_tde", "stdout", stdout, "stderr", stderr)

return err
}

func PostgreSQLParameters(outParameters *postgres.Parameters) {
outParameters.Mandatory.AppendToList("shared_preload_libraries", "pg_tde")
}

func AddVaultProvider(ctx context.Context, exec postgres.Executor, vault *crunchyv1beta1.PGTDEVaultSpec) error {
log := logging.FromContext(ctx)

caSecretPath := "NULL"
if vault.CASecret.Key != "" {
caSecretPath = fmt.Sprintf("'%s'", naming.PGTDEMountPath+"/"+vault.CASecret.Key)
}

stdout, stderr, err := exec.Exec(ctx,
strings.NewReader(strings.Join([]string{
// Quiet NOTICE messages from IF NOT EXISTS statements.
// - https://www.postgresql.org/docs/current/runtime-config-client.html
`SET client_min_messages = WARNING;`,
fmt.Sprintf(`SELECT pg_tde_add_global_key_provider_vault_v2(
'%s', '%s', '%s', '%s', %s
);`,
naming.PGTDEVaultProvider,
vault.Host,
vault.MountPath,
naming.PGTDEMountPath+"/"+vault.TokenSecret.Key,
caSecretPath,
),
fmt.Sprintf("SELECT pg_tde_create_key_using_global_key_provider('%s', '%s');",
naming.PGTDEGlobalKey,
naming.PGTDEVaultProvider,
),
fmt.Sprintf("SELECT pg_tde_set_default_key_using_global_key_provider('%s', '%s');",
naming.PGTDEGlobalKey,
naming.PGTDEVaultProvider,
),
}, "\n")),
map[string]string{
"ON_ERROR_STOP": "on", // Abort when any one statement fails.
"QUIET": "on", // Do not print successful statements to stdout.
}, nil)

if err != nil {
log.Info("failed to add pg_tde vault provider", "stdout", stdout, "stderr", stderr)
} else {
log.Info("added pg_tde vault provider", "stdout", stdout, "stderr", stderr)
}

return err
}

func ChangeVaultProvider(ctx context.Context, exec postgres.Executor, vault *crunchyv1beta1.PGTDEVaultSpec) error {
log := logging.FromContext(ctx)

stdout, stderr, err := exec.Exec(ctx,
strings.NewReader(strings.Join([]string{
// Quiet NOTICE messages from IF NOT EXISTS statements.
// - https://www.postgresql.org/docs/current/runtime-config-client.html
`SET client_min_messages = WARNING;`,
fmt.Sprintf(`SELECT pg_tde_change_global_key_provider_vault_v2(
'vault-provider', '%s', '%s', '%s', '%s'
);`,
vault.Host,
vault.MountPath,
naming.PGTDEMountPath+"/"+vault.TokenSecret.Key,
naming.PGTDEMountPath+"/"+vault.CASecret.Key,
),
}, "\n")),
map[string]string{
"ON_ERROR_STOP": "on", // Abort when any one statement fails.
"QUIET": "on", // Do not print successful statements to stdout.
}, nil)

if err != nil {
log.Info("failed to change pg_tde vault provider", "stdout", stdout, "stderr", stderr)
} else {
log.Info("changed pg_tde vault provider", "stdout", stdout, "stderr", stderr)
}

return err
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The internal/pgtde package lacks test coverage. Similar extension packages in the codebase (e.g., internal/pgvector/postgres_test.go, internal/pgaudit/postgres_test.go) have corresponding test files. Consider adding tests for the pgtde package functions, particularly for EnableInPostgreSQL, DisableInPostgreSQL, AddVaultProvider, and ChangeVaultProvider.

Copilot uses AI. Check for mistakes.
@JNKPercona
Copy link
Collaborator

Test Name Result Time
backup-enable-disable passed 00:08:07
builtin-extensions passed 00:04:44
custom-envs passed 00:19:04
custom-extensions failure 00:11:35
custom-tls passed 00:05:11
database-init-sql passed 00:02:04
demand-backup passed 00:25:53
finalizers passed 00:03:33
init-deploy passed 00:02:56
huge-pages passed 00:02:58
monitoring passed 00:07:11
monitoring-pmm3 passed 00:08:12
one-pod passed 00:05:50
operator-self-healing passed 00:07:50
pitr passed 00:11:35
scaling passed 00:04:49
scheduled-backup passed 00:24:54
self-healing passed 00:08:46
sidecars passed 00:02:50
standby-pgbackrest failure 00:10:59
standby-streaming passed 00:09:18
start-from-backup passed 00:11:18
tablespaces passed 00:06:53
telemetry-transfer passed 00:03:29
upgrade-consistency passed 00:05:16
upgrade-minor passed 00:05:20
users passed 00:04:33
Summary Value
Tests Run 27/27
Job Duration 01:16:40
Total Test Time 03:45:22

commit: 41438f5
image: perconalab/percona-postgresql-operator:PR-1440-41438f5d1

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