Databases AWS

XTDB 2.2.0 on AWS User Guide

| Product: XTDB 2.2.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying XTDB 2.2.0 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Apache Cassandra, FerretDB & NoSQL Database AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other databases in the listing are described on the listing's guide hub.

XTDB is an immutable, bitemporal SQL database. Every row keeps two timelines: valid time (when a fact is true in the real world) and system time (when the database recorded it). Updates and deletes never overwrite history, so you can ask what a row said on any past date (FOR VALID_TIME AS OF ...), see every version of a row (FOR ALL VALID_TIME), or audit exactly what the database knew at any moment (FOR SYSTEM_TIME AS OF ...) without trigger tables or soft-delete columns. XTDB 2 speaks the PostgreSQL wire protocol, so psql, JDBC and the usual PostgreSQL drivers connect to it unchanged.

The image runs the official XTDB 2.2.0 standalone server (the xtdb.jar published in the upstream ghcr.io/xtdb/xtdb:2.2.0 image, verified by SHA-256 at build time) directly under systemd on Ubuntu's OpenJDK 21. There is no Docker daemon on the instance.

What is on the image:

  • XTDB 2.2.0 (xtdb.service) with its PostgreSQL wire endpoint on 127.0.0.1:5432 and its health and metrics endpoints on 127.0.0.1:8080
  • A single-node standalone setup: the transaction log and storage live on local disk under /var/lib/xtdb
  • One login, xtdb, with a password unique to this instance, and a TLS certificate generated on the instance's first boot
  • A small bitemporal demo table, employees, with salary history, so you can try time travel straight away
  • sudo xtdb-psql, a helper that opens psql to the local database over TLS using the password from the credentials file, and sudo xtdb-set-password, which gives the login a new random password

Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3 (choose a bigger volume at launch for larger databases)

Security posture

Upstream XTDB 2.2.0 starts with authentication turned off: with no login configured it accepts any password, or none, for the built-in user xtdb. This image changes that:

  • Password required for every connection. XTDB is configured with a fixed user list holding one login, xtdb, and a single authentication rule, PASSWORD, for every client address, including connections from the instance itself. There is no trust rule, and any other user name is refused.
  • No default password. First boot creates a random password unique to this instance, stores only its argon2id hash in /etc/xtdb/config.yaml, and writes the password to /root/xtdb-credentials.txt, readable by root only. Before xtdb.service is allowed to start, first boot runs the server once in an isolated network namespace and proves that the new password works and that the old upstream default (xtdb/xtdb), a wrong password, an unknown user and a client without a password are all refused.
  • Flight SQL switched off. XTDB 2.2 adds an Arrow Flight SQL server and starts it by default; in 2.2.0 it has no authentication and no TLS, so anything on the instance could use it to bypass the password. The image turns it off (flightSql: null), and the build verifies that the server listens on ports 5432 and 8080 only.
  • No external sources. XTDB 2.2 can mirror data from an existing PostgreSQL database or Kafka Connect topic. None is configured, so the database ingests nothing it is not sent. If you add one, the metadata block below still applies.
  • Per-instance TLS certificate. First boot creates this instance's own RSA-3072 key and self-signed certificate (in a keystore with its own random password) and XTDB offers TLS to every client. No key ships in the image, so no two instances share one.
  • Localhost only. XTDB listens on 127.0.0.1, and the listing's security group opens SSH (22) only. Reach the database through an SSH tunnel (Step 7), or open it to your VPC as described there.
  • Use sslmode=require. XTDB sends passwords in PostgreSQL's cleartext-password exchange and, in 2.2.0, cannot refuse a client that does not ask for TLS. The tunnel encrypts that exchange; if you open the port to your VPC, every client must connect with sslmode=require (or verify-full).
  • No metadata access for the database. The XTDB server and its first-boot service are blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254), so neither a query nor an external source or object store you configure later can pick up the instance's role credentials. Amazon Time Sync (169.254.169.123) still works.
  • Sandboxed service. XTDB runs as the unprivileged xtdb user with no Linux capabilities, NoNewPrivileges, a read-only view of the operating system (ProtectSystem=strict, writable only under /var/lib/xtdb), a private /tmp and no device access.
  • 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, XTDB stays stopped and first boot tries again on the next boot from a fresh, empty node.
  • No secrets in logs. Passwords are never passed on a command line (the hashing tool reads the password on standard input), so they do not appear in /var/log/auth.log or the journal.
  • SSH keys only. The SSH server accepts key logins only: password and keyboard-interactive logins, empty passwords, direct root login and X11 forwarding are all switched off (pinned in /etc/ssh/sshd_config.d/00-cloudimg.conf), and root has no password. The firmware metadata refresh (fwupd-refresh) is masked, because EC2 instances have no firmware it can update.
  • Per-instance identity. SSH host keys, the machine ID and the XTDB node itself are created fresh on every instance; the image ships no data.

