WAL-G 3.0.9 on AWS User Guide
Overview
This guide covers launching and verifying WAL-G 3.0.9 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.
WAL-G is the open-source archival and restoration tool for PostgreSQL. It takes compressed, encrypted base backups (wal-g backup-push), archives every WAL segment as PostgreSQL writes it (wal-g wal-push, run by archive_command), and restores a backup plus the WAL needed to replay to any point after it (wal-g backup-fetch and wal-g wal-fetch). It writes to Amazon S3 and S3-compatible storage, or to a directory.
cloudimg builds WAL-G 3.0.9 from the upstream v3.0.9 tag (commit 3e493188db28) with Go 1.26.10, Brotli compression and libsodium encryption compiled in, and LZO left out. Six Go dependencies are raised to patched releases inside their own major versions so that govulncheck reports no reachable vulnerability; the scan reports ship in the image. PostgreSQL 18.6 comes from the official PostgreSQL (PGDG) apt repository, with the repository's signing key pinned.
WAL archiving is off when the instance starts. The image ships no bucket, no credential and no storage setting, and it makes no outbound connection for backups until you choose your storage in /etc/wal-g/wal-g.env and run sudo walg-enable. That one command checks the storage, switches on continuous archiving, takes the first base backup and enables the daily backup and weekly retention timers.
What is on the image:
- PostgreSQL 18.6 (
postgresql@18-main.service) listening on 127.0.0.1:5432 only, withwal_level = replicaand archiving off - WAL-G 3.0.9 (
/usr/local/bin/wal-g, a wrapper that loads/etc/wal-g/wal-g.env), with its licence, the licences of the libraries built into it, its Go module list and thegovulncheckreports in/usr/local/share/doc/wal-g/ walg-enable: switches archiving on (or off with--disable) and shows the state with--statuswalg-backup.timer(daily base backup, 01:00) andwalg-delete.timer(weekly retention keeping 7 full backups, Sunday 03:00), both disabled untilwalg-enablewalg-role-credentials.timer: fetches the instance's IAM role credentials for S3 every 15 minutes, disabled untilwalg-enable --instance-role- A libsodium encryption key and a
postgrespassword, both generated on your instance's first boot - An egress backstop (
walg-egress-guard.service) that controls where thepostgressystem user may connect, with an allow-list for storage inside your VPC - A self-test,
sudo /usr/local/sbin/walg-selftest.sh, that proves a backup, an archive, the encryption and a point-in-time restore in a throwaway cluster
Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3
Security posture
- No default password and no shared key. The image ships with no password on any PostgreSQL role and no encryption key. First boot generates a unique
postgrespassword and a unique 32-byte libsodium key and writes both to/root/wal-g-credentials.txt, readable by root only. The key is also in/etc/wal-g/libsodium.key(mode 0640, root:postgres) for WAL-G to read; it is in no other file. - Encrypted before it leaves the instance. Every base backup and WAL segment is encrypted with your instance's key before it is written to storage. Keep a copy of the key off the instance: without it nothing this instance archived can be restored.
- No credentials in the image. No bucket, access key or role is configured. For S3 you either attach an IAM role to the instance and run
walg-enable --instance-role, or put an access key in/etc/wal-g/wal-g.env(mode 0640, root:postgres). - No metadata access for the database. PostgreSQL (and so
archive_command), the backup and retention services and first boot are blocked by systemd from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). The instance-role credentials are fetched by a separate root-only service and handed to WAL-G in a file on memory-backed storage (/run/cloudimg-walg/role-credentials.env, mode 0640). Amazon Time Sync (169.254.169.123) still works. - Egress backstop for the database. The
postgressystem user (PostgreSQL and every WAL-G command) cannot connect to instance metadata, link-local or reserved ranges, IPv6, other loopback ports, or any address inside your VPC except the hosts you add to/etc/cloudimg/wal-g-egress-allow.conf(Step 7). Public endpoints, including Amazon S3 and an S3 gateway endpoint, stay reachable. - PostgreSQL is local only. It listens on 127.0.0.1,
pg_hba.confacceptsscram-sha-256(or peer authentication for thepostgressystem user on the local socket) and has notrust,md5orpasswordrule. The listing's security group opens SSH 22 only. - WAL-G opens no port. WAL-G is a command-line tool and its services run once and exit; it never listens on a socket.
- IMDSv2 required. The AMI is registered with
ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests. - Fails closed. If first boot cannot finish, it stops PostgreSQL rather than leave a half-configured server, and tries again on the next boot. The backup services refuse to run (exit 78, not retried) until storage is configured, and retry only a temporary failure such as PostgreSQL restarting.
- No secrets in logs. The password is set over standard input with statement logging switched off, and no credential is passed on a command line, so none appears in the PostgreSQL log or 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
- Somewhere to keep backups: an S3 bucket (and permission to create an IAM role or access key for it), or a directory such as a separate EBS volume
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 |
|---|---|---|
| WAL-G 3.0.9 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 WAL-G 3.0.9 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.
- To back up to S3 with an IAM role, choose the role's instance profile under Advanced details > IAM instance profile (Step 5 shows the policy). You can also attach it later.
- 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. Leave out --iam-instance-profile if you will use an access key or a directory:
SG=$(aws ec2 create-security-group --group-name wal-g-host \
--description "WAL-G PostgreSQL host (SSH only)" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-ip>/32
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 \
--iam-instance-profile Name=walg-backup-role \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=wal-g-host}]'
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 sets the postgres password, creates the encryption key and writes the credentials file. Show it without the two secret lines (read them with sudo cat /root/wal-g-credentials.txt and store them in your secrets manager, off this instance):
sudo grep -vE '^(POSTGRES_PASSWORD|WALG_LIBSODIUM_KEY)=' /root/wal-g-credentials.txt
# WAL-G 3.0.9 + PostgreSQL 18 - generated on first boot by wal-g-firstboot.service.
# These values are unique to this instance. Store them somewhere safe, OFF this instance: without
# WALG_LIBSODIUM_KEY nothing this instance archives can ever be restored.
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
WALG_LIBSODIUM_KEY_FILE=/etc/wal-g/libsodium.key
WALG_LIBSODIUM_KEY_TRANSFORM=hex
# WAL archiving is OFF until you point /etc/wal-g/wal-g.env at your storage (S3 or a directory) and run:
# sudo walg-enable (add --instance-role to use this instance's IAM role for S3)
# Status: sudo walg-enable --status
# Self-test: sudo /usr/local/sbin/walg-selftest.sh
# User guide: https://www.cloudimg.co.uk/guides/wal-g-3-0-9-on-ubuntu-24-04-aws/
Step 3: Confirm PostgreSQL and the default state
PostgreSQL runs and first boot has finished; the backup timers are installed but not enabled:
systemctl is-active postgresql@18-main.service walg-egress-guard.service wal-g-firstboot.service
systemctl is-enabled walg-backup.timer walg-delete.timer walg-role-credentials.timer
active
active
active
disabled
disabled
disabled
Check the versions and the WAL-G build (the binary records how it was built):
sudo -u postgres psql -tAc 'SELECT version()'
sudo -u postgres wal-g --version
grep -E 'build\t(-tags|GOEXPERIMENT|CGO_ENABLED)' /usr/local/share/doc/wal-g/buildinfo.txt
PostgreSQL 18.6 (Ubuntu 18.6-1.pgdg24.04+2) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit
wal-g version v3.0.9 3e49318 2026.11.09_21:35:26 PostgreSQL
build -tags=brotli,libsodium
build CGO_ENABLED=1
build GOEXPERIMENT=jsonv2
walg-enable --status shows that archiving is off and that WAL-G is not configured yet (exit 78 means "not configured"):
sudo walg-enable --status
walg-enable: archive_mode=off archive_command=none
walg-enable: pg_stat_archiver: archived=0 failed=0 last=-
walg-enable: storage: none (no WALG_*_PREFIX set in /etc/wal-g/wal-g.env)
walg-enable: walg-backup.timer: disabled / inactive
walg-enable: walg-delete.timer: disabled / inactive
walg-enable: walg-role-credentials.timer: disabled / inactive
walg-run: NOT CONFIGURED: /etc/wal-g/wal-g.env must set exactly one WALG_*_PREFIX (it sets 0); see the user guide
walg-enable: walg-run check: exit 78 (0 = configured, 78 = not configured, 75 = transient)
Step 4: Run the self-test
The self-test proves the appliance works end to end without touching your server or your storage: it checks the credentials file and the key, logs in to PostgreSQL with the right password and is refused with a wrong one, then in a throwaway PostgreSQL 18 cluster takes a base backup, writes a row after it, archives the WAL, confirms the archive holds no plaintext and that a wrong key cannot decrypt it, and restores both rows with your key:
sudo /usr/local/sbin/walg-selftest.sh
== WAL-G self-test ==
OK: credentials file 0600 root; this instance's libsodium key is in /etc/wal-g/libsodium.key (0640 root:postgres) and nowhere else in the configuration
OK: PostgreSQL listens on 127.0.0.1:5432 only; the per-VM password logs in, a wrong one is refused, the right one still works
OK: archiving on this server: off
== WAL-G round trip in a throwaway cluster (this instance's key; its own directory storage) ==
OK: scratch root /run/cloudimg-walg-selftest.IwjxYu is root-owned 0711; the runners and the wrong key were written as uid postgres (owner postgres) inside the postgres-owned /run/cloudimg-walg-selftest.IwjxYu/pg
OK: backup-push, then a row written after the backup; WAL through 000000010000000000000003 archived by wal-push
OK: the archive holds no plaintext; a wrong key cannot decrypt a segment, this instance's key can
OK: the scratch clusters' postgresql.conf appends ran as uid postgres (root wrote nothing inside /run/cloudimg-walg-selftest.IwjxYu/pg)
OK: backup-fetch + WAL replay restored the row from the backup AND the row written after it
OK: scratch tree /run/cloudimg-walg-selftest.IwjxYu removed
WALG_SELFTEST_OK: per-VM postgres password logs in on 127.0.0.1 only and a wrong one is refused; WAL-G backed up, archived, refused a wrong key and restored to a point after the backup with this instance's key.
Step 5: Choose where backups go
Edit /etc/wal-g/wal-g.env and uncomment exactly one storage prefix with its settings. The file already points WAL-G at your instance's key and at the local PostgreSQL:
sudo nano /etc/wal-g/wal-g.env
Option A: Amazon S3 with the instance's IAM role (recommended)
Create an IAM role for EC2 with a policy limited to your bucket and prefix, and attach it to the instance (Actions > Security > Modify IAM role in the EC2 console). For example, for the bucket my-walg-bucket and the prefix pg/host1:
{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::my-walg-bucket",
"Condition": {"StringLike": {"s3:prefix": ["pg/host1/*"]}}},
{"Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::my-walg-bucket/pg/host1/*"}
]
}
In /etc/wal-g/wal-g.env, uncomment and set the bucket and its Region:
WALG_S3_PREFIX=s3://my-walg-bucket/pg/host1
AWS_REGION=eu-west-2
Then switch archiving on with the instance role. walg-enable fetches the role's credentials (as root, over IMDSv2), enables their 15-minute refresh, checks the bucket, turns on archiving, takes the first base backup and enables the timers:
sudo walg-enable --instance-role
Option B: Amazon S3 with an access key
Create an IAM user with the same policy and an access key, and put the key in /etc/wal-g/wal-g.env with the bucket (the file is readable only by root and the postgres group):
WALG_S3_PREFIX=s3://my-walg-bucket/pg/host1
AWS_REGION=eu-west-2
AWS_ACCESS_KEY_ID=<your-access-key-id>
AWS_SECRET_ACCESS_KEY=<your-secret-access-key>
sudo walg-enable
Option C: A directory (for example a separate EBS volume)
Mount the volume, create a directory owned by postgres with mode 0700, and point WAL-G at it:
sudo install -d -o postgres -g postgres -m 0700 /mnt/walg/pg
WALG_FILE_PREFIX=/mnt/walg/pg
sudo walg-enable
A successful run ends like this (here with a directory):
walg-enable: checking the storage (wal-g backup-list as postgres)
walg-enable: switching continuous WAL archiving on (PostgreSQL restarts once)
walg-enable: WAL archiving works (000000010000000000000005 archived)
walg-enable: taking the first base backup (walg-backup.service)
walg-enable: base backups in the storage: 1
walg-enable: daily base backup (walg-backup.timer, 01:00) and weekly retention (walg-delete.timer, Sunday 03:00) enabled
WALG_ENABLE_OK
If the storage cannot be reached, walg-enable stops before changing anything and prints WAL-G's error; if the first WAL segment does not arrive within three minutes it switches archiving off again.
Step 6: Check the backups and the schedule
sudo walg-enable --status
systemctl list-timers 'walg-*'
Once storage is configured, --status also lists the newest base backups (sudo -u postgres wal-g backup-list shows them all):
walg-enable: archive_mode=on archive_command=wal-g
walg-enable: pg_stat_archiver: archived=9 failed=0 last=000000010000000000000006.00000028.backup
walg-enable: storage: WALG_FILE_PREFIX=/mnt/walg/pg
walg-enable: walg-backup.timer: enabled / active
walg-enable: walg-delete.timer: enabled / active
walg-enable: walg-role-credentials.timer: disabled / inactive
walg-enable: walg-run check: exit 0 (0 = configured, 78 = not configured, 75 = transient)
INFO: 2026/11/09 21:41:51.551264 List backups from storages: [default]
backup_name modified wal_file_name storage_name
base_000000010000000000000006 2026-11-09T21:41:50Z 000000010000000000000006 default
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-11-10 01:09:29 UTC 3h 27min Mon 2026-11-09 21:38:00 UTC - walg-backup.timer walg-backup.service
Sun 2026-11-15 03:11:17 UTC 5 days Mon 2026-11-09 21:38:00 UTC - walg-delete.timer walg-delete.service
2 timers listed.
Run a base backup now at any time with sudo systemctl start walg-backup.service; its log is in journalctl -u walg-backup.service. The services exit 78 (and are not retried) while storage is not configured, retry after two minutes (exit 75) if PostgreSQL or the instance-role credentials are briefly unavailable, and exit 1 without a retry when WAL-G itself fails (its own exit code is in the journal).
Step 7: Storage inside your VPC (S3 interface endpoint or MinIO)
Public S3 endpoints, including an S3 gateway endpoint, need no change. If WAL-G must reach a private address in your VPC, such as an S3 interface endpoint or a MinIO server, add that address to the egress allow-list and reload the guard (PostgreSQL keeps running):
echo '10.0.12.34/32 # S3 interface endpoint' | sudo tee -a /etc/cloudimg/wal-g-egress-allow.conf
sudo systemctl reload walg-egress-guard
Set the endpoint in /etc/wal-g/wal-g.env as well (for example AWS_ENDPOINT=https://bucket.vpce-0123-abcd.s3.eu-west-2.vpce.amazonaws.com or your MinIO URL, with AWS_S3_FORCE_PATH_STYLE=true for MinIO).
Step 8: Restore
A restore needs the backups in your storage and the libsodium key of the instance that made them. Restore onto a fresh instance of this image (or any PostgreSQL 18 host with WAL-G 3.0.9):
- Put the original key in place and point
wal-g.envat the same storage, read-only use only (do not runwalg-enableon the restore host while it shares the original's prefix). In the editor, replace the file's single line with the original instance'sWALG_LIBSODIUM_KEY(pasting it keeps it off the command line and out of your shell history; the file keeps its 0640 root:postgres mode):
bash
sudo nano /etc/wal-g/libsodium.key
sudo nano /etc/wal-g/wal-g.env # the same WALG_S3_PREFIX / WALG_FILE_PREFIX and credentials
- Stop PostgreSQL, move the empty data directory aside and fetch the latest base backup:
bash
sudo systemctl stop postgresql@18-main.service
sudo mv /var/lib/postgresql/18/main /var/lib/postgresql/18/main.empty
sudo -u postgres install -d -m 0700 /var/lib/postgresql/18/main
sudo -u postgres wal-g backup-fetch /var/lib/postgresql/18/main LATEST
- Tell PostgreSQL to replay the archived WAL and start it. Add
recovery_target_time = '2026-11-09 12:00:00+00'to stop at a point in time instead of the end of the archive:
bash
printf "restore_command = '/usr/local/bin/wal-g wal-fetch %%f %%p'\nrecovery_target_action = 'promote'\n" \
| sudo tee /etc/postgresql/18/main/conf.d/30-restore.conf >/dev/null
sudo -u postgres touch /var/lib/postgresql/18/main/recovery.signal
sudo systemctl start postgresql@18-main.service
- When
sudo -u postgres psql -tAc 'SELECT pg_is_in_recovery()'returnsf, the restore is complete. Remove30-restore.conf, then point the restored server at a new storage prefix before you runsudo walg-enable. The restored cluster keeps the original'spostgrespassword.
Step 9: Check the security controls
sudo -u postgres psql -tAc "SELECT rolname FROM pg_authid WHERE rolpassword IS NOT NULL"
sudo ss -Hltn
systemctl show -p IPAddressDeny postgresql@18-main.service walg-backup.service
sudo nft list table inet cloudimg_walg_egress | grep -c reject
postgres
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 200 127.0.0.1:5432 0.0.0.0:*
LISTEN 0 4096 [::]:22 [::]:*
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
5
Only postgres has a password (yours, set on first boot); off-box only SSH listens; PostgreSQL and the backup service are blocked from the metadata service; the backstop's refusal rules are loaded.
Switching archiving off
sudo walg-enable --disable
This switches archiving off, disables the timers and removes the instance-role credentials file. wal-g.env and the backups in your storage are left as they are.
Maintenance
- Keep the key safe. Copy
WALG_LIBSODIUM_KEYfrom/root/wal-g-credentials.txtto your secrets manager. Do not change/etc/wal-g/libsodium.keywhile backups made with it are still needed. - Change the postgres password. Run
sudo -u postgres psql, then\password postgres(it prompts and never puts the password on a command line). Update/root/wal-g-credentials.txtand your secrets manager. - Retention.
walg-delete.timerrunswal-g delete retain FULL 7 --confirmweekly. To keep a different number, runsudo systemctl edit walg-delete.serviceand overrideExecStartwith your ownwal-g deletecommand. - WAL-G settings. Add any WAL-G setting (for example
WALG_DELTA_MAX_STEPS,WALG_UPLOAD_CONCURRENCYorWALG_S3_STORAGE_CLASS) to/etc/wal-g/wal-g.env; every WAL-G command andarchive_commandread it. - PostgreSQL configuration. cloudimg's settings are in
/etc/postgresql/18/main/conf.d/10-cloudimg.conf;walg-enableadds20-cloudimg-walg-archive.conf. The configuration directories are owned by root (PostgreSQL only reads them). To reach PostgreSQL from other hosts, changelisten_addressesand add ahostssl ... scram-sha-256rule topg_hba.conf, configure TLS, and open 5432 in the security group to those hosts only. - Updates.
sudo apt update && sudo apt upgradeinstalls PostgreSQL 18 minor releases from PGDG and Ubuntu security updates. WAL-G is built into the image; cloudimg publishes a new version of this listing when WAL-G or one of its libraries needs an update. - Logs.
journalctl -u walg-backup.service -u walg-delete.service -u walg-role-credentials.service,/var/log/postgresql/postgresql-18-main.log(archive_command failures appear there), andjournalctl -u wal-g-firstboot.servicefor first boot.
What cloudimg changed from the upstream defaults
- WAL-G is built from the v3.0.9 tag with Brotli and libsodium compiled in and without LZO, and six Go dependencies are raised to patched releases within their major versions; the build's module list and
govulncheckreports ship in/usr/local/share/doc/wal-g/. - PostgreSQL comes from PGDG with the repository's signing key pinned to the published PostgreSQL fingerprint, listens on 127.0.0.1 only, and is ready for archiving with
wal_level = replica. - Archiving, the backup and retention timers and the instance-role credential refresh are installed but off;
walg-enableturns them on after it has checked your storage. - The
postgrespassword and the libsodium encryption key are generated on your instance's first boot; the image contains neither. - The
postgressystem user is confined by an nftables egress backstop with an allow-list, and PostgreSQL, the backup services 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 (WAL-G 3.0.9 on Ubuntu 24.04) and what you were doing.
WAL-G is developed by the WAL-G contributors and is licensed under the Apache License 2.0. PostgreSQL is a trademark of the PostgreSQL Community Association of Canada. Amazon S3 and AWS are trademarks of Amazon.com, Inc. or its affiliates. Ubuntu is a trademark of Canonical Ltd. cloudimg is not affiliated with or endorsed by these organisations.