PostgreSQL Anonymizer 18 on AWS User Guide
Overview
This guide covers launching and verifying PostgreSQL Anonymizer 18 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg 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 other tools in the listing are described on the listing's guide hub.
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, and then:
- Dynamic masking shows a role labelled
MASKEDanonymized values while the stored data stays untouched, so 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.noiseand more) generate the replacement values.
The image installs PostgreSQL 18.6 from the official PostgreSQL (PGDG) apt repository and PostgreSQL Anonymizer 3.3.2, compiled by cloudimg 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 is on the image:
- PostgreSQL 18.6 (
postgresql@18-main.service) on port 5432, with theanonlibrary preloaded - PostgreSQL Anonymizer 3.3.2 and its English fake-data datasets
- The demo database
anondb: acustomertable with a masking rule on every personal column, the masked login roleanalyst, and acustomer_statictable for trying static anonymization - A TLS certificate for PostgreSQL, the
postgresandanalystpasswords and the anonymization salt, all generated on your instance's first boot - An egress backstop (
postgresql-anonymizer-egress-guard.service) that controls where thepostgressystem 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 · Architecture: x86_64 · Root volume: 16 GiB gp3
Security posture
- No default password. The image ships with no password on any PostgreSQL role and no
analystrole at all. First boot generates uniquepostgresandanalystpasswords and writes them to/root/postgresql-anonymizer-credentials.txt, readable by root only. - A salt of your own.
anon.hash()pseudonyms are keyed byanon.salt. First boot generates a random 256-bit salt for your instance and stores it in a configuration file readable only by thepostgressystem user. It is never stored withALTER 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, but
pg_hba.confadmits network clients only withhostsslandscram-sha-256; any connection without TLS is rejected. The certificate and key are generated on your instance; no key ships in the image. - The masked role reaches the masked database only.
analystcan readanondb, where masking is on, and nothing else: it holds no cluster-wide read role and cannot connect to thepostgresortemplate1databases. Masking is enabled per database (see "Your own databases" in Step 6). - Masking cannot be bypassed by the superuser by accident.
anon.nosuperuseris on, so static and dynamic masking refuse to run as a superuser, andanon.restrict_to_trusted_schemasis on, so masking rules may only call 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
postgressystem user cannot connect to instance metadata, link-local or reserved ranges, IPv6, or any address inside your VPC except this instance's own PostgreSQL and any address you add to/etc/cloudimg/postgresql-anonymizer-egress-allow.conf. A compromised database session therefore cannot scan your VPC. - No metadata access for the database. PostgreSQL and the first-boot service are also blocked by systemd 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.logor the journal. - Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance; SSH accepts keys only (no passwords, 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 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
| Version | Operating system | SSH user |
|---|---|---|
| PostgreSQL Anonymizer 18 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened Adminer & Database Admin and Backup Tools AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once 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 (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group allows SSH (22) only. 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, and 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: the AMI already makes IMDSv2 required.
Step 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 two password lines are left out here; 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-cloudimg-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://www.cloudimg.co.uk/guides/postgresql-anonymizer-18-on-ubuntu-24-04-aws/
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.
Step 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/127.0.0.54 is the operating system's local DNS stub.
Step 4: Look at the masking rules
The customer table in anondb holds six rows of realistic (invented) personal data, and every personal column carries a masking rule declared with SECURITY LABEL FOR anon. The extension lists them 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 (the same input always gives the same pseudonym on this instance, so joins on it still work) and the balance is hidden.
Step 5: The same query, two roles
Run the same SELECT over TLS as the owner and as the masked analyst role. The passwords are read from the credentials file at run time, so they never appear on screen. First 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 the masked role, analyst (the pseudonym column is shortened 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; the pseudonyms do not. The stored rows are unchanged, which 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.
A connection without TLS is rejected by pg_hba.conf 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=wrong-password 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"
Step 6: Declare masking rules on your own table
Masking a column takes one statement. This example creates a staff table, labels its columns (anon.noise adds up to 10% random noise to the salary) and lets the masked role read it:
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, after its 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, plus 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 them. 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 one of your own databases, do it in this order, as postgres, inside that database. 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: 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, so 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.
Step 7: Static anonymization
Static anonymization replaces the stored values for everyone, including the owner, and 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 it 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; the table's owner runs it 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 it back, and confirm 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, and anon.anonymize_schema_parallel() spreads the work over background workers, which run as the role that called it. Take a backup first: there is no undo.
Step 8: Open PostgreSQL to your applications
The security group opens SSH only. Allow 5432 from the subnets your applications and analysts connect from, never from 0.0.0.0/0. Run 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 verify-full with the certificate from TLS_CERTIFICATE, whose fingerprint is in the credentials file). PostgreSQL itself only needs outbound connections if you use features such as postgres_fdw or dblink; to allow one, add the address or CIDR to /etc/cloudimg/postgresql-anonymizer-egress-allow.conf and run sudo systemctl reload postgresql-anonymizer-egress-guard (PostgreSQL keeps running).
Step 9: Run the self-test and check the security controls
The shipped self-test checks the whole instance in one go 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
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-cloudimg-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 cloudimg_anon_egress output | grep -c cloudimg-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-cloudimg-anon-salt.conf
0
on
on
6
Maintenance
- Change a password. Run
sudo -u postgres psql, then\password postgresor\password analyst(it prompts and never puts the password on a command line). Update/root/postgresql-anonymizer-credentials.txtand your secrets manager. - The salt. Changing
anon.saltin30-cloudimg-anon-salt.confchanges everyanon.hash()pseudonym, so keep it stable once you rely on pseudonyms, and back it up with your database. Never set it withALTER DATABASEorALTER ROLE: those settings are readable by every user. - PostgreSQL configuration. cloudimg's settings are in
/etc/postgresql/18/main/conf.d/10-cloudimg.conf(listener, password encryption,shared_preload_libraries = 'anon') and20-cloudimg-tls.conf(TLS, written by first boot). Client access rules are in/etc/postgresql/18/main/pg_hba.conf. Apply changes withsudo systemctl reload postgresql@18-main.service(orrestartfor settings that need it). Keepanoninshared_preload_libraries. - Your own databases. Run
CREATE EXTENSION anon;andSELECT anon.init();in each database you mask; masking rules are per database. - Your own certificate. Replace
/etc/postgresql/18/main/tls/server.crtandserver.key(ownerpostgres, key mode 0600) and runsudo systemctl reload postgresql@18-main.service. - Backups. Take EBS snapshots or
pg_dumpbackups regularly. Dynamic masking does not change stored data, so a backup taken as the owner contains the real values; protect it accordingly. - Updates.
sudo apt update && sudo apt upgradeinstalls PostgreSQL 18 minor releases from PGDG and Ubuntu security updates. Theanonextension is built for PostgreSQL 18 and is not an apt package; cloudimg publishes new versions of this image when PostgreSQL Anonymizer releases one. - Logs.
/var/log/postgresql/postgresql-18-main.log; first boot logs tojournalctl -u postgresql-anonymizer-firstboot.service.
What cloudimg 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 pinned by SHA-256 against its own
Cargo.lock, and the build toolchain is removed from the image. anonis 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.confadmits network clients only over TLS withscram-sha-256; the Debian "snakeoil" certificate is not used, and each instance generates its own. - The
postgresandanalystpasswords andanon.saltare generated on your instance's first boot; the image contains no password and no salt. - The
postgressystem user is confined by an nftables egress backstop, and PostgreSQL and first boot cannot reach the instance metadata service.
Support
cloudimg supports this image by email at support@cloudimg.co.uk. 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. cloudimg is not affiliated with or endorsed by these organisations.