diff --git a/role_scripts/10/primary/start.sh b/role_scripts/10/primary/start.sh index 62ffbfc..82b95fb 100755 --- a/role_scripts/10/primary/start.sh +++ b/role_scripts/10/primary/start.sh @@ -35,7 +35,11 @@ echo "max_replication_slots = 90" >>/tmp/postgresql.conf echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -88,6 +92,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -172,6 +292,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else @@ -212,4 +340,3 @@ if [[ "$STREAMING" == "synchronous" ]]; then fi # ref: https://superuser.com/a/246841/985093 cat /tmp/postgresql.conf $PGDATA/postgresql.conf >"/tmp/postgresql.conf.tmp" && mv "/tmp/postgresql.conf.tmp" "$PGDATA/postgresql.conf" - diff --git a/role_scripts/10/standby/ha_backup_job.sh b/role_scripts/10/standby/ha_backup_job.sh index d4c1064..b6ee739 100755 --- a/role_scripts/10/standby/ha_backup_job.sh +++ b/role_scripts/10/standby/ha_backup_job.sh @@ -60,10 +60,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -84,7 +87,17 @@ else echo "wal_keep_segments = 160" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi echo "max_replication_slots = 90" >>/tmp/postgresql.conf echo "wal_log_hints = on" >>/tmp/postgresql.conf diff --git a/role_scripts/10/standby/run.sh b/role_scripts/10/standby/run.sh index 31a441c..e7ca899 100755 --- a/role_scripts/10/standby/run.sh +++ b/role_scripts/10/standby/run.sh @@ -89,7 +89,6 @@ if [[ "$WAL_LIMIT_POLICY" == "ReplicationSlot" ]]; then done fi - if [[ ! -e "$PGDATA/PG_VERSION" ]]; then if [[ ! -e "/var/pv/IGNORE_FILESYSTEM_MOUNT_CHECK" ]]; then # Robust /var/pv mount availability check before any destructive operation or basebackup @@ -118,10 +117,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch $PGDATA/recovery.conf else @@ -154,7 +156,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi echo "max_replication_slots = 90" >>/tmp/postgresql.conf if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/10/standby/warm_stanby.sh b/role_scripts/10/standby/warm_stanby.sh index aa7a7d4..70d6834 100755 --- a/role_scripts/10/standby/warm_stanby.sh +++ b/role_scripts/10/standby/warm_stanby.sh @@ -33,7 +33,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi echo "hot_standby = off" >>/tmp/postgresql.conf diff --git a/role_scripts/11/primary/start.sh b/role_scripts/11/primary/start.sh index 88639ca..c57f807 100755 --- a/role_scripts/11/primary/start.sh +++ b/role_scripts/11/primary/start.sh @@ -41,7 +41,11 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -97,6 +101,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -213,6 +333,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else @@ -253,4 +381,3 @@ if [[ "$STREAMING" == "synchronous" ]]; then fi # ref: https://superuser.com/a/246841/985093 cat /tmp/postgresql.conf $PGDATA/postgresql.conf >"/tmp/postgresql.conf.tmp" && mv "/tmp/postgresql.conf.tmp" "$PGDATA/postgresql.conf" - diff --git a/role_scripts/11/standby/ha_backup_job.sh b/role_scripts/11/standby/ha_backup_job.sh index f5083be..15df5dc 100755 --- a/role_scripts/11/standby/ha_backup_job.sh +++ b/role_scripts/11/standby/ha_backup_job.sh @@ -60,10 +60,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -88,7 +91,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/11/standby/run.sh b/role_scripts/11/standby/run.sh index 4eb0a72..ffd1391 100755 --- a/role_scripts/11/standby/run.sh +++ b/role_scripts/11/standby/run.sh @@ -116,10 +116,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch /var/pv/data/standby.signal else @@ -157,7 +160,17 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/11/standby/warm_stanby.sh b/role_scripts/11/standby/warm_stanby.sh index 006617f..c8e1c58 100755 --- a/role_scripts/11/standby/warm_stanby.sh +++ b/role_scripts/11/standby/warm_stanby.sh @@ -35,7 +35,17 @@ echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf echo "hot_standby = off" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [[ "$STREAMING" == "synchronous" ]]; then # setup synchronous streaming replication diff --git a/role_scripts/12/primary/start.sh b/role_scripts/12/primary/start.sh index d11f018..2c40315 100755 --- a/role_scripts/12/primary/start.sh +++ b/role_scripts/12/primary/start.sh @@ -42,7 +42,11 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -117,6 +121,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -233,6 +353,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else diff --git a/role_scripts/12/standby/ha_backup_job.sh b/role_scripts/12/standby/ha_backup_job.sh index 1ae79cc..d79e8d5 100755 --- a/role_scripts/12/standby/ha_backup_job.sh +++ b/role_scripts/12/standby/ha_backup_job.sh @@ -59,10 +59,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -89,7 +92,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf # we are not doing any archiving by default but it's better to have this config in our postgresql.conf file in case of customization. echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/12/standby/run.sh b/role_scripts/12/standby/run.sh index d60ff98..4b3594e 100755 --- a/role_scripts/12/standby/run.sh +++ b/role_scripts/12/standby/run.sh @@ -89,7 +89,6 @@ if [[ "$WAL_LIMIT_POLICY" == "ReplicationSlot" ]]; then done fi - if [[ ! -e "$PGDATA/PG_VERSION" ]]; then if [[ ! -e "/var/pv/IGNORE_FILESYSTEM_MOUNT_CHECK" ]]; then # Robust /var/pv mount availability check before any destructive operation or basebackup @@ -117,10 +116,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch /var/pv/data/standby.signal else @@ -150,13 +152,22 @@ if [[ "$WAL_LIMIT_POLICY" == "ReplicationSlot" ]]; then echo "primary_slot_name = "$CLEAN_HOSTNAME"" >>/tmp/postgresql.conf fi - echo "wal_log_hints = on" >>/tmp/postgresql.conf # we are not doing any archiving by default but it's better to have this config in our postgresql.conf file in case of customization. echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/12/standby/warm_stanby.sh b/role_scripts/12/standby/warm_stanby.sh index 7c182c6..d491780 100755 --- a/role_scripts/12/standby/warm_stanby.sh +++ b/role_scripts/12/standby/warm_stanby.sh @@ -42,7 +42,17 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi echo "hot_standby = off" >>/tmp/postgresql.conf diff --git a/role_scripts/13/primary/start.sh b/role_scripts/13/primary/start.sh index 50e446c..cafc761 100755 --- a/role_scripts/13/primary/start.sh +++ b/role_scripts/13/primary/start.sh @@ -42,7 +42,11 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -138,6 +142,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -264,6 +384,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else diff --git a/role_scripts/13/standby/ha_backup_job.sh b/role_scripts/13/standby/ha_backup_job.sh index 9dbdf52..ee961bf 100755 --- a/role_scripts/13/standby/ha_backup_job.sh +++ b/role_scripts/13/standby/ha_backup_job.sh @@ -59,10 +59,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -89,7 +92,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf # we are not doing any archiving by default but it's better to have this config in our postgresql.conf file in case of customization. echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/13/standby/remote-replica.sh b/role_scripts/13/standby/remote-replica.sh index eacece0..5ac1c31 100755 --- a/role_scripts/13/standby/remote-replica.sh +++ b/role_scripts/13/standby/remote-replica.sh @@ -69,10 +69,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SOURCE_SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" fi fi @@ -99,7 +102,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/13/standby/run.sh b/role_scripts/13/standby/run.sh index 259d4f7..c6cc7b9 100755 --- a/role_scripts/13/standby/run.sh +++ b/role_scripts/13/standby/run.sh @@ -89,7 +89,6 @@ if [[ "$WAL_LIMIT_POLICY" == "ReplicationSlot" ]]; then done fi - if [[ ! -e "$PGDATA/PG_VERSION" ]]; then if [[ ! -e "/var/pv/IGNORE_FILESYSTEM_MOUNT_CHECK" ]]; then # Robust /var/pv mount availability check before any destructive operation or basebackup @@ -117,10 +116,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch /var/pv/data/standby.signal else @@ -159,7 +161,17 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/13/standby/warm_stanby.sh b/role_scripts/13/standby/warm_stanby.sh index f77be34..b39e1e8 100755 --- a/role_scripts/13/standby/warm_stanby.sh +++ b/role_scripts/13/standby/warm_stanby.sh @@ -38,7 +38,17 @@ echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf echo "hot_standby = off" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [[ "$STREAMING" == "synchronous" ]]; then # setup synchronous streaming replication diff --git a/role_scripts/14/primary/start.sh b/role_scripts/14/primary/start.sh index 9bff7aa..7ebe72e 100755 --- a/role_scripts/14/primary/start.sh +++ b/role_scripts/14/primary/start.sh @@ -42,7 +42,11 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -59,8 +63,6 @@ if [[ "$CLIENT_AUTH_MODE" == "scram" ]]; then echo "password_encryption = scram-sha-256" >>/tmp/postgresql.conf fi - - # ****************** Recovery config ************************** echo "recovery_target_timeline = 'latest'" >>/tmp/postgresql.conf # primary_conninfo is used for streaming replication @@ -144,6 +146,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -270,6 +388,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else diff --git a/role_scripts/14/standby/ha_backup_job.sh b/role_scripts/14/standby/ha_backup_job.sh index 5038380..444f001 100755 --- a/role_scripts/14/standby/ha_backup_job.sh +++ b/role_scripts/14/standby/ha_backup_job.sh @@ -59,10 +59,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -89,7 +92,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/14/standby/remote-replica.sh b/role_scripts/14/standby/remote-replica.sh index 0999e7f..6bc6fee 100755 --- a/role_scripts/14/standby/remote-replica.sh +++ b/role_scripts/14/standby/remote-replica.sh @@ -69,10 +69,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SOURCE_SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" fi fi @@ -99,7 +102,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/14/standby/run.sh b/role_scripts/14/standby/run.sh index 74ea25a..25a5fbe 100755 --- a/role_scripts/14/standby/run.sh +++ b/role_scripts/14/standby/run.sh @@ -89,7 +89,6 @@ if [[ "$WAL_LIMIT_POLICY" == "ReplicationSlot" ]]; then done fi - if [[ ! -e "$PGDATA/PG_VERSION" ]]; then if [[ ! -e "/var/pv/IGNORE_FILESYSTEM_MOUNT_CHECK" ]]; then # Robust /var/pv mount availability check before any destructive operation or basebackup @@ -117,10 +116,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch /var/pv/data/standby.signal else @@ -159,7 +161,17 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/14/standby/warm_stanby.sh b/role_scripts/14/standby/warm_stanby.sh index df05ee9..928ec3d 100755 --- a/role_scripts/14/standby/warm_stanby.sh +++ b/role_scripts/14/standby/warm_stanby.sh @@ -38,7 +38,17 @@ echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf echo "hot_standby = off" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [[ "$STREAMING" == "synchronous" ]]; then # setup synchronous streaming replication diff --git a/role_scripts/15/primary/start.sh b/role_scripts/15/primary/start.sh index 5f73009..f128bd2 100755 --- a/role_scripts/15/primary/start.sh +++ b/role_scripts/15/primary/start.sh @@ -42,7 +42,11 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -145,6 +149,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -271,6 +391,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else diff --git a/role_scripts/15/standby/ha_backup_job.sh b/role_scripts/15/standby/ha_backup_job.sh index 040a43c..ff53d0c 100755 --- a/role_scripts/15/standby/ha_backup_job.sh +++ b/role_scripts/15/standby/ha_backup_job.sh @@ -61,10 +61,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -91,7 +94,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/15/standby/remote-replica.sh b/role_scripts/15/standby/remote-replica.sh index 0999e7f..6bc6fee 100755 --- a/role_scripts/15/standby/remote-replica.sh +++ b/role_scripts/15/standby/remote-replica.sh @@ -69,10 +69,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SOURCE_SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" fi fi @@ -99,7 +102,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/15/standby/run.sh b/role_scripts/15/standby/run.sh index 2db653a..b0df61b 100755 --- a/role_scripts/15/standby/run.sh +++ b/role_scripts/15/standby/run.sh @@ -89,7 +89,6 @@ if [[ "$WAL_LIMIT_POLICY" == "ReplicationSlot" ]]; then done fi - if [[ ! -e "$PGDATA/PG_VERSION" ]]; then if [[ ! -e "/var/pv/IGNORE_FILESYSTEM_MOUNT_CHECK" ]]; then pv_df_output=$(cat /proc/mounts) @@ -116,10 +115,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch /var/pv/data/standby.signal else @@ -158,7 +160,17 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/15/standby/warm_stanby.sh b/role_scripts/15/standby/warm_stanby.sh index d6ae859..efecb83 100755 --- a/role_scripts/15/standby/warm_stanby.sh +++ b/role_scripts/15/standby/warm_stanby.sh @@ -39,7 +39,17 @@ echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf echo "hot_standby = off" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [[ "$STREAMING" == "synchronous" ]]; then # setup synchronous streaming replication diff --git a/role_scripts/16/primary/start.sh b/role_scripts/16/primary/start.sh index 5dc2a7b..21980b6 100755 --- a/role_scripts/16/primary/start.sh +++ b/role_scripts/16/primary/start.sh @@ -43,7 +43,11 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -143,6 +147,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -270,6 +390,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else diff --git a/role_scripts/16/standby/ha_backup_job.sh b/role_scripts/16/standby/ha_backup_job.sh index e9c8003..c654fcf 100755 --- a/role_scripts/16/standby/ha_backup_job.sh +++ b/role_scripts/16/standby/ha_backup_job.sh @@ -61,10 +61,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -91,7 +94,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/16/standby/remote-replica.sh b/role_scripts/16/standby/remote-replica.sh index 2b1728a..df5ba30 100755 --- a/role_scripts/16/standby/remote-replica.sh +++ b/role_scripts/16/standby/remote-replica.sh @@ -69,10 +69,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SOURCE_SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" fi fi @@ -99,7 +102,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/16/standby/run.sh b/role_scripts/16/standby/run.sh index 7a865db..afde4c0 100755 --- a/role_scripts/16/standby/run.sh +++ b/role_scripts/16/standby/run.sh @@ -116,18 +116,19 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch /var/pv/data/standby.signal else /run_scripts/role/warm_stanby.sh fi - - # setup postgresql.conf touch /tmp/postgresql.conf @@ -160,7 +161,17 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/16/standby/warm_stanby.sh b/role_scripts/16/standby/warm_stanby.sh index a1eff58..75a59ea 100755 --- a/role_scripts/16/standby/warm_stanby.sh +++ b/role_scripts/16/standby/warm_stanby.sh @@ -39,7 +39,17 @@ echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf echo "hot_standby = off" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [[ "$STREAMING" == "synchronous" ]]; then # setup synchronous streaming replication diff --git a/role_scripts/17/primary/start.sh b/role_scripts/17/primary/start.sh index 0eae773..b7b3d57 100755 --- a/role_scripts/17/primary/start.sh +++ b/role_scripts/17/primary/start.sh @@ -44,7 +44,11 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -148,6 +152,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -275,6 +395,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else diff --git a/role_scripts/17/standby/ha_backup_job.sh b/role_scripts/17/standby/ha_backup_job.sh index e9c8003..c654fcf 100755 --- a/role_scripts/17/standby/ha_backup_job.sh +++ b/role_scripts/17/standby/ha_backup_job.sh @@ -61,10 +61,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -91,7 +94,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/17/standby/remote-replica.sh b/role_scripts/17/standby/remote-replica.sh index 2b1728a..df5ba30 100755 --- a/role_scripts/17/standby/remote-replica.sh +++ b/role_scripts/17/standby/remote-replica.sh @@ -69,10 +69,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SOURCE_SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" fi fi @@ -99,7 +102,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/17/standby/run.sh b/role_scripts/17/standby/run.sh index 5e54fe0..afde4c0 100755 --- a/role_scripts/17/standby/run.sh +++ b/role_scripts/17/standby/run.sh @@ -89,7 +89,6 @@ if [[ "$WAL_LIMIT_POLICY" == "ReplicationSlot" ]]; then done fi - if [[ ! -e "$PGDATA/PG_VERSION" ]]; then if [[ ! -e "/var/pv/IGNORE_FILESYSTEM_MOUNT_CHECK" ]]; then # Robust /var/pv mount availability check before any destructive operation or basebackup @@ -117,18 +116,19 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch /var/pv/data/standby.signal else /run_scripts/role/warm_stanby.sh fi - - # setup postgresql.conf touch /tmp/postgresql.conf @@ -161,7 +161,17 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/17/standby/warm_stanby.sh b/role_scripts/17/standby/warm_stanby.sh index a1eff58..75a59ea 100755 --- a/role_scripts/17/standby/warm_stanby.sh +++ b/role_scripts/17/standby/warm_stanby.sh @@ -39,7 +39,17 @@ echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf echo "hot_standby = off" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [[ "$STREAMING" == "synchronous" ]]; then # setup synchronous streaming replication diff --git a/role_scripts/18/primary/start.sh b/role_scripts/18/primary/start.sh index 85c91fc..2c108ba 100755 --- a/role_scripts/18/primary/start.sh +++ b/role_scripts/18/primary/start.sh @@ -44,7 +44,11 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then echo "ssl =on" >>/tmp/postgresql.conf @@ -141,6 +145,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -268,6 +388,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else diff --git a/role_scripts/18/standby/ha_backup_job.sh b/role_scripts/18/standby/ha_backup_job.sh index e9c8003..c654fcf 100755 --- a/role_scripts/18/standby/ha_backup_job.sh +++ b/role_scripts/18/standby/ha_backup_job.sh @@ -61,10 +61,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -91,7 +94,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/18/standby/remote-replica.sh b/role_scripts/18/standby/remote-replica.sh index 2b1728a..df5ba30 100755 --- a/role_scripts/18/standby/remote-replica.sh +++ b/role_scripts/18/standby/remote-replica.sh @@ -69,10 +69,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SOURCE_SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" -d "sslmode=$SOURCE_SSL_MODE sslrootcert=/tls/certs/remote/ca.crt sslcert=/tls/certs/remote/client.crt sslkey=/tls/certs/remote/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=$PRIMARY_USER_NAME --progress --host="$PRIMARY_HOST" fi fi @@ -99,7 +102,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/18/standby/run.sh b/role_scripts/18/standby/run.sh index 5e54fe0..afde4c0 100755 --- a/role_scripts/18/standby/run.sh +++ b/role_scripts/18/standby/run.sh @@ -89,7 +89,6 @@ if [[ "$WAL_LIMIT_POLICY" == "ReplicationSlot" ]]; then done fi - if [[ ! -e "$PGDATA/PG_VERSION" ]]; then if [[ ! -e "/var/pv/IGNORE_FILESYSTEM_MOUNT_CHECK" ]]; then # Robust /var/pv mount availability check before any destructive operation or basebackup @@ -117,18 +116,19 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi touch /var/pv/data/standby.signal else /run_scripts/role/warm_stanby.sh fi - - # setup postgresql.conf touch /tmp/postgresql.conf @@ -161,7 +161,17 @@ else echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf fi -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/18/standby/warm_stanby.sh b/role_scripts/18/standby/warm_stanby.sh index a1eff58..75a59ea 100755 --- a/role_scripts/18/standby/warm_stanby.sh +++ b/role_scripts/18/standby/warm_stanby.sh @@ -39,7 +39,17 @@ echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf echo "hot_standby = off" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [[ "$STREAMING" == "synchronous" ]]; then # setup synchronous streaming replication diff --git a/role_scripts/9/primary/start.sh b/role_scripts/9/primary/start.sh index 9978c85..25a4d9c 100755 --- a/role_scripts/9/primary/start.sh +++ b/role_scripts/9/primary/start.sh @@ -29,7 +29,11 @@ fi echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf echo "max_replication_slots = 90" >>/tmp/postgresql.conf if [[ "${SSL:-0}" == "ON" ]]; then @@ -83,6 +87,122 @@ echo psql+=(--username "$POSTGRES_USER" --dbname "$POSTGRES_DB") echo +# pg_tde principal-key bootstrap. Runs once on first initialization, after the +# database and superuser exist and before any encrypted table or the +# default_table_access_method flip can be used. Idempotent on retries. +if [[ "${TDE_ENABLED:-false}" == "true" && "${BOOTSTRAP}" == "true" ]]; then + # Run every pg_tde key operation in this block at the configured cipher. The + # post-bootstrap config that carries pg_tde.cipher is not loaded yet, so without + # this the principal key would be created (and validated) at the wrong length. + export PGOPTIONS="-c pg_tde.cipher=${TDE_CIPHER:-aes_128}" + # tde_bootstrap_fatal aborts a failed first-time TDE bootstrap loudly. This runs + # only during initial initialization (BOOTSTRAP=true), so there is no user data + # yet; wipe the half-initialized data dir and exit non-zero. That forces a full + # re-bootstrap on the next start instead of falling through to a keyless boot + # (which would silently disable encryption). The pod crash-loops visibly until + # the key provider is reachable. + tde_bootstrap_fatal() { + echo "pg_tde FATAL: $1" + pg_ctl -D "$PGDATA" -m immediate -w stop || true + rm -rf "${PGDATA:?}"/* || true + exit 1 + } + # Enable the extension in the default databases. template1 makes it the + # default for future databases. + for TDE_DB in template1 "$POSTGRES_DB"; do + psql -U postgres -d "$TDE_DB" -v ON_ERROR_STOP=1 < TDE_VAULT_NAMESPACE) only when + # it is set, so the common namespace-less case keeps the 5-arg form; the + # namespace was otherwise accepted in the CRD but silently dropped here. + if [[ -n "${TDE_VAULT_NAMESPACE:-}" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}','${TDE_VAULT_NAMESPACE}');" || true + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_vault_v2('${TDE_PROVIDER_NAME}','${TDE_VAULT_ADDR}','${TDE_VAULT_MOUNT}','${TDE_VAULT_TOKEN_PATH}','${TDE_VAULT_CA_PATH}');" || true + fi + ;; + kmip) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_global_key_provider_kmip('${TDE_PROVIDER_NAME}','${TDE_KMIP_ADDR}',${TDE_KMIP_PORT},'${TDE_KMIP_CLIENT_CERT_PATH}','${TDE_KMIP_CLIENT_KEY_PATH}','${TDE_KMIP_CA_PATH}');" || true + ;; + file) + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_add_database_key_provider_file('${TDE_PROVIDER_NAME}','${TDE_FILE_PATH}');" || true + ;; + esac + + # Create then set the per-database principal key. pg_tde requires the key to + # exist before it can be set as principal, so create precedes set. The setter + # matches the provider scope (file is database-scoped, vault/kmip are global). + # Key setup is strict: a failure fails the bootstrap loudly rather than booting + # with no principal key, which would silently break encrypted-table creation. + if [[ "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_database_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set database principal key '${TDE_KEY_NAME}'" + else + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create principal key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_key_using_global_key_provider('${TDE_KEY_NAME}','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set global principal key '${TDE_KEY_NAME}'" + + # Set a server-wide DEFAULT principal key so databases created later inherit + # encryption. pg_tde stores the principal key per database OID and CREATE + # DATABASE does NOT copy it (a key set in template1 is not carried to its + # clones), so without a server default the first encrypted CREATE TABLE in a + # newly created database fails "principal key not configured". This only + # matters when new tables are encrypted by default or enforced cluster-wide, + # so gate it on TDE_DEFAULT_AM/TDE_ENFORCE (mirroring how the WAL block below + # gates on TDE_ENCRYPT_WAL): a plain opt-in-per-table cluster does not need a + # server default and should not take the strict-set crash-loop risk for it. + # The default key is a global-provider feature; the file keyring (standalone, + # database-scoped) has no server default, so this applies to vault/kmip only. + # Create precedes set, and the set is strict like the principal-key set above. + if [[ "${TDE_DEFAULT_AM:-false}" == "true" || "${TDE_ENFORCE:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create default key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_default_key_using_global_key_provider('${TDE_KEY_NAME}-default','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set default principal key '${TDE_KEY_NAME}-default'" + fi + fi + + # WAL encryption is global-only: the file keyring is a database-scoped + # provider and cannot back a server key. The operator webhook already rejects + # this combination, so this is a fail-closed backstop with a clear message + # (instead of a swallowed error that crash-loops on the misleading set-key fatal). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" && "${TDE_PROVIDER_KIND:-}" == "file" ]]; then + tde_bootstrap_fatal "WAL encryption requires a global (vault or kmip) key provider; the file keyring cannot back WAL encryption" + fi + # If WAL encryption is requested, create + set the server (WAL) key too (global only). + if [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]]; then + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_create_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || echo "pg_tde: create WAL server key returned non-zero (may already exist), continuing to set" + psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c \ + "SELECT pg_tde_set_server_key_using_global_key_provider('${TDE_KEY_NAME}-wal','${TDE_PROVIDER_NAME}');" \ + || tde_bootstrap_fatal "failed to set WAL server key '${TDE_KEY_NAME}-wal'" + fi +fi + +unset PGOPTIONS + if [[ "$BOOTSTRAP" == "true" ]];then # initialize database for f in "$INITDB"/*; do @@ -167,6 +287,14 @@ fi mv /tmp/pg_hba.conf "$PGDATA/pg_hba.conf" touch /tmp/postgresql.conf +# pg_tde runtime GUCs. Authored in the post-bootstrap rewrite, after the +# extension and principal key exist, so default_table_access_method is safe. +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf else @@ -208,4 +336,3 @@ fi # ref: https://superuser.com/a/246841/985093 cat /tmp/postgresql.conf $PGDATA/postgresql.conf >"/tmp/postgresql.conf.tmp" && mv "/tmp/postgresql.conf.tmp" "$PGDATA/postgresql.conf" - diff --git a/role_scripts/9/standby/ha_backup_job.sh b/role_scripts/9/standby/ha_backup_job.sh index 28c891b..6b2830c 100755 --- a/role_scripts/9/standby/ha_backup_job.sh +++ b/role_scripts/9/standby/ha_backup_job.sh @@ -60,10 +60,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi fi @@ -84,9 +87,18 @@ else echo "wal_keep_segments = 160" >>/tmp/postgresql.conf fi - echo "wal_log_hints = on" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi echo "max_replication_slots = 90" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf diff --git a/role_scripts/9/standby/run.sh b/role_scripts/9/standby/run.sh index 5b47a9f..bb43de4 100755 --- a/role_scripts/9/standby/run.sh +++ b/role_scripts/9/standby/run.sh @@ -119,10 +119,13 @@ if [[ ! -e "$PGDATA/PG_VERSION" ]]; then mkdir -p "$PGDATA" rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" + BASEBACKUP=pg_basebackup + [[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup + echo "pg_tde: seeding standby with '$BASEBACKUP' (TDE_ENABLED=${TDE_ENABLED:-false})" if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" + "$BASEBACKUP" -X fetch --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" else - pg_basebackup -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" + "$BASEBACKUP" -X fetch --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" fi else /run_scripts/role/warm_stanby.sh @@ -149,7 +152,17 @@ echo "wal_log_hints = on" >>/tmp/postgresql.conf echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi if [ "$STANDBY" == "hot" ]; then echo "hot_standby = on" >>/tmp/postgresql.conf diff --git a/role_scripts/9/standby/warm_stanby.sh b/role_scripts/9/standby/warm_stanby.sh index 48184af..779897a 100755 --- a/role_scripts/9/standby/warm_stanby.sh +++ b/role_scripts/9/standby/warm_stanby.sh @@ -31,7 +31,17 @@ fi echo "max_replication_slots = 90" >>/tmp/postgresql.conf echo "wal_log_hints = on" >>/tmp/postgresql.conf -echo "shared_preload_libraries = 'pg_stat_statements'" >>/tmp/postgresql.conf +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + [[ "${TDE_DEFAULT_AM:-false}" == "true" ]] && echo "default_table_access_method = tde_heap" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf diff --git a/scripts/do_pg_basebackup.sh b/scripts/do_pg_basebackup.sh index 6341b98..c3f6937 100755 --- a/scripts/do_pg_basebackup.sh +++ b/scripts/do_pg_basebackup.sh @@ -8,8 +8,10 @@ rm -rf "$PGDATA"/* chmod 0700 "$PGDATA" echo "attempting pg_basebackup..." +BASEBACKUP=pg_basebackup +[[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "password=$POSTGRES_PASSWORD sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" &>/dev/null + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --max-rate=1024M --username=postgres --progress --host="$PRIMARY_HOST" -d "password=$POSTGRES_PASSWORD sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" &>/dev/null else - pg_basebackup -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "password=$POSTGRES_PASSWORD" &>/dev/null + "$BASEBACKUP" -Xs -c fast --no-password --max-rate=1024M --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "password=$POSTGRES_PASSWORD" &>/dev/null fi diff --git a/scripts/recover_replica.sh b/scripts/recover_replica.sh index 7744474..5d2abc4 100755 --- a/scripts/recover_replica.sh +++ b/scripts/recover_replica.sh @@ -8,10 +8,12 @@ mv /run_scripts/role/run.sh /run_scripts/role/run.sh.bc pg_ctl stop -D $PGDATA rm -rf /var/pv/data +BASEBACKUP=pg_basebackup +[[ "${TDE_ENABLED:-false}" == "true" ]] && BASEBACKUP=pg_tde_basebackup if [[ "${SSL:-0}" == "ON" ]]; then - pg_basebackup -Xs -c fast --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "password=$POSTGRES_PASSWORD sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" &>/dev/null + "$BASEBACKUP" -Xs -c fast --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "password=$POSTGRES_PASSWORD sslmode=$SSL_MODE sslrootcert=/tls/certs/client/ca.crt sslcert=/tls/certs/client/client.crt sslkey=/tls/certs/client/client.key" &>/dev/null else - pg_basebackup -Xs -c fast --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "password=$POSTGRES_PASSWORD" &>/dev/null + "$BASEBACKUP" -Xs -c fast --no-password --pgdata "$PGDATA" --username=postgres --progress --host="$PRIMARY_HOST" -d "password=$POSTGRES_PASSWORD" &>/dev/null fi touch /var/pv/data/standby.signal diff --git a/scripts/restore.sh b/scripts/restore.sh index 5c3e694..a5c7723 100755 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -84,6 +84,18 @@ fi echo "archive_mode = always" >>/tmp/postgresql.conf echo "archive_command = '/bin/true'" >>/tmp/postgresql.conf echo "logging_collector = on" >>/tmp/postgresql.conf +# pg_tde must be preloaded during recovery/restore or an encrypted +# cluster corrupts. Compose the list with pg_tde first when enabled. +PRELOAD="pg_stat_statements" +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + PRELOAD="${TDE_EXTENSION:-pg_tde},${PRELOAD}" +fi +echo "shared_preload_libraries = '${PRELOAD}'" >>/tmp/postgresql.conf +if [[ "${TDE_ENABLED:-false}" == "true" ]]; then + [[ "${TDE_ENCRYPT_WAL:-false}" == "true" ]] && echo "pg_tde.wal_encrypt = on" >>/tmp/postgresql.conf + [[ "${TDE_ENFORCE:-false}" == "true" ]] && echo "pg_tde.enforce_encryption = on" >>/tmp/postgresql.conf + echo "pg_tde.cipher = '${TDE_CIPHER:-aes_128}'" >>/tmp/postgresql.conf +fi cat /run_scripts/role/postgresql.conf >>/tmp/postgresql.conf mv /tmp/postgresql.conf "$PGDATA/postgresql.conf" echo "max_replication_slots = 90" >>/tmp/postgresql.conf @@ -112,7 +124,6 @@ until pg_isready -U postgres; do fi done - if [[ "$HOST" =~ -0$ ]]; then echo "Primary restore mode"