From fb0ed085655a523b0952f63d17e62fc6d5ff5c49 Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Thu, 23 Apr 2026 16:00:02 +0300 Subject: [PATCH 1/6] feat(postgres): support all PostgreSQL configuration parameters Allow users to configure any PostgreSQL server parameter via the postgresql.parameters field. All parameter values must be provided as strings (numeric values should be quoted). Security measures: - Block parameters that allow shell command execution (archive_command, restore_command, ssl_passphrase_command, log_destination, event_source) - Validation implemented in templates to prevent security vulnerabilities Generated files updated: - types.go: Add Parameters field with map[string]string type - README.md: Add parameter documentation and warnings - values.schema.json: Update schema for new field - CRD: Update OpenAPI schema with new field and reordered properties CloudNativePG manages certain parameters automatically (archive_mode, primary_conninfo, wal_level). Users should not override these as it may break backup/replication functionality. Signed-off-by: IvanHunters --- api/apps/v1alpha1/postgresql/types.go | 12 +++------ packages/apps/postgres/README.md | 9 +++---- packages/apps/postgres/templates/db.yaml | 12 ++++++--- packages/apps/postgres/values.schema.json | 14 +++++----- packages/apps/postgres/values.yaml | 26 +++++++++++++++---- .../system/postgres-rd/cozyrds/postgres.yaml | 2 +- 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/api/apps/v1alpha1/postgresql/types.go b/api/apps/v1alpha1/postgresql/types.go index a2ff77a8..ad8f2b36 100644 --- a/api/apps/v1alpha1/postgresql/types.go +++ b/api/apps/v1alpha1/postgresql/types.go @@ -112,15 +112,9 @@ type DatabaseRoles struct { } type PostgreSQL struct { - // PostgreSQL server parameters. - // +kubebuilder:default:={} - Parameters PostgreSQLParameters `json:"parameters,omitempty"` -} - -type PostgreSQLParameters struct { - // Maximum number of concurrent connections to the database server. - // +kubebuilder:default:=100 - MaxConnections int `json:"max_connections,omitempty"` + // PostgreSQL server parameters. All values must be strings (enclose numbers in quotes). See PostgreSQL documentation for available parameters. WARNING: Some parameters are managed by CloudNativePG and should NOT be overridden (archive_mode, archive_command, restore_command, primary_conninfo, etc.). Incorrect values may break backup/replication or require pod restart. + // +kubebuilder:default:={"max_connections":"100"} + Parameters map[string]string `json:"parameters,omitempty"` } type Quorum struct { diff --git a/packages/apps/postgres/README.md b/packages/apps/postgres/README.md index 4cda7284..a2fb76eb 100644 --- a/packages/apps/postgres/README.md +++ b/packages/apps/postgres/README.md @@ -81,11 +81,10 @@ See: ### Application-specific parameters -| Name | Description | Type | Value | -| --------------------------------------- | ---------------------------------------------------------------- | -------- | ----- | -| `postgresql` | PostgreSQL server configuration. | `object` | `{}` | -| `postgresql.parameters` | PostgreSQL server parameters. | `object` | `{}` | -| `postgresql.parameters.max_connections` | Maximum number of concurrent connections to the database server. | `int` | `100` | +| Name | Description | Type | Value | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----- | +| `postgresql` | PostgreSQL server configuration. | `object` | `{}` | +| `postgresql.parameters` | PostgreSQL server parameters. All values must be strings (enclose numbers in quotes). See PostgreSQL documentation for available parameters. WARNING: Some parameters are managed by CloudNativePG and should NOT be overridden (archive_mode, archive_command, restore_command, primary_conninfo, etc.). Incorrect values may break backup/replication or require pod restart. | `map[string]string` | `{}` | ### Quorum-based synchronous replication diff --git a/packages/apps/postgres/templates/db.yaml b/packages/apps/postgres/templates/db.yaml index 2cdaec1d..30771667 100644 --- a/packages/apps/postgres/templates/db.yaml +++ b/packages/apps/postgres/templates/db.yaml @@ -60,9 +60,15 @@ spec: {{- end }} postgresql: parameters: - max_wal_senders: "30" - {{- with .Values.postgresql.parameters.max_connections }} - max_connections: "{{ . }}" + {{- $defaults := dict "max_wal_senders" "30" }} + {{- $params := merge .Values.postgresql.parameters $defaults }} + {{- /* Security: Block parameters that could allow shell command execution */ -}} + {{- $dangerousParams := list "archive_command" "restore_command" "ssl_passphrase_command" "log_destination" "event_source" }} + {{- range $key, $value := $params }} + {{- if has $key $dangerousParams }} + {{- fail (printf "SECURITY: Parameter '%s' is blocked for security reasons. This parameter could allow arbitrary shell command execution and is managed by CloudNativePG operator." $key) }} + {{- end }} + {{ $key }}: "{{ $value }}" {{- end }} minSyncReplicas: {{ .Values.quorum.minSyncReplicas }} diff --git a/packages/apps/postgres/values.schema.json b/packages/apps/postgres/values.schema.json index 98e29822..470fca2f 100644 --- a/packages/apps/postgres/values.schema.json +++ b/packages/apps/postgres/values.schema.json @@ -97,15 +97,13 @@ "default": {}, "properties": { "parameters": { - "description": "PostgreSQL server parameters.", + "description": "PostgreSQL server parameters. All values must be strings (enclose numbers in quotes). See PostgreSQL documentation for available parameters. WARNING: Some parameters are managed by CloudNativePG and should NOT be overridden (archive_mode, archive_command, restore_command, primary_conninfo, etc.). Incorrect values may break backup/replication or require pod restart.", "type": "object", - "default": {}, - "properties": { - "max_connections": { - "description": "Maximum number of concurrent connections to the database server.", - "type": "integer", - "default": 100 - } + "default": { + "max_connections": "100" + }, + "additionalProperties": { + "type": "string" } } } diff --git a/packages/apps/postgres/values.yaml b/packages/apps/postgres/values.yaml index 2ceaa9cf..07824e80 100644 --- a/packages/apps/postgres/values.yaml +++ b/packages/apps/postgres/values.yaml @@ -48,16 +48,32 @@ version: v18 ## @section Application-specific parameters ## -## @typedef {struct} PostgreSQLParameters - PostgreSQL server parameters. -## @field {int} [max_connections] - Maximum number of concurrent connections to the database server. - ## @typedef {struct} PostgreSQL - PostgreSQL server configuration. -## @field {PostgreSQLParameters} [parameters] - PostgreSQL server parameters. +## @field {map[string]string} [parameters] - PostgreSQL server parameters. All values must be strings (enclose numbers in quotes). See PostgreSQL documentation for available parameters. WARNING: Some parameters are managed by CloudNativePG and should NOT be overridden (archive_mode, archive_command, restore_command, primary_conninfo, etc.). Incorrect values may break backup/replication or require pod restart. ## @param {PostgreSQL} postgresql - PostgreSQL server configuration. postgresql: parameters: - max_connections: 100 + max_connections: "100" +## Additional parameters examples: +## shared_buffers: "1GB" +## effective_cache_size: "4GB" +## bgwriter_delay: "200ms" +## commit_delay: "100" +## commit_siblings: "5" +## work_mem: "256MB" +## +## SECURITY: The following parameters are BLOCKED for security reasons +## (could allow arbitrary shell command execution): +## archive_command, restore_command, ssl_passphrase_command, +## log_destination, event_source +## +## IMPORTANT: Do NOT override CloudNativePG-managed parameters: +## archive_mode, primary_conninfo, wal_level, max_replication_slots +## +## NOTE: Some parameters require pod restart (not just reload): +## shared_buffers, max_connections, max_prepared_transactions, +## wal_buffers, max_wal_senders ## ## @section Quorum-based synchronous replication diff --git a/packages/system/postgres-rd/cozyrds/postgres.yaml b/packages/system/postgres-rd/cozyrds/postgres.yaml index 72d013f1..6f6ed3de 100644 --- a/packages/system/postgres-rd/cozyrds/postgres.yaml +++ b/packages/system/postgres-rd/cozyrds/postgres.yaml @@ -8,7 +8,7 @@ spec: singular: postgres plural: postgreses openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of Postgres replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each PostgreSQL replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"version":{"description":"PostgreSQL major version to deploy","type":"string","default":"v18","enum":["v18","v17","v16","v15","v14","v13"]},"postgresql":{"description":"PostgreSQL server configuration.","type":"object","default":{},"properties":{"parameters":{"description":"PostgreSQL server parameters.","type":"object","default":{},"properties":{"max_connections":{"description":"Maximum number of concurrent connections to the database server.","type":"integer","default":100}}}}},"quorum":{"description":"Quorum configuration for synchronous replication.","type":"object","default":{},"required":["maxSyncReplicas","minSyncReplicas"],"properties":{"maxSyncReplicas":{"description":"Maximum number of synchronous replicas allowed (must be less than total replicas).","type":"integer","default":0},"minSyncReplicas":{"description":"Minimum number of synchronous replicas required for commit.","type":"integer","default":0}}},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"password":{"description":"Password for the user.","type":"string"},"replication":{"description":"Whether the user has replication privileges.","type":"boolean"}}}},"databases":{"description":"Databases configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"extensions":{"description":"List of enabled PostgreSQL extensions.","type":"array","items":{"type":"string"}},"roles":{"description":"Roles assigned to users.","type":"object","properties":{"admin":{"description":"List of users with admin privileges.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of users with read-only privileges.","type":"array","items":{"type":"string"}}}}}}},"backup":{"description":"Backup configuration.","type":"object","default":{},"required":["enabled"],"properties":{"destinationPath":{"description":"Destination path for backups (e.g. s3://bucket/path/).","type":"string","default":"s3://bucket/path/to/folder/"},"enabled":{"description":"Enable regular backups.","type":"boolean","default":false},"endpointURL":{"description":"S3 endpoint URL for uploads.","type":"string","default":"http://minio-gateway-service:9000"},"retentionPolicy":{"description":"Retention policy (e.g. \"30d\").","type":"string","default":"30d"},"s3AccessKey":{"description":"Access key for S3 authentication.","type":"string","default":""},"s3SecretKey":{"description":"Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"Cron schedule for automated backups.","type":"string","default":"0 2 * * * *"}}},"bootstrap":{"description":"Bootstrap configuration.","type":"object","default":{},"required":["enabled","oldName"],"properties":{"enabled":{"description":"Whether to restore from a backup.","type":"boolean","default":false},"oldName":{"description":"Previous cluster name before deletion.","type":"string","default":""},"recoveryTime":{"description":"Timestamp (RFC3339) for point-in-time recovery; empty means latest.","type":"string","default":""},"serverName":{"description":"Barman server name (S3 path prefix) used by the original cluster when writing backups. Set this only when the original cluster had an explicit barmanObjectStore.serverName that differed from its Kubernetes resource name.","type":"string","default":""}}}}} + {"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of Postgres replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each PostgreSQL replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"version":{"description":"PostgreSQL major version to deploy","type":"string","default":"v18","enum":["v18","v17","v16","v15","v14","v13"]},"postgresql":{"description":"PostgreSQL server configuration.","type":"object","default":{},"properties":{"parameters":{"description":"PostgreSQL server parameters. All values must be strings (enclose numbers in quotes). See PostgreSQL documentation for available parameters. WARNING: Some parameters are managed by CloudNativePG and should NOT be overridden (archive_mode, archive_command, restore_command, primary_conninfo, etc.). Incorrect values may break backup/replication or require pod restart.","type":"object","default":{"max_connections":"100"},"additionalProperties":{"type":"string"}}}},"quorum":{"description":"Quorum configuration for synchronous replication.","type":"object","default":{},"required":["maxSyncReplicas","minSyncReplicas"],"properties":{"maxSyncReplicas":{"description":"Maximum number of synchronous replicas allowed (must be less than total replicas).","type":"integer","default":0},"minSyncReplicas":{"description":"Minimum number of synchronous replicas required for commit.","type":"integer","default":0}}},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"password":{"description":"Password for the user.","type":"string"},"replication":{"description":"Whether the user has replication privileges.","type":"boolean"}}}},"databases":{"description":"Databases configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"extensions":{"description":"List of enabled PostgreSQL extensions.","type":"array","items":{"type":"string"}},"roles":{"description":"Roles assigned to users.","type":"object","properties":{"admin":{"description":"List of users with admin privileges.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of users with read-only privileges.","type":"array","items":{"type":"string"}}}}}}},"backup":{"description":"Backup configuration.","type":"object","default":{},"required":["enabled"],"properties":{"destinationPath":{"description":"Destination path for backups (e.g. s3://bucket/path/).","type":"string","default":"s3://bucket/path/to/folder/"},"enabled":{"description":"Enable regular backups.","type":"boolean","default":false},"endpointURL":{"description":"S3 endpoint URL for uploads.","type":"string","default":"http://minio-gateway-service:9000"},"retentionPolicy":{"description":"Retention policy (e.g. \"30d\").","type":"string","default":"30d"},"s3AccessKey":{"description":"Access key for S3 authentication.","type":"string","default":""},"s3SecretKey":{"description":"Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"Cron schedule for automated backups.","type":"string","default":"0 2 * * * *"}}},"bootstrap":{"description":"Bootstrap configuration.","type":"object","default":{},"required":["enabled","oldName"],"properties":{"enabled":{"description":"Whether to restore from a backup.","type":"boolean","default":false},"oldName":{"description":"Previous cluster name before deletion.","type":"string","default":""},"recoveryTime":{"description":"Timestamp (RFC3339) for point-in-time recovery; empty means latest.","type":"string","default":""},"serverName":{"description":"Barman server name (S3 path prefix) used by the original cluster when writing backups. Set this only when the original cluster had an explicit barmanObjectStore.serverName that differed from its Kubernetes resource name.","type":"string","default":""}}}}}} release: prefix: postgres- labels: From f8001e23b68980438beccfad7d3b711b7e8c8ac1 Mon Sep 17 00:00:00 2001 From: "ohotnikov.ivan" Date: Mon, 27 Apr 2026 14:54:14 +0300 Subject: [PATCH 2/6] fix(postgres): address review feedback - Regenerate deepcopy artifacts to remove orphaned PostgreSQLParameters methods - Update e2e test to use string value for max_connections parameter - Fix template to avoid .Values mutation and use quote function for safe YAML escaping Signed-off-by: ohotnikov.ivan --- .../postgresql/zz_generated.deepcopy.go | 25 ++++++------------- hack/e2e-apps/postgres.bats | 2 +- packages/apps/postgres/templates/db.yaml | 4 +-- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/api/apps/v1alpha1/postgresql/zz_generated.deepcopy.go b/api/apps/v1alpha1/postgresql/zz_generated.deepcopy.go index 5fba855e..89a42778 100644 --- a/api/apps/v1alpha1/postgresql/zz_generated.deepcopy.go +++ b/api/apps/v1alpha1/postgresql/zz_generated.deepcopy.go @@ -85,7 +85,7 @@ func (in *ConfigSpec) DeepCopyInto(out *ConfigSpec) { *out = *in in.Resources.DeepCopyInto(&out.Resources) out.Size = in.Size.DeepCopy() - out.Postgresql = in.Postgresql + in.Postgresql.DeepCopyInto(&out.Postgresql) out.Quorum = in.Quorum if in.Users != nil { in, out := &in.Users, &out.Users @@ -164,7 +164,13 @@ func (in *DatabaseRoles) DeepCopy() *DatabaseRoles { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PostgreSQL) DeepCopyInto(out *PostgreSQL) { *out = *in - out.Parameters = in.Parameters + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQL. @@ -177,21 +183,6 @@ func (in *PostgreSQL) DeepCopy() *PostgreSQL { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PostgreSQLParameters) DeepCopyInto(out *PostgreSQLParameters) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLParameters. -func (in *PostgreSQLParameters) DeepCopy() *PostgreSQLParameters { - if in == nil { - return nil - } - out := new(PostgreSQLParameters) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Quorum) DeepCopyInto(out *Quorum) { *out = *in diff --git a/hack/e2e-apps/postgres.bats b/hack/e2e-apps/postgres.bats index b86bc664..b1d3de51 100644 --- a/hack/e2e-apps/postgres.bats +++ b/hack/e2e-apps/postgres.bats @@ -16,7 +16,7 @@ spec: storageClass: "" postgresql: parameters: - max_connections: 100 + max_connections: "100" quorum: minSyncReplicas: 0 maxSyncReplicas: 0 diff --git a/packages/apps/postgres/templates/db.yaml b/packages/apps/postgres/templates/db.yaml index 30771667..1800a515 100644 --- a/packages/apps/postgres/templates/db.yaml +++ b/packages/apps/postgres/templates/db.yaml @@ -61,14 +61,14 @@ spec: postgresql: parameters: {{- $defaults := dict "max_wal_senders" "30" }} - {{- $params := merge .Values.postgresql.parameters $defaults }} + {{- $params := merge (dict) $defaults .Values.postgresql.parameters }} {{- /* Security: Block parameters that could allow shell command execution */ -}} {{- $dangerousParams := list "archive_command" "restore_command" "ssl_passphrase_command" "log_destination" "event_source" }} {{- range $key, $value := $params }} {{- if has $key $dangerousParams }} {{- fail (printf "SECURITY: Parameter '%s' is blocked for security reasons. This parameter could allow arbitrary shell command execution and is managed by CloudNativePG operator." $key) }} {{- end }} - {{ $key }}: "{{ $value }}" + {{ $key }}: {{ $value | quote }} {{- end }} minSyncReplicas: {{ .Values.quorum.minSyncReplicas }} From 96c7c16c21385f836d8f8f08d15799f7f802ffcb Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Mon, 27 Apr 2026 18:28:58 +0300 Subject: [PATCH 3/6] fix(postgres): correct merge argument order for parameter precedence Fix regression where defaults were overriding user-supplied parameters. Sprig merge gives precedence to first argument, so user values must be merged before defaults to maintain correct override behavior. Before: merge (dict) $defaults .Values.postgresql.parameters After: merge (dict) .Values.postgresql.parameters $defaults Now max_wal_senders and other user parameters correctly override defaults. Signed-off-by: IvanHunters --- packages/apps/postgres/templates/db.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/postgres/templates/db.yaml b/packages/apps/postgres/templates/db.yaml index 1800a515..de8c24f7 100644 --- a/packages/apps/postgres/templates/db.yaml +++ b/packages/apps/postgres/templates/db.yaml @@ -61,7 +61,7 @@ spec: postgresql: parameters: {{- $defaults := dict "max_wal_senders" "30" }} - {{- $params := merge (dict) $defaults .Values.postgresql.parameters }} + {{- $params := merge (dict) .Values.postgresql.parameters $defaults }} {{- /* Security: Block parameters that could allow shell command execution */ -}} {{- $dangerousParams := list "archive_command" "restore_command" "ssl_passphrase_command" "log_destination" "event_source" }} {{- range $key, $value := $params }} From fac0622499357bf1f2c4e6a5b29dc1b16864efb7 Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Mon, 27 Apr 2026 18:46:42 +0300 Subject: [PATCH 4/6] fix(postgres): make security denylist case-insensitive PostgreSQL parameter names are case-insensitive, but the denylist check was case-sensitive, allowing bypass via mixed-case names (e.g., ARCHIVE_COMMAND instead of archive_command). Normalize parameter names to lowercase before checking against the denylist to prevent security bypass. Thanks to CodeRabbit for catching this security issue. Signed-off-by: IvanHunters --- packages/apps/postgres/templates/db.yaml | 3 ++- .../PrometheusRule.yaml => templates/prometheusrule.yaml} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename packages/system/kubevirt-operator/{alerts/PrometheusRule.yaml => templates/prometheusrule.yaml} (100%) diff --git a/packages/apps/postgres/templates/db.yaml b/packages/apps/postgres/templates/db.yaml index de8c24f7..6c1fda12 100644 --- a/packages/apps/postgres/templates/db.yaml +++ b/packages/apps/postgres/templates/db.yaml @@ -65,7 +65,8 @@ spec: {{- /* Security: Block parameters that could allow shell command execution */ -}} {{- $dangerousParams := list "archive_command" "restore_command" "ssl_passphrase_command" "log_destination" "event_source" }} {{- range $key, $value := $params }} - {{- if has $key $dangerousParams }} + {{- $normalizedKey := lower $key }} + {{- if has $normalizedKey $dangerousParams }} {{- fail (printf "SECURITY: Parameter '%s' is blocked for security reasons. This parameter could allow arbitrary shell command execution and is managed by CloudNativePG operator." $key) }} {{- end }} {{ $key }}: {{ $value | quote }} diff --git a/packages/system/kubevirt-operator/alerts/PrometheusRule.yaml b/packages/system/kubevirt-operator/templates/prometheusrule.yaml similarity index 100% rename from packages/system/kubevirt-operator/alerts/PrometheusRule.yaml rename to packages/system/kubevirt-operator/templates/prometheusrule.yaml From a753088fb4a9356c6a12293ff80633c37c1dfb54 Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Tue, 28 Apr 2026 13:01:44 +0300 Subject: [PATCH 5/6] fix(kubevirt-operator): escape Prometheus template variables in PrometheusRule Helm was trying to evaluate {{ $labels.xxx }} as Helm variables, causing "undefined variable" errors during E2E tests. Wrap Prometheus template expressions in backticks to pass them through Helm unchanged. Signed-off-by: IvanHunters --- .../templates/prometheusrule.yaml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/system/kubevirt-operator/templates/prometheusrule.yaml b/packages/system/kubevirt-operator/templates/prometheusrule.yaml index 72fabfa0..5984ce43 100644 --- a/packages/system/kubevirt-operator/templates/prometheusrule.yaml +++ b/packages/system/kubevirt-operator/templates/prometheusrule.yaml @@ -17,12 +17,10 @@ spec: ) == 1 labels: severity: critical - namespace: "{{ $labels.exported_namespace }}" + namespace: '{{ "{{" }} $labels.exported_namespace {{ "}}" }}' annotations: - summary: "VM '{{ $labels.name }}' in namespace '{{ $labels.exported_namespace }}' is not Running for more than 10 minutes" - description: > - The VM '{{ $labels.name }}' in namespace '{{ $labels.exported_namespace }}' has been in a non-Running state - for more than 10 minutes. Please investigate the issue. + summary: 'VM {{ "{{" }} $labels.name {{ "}}" }} in namespace {{ "{{" }} $labels.exported_namespace {{ "}}" }} is not Running for more than 10 minutes' + description: 'The VM {{ "{{" }} $labels.name {{ "}}" }} in namespace {{ "{{" }} $labels.exported_namespace {{ "}}" }} has been in a non-Running state for more than 10 minutes. Please investigate the issue.' - alert: VMINotRunningFor10Minutes expr: | max_over_time( @@ -34,9 +32,7 @@ spec: ) == 1 labels: severity: critical - namespace: "{{ $labels.exported_namespace }}" + namespace: '{{ "{{" }} $labels.exported_namespace {{ "}}" }}' annotations: - summary: "VMI '{{ $labels.name }}' in namespace '{{ $labels.exported_namespace }}' is not Running for more than 10 minutes" - description: > - The VM '{{ $labels.name }}' in namespace '{{ $labels.exported_namespace }}' has been in a non-Running state - for more than 10 minutes. Please investigate the issue. + summary: 'VMI {{ "{{" }} $labels.name {{ "}}" }} in namespace {{ "{{" }} $labels.exported_namespace {{ "}}" }} is not Running for more than 10 minutes' + description: 'The VMI {{ "{{" }} $labels.name {{ "}}" }} in namespace {{ "{{" }} $labels.exported_namespace {{ "}}" }} has been in a non-Running state for more than 10 minutes. Please investigate the issue.' From c2bad2ab5d71883630c2285f26afa181a23cb2be Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Thu, 30 Apr 2026 01:46:39 +0300 Subject: [PATCH 6/6] chore(postgres): regenerate postgres.yaml after rebase Signed-off-by: IvanHunters --- packages/system/postgres-rd/cozyrds/postgres.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/system/postgres-rd/cozyrds/postgres.yaml b/packages/system/postgres-rd/cozyrds/postgres.yaml index 6f6ed3de..29a0056d 100644 --- a/packages/system/postgres-rd/cozyrds/postgres.yaml +++ b/packages/system/postgres-rd/cozyrds/postgres.yaml @@ -8,7 +8,7 @@ spec: singular: postgres plural: postgreses openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of Postgres replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each PostgreSQL replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"version":{"description":"PostgreSQL major version to deploy","type":"string","default":"v18","enum":["v18","v17","v16","v15","v14","v13"]},"postgresql":{"description":"PostgreSQL server configuration.","type":"object","default":{},"properties":{"parameters":{"description":"PostgreSQL server parameters. All values must be strings (enclose numbers in quotes). See PostgreSQL documentation for available parameters. WARNING: Some parameters are managed by CloudNativePG and should NOT be overridden (archive_mode, archive_command, restore_command, primary_conninfo, etc.). Incorrect values may break backup/replication or require pod restart.","type":"object","default":{"max_connections":"100"},"additionalProperties":{"type":"string"}}}},"quorum":{"description":"Quorum configuration for synchronous replication.","type":"object","default":{},"required":["maxSyncReplicas","minSyncReplicas"],"properties":{"maxSyncReplicas":{"description":"Maximum number of synchronous replicas allowed (must be less than total replicas).","type":"integer","default":0},"minSyncReplicas":{"description":"Minimum number of synchronous replicas required for commit.","type":"integer","default":0}}},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"password":{"description":"Password for the user.","type":"string"},"replication":{"description":"Whether the user has replication privileges.","type":"boolean"}}}},"databases":{"description":"Databases configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"extensions":{"description":"List of enabled PostgreSQL extensions.","type":"array","items":{"type":"string"}},"roles":{"description":"Roles assigned to users.","type":"object","properties":{"admin":{"description":"List of users with admin privileges.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of users with read-only privileges.","type":"array","items":{"type":"string"}}}}}}},"backup":{"description":"Backup configuration.","type":"object","default":{},"required":["enabled"],"properties":{"destinationPath":{"description":"Destination path for backups (e.g. s3://bucket/path/).","type":"string","default":"s3://bucket/path/to/folder/"},"enabled":{"description":"Enable regular backups.","type":"boolean","default":false},"endpointURL":{"description":"S3 endpoint URL for uploads.","type":"string","default":"http://minio-gateway-service:9000"},"retentionPolicy":{"description":"Retention policy (e.g. \"30d\").","type":"string","default":"30d"},"s3AccessKey":{"description":"Access key for S3 authentication.","type":"string","default":""},"s3SecretKey":{"description":"Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"Cron schedule for automated backups.","type":"string","default":"0 2 * * * *"}}},"bootstrap":{"description":"Bootstrap configuration.","type":"object","default":{},"required":["enabled","oldName"],"properties":{"enabled":{"description":"Whether to restore from a backup.","type":"boolean","default":false},"oldName":{"description":"Previous cluster name before deletion.","type":"string","default":""},"recoveryTime":{"description":"Timestamp (RFC3339) for point-in-time recovery; empty means latest.","type":"string","default":""},"serverName":{"description":"Barman server name (S3 path prefix) used by the original cluster when writing backups. Set this only when the original cluster had an explicit barmanObjectStore.serverName that differed from its Kubernetes resource name.","type":"string","default":""}}}}}} + {"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of Postgres replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each PostgreSQL replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"micro","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"version":{"description":"PostgreSQL major version to deploy","type":"string","default":"v18","enum":["v18","v17","v16","v15","v14","v13"]},"postgresql":{"description":"PostgreSQL server configuration.","type":"object","default":{},"properties":{"parameters":{"description":"PostgreSQL server parameters. All values must be strings (enclose numbers in quotes). See PostgreSQL documentation for available parameters. WARNING: Some parameters are managed by CloudNativePG and should NOT be overridden (archive_mode, archive_command, restore_command, primary_conninfo, etc.). Incorrect values may break backup/replication or require pod restart.","type":"object","default":{"max_connections":"100"},"additionalProperties":{"type":"string"}}}},"quorum":{"description":"Quorum configuration for synchronous replication.","type":"object","default":{},"required":["maxSyncReplicas","minSyncReplicas"],"properties":{"maxSyncReplicas":{"description":"Maximum number of synchronous replicas allowed (must be less than total replicas).","type":"integer","default":0},"minSyncReplicas":{"description":"Minimum number of synchronous replicas required for commit.","type":"integer","default":0}}},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"password":{"description":"Password for the user.","type":"string"},"replication":{"description":"Whether the user has replication privileges.","type":"boolean"}}}},"databases":{"description":"Databases configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"extensions":{"description":"List of enabled PostgreSQL extensions.","type":"array","items":{"type":"string"}},"roles":{"description":"Roles assigned to users.","type":"object","properties":{"admin":{"description":"List of users with admin privileges.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of users with read-only privileges.","type":"array","items":{"type":"string"}}}}}}},"backup":{"description":"Backup configuration.","type":"object","default":{},"required":["enabled"],"properties":{"destinationPath":{"description":"Destination path for backups (e.g. s3://bucket/path/).","type":"string","default":"s3://bucket/path/to/folder/"},"enabled":{"description":"Enable regular backups.","type":"boolean","default":false},"endpointURL":{"description":"S3 endpoint URL for uploads.","type":"string","default":"http://minio-gateway-service:9000"},"retentionPolicy":{"description":"Retention policy (e.g. \"30d\").","type":"string","default":"30d"},"s3AccessKey":{"description":"Access key for S3 authentication.","type":"string","default":""},"s3SecretKey":{"description":"Secret key for S3 authentication.","type":"string","default":""},"schedule":{"description":"Cron schedule for automated backups.","type":"string","default":"0 2 * * * *"}}},"bootstrap":{"description":"Bootstrap configuration.","type":"object","default":{},"required":["enabled","oldName"],"properties":{"enabled":{"description":"Whether to restore from a backup.","type":"boolean","default":false},"oldName":{"description":"Previous cluster name before deletion.","type":"string","default":""},"recoveryTime":{"description":"Timestamp (RFC3339) for point-in-time recovery; empty means latest.","type":"string","default":""},"serverName":{"description":"Barman server name (S3 path prefix) used by the original cluster when writing backups. Set this only when the original cluster had an explicit barmanObjectStore.serverName that differed from its Kubernetes resource name.","type":"string","default":""}}}}} release: prefix: postgres- labels: