Deployment guide

PostgreSQL Anonymizer 18

Overview

This guide covers launching and verifying PostgreSQL Anonymizer 18 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud Hardened Adminer & Database Admin and Backup Tools AMIs listing on AWS Marketplace. You pick it as the software version when you subscribe and launch. The guide hub describes the other tools in the listing.

PostgreSQL Anonymizer (anon) is a PostgreSQL extension that hides or replaces personal data inside the database. You declare masking rules in SQL with SECURITY LABEL statements. The extension then offers three capabilities.

Dynamic masking shows a role labelled MASKED anonymized values while the stored data stays untouched. Analysts and support staff can query production-shaped data without seeing the real names, contact details or identifiers.

Static anonymization rewrites the stored values in place, for example to produce a sanitized copy of a database for development or testing.

Faking and pseudonymization functions (anon.fake_*, anon.partial, anon.hash, anon.noise and more) generate the replacement values.

The image installs PostgreSQL 18.6 from the official PostgreSQL (PGDG) apt repository, and PostgreSQL Anonymizer 3.3.2, which SecureAnyCloud compiles from the pinned upstream release. The compiler and Rust toolchain used for the build are removed before the image is captured. Every instance boots with a demo database, anondb, in which the same query returns real rows to the owner and masked rows to the analyst role.

What you get:

  • PostgreSQL 18.6 (postgresql@18-main.service) on port 5432, with the anon library preloaded
  • PostgreSQL Anonymizer 3.3.2 and its English fake-data datasets
  • The demo database anondb: a customer table with a masking rule on every personal column, the masked login role analyst, and a customer_static table for trying static anonymization
  • A TLS certificate for PostgreSQL, the postgres and analyst passwords and the anonymization salt, all generated on your instance's first boot
  • An egress backstop (postgresql-anonymizer-egress-guard.service) that controls where the postgres system user may connect
  • A self-test, sudo /usr/local/sbin/postgresql-anonymizer-selftest.sh, that checks logins, refusals and masking end to end
  • Default user ubuntu, x86_64 architecture and a 16 GiB gp3 root volume

Security posture

  • No default password. The image ships with no password on any PostgreSQL role and no analyst role at all. First boot generates unique postgres and analyst passwords and writes them to /root/postgresql-anonymizer-credentials.txt, which only root can read.
  • A salt of your own. anon.hash() pseudonyms are keyed by anon.salt. First boot generates a random 256-bit salt for your instance and stores it in a configuration file that only the postgres system user can read. The salt is never stored with ALTER DATABASE ... SET, which any database user could read, and a masked role cannot read it.
  • TLS only on the network. PostgreSQL listens on 5432 on every interface, so applications and analysts can reach it. pg_hba.conf admits network clients only with hostssl and scram-sha-256, and any connection without TLS is rejected. The certificate and key are generated on your instance, and no key ships in the image.
  • The masked role reaches the masked database only. analyst can read anondb, where masking is on, and nothing else. It holds no cluster-wide read role and cannot connect to the postgres or template1 databases. Masking is enabled per database (see "Your own databases" in Step 6).
  • The superuser cannot bypass masking by accident. anon.nosuperuser is on, so static and dynamic masking refuse to run as a superuser. anon.restrict_to_trusted_schemas is on, so masking rules can call only functions from trusted schemas.
  • SSH is the only open port. The listing's default security group opens 22 only. You open 5432 yourself, to your own subnets only (Step 8).
  • Egress backstop for the database. The postgres system user cannot connect to instance metadata, link-local or reserved ranges, IPv6, or any address inside your VPC. The exceptions are this instance's own PostgreSQL and any address you add to /etc/sac/postgresql-anonymizer-egress-allow.conf. A compromised database session therefore cannot scan your VPC.
  • No metadata access for the database. systemd also blocks PostgreSQL and the first-boot service from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). Amazon Time Sync (169.254.169.123) still works.
  • IMDSv2 required. The AMI is registered with ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests.
  • No secrets in logs. Passwords are set over standard input with statement logging switched off, so they do not appear in the PostgreSQL log, /var/log/auth.log or the journal.
  • Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance. SSH accepts keys only, with no passwords and no root login.

