diff --git a/CODEOWNERS b/CODEOWNERS index 9c28922a..0d820d36 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -10,3 +10,4 @@ /timescaledb-oss/ @cloudnative-pg/postgres-extensions-containers-owners @shusaan /wal2json/ @cloudnative-pg/postgres-extensions-containers-owners @solidDoWant /pg-ivm/ @cloudnative-pg/postgres-extensions-containers-owners @shusaan +/postgresql-anonymizer/ @cloudnative-pg/postgres-extensions-containers-owners @pchovelon @daamien diff --git a/SECURITY-INSIGHTS.yml b/SECURITY-INSIGHTS.yml index 2bddb2e7..a59ce941 100644 --- a/SECURITY-INSIGHTS.yml +++ b/SECURITY-INSIGHTS.yml @@ -57,6 +57,8 @@ repository: comment: GitHub packages for the TimescaleDB (OSS) extension image - uri: https://github.com/cloudnative-pg/postgres-extensions-containers/pkgs/container/wal2json comment: GitHub packages for the wal2json extension image + - uri: https://github.com/cloudnative-pg/postgres-extensions-containers/pkgs/container/postgresql_anonymizer + comment: GitHub packages for the PostgreSQL Anonymizer extension image security: tools: diff --git a/postgresql-anonymizer/Dockerfile b/postgresql-anonymizer/Dockerfile new file mode 100644 index 00000000..4ff98754 --- /dev/null +++ b/postgresql-anonymizer/Dockerfile @@ -0,0 +1,33 @@ +ARG BASE=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie +FROM $BASE AS builder + +ARG PG_MAJOR +ARG EXT_VERSION + +USER 0 + +RUN apt-get update && \ + apt-get install -y curl lsb-release + +#RUN CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME") +#RUN echo "deb https://repo.dalibo.com/apt/ $CODENAME main" > /etc/apt/sources.list.d/dalibo_labs.list +RUN curl -fsSL https://repo.dalibo.com/install.sh | sh +# RUN cat /etc/apt/sources.list.d/dalibo_labs.list +# RUN curl -fsSL -o /etc/apt/trusted.gpg.d/dalibo_labs.asc https://repo.dalibo.com/dalibo_labs.asc + +RUN apt-get update && \ + apt-get install -y postgresql_anonymizer_${PG_MAJOR}=${EXT_VERSION} + +FROM scratch +ARG PG_MAJOR + +# Licenses +#COPY --from=builder /usr/share/doc/postgresql-18-pganon/copyright /licenses/postgresql-18-pganon/ + +# Libraries +COPY --from=builder /usr/lib/postgresql/${PG_MAJOR}/lib/anon* /lib/ + +# Share +COPY --from=builder /usr/share/postgresql/${PG_MAJOR}/extension/anon* /share/extension/ + +USER 65532:65532 diff --git a/postgresql-anonymizer/README.md b/postgresql-anonymizer/README.md new file mode 100644 index 00000000..2e37f886 --- /dev/null +++ b/postgresql-anonymizer/README.md @@ -0,0 +1,155 @@ +# PostgreSQL Anonymizer + + +The [PostgreSQL Anonymizer](https://gitlab.com/dalibo/postgresql_anonymizer) (or +`anon`) is an extension to mask or replace [personally identifiable information] (PII) or +commercially sensitive data from a Postgres database. For more information, see the [official documentation](https://postgresql-anonymizer.readthedocs.io). + +## Usage + +### 1. Add the PostgreSQL Anonymizer extension image to your Cluster + +Define the PostgreSQL Anonymizer extension `anon` under the +`postgresql.extensions` section of your `Cluster` resource. + +For example: + +```yaml +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-postgresql-anonymizer +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie + instances: 1 + + storage: + size: 1Gi + + postgresql: + extensions: + - name: anon + image: + # renovate: suite=trixie-pgdg depName=postgresql-18-postgresql-anonymizer + reference: ghcr.io/cloudnative-pg/postgresql_anonymizer:3.2.2-18-trixie +``` + +### 2. Enable the extension in a database + +You can install PostgreSQL Anonymizer extension `anon` in a specific database by +creating or updating a `Database` resource. For example, to enable it in a +`db` database: + +```yaml +apiVersion: postgresql.cnpg.io/v1 +kind: Database +metadata: + name: database-postgresql-anonymizer +spec: + name: db + owner: app + cluster: + name: cluster-postgresql-anonymizer + extensions: + - name: anon + # renovate: suite=trixie-pgdg depName=postgresql-18-postgresql-anonymizer extractVersion=^(?\d+\.\d+\.\d+) + version: '3.2.2' +``` + +On top of the creation, it's recommended to load the extension in the `db` database. +This can be done with this command : + +```sql +ALTER DATABASE db SET session_preload_libraries = 'anon'; +``` + +The setting will be applied for the next sessions. +You need to reconnect to the database for the change to visible. + +### 3. Initialize the extension + +After reconnection, you can initialize the extension with the `anon.init()` function : + +```sql +db=# SELECT anon.init('/extensions/anon/share/extension/'); + init +------ + t +(1 row) +``` + +### 4. Verify installation + +Once the `db` database is ready, connect to it with `psql -d db` and run : + +```sql +SELECT * FROM pg_extension WHERE extname = 'anon'; +``` + +```sql +db=# SELECT * FROM pg_extension WHERE extname = 'anon'; + oid | extname | extowner | extnamespace | extrelocatable | extversion | extconfig | extcondition +-------+---------+----------+--------------+----------------+------------+-----------+-------------- + 16394 | anon | 10 | 2200 | f | 3.2.2 | | +(1 row) +``` + +If the result is empty, the extension is not declared in your database. + +Finally, look at the state of the extension with : + +```sql +SELECT anon.is_initialized(); +``` + +```console +db=# SELECT anon.is_initialized(); + is_initialized +---------------- + t +(1 row) +``` + +If the result is not `t`, the extension's data is not present. + +## Contributors + +This extension is maintained by: + +- Damien Clochard (@daamien) +- Pierrick Chovelon (@pchovelon) + +The maintainers are responsible for: + +- Monitoring upstream releases and security vulnerabilities. +- Ensuring compatibility with supported PostgreSQL versions. +- Reviewing and merging contributions specific to this extension's container + image and lifecycle. + +--- + +## Licenses and Copyright + +The PostgreSQL License +=============================================================================== + +Copyright (c) 2018-2026, DALIBO SCOP + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose, without fee, and without a written agreement +is hereby granted, provided that the above copyright notice and this paragraph +and the following two paragraphs appear in all copies. + +IN NO EVENT SHALL DALIBO SCOP BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, +SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING +OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF DALIBO SCOP +HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +DALIBO SCOP SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +AND DALIBO SCOP HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, +UPDATES, ENHANCEMENTS, OR MODIFICATIONS. diff --git a/postgresql-anonymizer/metadata.hcl b/postgresql-anonymizer/metadata.hcl new file mode 100644 index 00000000..85a821a1 --- /dev/null +++ b/postgresql-anonymizer/metadata.hcl @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright © contributors to CloudNativePG, established as CloudNativePG a Series of LF Projects, LLC. +# SPDX-License-Identifier: Apache-2.0 +metadata = { + name = "postgresql-anonymizer" + sql_name = "anon" + image_name = "postgresql_anonymizer" + licenses = ["PostgreSQL"] + shared_preload_libraries = [] + postgresql_parameters = {} + extension_control_path = [] + dynamic_library_path = [] + ld_library_path = [] + bin_path = [] + env = {} + auto_update_os_libs = false + required_extensions = [] + create_extension = true + + versions = { + bookworm = { + "18" = { + // renovate: suite=bookworm-pgdg depName=postgresql-18-postgresql-anonymizer + package = "3.2.2" + sql = "3.2.2" + } + } + trixie = { + "18" = { + // renovate: suite=trixie-pgdg depName=postgresql-18-postgresql-anonymizer + package = "3.2.2" + sql = "3.2.2" + } + } + } +} diff --git a/postgresql-anonymizer/test/chainsaw-test.yaml b/postgresql-anonymizer/test/chainsaw-test.yaml new file mode 100644 index 00000000..1afc29a9 --- /dev/null +++ b/postgresql-anonymizer/test/chainsaw-test.yaml @@ -0,0 +1,28 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: verify-postgresql-anonymizer-extension +spec: + timeouts: + apply: 5s + assert: 3m + delete: 30s + description: Verify postgresql_anonymizer extension is properly installed + steps: + - name: Create a Cluster and a Database with the extension + try: + - apply: + file: cluster.yaml + - apply: + file: database.yaml + - assert: + file: cluster-assert.yaml + - assert: + file: database-assert.yaml + + - name: Verify extension is installed + try: + - apply: + file: check-extension.yaml + - assert: + file: check-extension-assert.yaml diff --git a/postgresql-anonymizer/test/check-extension-assert.yaml b/postgresql-anonymizer/test/check-extension-assert.yaml new file mode 100644 index 00000000..a7971fc7 --- /dev/null +++ b/postgresql-anonymizer/test/check-extension-assert.yaml @@ -0,0 +1,6 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: extension-installed +status: + succeeded: 1 diff --git a/postgresql-anonymizer/test/check-extension.yaml b/postgresql-anonymizer/test/check-extension.yaml new file mode 100644 index 00000000..8d52f443 --- /dev/null +++ b/postgresql-anonymizer/test/check-extension.yaml @@ -0,0 +1,38 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: extension-installed +spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: data-test + env: + # Clear the image-baked PGSSLROOTCERT=system so libpq doesn't + # try to verify the CNPG self-signed CA against the OS trust store. + - name: PGSSLROOTCERT + - name: EXT_SQL_NAME + value: ($values.sql_name) + - name: EXT_VERSION + value: ($values.version) + - name: CREATE_EXTENSION + value: (to_string($values.create_extension)) + - name: DB_URI + valueFrom: + secretKeyRef: + name: (join('-', [$values.name, 'app'])) + key: uri + # renovate: datasource=docker depName=alpine/psql versioning=docker + image: alpine/psql:18.6@sha256:466b9fb940b45d317099c89d26ee8f1078dced59ef9304f9f4eab2d5fc616d46 + command: ['sh', '-c'] + args: + - | + set -e + if [ "$CREATE_EXTENSION" != "true" ]; then + echo "Skipping extension check (create_extension=false)" + exit 0 + fi + DB_URI=$(echo $DB_URI | sed "s|/\*|/|") + test "$(psql "$DB_URI" -tAc "SELECT EXISTS (SELECT FROM pg_catalog.pg_extension WHERE extname = '${EXT_SQL_NAME}' AND extversion = '${EXT_VERSION}')" -q)" = "t" + echo "Extension '${EXT_SQL_NAME} v${EXT_VERSION}' is installed!" diff --git a/postgresql-anonymizer/test/cluster-assert.yaml b/postgresql-anonymizer/test/cluster-assert.yaml new file mode 100644 index 00000000..aa221230 --- /dev/null +++ b/postgresql-anonymizer/test/cluster-assert.yaml @@ -0,0 +1,8 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: ($values.name) +status: + readyInstances: 1 + phase: Cluster in healthy state + image: ($values.pg_image) diff --git a/postgresql-anonymizer/test/cluster.yaml b/postgresql-anonymizer/test/cluster.yaml new file mode 100644 index 00000000..48d6fe06 --- /dev/null +++ b/postgresql-anonymizer/test/cluster.yaml @@ -0,0 +1,15 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: ($values.name) +spec: + imageName: ($values.pg_image) + instances: 1 + + storage: + size: 1Gi + + postgresql: + parameters: ($values.postgresql_parameters) + shared_preload_libraries: ($values.shared_preload_libraries) + extensions: ($values.extensions) diff --git a/postgresql-anonymizer/test/database-assert.yaml b/postgresql-anonymizer/test/database-assert.yaml new file mode 100644 index 00000000..9417b87b --- /dev/null +++ b/postgresql-anonymizer/test/database-assert.yaml @@ -0,0 +1,5 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Database +metadata: + name: (join('-', [$values.name, 'app'])) +status: ($values.database_assert_status) diff --git a/postgresql-anonymizer/test/database.yaml b/postgresql-anonymizer/test/database.yaml new file mode 100644 index 00000000..2d6810c2 --- /dev/null +++ b/postgresql-anonymizer/test/database.yaml @@ -0,0 +1,10 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Database +metadata: + name: (join('-', [$values.name, 'app'])) +spec: + name: app + owner: app + cluster: + name: ($values.name) + extensions: ($values.database_config.extensions_spec)