Third-party libraries. XTDB 2.2.0 bundles netty and Jackson releases with published advisories. The netty ones sit in the Flight SQL server and outbound connectors, which this image does not start or configure; the PostgreSQL endpoint uses the JDK's own sockets and TLS. The Jackson ones need either polymorphic typing, which XTDB does not enable, or JSON sent by a signed-in user. cloudimg will publish an updated version when an XTDB release carries patched libraries.

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. A PostgreSQL client on your workstation (psql, DBeaver, or your application's PostgreSQL driver)

Recommended instance type: m5.large. XTDB runs on the JVM and sizes its heap to 40% of the instance's memory; choose a larger memory size for bigger working sets.

Connecting to your instance

Version Operating system SSH user
XTDB 2.2.0 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened Apache Cassandra, FerretDB & NoSQL Database 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 XTDB 2.2.0 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. Do not open 5432 or 8080 to the internet.
  6. 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 xtdb \
  --description "XTDB" --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=xtdb}]'

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 about a minute of launch. It creates this instance's password and TLS keystore, writes the password hash into the server configuration, proves the logins, loads the demo data and then writes the credentials file. Show the connection details (the password line is left out here; read it with sudo cat /root/xtdb-credentials.txt and keep it in your secrets manager):

sudo grep -v '^XTDB_PASSWORD=' /root/xtdb-credentials.txt
# XTDB 2.2.0 - credentials for THIS instance (generated at first boot; not shared with any other instance)
#
# XTDB speaks the PostgreSQL wire protocol on 127.0.0.1:5432 (loopback only; the security group opens
# SSH only). From your workstation open an SSH tunnel, then connect with any PostgreSQL client:
#   ssh -i <your-key.pem> -N -L 5432:127.0.0.1:5432 ubuntu@<instance-public-ip>
#   psql "host=127.0.0.1 port=5432 dbname=xtdb user=xtdb sslmode=require"
# On the instance itself: sudo xtdb-psql      New password: sudo xtdb-set-password
XTDB_HOST=127.0.0.1
XTDB_PORT=5432
XTDB_DATABASE=xtdb
XTDB_USER=xtdb
XTDB_TLS_CERTIFICATE=/etc/xtdb/tls/xtdb-server.crt

sudo xtdb-psql reads the password from that file into a temporary, root-only password file for each session, so it never appears on screen or on a command line.

Step 3: Confirm XTDB is running

systemctl is-active xtdb-firstboot.service xtdb.service
java -version 2>&1 | head -1
sudo ss -Hltn
curl -s -w '\n' http://127.0.0.1:8080/healthz/alive
active
active
openjdk version "21.0.12.1" 2026-08-18
LISTEN 0      4096        127.0.0.53%lo:53   0.0.0.0:*
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                 [::]:22      [::]:*
LISTEN 0      50     [::ffff:127.0.0.1]:5432       *:*
LISTEN 0      50     [::ffff:127.0.0.1]:8080       *:*
Alive.