Prerequisites

  • An AWS account and permission to launch EC2 instances and create security groups
  • An EC2 key pair in the Region you launch in
  • A VPC subnet, with a public IP address on the instance if you connect over the internet
  • Recommended instance type: m5.large. Size the instance and its EBS volume for your PostgreSQL workload.

Connecting to your instance

Connect over SSH. The PostgreSQL Anonymizer 18 on Ubuntu 24.04 image runs Ubuntu 24.04 LTS, and the SSH user is ubuntu.

1

Launch the instance

Option A: AWS Marketplace console

  • Open the SecureAnyCloud Hardened Adminer & Database Admin and Backup Tools AMIs listing on AWS Marketplace. Choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  • When the subscription is active, choose Launch your software.
  • Choose One-click launch from AWS Marketplace, or Launch from EC2 Console for the full launch wizard. In Version, choose PostgreSQL Anonymizer 18 on Ubuntu 24.04, then choose your Region.
  • Pick an instance type and your key pair. m5.large is recommended, and the software charge is $0.04 per vCPU per hour.
  • Set the security group. The listing's default group allows SSH (22) only, so restrict it to your own IP address. Step 8 adds the PostgreSQL rule.
  • Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page. Replace the VPC, subnet, key pair and CIDR with your own:

SG=$(aws ec2 create-security-group --group-name postgresql-anonymizer \
  --description "PostgreSQL Anonymizer" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=postgresql-anonymizer}]'

You do not need --metadata-options, because the AMI already makes IMDSv2 required.

2

Connect and read your credentials

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a minute of launch. It creates this instance's TLS certificate, anonymization salt and passwords, builds the demo database and then writes the credentials file.

Show the connection details. The command leaves out the two password lines. Read them with sudo cat /root/postgresql-anonymizer-credentials.txt and keep them in your secrets manager.

sudo grep -vE '^(POSTGRES_PASSWORD|ANALYST_PASSWORD)=' /root/postgresql-anonymizer-credentials.txt
# PostgreSQL Anonymizer 3.3.2 + PostgreSQL 18 - generated on first boot by postgresql-anonymizer-firstboot.service.
# These values are unique to this instance. Store them somewhere safe. POSTGRES_HOST=172.31.89.177 POSTGRES_PORT=5432 POSTGRES_USER=postgres POSTGRES_SSLMODE=require ANON_DATABASE=anondb ANALYST_USER=analyst TLS_CERTIFICATE=/etc/postgresql/18/main/tls/server.crt TLS_CERTIFICATE_SHA256=<certificate-sha256-fingerprint> ANON_SALT_FILE=/etc/postgresql/18/main/conf.d/30-sac-anon-salt.conf
# The analyst role is MASKED: it reads anonymized values from anondb; the postgres role reads the real ones.
# Static anonymization (rewrites data in place):
sudo -u postgres psql -d anondb -c "SET ROLE data_steward; SELECT anon.anonymize_table('customer_static');"
# PostgreSQL listens on 5432 on every interface but admits network clients over TLS (scram-sha-256) only.
# The security group opens SSH 22 only: open 5432 to your application subnets yourself.
# Self-test:
sudo /usr/local/sbin/postgresql-anonymizer-selftest.sh
# User guide: https://secureanycloud.com/stacks/postgresql-anonymizer-18/

POSTGRES_PASSWORD is the postgres superuser's password for network logins. ANALYST_PASSWORD is the password of the masked analyst role. On the instance itself, sudo -u postgres psql logs in without a password.

3

Confirm PostgreSQL and the extension are running

