Atuin 18.24.5 on AWS User Guide
Overview
This guide covers launching and using the Atuin 18.24.5 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Atuin replaces your shell history with a searchable database and syncs it between your machines. The history is end-to-end encrypted: each Atuin client encrypts every command with a key that never leaves your machines, so the sync server only ever stores ciphertext. This image is that sync server, on your own AWS account, instead of the public Atuin server.
The cloudimg image runs Atuin as a private service:
- HTTPS from the first boot. nginx serves Atuin on port 443 with a certificate generated on your instance's first boot. Port 80 only redirects to HTTPS, and a request for any name other than your instance's own gets no answer.
- Registration is closed. Nobody can create an account over the network. The image creates one account,
cloudimg, on first boot, and you add more withsudo atuin-admin. Atuin refuses to start if its configuration (or an environment override) opens registration. - No default login. The image contains no password, no certificate and no host name. First boot generates them and writes them to
/var/lib/cloudimg/atuin-credentials.txt(root only). The password is set with Atuin's own admin command, through standard input, never on a command line. - Database on a socket. PostgreSQL 16 listens on its local socket only and Atuin signs in to it as its Linux user, so there is no database password to manage or leak. The database itself is created on your instance's first boot: the image ships an empty one.
- Rate limits that cannot be dodged. Sign-in and account requests are limited per client address. nginx always replaces the
X-Forwarded-Forheader with the address it accepted the connection from, and Atuin trusts that header only from nginx, so a client cannot pretend to be someone else. - No credentials in the logs. The access log records each request's path and status without its query string, and nginx's error log does not record requests (Step 10).
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: the official Atuin 18.24.5 server release binary (/usr/local/bin/atuin-server, checked against its published SHA-256 at build time), PostgreSQL 16, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The build record and Atuin's MIT licence are in /usr/share/doc/cloudimg-atuin/.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
- A security group that allows TCP 443 (Atuin sync), TCP 80 (redirects to HTTPS) and TCP 22 (SSH). Open 443 to the networks your machines sync from; keep 22 open to your own networks only.
- The Atuin client (18.x) on each machine whose history you want to sync (Step 5).
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Atuin 18.24.5 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a smaller instance such as t3.small is enough for a few users), your key pair, your subnet and the security group above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=atuin}]'
An Elastic IP keeps the server's address, and so its name and certificate, the same across stops and starts.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Atuin 18.24.5 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes under a minute: it creates the PostgreSQL database, sets the server's name, generates the certificate and the cloudimg account, and only then starts Atuin and nginx. Check that everything is running:
systemctl is-active atuin-firstboot postgresql@16-main atuin nginx \
&& sudo test -f /var/lib/cloudimg/atuin-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. Atuin listens on the loopback address 127.0.0.1:8888 behind nginx, and PostgreSQL has no network port at all:
sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\.0\.0\.5[34]/ {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u | tee /dev/stderr \
| awk '$1 !~ /^127\./ {sub(/.*:/, "", $1); print $1}' | sort -un | xargs | grep -qx '22 80 443' && echo "network listeners: 22 80 443"
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:8888 users:(("atuin-server"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
network listeners: 22 80 443
Step 4: Get your sync address and credentials
Run sudo cat /var/lib/cloudimg/atuin-credentials.txt to see your sync address, account and password. The file is readable by root only and holds these settings, all generated for this instance:
sudo sed -n 's/^\(ATUIN_[A-Z0-9_]*\)=.*/\1/p' /var/lib/cloudimg/atuin-credentials.txt | tee /dev/stderr \
| grep -qx ATUIN_PASSWORD && echo "credentials present"
ATUIN_SYNC_ADDRESS
ATUIN_HOST
ATUIN_USERNAME
ATUIN_EMAIL
ATUIN_PASSWORD
ATUIN_TLS_CERT
ATUIN_TLS_SHA256_FINGERPRINT
ATUIN_REGISTRATION
credentials present
ATUIN_SYNC_ADDRESS is https://<your instance's public DNS name>, the address every client syncs with. ATUIN_USERNAME is cloudimg and ATUIN_PASSWORD is its password. The settings that are not secret can be shown on their own:
sudo grep -E '^ATUIN_(SYNC_ADDRESS|USERNAME|TLS_SHA256_FINGERPRINT|REGISTRATION)=' /var/lib/cloudimg/atuin-credentials.txt \
| tee /dev/stderr | grep -qx 'ATUIN_REGISTRATION=closed'
ATUIN_SYNC_ADDRESS=https://ec2-203-0-113-10.compute-1.amazonaws.com
ATUIN_USERNAME=cloudimg
ATUIN_TLS_SHA256_FINGERPRINT=3A:5F:...:C2
ATUIN_REGISTRATION=closed
The certificate is self-signed and generated on your instance, so your machines have to be told to trust it (Step 5). For a certificate they trust already, use your own domain (Step 9).
Step 5: Set up the Atuin client on your machine
Install the Atuin client on each machine, for example from the Atuin releases (use 18.x; the server is 18.24.5), with Homebrew (brew install atuin) or with Atuin's installer, then add it to your shell as the Atuin documentation describes (atuin init bash, zsh or fish).
Copy the server's certificate to the machine and check that it is the one first boot generated: the fingerprint must match ATUIN_TLS_SHA256_FINGERPRINT.
mkdir -p ~/.config/atuin
scp -i <your-key.pem> ubuntu@<public-ip>:/etc/ssl/atuin/atuin.crt ~/.config/atuin/server.crt
openssl x509 -in ~/.config/atuin/server.crt -noout -fingerprint -sha256
Point the client at your server. In ~/.config/atuin/config.toml:
sync_address = "https://ec2-203-0-113-10.compute-1.amazonaws.com"
update_check = false
The client must trust the certificate when it talks to the server. Make a CA bundle of your system's certificates plus this one and point SSL_CERT_FILE at it in your shell's start-up file, before the atuin init line (on macOS the system bundle is /etc/ssl/cert.pem, on Debian and Ubuntu /etc/ssl/certs/ca-certificates.crt):
cat /etc/ssl/cert.pem ~/.config/atuin/server.crt > ~/.config/atuin/ca-bundle.crt
echo 'export SSL_CERT_FILE="$HOME/.config/atuin/ca-bundle.crt"' >> ~/.zshrc
Then sign in on your first machine. Atuin asks for the password (ATUIN_PASSWORD) and, because registration is closed, for an encryption key: press Enter to use the key this machine generated when it recorded its first command.
atuin login -u cloudimg
atuin sync
On every other machine, run atuin key on the first machine and enter what it prints when atuin login asks for the key. All your machines must use the same key: it is what encrypts your history, and the server never sees it. Keep a copy somewhere safe, because without it nobody (including cloudimg) can read the synced history.
Step 6: Try it from the instance itself
You can watch a sync from the instance itself, with two throw-away client directories standing in for a laptop and a desktop. Download the same client release and check it against the SHA-256 that Atuin publishes:
mkdir -p /tmp/atuin-demo && cd /tmp/atuin-demo
curl -fsSLO https://github.com/atuinsh/atuin/releases/download/v18.24.5/atuin-x86_64-unknown-linux-gnu.tar.gz
echo "e16a95f10e9592543d2d578e44c2f1dccf89b57915c2e2c6935b435b3f43cee4 atuin-x86_64-unknown-linux-gnu.tar.gz" | sha256sum -c \
&& tar xzf atuin-x86_64-unknown-linux-gnu.tar.gz \
&& ./atuin-x86_64-unknown-linux-gnu/atuin --version | tee /dev/stderr | grep -q '^atuin 18\.24\.5 '
atuin-x86_64-unknown-linux-gnu.tar.gz: OK
atuin 18.24.5 (ad5bf5c35b7788c05b8d936f5da59c9ab1ac320e)
Give the "laptop" and the "desktop" each a configuration that points at this server:
cd /tmp/atuin-demo
SYNC="$(sudo sed -n 's/^ATUIN_SYNC_ADDRESS=//p' /var/lib/cloudimg/atuin-credentials.txt)"
for m in laptop desktop; do
mkdir -p "$m/.config/atuin"
printf 'sync_address = "%s"\nupdate_check = false\nauto_sync = false\n' "$SYNC" > "$m/.config/atuin/config.toml"
done
grep -c "^sync_address = \"https://" laptop/.config/atuin/config.toml desktop/.config/atuin/config.toml | tee /dev/stderr | grep -c ':1$' | grep -qx 2
laptop/.config/atuin/config.toml:1
desktop/.config/atuin/config.toml:1
The laptop records a command, signs in as cloudimg (the password goes in on standard input) and syncs. SSL_CERT_FILE tells the client to trust this instance's certificate:
A=/tmp/atuin-demo/atuin-x86_64-unknown-linux-gnu/atuin
export SSL_CERT_FILE=/etc/ssl/atuin/atuin.crt HOME=/tmp/atuin-demo/laptop
export ATUIN_SESSION="$($A uuid)"
ID="$($A history start -- 'echo synced-by-atuin-on-aws')" && $A history end --exit 0 "$ID"
if ! $A status >/dev/null 2>&1; then
sudo sed -n 's/^ATUIN_PASSWORD=//p' /var/lib/cloudimg/atuin-credentials.txt | $A login -u cloudimg -p - -k "" 2>&1 | tail -n 1
fi
$A sync 2>&1 | tail -n 1 | tee /dev/stderr | grep -q '^Sync complete!'
Logged in!
Sync complete! 1 items in history database, force: false
The desktop signs in with the same account and the laptop's encryption key (passed in the environment, as atuin login reads it from ATUIN_ENCRYPTION_KEY), syncs, and finds the laptop's command:
A=/tmp/atuin-demo/atuin-x86_64-unknown-linux-gnu/atuin
export SSL_CERT_FILE=/etc/ssl/atuin/atuin.crt HOME=/tmp/atuin-demo/desktop
export ATUIN_SESSION="$($A uuid)"
if ! $A status >/dev/null 2>&1; then
sudo sed -n 's/^ATUIN_PASSWORD=//p' /var/lib/cloudimg/atuin-credentials.txt \
| ATUIN_ENCRYPTION_KEY="$(HOME=/tmp/atuin-demo/laptop $A key)" $A login -u cloudimg -p - 2>&1 | tail -n 1
fi
$A sync 2>&1 | tail -n 1
$A history list --cmd-only | tee /dev/stderr | grep -qx 'echo synced-by-atuin-on-aws' && echo "the desktop has the laptop's command"
Logged in!
Sync complete! 1 items in history database, force: false
echo synced-by-atuin-on-aws
the desktop has the laptop's command
The server stored only ciphertext: the command's text is nowhere in its database.
sudo -u postgres psql -X -d atuin -tAc "SELECT count(*) FROM store" | tee /dev/stderr | grep -qvx 0 \
&& { sudo -u postgres psql -X -d atuin -c "COPY store TO STDOUT" | grep -c 'synced-by-atuin-on-aws' || true; } \
| tee /dev/stderr | grep -qx 0 && echo "stored encrypted"
1
0
stored encrypted
When you are done, remove the demo with rm -rf /tmp/atuin-demo.
Step 7: Registration is closed
A new client cannot create an account on this server:
A=/tmp/atuin-demo/atuin-x86_64-unknown-linux-gnu/atuin
mkdir -p /tmp/atuin-demo/stranger/.config/atuin && cp /tmp/atuin-demo/laptop/.config/atuin/config.toml /tmp/atuin-demo/stranger/.config/atuin/
OUT="$(echo not-a-real-password | SSL_CERT_FILE=/etc/ssl/atuin/atuin.crt HOME=/tmp/atuin-demo/stranger \
$A register -u stranger -e stranger@example.com -p - 2>&1 || true)"
[[ "$OUT" == *"400 Bad Request - this server is not open for registrations"* ]] \
&& echo "registration refused: 400 Bad Request - this server is not open for registrations"
400 Bad Request - this server is not open for registrations
registration refused
The image keeps it that way on every start: atuin.service runs atuin-config-check first, which refuses to start Atuin if /etc/atuin/server.toml or an ATUIN_OPEN_REGISTRATION environment setting opens registration, sets a listen address other than loopback, a registration webhook, a trusted proxy other than this instance's nginx, or a password in the database address. If you really want public sign-ups, create /etc/atuin/allow-open-registration as well as setting open_registration = true, and keep the rate limits in Step 10 in mind.
Step 8: Add accounts and change passwords
Create an account for each person with Atuin's own admin command. It asks for the new password twice (or reads it from standard input), so the password never appears on a command line:
sudo atuin-admin user create alice --email alice@example.com
sudo atuin-admin user set-password alice
sudo atuin-admin user delete alice
Usernames are letters, digits and -, up to 32 characters. Atuin stores passwords as Argon2id hashes. To give the cloudimg account a new random password and write it to the credentials file (sessions that are already signed in stay signed in):
sudo atuin-rotate-password
Step 9: Use your own domain
Point a DNS name at the instance (an Elastic IP keeps it stable), then make it the server's address. This generates a new certificate for that name and pins nginx to it:
sudo atuin-set-site atuin.example.com
Every client then needs the new certificate (Step 5) and the new sync_address. To use a certificate from a public CA instead, for example one issued by certbot with a DNS challenge, run atuin-set-site first, then replace /etc/ssl/atuin/atuin.crt (full chain) and /etc/ssl/atuin/atuin.key (mode 0600) and run sudo systemctl reload nginx; clients then need no SSL_CERT_FILE.
Step 10: What the front door refuses
nginx answers only for the server's own names and passes requests to Atuin on loopback:
H="$(sudo sed -n 's/^ATUIN_HOST=//p' /var/lib/cloudimg/atuin-credentials.txt)"
{ curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/atuin/atuin.crt --resolve "$H:443:127.0.0.1" "https://$H/healthz"
curl -s -o /dev/null -w '%{http_code}\n' -k -H 'Host: someone-else.example' https://127.0.0.1/
curl -s -o /dev/null -w '%{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
} | xargs | tee /dev/stderr | grep -qx '200 000 301' && echo "front door OK"
200 000 301
front door OK
- Rate limits. Sign-in, registration and account requests are limited to 30 a minute per client address (bursts of 20); the rest of the API to 50 a second (bursts of 500), with at most 64 connections per address. Over the limit, nginx answers
429. The limit applies to every spelling of those paths (//login,/LOGIN,/%6Cogin,/login;xand so on), because nginx matches them after normalising the path. - X-Forwarded-For. nginx replaces any
X-Forwarded-Forheader with the address it accepted the connection from and blanksX-Real-IP,Forwardedand similar headers, and Atuin trusts the header only from127.0.0.1, so a client cannot change the address the rate limits and logs use. If you put a load balancer in front of the instance, every client then shares the load balancer's address for the rate limits. - Sizes and types. Request bodies are limited to 4 MiB and a single history record to 1 MiB. Compressed request bodies (
Content-Encoding) get415,TRACEgets405, and requests carrying a browserOriginheader get403: the sync API is for Atuin clients, not web pages. - Headers. Responses carry
Strict-Transport-Security,X-Content-Type-Options: nosniff,X-Frame-Options: DENY, aContent-Security-Policyofdefault-src 'none',Referrer-Policy: no-referrerandCache-Control: no-store, and nginx does not advertise its version.
What is logged: nginx's access log (/var/log/nginx/access.log) records the client address, request path and status, without query strings. nginx's error log records only start-up problems, not refused requests, so a password in a rejected request never reaches the disk. Atuin logs warnings and errors to the journal (journalctl -u atuin), not individual requests.
Step 11: Run the self-test
The image ships a self-test that checks the services and listening ports, that registration is refused (through nginx and on Atuin directly), that a wrong password is refused, and that the cloudimg account signs in over HTTPS with this instance's certificate:
sudo atuin-selftest | tail -n 1 | tee /dev/stderr | grep -q '^ATUIN_SELFTEST_OK '
ATUIN_SELFTEST_OK registration refused (nginx + Atuin), wrong password refused, cloudimg logs in over https://ec2-203-0-113-10.compute-1.amazonaws.com/ with this instance's certificate
Step 12: Back up and restore
Everything Atuin stores (accounts, sessions and the encrypted history) is in the PostgreSQL database atuin. Back it up with:
sudo -u postgres pg_dump -Fc atuin > atuin-$(date +%F).dump
Restore it on a new instance of this image with sudo -u postgres pg_restore --clean --if-exists -d atuin atuin-<date>.dump and sudo systemctl restart atuin. The history stays encrypted with your clients' key, so keep that key (Step 5) as well.
Step 13: Security notes and the build record
The certificate generated at first boot is a server certificate only: it cannot sign other certificates.
openssl x509 -in /etc/ssl/atuin/atuin.crt -noout -ext basicConstraints,keyUsage,extendedKeyUsage | tee /dev/stderr | grep -q 'CA:FALSE'
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
The server binary is the 18.24.5 release, checked against its published SHA-256 at build time, and the database role has no password:
/usr/local/bin/atuin-server --version | tee /dev/stderr | grep -qx 'atuin-server 18.24.5'
sed -n '1,3p' /usr/share/doc/cloudimg-atuin/BUILD-INFO.txt
sudo -u postgres psql -X -tAc "SELECT 'roles with a password: ' || count(*) FROM pg_authid WHERE rolpassword IS NOT NULL" \
| tee /dev/stderr | grep -qx 'roles with a password: 0'
atuin-server 18.24.5
atuin-server 18.24.5 (github.com/atuinsh/atuin release v18.24.5, 2026-11-04), MIT (atuin-LICENSE)
atuin-server-x86_64-unknown-linux-gnu.tar.gz sha256 c0a18cca6edf959dd032f38b08c59c3504fadf92b9d2706868b49e16a17c131f (= upstream .sha256)
binary /usr/local/bin/atuin-server sha256 567e7a35f4267af249e36a428dbad1f9ab50a160f734e55a9f73dd21f3d71e64
roles with a password: 0
Atuin is MIT licensed; its licence is in /usr/share/doc/cloudimg-atuin/atuin-LICENSE. Atuin runs as its own unprivileged user atuin with no login shell, no capabilities, a read-only view of the system and no access to the credentials file or the TLS key. Atuin, nginx and PostgreSQL cannot reach the EC2 instance metadata service, and Atuin makes no outbound requests (its only outbound feature, a registration webhook, is refused by the configuration check). PostgreSQL 16 and nginx come from Ubuntu's main archive.
Step 14: Keep it up to date
Ubuntu security updates, including PostgreSQL and nginx, install automatically. To apply everything now:
sudo apt-get update && sudo apt-get upgrade -y
New Atuin releases arrive as new versions of the AWS Marketplace listing: launch the new version, restore your database backup into it (Step 12) and run sudo atuin-set-site with your domain if you use one. Atuin upgrades the database schema when it starts.
Support
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
The other versions on this listing are on the developer and API tools guide.