Skip to content

Commit 4e57f7d

Browse files
authored
feat(postgres): add pgsodium, gzip, and pgzstd extensions (#60) (#61)
# Squash Merge Notes: pgsodium, gzip, and pgzstd Extensions **PR:** [#61](#61) **Issue:** [#60](#60) **Branch:** `paudley/pgsodium-gzip-pgzstd-extensions` **Base:** `main` @ `93258e4` **Squash commit:** `feat(postgres): add pgsodium, gzip, and pgzstd extensions (#60)` --- ## Overview Adds three new PostgreSQL extensions to the core_data platform, all built from source since none are available as PGDG apt packages for PostgreSQL 17. Also introduces a startup enforcement mechanism for `shared_preload_libraries` to prevent configuration drift. **Total diff:** 6 files changed, +130 insertions, -3 deletions --- ## Extensions Added ### pgsodium (v3.1.9) - **Source:** [github.com/michelp/pgsodium](https://github.com/michelp/pgsodium) - **Purpose:** Modern cryptographic functions wrapping libsodium — encryption, decryption, hashing, key derivation, digital signatures, and Transparent Column Encryption (TCE) with server-side key management. - **Build dependency:** `libsodium-dev` - **Runtime requirement:** Must be in `shared_preload_libraries` for key management and TCE features. - **Key management:** Uses `pgsodium.getkey_script` pointing to `/opt/core_data/tools/pgsodium_getkey.sh`, which reads the root encryption key from `/opt/core_data/secrets/pgsodium.key` (configurable via `PGSODIUM_KEY_FILE` env var). The key must be exactly 32 bytes (256-bit) of raw key material. - **Complements:** Existing `pgcrypto` extension. pgsodium provides libsodium's modern algorithms (XChaCha20-Poly1305, Argon2id, Ed25519) versus pgcrypto's OpenSSL-based primitives. ### pgsql-gzip (extension name: `gzip`) - **Source:** [github.com/pramsey/pgsql-gzip](https://github.com/pramsey/pgsql-gzip) - **Purpose:** SQL-native gzip compression and decompression of `bytea` data. Provides `gzip()` and `gunzip()` functions for compressing large text/binary payloads and interoperating with gzip-compressed data from external systems. - **Build dependency:** `zlib1g-dev` - **Runtime requirement:** None (no `shared_preload_libraries` entry needed). ### pgzstd - **Source:** [github.com/grahamedgecombe/pgzstd](https://github.com/grahamedgecombe/pgzstd) - **Purpose:** Zstandard (zstd) compression and decompression in SQL. Provides `zstd_compress()` and `zstd_decompress()` functions. Zstandard achieves better compression ratios than gzip at comparable speeds and significantly faster decompression — ideal for high-throughput data pipelines. - **Build dependency:** `libzstd-dev` - **Runtime requirement:** None (no `shared_preload_libraries` entry needed). --- ## File-by-File Changes ### `postgres/Dockerfile` **Build dependencies added to apt-get install (alphabetically ordered):** - `libsodium-dev` — required by pgsodium for libsodium C library headers - `libzstd-dev` — required by pgzstd for Zstandard C library headers - `zlib1g-dev` — required by pgsql-gzip for zlib C library headers (may already be present as transitive dependency, but made explicit) **Three new `RUN` blocks added after the existing pg_squeeze build**, following the identical source-build pattern established by Apache AGE and pg_squeeze: 1. **pgsodium** — Clones tag `v3.1.9` with `--depth 1 --single-branch`, builds with `make`/`make install` using `PG_CONFIG`, cleans up temp directory. Version is parameterized via `ARG PGSODIUM_VERSION=3.1.9` for build-time configurability. 2. **pgsql-gzip** — Clones latest `--depth 1`, standard `make`/`make install`, cleanup. No version tag pinning (upstream does not tag releases consistently). 3. **pgzstd** — Clones latest `--depth 1`, standard `make`/`make install`, cleanup. No version tag pinning. ### `scripts/lib/extensions_list.sh` **`CORE_EXTENSION_LIST` array — 3 entries added (lines 31-33):** - `pgsodium` — placed after `pgcrypto` (logical grouping: crypto-related extensions) - `gzip` — placed after `pgsodium` - `pgzstd` — placed after `gzip` (compression extensions grouped together) This array drives `02-enable-extensions.sh`, which iterates it to run `CREATE EXTENSION IF NOT EXISTS` in every non-template database plus `template1`. Adding entries here is sufficient for automatic creation in all databases at startup — no changes to `02-enable-extensions.sh` were needed. **New `REQUIRED_PRELOAD_LIBRARIES` array added (lines 50-62):** ``` age, pgaudit, pg_stat_statements, pg_cron, pg_squeeze, auto_explain, pg_buffercache, pg_partman_bgw, pgsodium ``` This is the canonical single source of truth for which libraries must be present in `shared_preload_libraries`. It is consumed by the new `enforce_shared_preload_libraries()` function in `00-render-config.sh`. ### `postgres/conf/postgresql.conf.tpl` **Line 13 — `shared_preload_libraries` updated:** - Before: `'age,pgaudit,pg_stat_statements,pg_cron,pg_squeeze,auto_explain,pg_buffercache,pg_partman_bgw'` - After: `'age,pgaudit,pg_stat_statements,pg_cron,pg_squeeze,auto_explain,pg_buffercache,pg_partman_bgw,pgsodium'` **Line 90 — new pgsodium configuration added:** - `pgsodium.getkey_script = '/opt/core_data/tools/pgsodium_getkey.sh'` — tells pgsodium how to retrieve the root encryption key for TCE. Placed before `datestyle` at the end of the config file. ### `postgres/initdb/00-render-config.sh` **New function `enforce_shared_preload_libraries()` (lines 18-49):** Sources `extensions_list.sh` to get the `REQUIRED_PRELOAD_LIBRARIES` array, then: 1. Reads the current `shared_preload_libraries` value from `postgresql.conf` 2. Checks each required library against the current value using comma-delimited matching 3. If any are missing: appends them to the value, patches the config file in-place with `sed -i`, logs a WARNING with the missing entries, logs the corrected value, and returns exit code 1 (signaling restart needed) 4. If all present: returns 0 (no action needed) **Early-exit path updated (lines 135-149):** Previously, when the config sentinel existed and `FORCE_RENDER_CONFIG!=1`, the script only refreshed network allow entries and issued a `pg_ctl reload`. Now it also calls `enforce_shared_preload_libraries()`. If the function corrects the config (returns 1), a `pg_ctl -m fast -w restart` is issued instead of a reload — because `shared_preload_libraries` is a `postmaster`-context GUC that requires a full restart to take effect. A simple reload would silently leave the old libraries loaded. **Post-render safety belt (lines 233-236):** After a fresh template render (both first-time and `FORCE_RENDER_CONFIG=1`), `enforce_shared_preload_libraries()` runs as a safety belt to catch any drift between the template's hardcoded value and the canonical `REQUIRED_PRELOAD_LIBRARIES` array. This guards against the template and the list getting out of sync during development. ### `postgres/tools/pgsodium_getkey.sh` (NEW FILE) Server key retrieval script for pgsodium's Transparent Column Encryption: - Uses `set -euo pipefail` for strict error handling - Reads key from `PGSODIUM_KEY_FILE` env var (default: `/opt/core_data/secrets/pgsodium.key`) - If the key file is missing or not readable, prints an actionable error message with the exact command to generate a key, then exits with code 1 - On success, outputs the raw key to stdout (consumed by pgsodium internally) ### `scripts/lib/extensions.sh` **`exercise_extensions()` — 5 new smoke queries added (lines 38-48):** | Extension | Smoke Test | Validates | |-----------|-----------|-----------| | pgsodium | `pgsodium.crypto_pwhash_str('test_password')` | Argon2id password hashing works | | gzip | `gzip('hello world')` + `gunzip(gzip(...))` | Compression produces output, roundtrip preserves data | | pgzstd | `zstd_compress('hello world'::bytea)` + `zstd_decompress(...)` | Compression produces output, roundtrip preserves data | Note: gzip roundtrip uses `convert_from(gunzip(...), 'UTF8')` because `gunzip()` returns `bytea`. pgzstd roundtrip compares `bytea` directly. **`run_pgtap_smoke()` — plan count and assertions updated:** - Plan count: `39` → `42` (3 new assertions) - Added `has_extension('pgsodium', ...)`, `has_extension('gzip', ...)`, `has_extension('pgzstd', ...)` after the existing `pgcrypto` assertion (logical grouping) --- ## Architecture Decisions ### Why source builds (not apt packages) None of these three extensions are available in the PGDG apt repository for PostgreSQL 17. All three follow the identical build pattern already established by Apache AGE and pg_squeeze: shallow git clone → `make` with `PG_CONFIG` → `make install` → cleanup. ### Why `REQUIRED_PRELOAD_LIBRARIES` is separate from `CORE_EXTENSION_LIST` Not all extensions in `CORE_EXTENSION_LIST` require preloading (most don't — only 9 out of 49 do). A separate canonical list prevents accidentally adding non-preloadable extensions to `shared_preload_libraries`, which would cause postgres to fail to start. ### Why restart instead of reload for preload corrections `shared_preload_libraries` is a `postmaster`-context GUC in PostgreSQL. It can only take effect when the server process starts. A `pg_ctl reload` (SIGHUP) updates `SIGHUP`-context parameters but silently ignores `postmaster`-context changes. Using restart ensures the correction actually takes effect. ### Why enforce on every startup The config rendering has an early-exit path: once the sentinel file exists, the template is not re-rendered (unless `FORCE_RENDER_CONFIG=1`). This means manual edits to `postgresql.conf` that remove required preload libraries would persist across container restarts. The enforcement function closes this gap by validating config state independently of whether templates are rendered. ### Why pgsodium needs `shared_preload_libraries` but gzip/pgzstd do not pgsodium's key management system (which powers TCE) requires a background worker process that must be started at server boot. The `pgsodium.getkey_script` is invoked during this early initialization phase to obtain the root key. gzip and pgzstd are pure SQL function libraries with no background workers or boot-time requirements. --- ## Operational Notes ### Generating a pgsodium key Before using pgsodium's TCE features, generate a root key: ```bash head -c 32 /dev/urandom | od -A n -t x1 | tr -d ' \n' > secrets/pgsodium.key ``` The key file must be readable by the postgres user inside the container. It is accessed via the shared secrets volume at `/opt/core_data/secrets/pgsodium.key`. ### Extension count - **CORE_EXTENSION_LIST:** 46 → 49 extensions - **REQUIRED_PRELOAD_LIBRARIES:** 9 entries (new array) - **pgTap assertions:** 39 → 42 ### Docker image size impact Three additional source builds and three new shared library dependencies (`libsodium`, `zlib`, `libzstd`) will increase the Docker image size. The runtime libraries are small (libsodium ~500KB, zlib ~100KB, libzstd ~600KB). Build tools are already present in the image for AGE/pg_squeeze compilation.
1 parent 93258e4 commit 4e57f7d

6 files changed

Lines changed: 130 additions & 3 deletions

File tree

postgres/Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ RUN set -euxo pipefail; \
4545
gettext-base \
4646
bison \
4747
flex \
48+
libsodium-dev \
4849
libssl-dev \
4950
libxml2-dev \
5051
libxslt1-dev \
5152
libreadline-dev \
53+
libzstd-dev \
54+
zlib1g-dev \
5255
"postgresql-server-dev-${PG_MAJOR}" \
5356
"postgresql-${PG_MAJOR}-pgaudit" \
5457
"postgresql-${PG_MAJOR}-pgvector" \
@@ -93,6 +96,31 @@ RUN set -eux; \
9396
make install PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config"; \
9497
rm -rf /tmp/pg_squeeze
9598

99+
# Build pgsodium from source
100+
ARG PGSODIUM_VERSION=3.1.9
101+
RUN set -eux; \
102+
git clone --depth 1 --branch "v${PGSODIUM_VERSION}" --single-branch https://github.com/michelp/pgsodium.git /tmp/pgsodium; \
103+
cd /tmp/pgsodium; \
104+
make PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config"; \
105+
make install PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config"; \
106+
rm -rf /tmp/pgsodium
107+
108+
# Build pgsql-gzip from source
109+
RUN set -eux; \
110+
git clone --depth 1 https://github.com/pramsey/pgsql-gzip.git /tmp/pgsql-gzip; \
111+
cd /tmp/pgsql-gzip; \
112+
make PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config"; \
113+
make install PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config"; \
114+
rm -rf /tmp/pgsql-gzip
115+
116+
# Build pgzstd from source
117+
RUN set -eux; \
118+
git clone --depth 1 https://github.com/grahamedgecombe/pgzstd.git /tmp/pgzstd; \
119+
cd /tmp/pgzstd; \
120+
make PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config"; \
121+
make install PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config"; \
122+
rm -rf /tmp/pgzstd
123+
96124
# Copy configuration templates and initialization scripts
97125
COPY postgres/conf /opt/core_data/conf
98126
COPY postgres/initdb /docker-entrypoint-initdb.d

postgres/conf/postgresql.conf.tpl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ shared_buffers = ${PG_SHARED_BUFFERS}
1010
work_mem = ${PG_WORK_MEM}
1111
maintenance_work_mem = ${PG_MAINTENANCE_WORK_MEM}
1212
effective_cache_size = ${PG_EFFECTIVE_CACHE_SIZE}
13-
shared_preload_libraries = 'age,pgaudit,pg_stat_statements,pg_cron,pg_squeeze,auto_explain,pg_buffercache,pg_partman_bgw'
13+
shared_preload_libraries = 'age,pgaudit,pg_stat_statements,pg_cron,pg_squeeze,auto_explain,pg_buffercache,pg_partman_bgw,pgsodium'
1414
wal_level = logical
1515
archive_mode = on
1616
archive_command = 'pgbackrest --config=/var/lib/postgresql/data/pgbackrest.conf --stanza=main archive-push %p'
@@ -87,5 +87,6 @@ ssl = ${POSTGRES_SSL_ENABLED}
8787
ssl_cert_file = '${POSTGRES_SSL_CERT_FILE}'
8888
ssl_key_file = '${POSTGRES_SSL_KEY_FILE}'
8989
ssl_prefer_server_ciphers = on
90+
pgsodium.getkey_script = '/opt/core_data/tools/pgsodium_getkey.sh'
9091
datestyle = 'iso, mdy'
9192
timezone = '${TZ}'

postgres/initdb/00-render-config.sh

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,40 @@ NETWORK_ACCESS_DIR=${NETWORK_ACCESS_DIR:-/opt/core_data/network_access}
1515
NETWORK_ALLOW_FILE=${NETWORK_ALLOW_FILE:-${NETWORK_ACCESS_DIR}/allow.list}
1616
FORCE_RENDER_CONFIG=${FORCE_RENDER_CONFIG:-0}
1717

18+
# shellcheck disable=SC1091
19+
# shellcheck source=/opt/core_data/scripts/lib/extensions_list.sh
20+
source /opt/core_data/scripts/lib/extensions_list.sh
21+
22+
# Enforce that all required libraries are present in shared_preload_libraries.
23+
# Called on every startup to prevent config drift from manual edits.
24+
# Returns 0 if no changes needed, 1 if config was corrected (reload required).
25+
enforce_shared_preload_libraries() {
26+
local conf_file="${PGDATA}/postgresql.conf"
27+
[[ -f "${conf_file}" ]] || return 0
28+
29+
local current
30+
current=$(grep -E "^shared_preload_libraries" "${conf_file}" | sed "s/shared_preload_libraries *= *'\\(.*\\)'/\\1/")
31+
32+
local missing=()
33+
for lib in "${REQUIRED_PRELOAD_LIBRARIES[@]}"; do
34+
if ! echo ",${current}," | grep -q ",${lib},"; then
35+
missing+=("${lib}")
36+
fi
37+
done
38+
39+
if [[ ${#missing[@]} -gt 0 ]]; then
40+
echo "[core_data] WARNING: shared_preload_libraries missing required entries: ${missing[*]}" >&2
41+
local new_value="${current}"
42+
for lib in "${missing[@]}"; do
43+
new_value="${new_value},${lib}"
44+
done
45+
sed -i "s|^shared_preload_libraries *= *'.*'|shared_preload_libraries = '${new_value}'|" "${conf_file}"
46+
echo "[core_data] Corrected shared_preload_libraries to: ${new_value}" >&2
47+
return 1
48+
fi
49+
return 0
50+
}
51+
1852
apply_network_allow_entries() {
1953
local hba_path="${PGDATA}/pg_hba.conf"
2054
if [[ ! -f "${hba_path}" ]]; then
@@ -101,8 +135,17 @@ if [[ -f "${SENTINEL}" ]]; then
101135
if [[ "${FORCE_RENDER_CONFIG}" != "1" ]]; then
102136
echo "[core_data] Configuration already rendered; refreshing network allow entries." >&2
103137
apply_network_allow_entries
138+
local needs_reload=0
139+
if ! enforce_shared_preload_libraries; then
140+
needs_reload=1
141+
fi
104142
if pg_ctl -D "${PGDATA}" status >/dev/null 2>&1; then
105-
if ! pg_ctl -D "${PGDATA}" reload >/dev/null 2>&1; then
143+
if [[ "${needs_reload}" -eq 1 ]]; then
144+
echo "[core_data] Restarting PostgreSQL to apply corrected shared_preload_libraries." >&2
145+
if ! pg_ctl -D "${PGDATA}" -m fast -w restart >/dev/null 2>&1; then
146+
echo "[core_data] WARNING: pg_ctl restart failed after shared_preload_libraries correction." >&2
147+
fi
148+
elif ! pg_ctl -D "${PGDATA}" reload >/dev/null 2>&1; then
106149
echo "[core_data] WARNING: pg_ctl reload failed while refreshing network allow entries." >&2
107150
fi
108151
fi
@@ -190,6 +233,10 @@ pg1-path=${PGDATA}
190233
pg1-port=5433
191234
CONF
192235

236+
# Safety belt: enforce shared_preload_libraries even after fresh render
237+
# to catch any drift between the template and the canonical list.
238+
enforce_shared_preload_libraries || true
239+
193240
echo "[core_data] Rendered PostgreSQL configs and pgBackRest configuration." >&2
194241

195242
# Start postgres with our rendered config (which includes shared_preload_libraries).

postgres/tools/pgsodium_getkey.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
# SPDX-FileCopyrightText: 2025 Blackcat Informatics® Inc.
3+
# SPDX-License-Identifier: MIT
4+
5+
# pgsodium server key retrieval script.
6+
# Called by pgsodium to obtain the root encryption key for Transparent Column Encryption.
7+
# The key must be exactly 32 bytes (256 bits) of raw key material.
8+
9+
set -euo pipefail
10+
11+
PGSODIUM_KEY_FILE="${PGSODIUM_KEY_FILE:-/opt/core_data/secrets/pgsodium.key}"
12+
13+
if [[ ! -r "${PGSODIUM_KEY_FILE}" ]]; then
14+
echo "[core_data] ERROR: pgsodium key file not found or not readable: ${PGSODIUM_KEY_FILE}" >&2
15+
echo "[core_data] Generate a key with: head -c 32 /dev/urandom | od -A n -t x1 | tr -d ' \\n' > ${PGSODIUM_KEY_FILE}" >&2
16+
exit 1
17+
fi
18+
19+
cat "${PGSODIUM_KEY_FILE}"

scripts/lib/extensions.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ RESET search_path;
3535
-- pgcrypto smoke
3636
SELECT encode(digest('core_data', 'sha256'), 'hex');
3737
38+
-- pgsodium smoke
39+
SELECT length(pgsodium.crypto_pwhash_str('test_password')) > 0 AS pgsodium_ok;
40+
41+
-- gzip smoke
42+
SELECT length(gzip('hello world')) > 0 AS gzip_compress_ok;
43+
SELECT convert_from(gunzip(gzip('hello world')), 'UTF8') = 'hello world' AS gzip_roundtrip_ok;
44+
45+
-- pgzstd smoke
46+
SELECT length(zstd_compress('hello world'::bytea)) > 0 AS zstd_compress_ok;
47+
SELECT zstd_decompress(zstd_compress('hello world'::bytea)) = 'hello world'::bytea AS zstd_roundtrip_ok;
48+
3849
-- uuid-ossp smoke
3950
SELECT uuid_generate_v4();
4051
@@ -122,7 +133,7 @@ run_pgtap_smoke() {
122133
psql --set ON_ERROR_STOP=on --username "${POSTGRES_SUPERUSER:-postgres}" --dbname "${database}" <<'SQL'
123134
CREATE SCHEMA IF NOT EXISTS test_core_data;
124135
SET search_path = test_core_data, public;
125-
SELECT plan(39);
136+
SELECT plan(42);
126137
SELECT ok(current_schema = 'test_core_data', 'search_path set to test schema');
127138
SELECT has_extension('vector', 'vector extension installed');
128139
SELECT has_extension('postgis', 'postgis extension installed');
@@ -134,6 +145,9 @@ SELECT has_extension('pg_stat_statements', 'pg_stat_statements extension install
134145
SELECT ok(position('auto_explain' in current_setting('shared_preload_libraries')) > 0, 'auto_explain registered in shared_preload_libraries');
135146
SELECT has_extension('pg_buffercache', 'pg_buffercache extension installed');
136147
SELECT has_extension('pgcrypto', 'pgcrypto extension installed');
148+
SELECT has_extension('pgsodium', 'pgsodium extension installed');
149+
SELECT has_extension('gzip', 'gzip extension installed');
150+
SELECT has_extension('pgzstd', 'pgzstd extension installed');
137151
SELECT has_extension('citext', 'citext extension installed');
138152
SELECT has_extension('cube', 'cube extension installed');
139153
SELECT has_extension('hstore', 'hstore extension installed');

scripts/lib/extensions_list.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ CORE_EXTENSION_LIST=(
2828
pg_stat_statements
2929
pg_trgm
3030
pgcrypto
31+
pgsodium
32+
gzip
33+
pgzstd
3134
pgstattuple
3235
pgtap
3336
pgaudit
@@ -44,3 +47,18 @@ CORE_EXTENSION_LIST=(
4447
uuid-ossp
4548
vector
4649
)
50+
51+
# Canonical list of libraries that must be in shared_preload_libraries.
52+
# Enforced on every container startup regardless of config state.
53+
# shellcheck disable=SC2034
54+
REQUIRED_PRELOAD_LIBRARIES=(
55+
age
56+
pgaudit
57+
pg_stat_statements
58+
pg_cron
59+
pg_squeeze
60+
auto_explain
61+
pg_buffercache
62+
pg_partman_bgw
63+
pgsodium
64+
)

0 commit comments

Comments
 (0)