psql --version sudo -u postgres
psql -d anondb -tAc "SELECT extname || ' ' || extversion FROM pg_extension WHERE extname = 'anon';" systemctl is-active postgresql@18-main.service postgresql-anonymizer-egress-guard.service
sudo ss -Hltn
psql (PostgreSQL) 18.6 (Ubuntu 18.6-1.pgdg24.04+2) anon 3.3.2 active active LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* LISTEN 0 200 0.0.0.0:5432 0.0.0.0:* LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* LISTEN 0 200 [::]:5432 [::]:* LISTEN 0 4096 [::]:22 [::]:*

PostgreSQL listens on 5432 and SSH on 22. Port 53 on 127.0.0.53 and 127.0.0.54 is the operating system's local DNS stub.

4

Look at the masking rules

The customer table in anondb holds six rows of realistic but invented personal data. Every personal column carries a masking rule declared with SECURITY LABEL FOR anon. The extension lists the rules in anon.pg_masking_rules:

sudo -u postgres psql -d anondb -c "SELECT attname AS column, coalesce(masking_function, 'VALUE ' || masking_value) AS rule FROM anon.pg_masking_rules WHERE attrelid = 'public.customer'::regclass ORDER BY attnum;"
column | rule -------------+---------------------------------------- first_name | anon.fake_first_name() last_name | anon.fake_last_name() email | anon.fake_email() phone | anon.partial(phone, 4, $$*******$$, 0) city | anon.fake_city() national_id | anon.hash(national_id) balance | VALUE NULL (7 rows)

Names, email addresses and cities are replaced with fake values. The phone number keeps its first four characters. The national identifier becomes a salted SHA-256 pseudonym, and the same input always gives the same pseudonym on this instance, so joins on it still work. The balance is hidden.

5

The same query, two roles

Run the same SELECT over TLS as the owner and as the masked analyst role. The commands read the passwords from the credentials file at run time, so they never appear on screen. First, run it as the owner, postgres:

H="$(sudo sed -n 's/^POSTGRES_HOST=//p' /root/postgresql-anonymizer-credentials.txt)" export PGPASSWORD="$(sudo sed -n 's/^POSTGRES_PASSWORD=//p' /root/postgresql-anonymizer-credentials.txt)" psql "host=$H port=5432 dbname=anondb user=postgres sslmode=require" \
  -c "SELECT id, first_name, last_name, email, phone, city, national_id, balance FROM customer ORDER BY id LIMIT 4;" unset PGPASSWORD
id | first_name | last_name | email | phone | city | national_id | balance ----+------------+-----------+-----------------------------------------+-----------------+------------+-------------+---------- 1 | Sarah | Whitfield | sarah.whitfield@northgate-legal.example | +44 7700 900412 | Manchester | QQ123456C | 4820.55 2 | Daniel | Okafor | d.okafor@brightlane-health.example | +44 7700 900198 | Leeds | QQ998877A | 12750.00 3 | Priya | Raman | priya.raman@meridian-finance.example | +44 7700 900733 | Bristol | QQ445566B | 310.20 4 | Tomasz | Nowak | tomasz.nowak@harbourpoint.example | +44 7700 900021 | Glasgow | QQ112233D | 8905.75 (4 rows)

Now run it as the masked role, analyst. The query shortens the pseudonym column to 16 characters to fit the screen:

H="$(sudo sed -n 's/^POSTGRES_HOST=//p' /root/postgresql-anonymizer-credentials.txt)" export PGPASSWORD="$(sudo sed -n 's/^ANALYST_PASSWORD=//p' /root/postgresql-anonymizer-credentials.txt)" psql "host=$H port=5432 dbname=anondb user=analyst sslmode=require" \
  -c "SELECT id, first_name, last_name, email, phone, city, left(national_id, 16) AS national_id, balance FROM customer ORDER BY id LIMIT 4;" unset PGPASSWORD
id | first_name | last_name | email | phone | city | national_id | balance ----+------------+-----------+----------------------------+-------------+-----------------+------------------+--------- 1 | George | Norman | leslie90@example.org | +44 ******* | Robinsonton | 794d9ce209125276 | 2 | John | Munoz | amberwilliams@example.com | +44 ******* | South Jesustown | 7dd450636fb96d3a | 3 | Shaun | Rowe | michelle99@example.com | +44 ******* | New Connietown | 0962740060d7e830 | 4 | Marie | Garcia | carolhernandez@example.net | +44 ******* | Port Tylerstad | ecf962ecff00ccf6 | (4 rows)

The fake names, email addresses and cities change on every query, but the pseudonyms do not. The stored rows are unchanged, as the owner's query above still shows. The masked role cannot write to the table and cannot read the salt:

H="$(sudo sed -n 's/^POSTGRES_HOST=//p' /root/postgresql-anonymizer-credentials.txt)" export PGPASSWORD="$(sudo sed -n 's/^ANALYST_PASSWORD=//p' /root/postgresql-anonymizer-credentials.txt)" psql "host=$H port=5432 dbname=anondb user=analyst sslmode=require" -c "UPDATE customer SET balance = 0 WHERE id = 1;" 2>&1 || true psql "host=$H port=5432 dbname=anondb user=analyst sslmode=require" -c "SHOW anon.salt;" 2>&1 || true unset PGPASSWORD
ERROR: Anon: role is masked ERROR: permission denied to examine "anon.salt" DETAIL: Only roles with privileges of the "pg_read_all_settings" role may examine this parameter.

pg_hba.conf rejects a connection without TLS before any password is checked, and a wrong password is refused:

H="$(sudo sed -n 's/^POSTGRES_HOST=//p' /root/postgresql-anonymizer-credentials.txt)" for mode in disable require; do OUT="$(PGPASSWORD=<set-at-first-boot> psql "host=$H port=5432 dbname=anondb user=analyst sslmode=$mode" -c 'SELECT 1' 2>&1 || true)" echo "${OUT#*failed: }" done
FATAL: pg_hba.conf rejects connection for host "172.31.89.177", user "analyst", database "anondb", no encryption FATAL: password authentication failed for user "analyst"
6

Declare masking rules on your own table

Masking a column takes one statement. This example creates a staff table, labels its columns and lets the masked role read it. The function anon.noise adds up to 10% random noise to the salary.

sudo -u postgres psql -d anondb <<'SQL' CREATE TABLE staff (id int PRIMARY KEY, full_name text, work_email text, salary numeric(10,2)); INSERT INTO staff VALUES (1, 'Helen Ashworth', 'helen.ashworth@corp.example', 61500.00), (2, 'Marcus Oyelaran', 'marcus.oyelaran@corp.example', 58250.00); SECURITY LABEL FOR anon ON COLUMN staff.full_name IS 'MASKED WITH FUNCTION anon.fake_last_name()'; SECURITY LABEL FOR anon ON COLUMN staff.work_email IS 'MASKED WITH FUNCTION anon.fake_email()'; SECURITY LABEL FOR anon ON COLUMN staff.salary IS 'MASKED WITH FUNCTION anon.noise(salary, 0.1)'; GRANT SELECT ON staff TO analyst; SQL
CREATE TABLE INSERT 0 2 SECURITY LABEL SECURITY LABEL SECURITY LABEL GRANT

The demo's grants cover only the tables that existed on first boot. Every table you add to anondb later needs its own GRANT SELECT ON <table> TO analyst;, as above. Run it after the table's masking labels are in place.

Read it back as the masked role:

H="$(sudo sed -n 's/^POSTGRES_HOST=//p' /root/postgresql-anonymizer-credentials.txt)" export PGPASSWORD="$(sudo sed -n 's/^ANALYST_PASSWORD=//p' /root/postgresql-anonymizer-credentials.txt)" psql "host=$H port=5432 dbname=anondb user=analyst sslmode=require" -c "SELECT * FROM staff ORDER BY id;" unset PGPASSWORD
id | full_name | work_email | salary ----+-----------+----------------------+---------- 1 | Bowers | xbenitez@example.org | 64485.61 2 | Holmes | qadkins@example.com | 56540.43 (2 rows)

To mask another role, label it: SECURITY LABEL FOR anon ON ROLE <role> IS 'MASKED';. A masked role also needs read access to the tables it queries. The demo grants analyst USAGE and SELECT on the public schema of anondb only. It also grants SELECT on the extension's fake-data tables (anon.first_name, anon.email and the others), which the fake_*() functions read.

Your own databases

Masking is enabled per database, not for the whole server. It is on in anondb only. A database you create or restore has no masking until you switch it on there with ALTER DATABASE <db> SET anon.transparent_dynamic_masking TO true and declare the security labels for its personal columns. Until then, any role that can read its tables reads the real values.

For that reason, the masked analyst role has no access to any database except anondb. It is not a member of pg_read_all_data or any other built-in role, and it cannot connect to postgres or template1. PostgreSQL lets every role connect to a database you create, but analyst cannot read any of its tables until you grant access. The image never grants that for you. Check it from the instance:

H="$(sudo sed -n 's/^POSTGRES_HOST=//p' /root/postgresql-anonymizer-credentials.txt)" export PGPASSWORD="$(sudo sed -n 's/^ANALYST_PASSWORD=//p' /root/postgresql-anonymizer-credentials.txt)" OUT="$(psql "host=$H port=5432 dbname=postgres user=analyst sslmode=require" -c "SELECT 1;" 2>&1 || true)" echo "${OUT#*failed: }" psql "host=$H port=5432 dbname=anondb user=analyst sslmode=require" -tAc "SELECT pg_has_role('pg_read_all_data', 'MEMBER');" unset PGPASSWORD
FATAL: permission denied for database "postgres" DETAIL: User does not have CONNECT privilege. f

To give a masked role access to one of your own databases, work as postgres inside that database and follow this order. Switch masking on and label the columns first. Grant the masked role access only after that:

CREATE EXTENSION IF NOT EXISTS anon; ALTER DATABASE mydb SET anon.transparent_dynamic_masking TO true; SECURITY LABEL FOR anon ON COLUMN public.patient.full_name IS 'MASKED WITH FUNCTION anon.fake_last_name()'; SECURITY LABEL FOR anon ON COLUMN public.patient.email IS 'MASKED WITH FUNCTION anon.fake_email()'; -- only now, with masking on and every personal column labelled: GRANT CONNECT ON DATABASE mydb TO analyst; GRANT USAGE ON SCHEMA public TO analyst; GRANT SELECT ON ALL TABLES IN SCHEMA public TO analyst; -- the fake_*() functions read the extension's own synthetic datasets in the anon schema: GRANT SELECT ON anon.address, anon.city, anon.company, anon.country, anon.email, anon.first_name, anon.iban, anon.last_name, anon.lorem_ipsum, anon.postcode, anon.siret, anon.street_name TO analyst;

ALTER DATABASE ... SET applies to new sessions, so reconnect before you test. Do not grant a masked role pg_read_all_data, because it reads every table in every database, including the ones where masking is off.

PostgreSQL gives PUBLIC the CONNECT privilege on every database you create. For each new database, either label its columns and enable masking there, or run REVOKE CONNECT ON DATABASE mydb FROM PUBLIC; straight after CREATE DATABASE. If a dump you restore grants SELECT to PUBLIC, switch masking on and label the columns before you restore it.

7

Static anonymization

Static anonymization replaces the stored values for everyone, including the owner, and it cannot be undone. The demo provides customer_static, a copy of the customer data with the same rules, owned by the non-login role data_steward. Look at the table first:

sudo -u postgres psql -d anondb -c "SELECT id, first_name, last_name, email, national_id, balance FROM customer_static ORDER BY id LIMIT 3;"
id | first_name | last_name | email | national_id | balance ----+------------+-----------+-----------------------------------------+-------------+---------- 1 | Sarah | Whitfield | sarah.whitfield@northgate-legal.example | QQ123456C | 4820.55 2 | Daniel | Okafor | d.okafor@brightlane-health.example | QQ998877A | 12750.00 3 | Priya | Raman | priya.raman@meridian-finance.example | QQ445566B | 310.20 (3 rows)