Only SSH listens on a public address. XTDB's PostgreSQL wire endpoint (5432) and its health and metrics endpoints (8080: /healthz/alive, /healthz/started, /healthz/ready, /metrics) listen on localhost, and nothing else: there is no Flight SQL port. Port 53 on 127.0.0.53/127.0.0.54 is Ubuntu's local DNS resolver.

Step 4: Connect with psql

sudo xtdb-psql -c '\conninfo' -c 'SELECT version()'
You are connected to database "xtdb" as user "xtdb" on host "127.0.0.1" at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
          version           
----------------------------
 PostgreSQL 16 (XTDB 2.2.0)
(1 row)

Run sudo xtdb-psql with no arguments for an interactive session.

Step 5: Query across time

The demo table employees records salary changes as new valid-time periods. Every version of every row:

sudo xtdb-psql -c "SELECT _id, name, salary, _valid_from, _valid_to FROM employees FOR ALL VALID_TIME WHERE _id <= 3 ORDER BY _id, _valid_from"
 _id |   name    | salary |        _valid_from        |         _valid_to         
-----+-----------+--------+---------------------------+---------------------------
   1 | Alice Ng  |  85000 | 2023-01-01 00:00:00+00:00 | 2024-01-01 00:00:00+00:00
   1 | Alice Ng  |  95000 | 2024-01-01 00:00:00+00:00 | 2025-01-01 00:00:00+00:00
   1 | Alice Ng  | 110000 | 2025-01-01 00:00:00+00:00 | 
   2 | Ben Cole  |  70000 | 2023-06-01 00:00:00+00:00 | 2024-06-01 00:00:00+00:00
   2 | Ben Cole  |  78000 | 2024-06-01 00:00:00+00:00 | 
   3 | Cara Diaz | 120000 | 2024-03-01 00:00:00+00:00 | 
(6 rows)

What the table said on 1 July 2023, and what it says today:

sudo xtdb-psql -c "SELECT _id, name, salary FROM employees FOR VALID_TIME AS OF DATE '2023-07-01' ORDER BY _id"
sudo xtdb-psql -c "SELECT _id, name, salary FROM employees WHERE _id <= 3 ORDER BY _id"
 _id |   name   | salary 
-----+----------+--------
   1 | Alice Ng |  85000
   2 | Ben Cole |  70000
(2 rows)

 _id |   name    | salary 
-----+-----------+--------
   1 | Alice Ng  | 110000
   2 | Ben Cole  |  78000
   3 | Cara Diaz | 120000
(3 rows)

Cara Diaz did not exist on 1 July 2023 (her row is valid from March 2024), so the time-travel query leaves her out.

Step 6: Change data and audit it

An UPDATE never overwrites. Change a salary, then look at the row's system-time history: the old value is still there, closed at the moment of the update.

sudo xtdb-psql -c "UPDATE employees SET salary = 125000 WHERE _id = 3"
sudo xtdb-psql -c "SELECT _id, salary, _system_from, _system_to FROM employees FOR ALL SYSTEM_TIME WHERE _id = 3 ORDER BY _system_from"
UPDATE 0
 _id | salary |           _system_from           |            _system_to            
-----+--------+----------------------------------+----------------------------------
   3 | 120000 | 2026-10-24 04:08:46.009759+00:00 | 2026-10-24 04:10:37.263313+00:00
   3 | 125000 | 2026-10-24 04:10:37.263313+00:00 | 
(2 rows)

XTDB does not report row counts for writes (UPDATE 0, INSERT 0 0); the history query shows the change took effect. The update also starts a new valid-time period for the row from now, so FOR ALL VALID_TIME now lists both salaries. Tables need no CREATE TABLE: inserting a row with an _id creates the table. (Since XTDB 2.2, querying a table or column that does not exist is an error rather than an empty result.)

sudo xtdb-psql -c "INSERT INTO orders (_id, customer, total) VALUES ('o-1001', 'Acme Ltd', 249.00)"
sudo xtdb-psql -c "SELECT * FROM orders"
INSERT 0 0
  _id   | customer | total 
--------+----------+-------
 o-1001 | Acme Ltd | 249.0
(1 row)

Step 7: Connect from your workstation or application

Through an SSH tunnel (recommended)

On your workstation, forward a local port to the instance's localhost-only endpoint, then connect with any PostgreSQL client using the password from Step 2:

ssh -i ~/.ssh/my-key-pair.pem -N -L 5432:127.0.0.1:5432 ubuntu@<public-ip>
psql "host=127.0.0.1 port=5432 dbname=xtdb user=xtdb sslmode=require"

A JDBC application uses jdbc:postgresql://127.0.0.1:5432/xtdb?sslmode=require with user xtdb. To check the server certificate as well, copy /etc/xtdb/tls/xtdb-server.crt to your workstation and use sslmode=verify-full sslrootcert=xtdb-server.crt with a host name the certificate lists (localhost works through the tunnel).

From your VPC

To let application servers in your VPC connect directly:

  1. Edit /etc/xtdb/config.yaml and change host: 127.0.0.1 under server: to 0.0.0.0 (or to the instance's private IP).
  2. Run sudo systemctl restart xtdb.service.
  3. Add an inbound rule for TCP 5432 to the instance's security group from your application subnet's CIDR only, never 0.0.0.0/0.
  4. Connect with sslmode=require (or verify-full) from every client. XTDB 2.2.0 cannot refuse a client that skips TLS, and such a client would send the password unencrypted.

The certificate was created at first boot for the instance's host name and private IP addresses, so verify-full works against the private IP. Leave flightSql: null in place: XTDB 2.2.0's Flight SQL server would accept unauthenticated, unencrypted queries from anyone who can reach its port.

Step 8: Check the security controls

The configuration: localhost listeners, Flight SQL off, the single xtdb login (its password exists here only as an argon2id hash), the single PASSWORD rule, and a keystore password read from the root-only /etc/xtdb/xtdb.env rather than written in the file. The hash is masked in the output below:

sudo sed -E 's/(!Argon2id ")[^"]+"/\1<argon2id hash, unique to this instance>"/' /etc/xtdb/config.yaml
# cloudimg XTDB 2.2.0 - single-node standalone. Change server.host only together with the
# security group (open 5432 to your VPC CIDR, never 0.0.0.0/0) and connect with sslmode=require.
server:
  host: 127.0.0.1
  port: 5432
  ssl:
    keyStore: /etc/xtdb/tls/xtdb-keystore.jks
    keyStorePassword: !Env XTDB_KEYSTORE_PASSWORD

# Arrow Flight SQL is OFF: XTDB 2.2.0's Flight SQL server has no authentication and no TLS.
flightSql: null

# Logins: user xtdb with this instance's password (argon2id hash; set a new one with: sudo xtdb-set-password).
authn: !UserList
  users:
    xtdb: !Argon2id "<argon2id hash, unique to this instance>"
  rules:
    - method: PASSWORD

log: !Local
  path: /var/lib/xtdb/log

storage: !Local
  path: /var/lib/xtdb/buffers

healthz:
  host: 127.0.0.1
  port: 8080

The upstream default login and an unknown user are refused (this test connects without TLS, on localhost only, so the refusal message is shown; over TLS, XTDB reports a refused login as SSL error: wrong version number):

for u in xtdb admin; do
  OUT="$(PGPASSWORD=xtdb psql -X -w "host=127.0.0.1 port=5432 dbname=xtdb user=$u sslmode=disable" -c 'SELECT 1' 2>&1)"
  case "$OUT" in
    *"password authentication failed for user: $u"*) echo "login $u/xtdb: refused (password authentication failed)" ;;
    *) echo "UNEXPECTED: $OUT"; exit 1 ;;
  esac
done
login xtdb/xtdb: refused (password authentication failed)
login admin/xtdb: refused (password authentication failed)

The certificate the endpoint serves is this instance's own:

openssl s_client -starttls postgres -connect 127.0.0.1:5432 </dev/null 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=O = XTDB on this instance, CN = ip-172-31-92-194
X509v3 Subject Alternative Name: 
    DNS:localhost, IP Address:127.0.0.1, DNS:ip-172-31-92-194, DNS:ip-172-31-92-194.ec2.internal, IP Address:172.31.92.194

The service sandbox and the metadata block, then a metadata request made from inside XTDB's own control group (000 means the connection was blocked):

systemctl show -p User -p NoNewPrivileges -p ProtectSystem -p IPAddressDeny xtdb.service
CODE="$(sudo bash -c 'echo $$ > "/sys/fs/cgroup$(systemctl show -p ControlGroup --value xtdb.service)/cgroup.procs"; curl -s -m 5 -o /dev/null -w "%{http_code}" -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 60" http://169.254.169.254/latest/api/token || true')"
echo "metadata request from inside xtdb.service: ${CODE}"; [ "${CODE}" = "000" ]
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
User=xtdb
ProtectSystem=strict
NoNewPrivileges=yes
metadata request from inside xtdb.service: 000

Maintenance

  • Change the password. XTDB 2.2 has no ALTER USER; logins live in /etc/xtdb/config.yaml. Run sudo xtdb-set-password: it creates a new random password, writes its argon2id hash into the config and the password into /root/xtdb-credentials.txt, restarts XTDB, and checks that the new password works and the old one is refused (on any failure it puts the previous files back). Then update your applications with the new password from the credentials file.
  • More logins. Add a line under users: in /etc/xtdb/config.yaml with a hash from sudo xtdb-hash-password < file-with-the-password (it reads the password on standard input, never as an argument), then restart xtdb.service. Every login gets the same PASSWORD rule.
  • Your own certificate. To serve a certificate from your own CA, convert your key and certificate to a JKS keystore (openssl pkcs12 -export, then keytool -importkeystore -deststoretype JKS) protected by the password in /etc/xtdb/xtdb.env, replace /etc/xtdb/tls/xtdb-keystore.jks (owner root, group xtdb, mode 0640) and restart xtdb.service.
  • Backups. The whole database is the directory /var/lib/xtdb. For a consistent copy, stop the service (sudo systemctl stop xtdb.service), copy or snapshot the directory (or take an EBS snapshot of the volume), then start it again. Back up /etc/xtdb/config.yaml and /etc/xtdb/tls with it if you want to keep the same login and certificate.
  • Memory. The JVM heap is 40% of the instance's memory (-XX:MaxRAMPercentage=40 in /usr/local/sbin/xtdb-server); XTDB also uses off-heap memory for its Arrow buffers, so leave the rest for it.
  • Updates. sudo apt update && sudo apt upgrade keeps Ubuntu and OpenJDK 21 patched; unattended security upgrades are on. The XTDB server is the pinned 2.2.0 jar at /opt/xtdb/xtdb.jar.
  • Logs. journalctl -u xtdb.service shows the server log; journalctl -u xtdb-firstboot.service shows what first boot did.

Support

cloudimg supports this image by email at support@cloudimg.co.uk. Include the instance ID, the version (XTDB 2.2.0 on Ubuntu 24.04) and what you were doing.

XTDB is open-source software licensed under the Mozilla Public License 2.0; the licence text is on the instance at /usr/share/doc/xtdb/LICENSE. XTDB is a trademark of its owners. PostgreSQL is a trademark of the PostgreSQL Community Association of Canada. Ubuntu is a trademark of Canonical Ltd. cloudimg is not affiliated with or endorsed by these organisations.