Wg
Databases AWS

WAL-G 3.0.9 on AWS User Guide

| Product: WAL-G 3.0.9 on Ubuntu 24.04 on AWS

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, with wal_level = replica and 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 the govulncheck reports in /usr/local/share/doc/wal-g/
  • walg-enable: switches archiving on (or off with --disable) and shows the state with --status
  • walg-backup.timer (daily base backup, 01:00) and walg-delete.timer (weekly retention keeping 7 full backups, Sunday 03:00), both disabled until walg-enable
  • walg-role-credentials.timer: fetches the instance's IAM role credentials for S3 every 15 minutes, disabled until walg-enable --instance-role
  • A libsodium encryption key and a postgres password, both generated on your instance's first boot
  • An egress backstop (walg-egress-guard.service) that controls where the postgres system 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 postgres password 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 postgres system 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.conf accepts scram-sha-256 (or peer authentication for the postgres system user on the local socket) and has no trust, md5 or password rule. 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

  1. An AWS account and permission to launch EC2 instances and create security groups
  2. An EC2 key pair in the Region you launch in
  3. A VPC subnet, with a public IP on the instance if you connect over the internet
  4. 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

  1. 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.
  2. Once the subscription is active, choose Launch your software.
  3. 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.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: the listing's default group allows SSH (22) only. Restrict it to your own IP address.
  6. 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.
  7. 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):

  1. Put the original key in place and point wal-g.env at the same storage, read-only use only (do not run walg-enable on the restore host while it shares the original's prefix). In the editor, replace the file's single line with the original instance's WALG_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

  1. 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

  1. 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

  1. When sudo -u postgres psql -tAc 'SELECT pg_is_in_recovery()' returns f, the restore is complete. Remove 30-restore.conf, then point the restored server at a new storage prefix before you run sudo walg-enable. The restored cluster keeps the original's postgres password.

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_KEY from /root/wal-g-credentials.txt to your secrets manager. Do not change /etc/wal-g/libsodium.key while 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.txt and your secrets manager.
  • Retention. walg-delete.timer runs wal-g delete retain FULL 7 --confirm weekly. To keep a different number, run sudo systemctl edit walg-delete.service and override ExecStart with your own wal-g delete command.
  • WAL-G settings. Add any WAL-G setting (for example WALG_DELTA_MAX_STEPS, WALG_UPLOAD_CONCURRENCY or WALG_S3_STORAGE_CLASS) to /etc/wal-g/wal-g.env; every WAL-G command and archive_command read it.
  • PostgreSQL configuration. cloudimg's settings are in /etc/postgresql/18/main/conf.d/10-cloudimg.conf; walg-enable adds 20-cloudimg-walg-archive.conf. The configuration directories are owned by root (PostgreSQL only reads them). To reach PostgreSQL from other hosts, change listen_addresses and add a hostssl ... scram-sha-256 rule to pg_hba.conf, configure TLS, and open 5432 in the security group to those hosts only.
  • Updates. sudo apt update && sudo apt upgrade installs 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), and journalctl -u wal-g-firstboot.service for 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 govulncheck reports 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-enable turns them on after it has checked your storage.
  • The postgres password and the libsodium encryption key are generated on your instance's first boot; the image contains neither.
  • The postgres system 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.