Because anon.nosuperuser is on, the superuser is refused, so the table's owner runs the anonymization instead:

sudo -u postgres psql -d anondb -tAc "SELECT anon.anonymize_table('public.customer_static');" 2>&1 || true
sudo -u postgres psql -d anondb -tAc "SET ROLE data_steward; SELECT anon.anonymize_table('public.customer_static');"
ERROR: Anon: Static and dynamic masking cannot be used with a superuser if anon.nosuperuser is set to true. SET t

Read the table back, and confirm that no original identifier or address is left:

sudo -u postgres psql -d anondb -c "SELECT id, first_name, last_name, email, left(national_id, 16) AS national_id, balance FROM customer_static ORDER BY id LIMIT 3;"
sudo -u postgres psql -d anondb -tAc "SELECT count(*) FROM customer_static WHERE national_id LIKE 'QQ%' OR email LIKE '%northgate-legal%';"
id | first_name | last_name | email | national_id | balance ----+------------+-----------+-----------------------------+------------------+--------- 1 | Trevor | Mcgee | jacquelinewalsh@example.org | 794d9ce209125276 | 2 | Veronica | Lawrence | kennedyrachel@example.net | 7dd450636fb96d3a | 3 | Jennifer | Baird | aguilarjustin@example.org | 0962740060d7e830 | (3 rows) 0

anon.anonymize_database() anonymizes every labelled table in a database at once. anon.anonymize_schema_parallel() spreads the work over background workers, which run as the role that called it. Take a backup first, because there is no undo.

8

Open PostgreSQL to your applications

The security group opens SSH only. Allow 5432 from the subnets your applications and analysts connect from, and never from 0.0.0.0/0. Run this on your workstation:

aws ec2 authorize-security-group-ingress --group-id <your-sg-id> \
  --protocol tcp --port 5432 --cidr <your-subnet-cidr>

Clients connect with sslmode=require, or with verify-full and the certificate from TLS_CERTIFICATE, whose fingerprint is in the credentials file.

PostgreSQL itself needs outbound connections only if you use features such as postgres_fdw or dblink. To allow one, add the address or CIDR to /etc/sac/postgresql-anonymizer-egress-allow.conf and run sudo systemctl reload postgresql-anonymizer-egress-guard. PostgreSQL keeps running.

9

Run the self-test and check the security controls

The shipped self-test checks the whole instance in one run and prints ANON_SELFTEST_OK on success:

sudo /usr/local/sbin/postgresql-anonymizer-selftest.sh
== PostgreSQL Anonymizer self-test == OK: credentials file 0600 root OK: PostgreSQL listens on 0.0.0.0:5432 and [::]:5432 OK: postgres logs in over TLS (anon 3.3.2); a wrong password and plaintext are refused OK: analyst reads 6 MASKED rows (fake names, '+44 *******', SHA-256 national ids, NULL balances); the owner reads the real rows OK: analyst cannot write, cannot read anon.salt; a wrong analyst password is refused OK: analyst reaches anondb only: no pg_read_all_data, no CONNECT on database postgres ANON_SELFTEST_OK

Confirm these controls: the PostgreSQL units carry the metadata block, only postgres and analyst have passwords, the salt file is readable by postgres only and is not stored in the database, the masking safeguards are on, and the egress backstop is loaded.

