Conversation
There was a problem hiding this comment.
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.
internal/postgres/reconcile.go
Outdated
| pgTDECASecret := &corev1.SecretProjection{ | ||
| LocalObjectReference: corev1.LocalObjectReference{ | ||
| Name: inCluster.Spec.Extensions.PGTDE.Vault.CASecret.Name, | ||
| }, | ||
| } |
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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.
| 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, |
| 'vault-provider', '%s', '%s', '%s', '%s' | ||
| );`, |
There was a problem hiding this comment.
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.
| 'vault-provider', '%s', '%s', '%s', '%s' | |
| );`, | |
| '%s', '%s', '%s', '%s', '%s' | |
| );`, | |
| naming.PGTDEVaultProvider, |
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| 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, | ||
| ), |
There was a problem hiding this comment.
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.
internal/postgres/reconcile.go
Outdated
| pgTDETokenSecret := &corev1.SecretProjection{ | ||
| LocalObjectReference: corev1.LocalObjectReference{ | ||
| Name: inCluster.Spec.Extensions.PGTDE.Vault.TokenSecret.Name, | ||
| }, | ||
| } |
There was a problem hiding this comment.
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.
| crunchyv1beta1 "github.com/percona/percona-postgresql-operator/v2/pkg/apis/postgres-operator.crunchydata.com/v1beta1" | ||
| ) | ||
|
|
||
| // EnableInPostgreSQL installs pgvector triggers into every database. |
There was a problem hiding this comment.
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.
| // EnableInPostgreSQL installs pgvector triggers into every database. | |
| // EnableInPostgreSQL installs and updates the pg_tde extension in every database. |
internal/postgres/reconcile.go
Outdated
| 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, | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| 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, | ||
| ), |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
commit: 41438f5 |
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
Needs Doc) and QA (Needs QA)?Tests
Config/Logging/Testability