systemctl show -p IPAddressDeny postgresql@18-main.service postgresql-anonymizer-firstboot.service
sudo -u postgres psql -tAc "SELECT rolname, rolsuper, rolcanlogin, rolpassword IS NOT NULL AS has_password FROM pg_authid WHERE rolname IN ('postgres','analyst','data_steward') ORDER BY 1;"
sudo stat -c '%a %U:%G %n' /etc/postgresql/18/main/conf.d/30-sac-anon-salt.conf
sudo -u postgres psql -tAc "SELECT count(*) FROM pg_db_role_setting WHERE setconfig::text LIKE '%anon.salt%';"
sudo -u postgres psql -tAc "SHOW anon.nosuperuser;" -c "SHOW anon.restrict_to_trusted_schemas;"
sudo nft list chain inet sac_anon_egress output | grep -c sac-backstop
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 analyst|f|t|t data_steward|f|f|f postgres|t|t|t 600 postgres:postgres /etc/postgresql/18/main/conf.d/30-sac-anon-salt.conf 0 on on 6

Maintenance

  • Change a password. Run sudo -u postgres psql, then \password postgres or \password analyst. The command prompts for the password and never puts it on a command line. Then update /root/postgresql-anonymizer-credentials.txt and your secrets manager.
  • The salt. Changing anon.salt in 30-sac-anon-salt.conf changes every anon.hash() pseudonym. Keep it stable once you rely on pseudonyms, and back it up with your database. Never set it with ALTER DATABASE or ALTER ROLE, because every user can read those settings.
  • PostgreSQL configuration. SecureAnyCloud's settings are in /etc/postgresql/18/main/conf.d/10-sac.conf (listener, password encryption, shared_preload_libraries = 'anon') and 20-sac-tls.conf (TLS, written by first boot). Client access rules are in /etc/postgresql/18/main/pg_hba.conf. Apply changes with sudo systemctl reload postgresql@18-main.service, or restart for settings that need it. Keep anon in shared_preload_libraries.
  • Your own databases. Run CREATE EXTENSION anon; and SELECT anon.init(); in each database you mask. Masking rules are per database.
  • Your own certificate. Replace /etc/postgresql/18/main/tls/server.crt and server.key (owner postgres, key mode 0600), then run sudo systemctl reload postgresql@18-main.service.
  • Backups. Take EBS snapshots or pg_dump backups regularly. Dynamic masking does not change stored data, so a backup taken as the owner contains the real values. Protect it accordingly.
  • Updates. Running sudo apt update && sudo apt upgrade installs PostgreSQL 18 minor releases from PGDG and Ubuntu security updates. The anon extension is built for PostgreSQL 18 and is not an apt package. SecureAnyCloud publishes new versions of this image when PostgreSQL Anonymizer releases one.
  • Logs. PostgreSQL logs to /var/log/postgresql/postgresql-18-main.log. First boot logs to journalctl -u postgresql-anonymizer-firstboot.service.

What SecureAnyCloud changed from the upstream defaults

  • PostgreSQL comes from PGDG, with the repository's signing key pinned to the published PostgreSQL fingerprint. PostgreSQL Anonymizer 3.3.2 is compiled from the release tarball, which is pinned by SHA-256, against its own Cargo.lock. The build toolchain is removed from the image.
  • anon is preloaded, and the demo database, masked role and masking rules are created on first boot.
  • PostgreSQL listens on every interface (the Debian default is localhost), but pg_hba.conf admits network clients only over TLS with scram-sha-256. The Debian "snakeoil" certificate is not used, and each instance generates its own.
  • The postgres and analyst passwords and anon.salt are generated on your instance's first boot. The image contains no password and no salt.
  • An nftables egress backstop confines the postgres system user, and PostgreSQL and first boot cannot reach the instance metadata service.

Support

SecureAnyCloud supports this image by email at sales@secureanycloud.com. Include the instance ID, the version (PostgreSQL Anonymizer 18 on Ubuntu 24.04) and what you were doing.

PostgreSQL is a trademark of the PostgreSQL Community Association of Canada. PostgreSQL Anonymizer is developed by Dalibo and its contributors and is licensed under the PostgreSQL License. Ubuntu is a trademark of Canonical Ltd. SecureAnyCloud is not affiliated with, or endorsed by, these organisations.

Deploy on AWS

Launch PostgreSQL Anonymizer 18 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS