Applications AWS

hledger 1.52.4 on AWS User Guide

| Product: hledger 1.52.4 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the hledger 1.52.4 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg accounting and HR software listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

hledger is open source plain text accounting. Your whole ledger is one human readable text file, the journal, in which every transaction is a few lines of dated, balanced double entry postings. hledger reads that file and produces balance sheets, income statements, registers, budgets and multi-currency reports. There is no database to administer: the file is the single source of truth, it diffs cleanly, and it can live in version control. The image ships the three upstream programs: the hledger command line, the hledger-ui terminal interface and the hledger-web browser interface, which shows the journal, account registers with balance charts, and a form to add transactions.

The cloudimg image is a hardened, ready-to-use hledger server:

  • The upstream release. The official hledger 1.52.4 static Linux binaries, verified against the release's SHA-256, with their manual pages, info pages and bash completion. Nothing is recompiled. The GPL licence text and the source offer are installed beside them.
  • hledger-web is never exposed directly. hledger-web has no access control of its own. On this image it listens on a private Unix socket (/run/hledger-web/hledger-web.sock), not on any TCP port, and only nginx can connect to it.
  • No default login. nginx requires a username and password for every page and API call. First boot creates one account, admin, with a random password unique to your instance, and stores only its bcrypt hash for nginx. Until first boot has finished, neither hledger-web nor nginx starts, so the instance never serves an open journal.
  • HTTPS from the first boot. nginx serves hledger-web on 443 with a certificate generated on your instance (an end-entity certificate, not a certificate authority). Port 80 redirects to HTTPS. The server's name is set from the instance's EC2 public DNS name at first boot and requests for any other name are dropped. You can change it to your own domain.
  • Append-only web access. hledger-web runs with --allow=add: signed-in users can view reports and add transactions, but cannot edit, upload or download the journal file through the browser. Writes from other web sites are refused (protection against cross-site request forgery), and nginx passes hledger-web the real client address from the connection, never a header a client sends.
  • Metadata service blocked. hledger-web, nginx and the first-boot service cannot reach the EC2 instance metadata service. The AMI requires IMDSv2.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: hledger, hledger-ui and hledger-web 1.52.4 (GPL-3.0-or-later), nginx, apache2-utils (for htpasswd), cloud-init and the AWS Systems Manager agent, all from Ubuntu 24.04 except hledger.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large (2 vCPUs, 8 GiB). hledger is light: a t3.small is enough for a personal or small business ledger.

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
  • A security group that allows TCP 22 (SSH) from your own networks only, TCP 443 from wherever your users are, and TCP 80 (redirects to HTTPS). No other port is needed: hledger-web has no network port.
  • For production, a DNS name you control (for example ledger.example.com) and, ideally, a certificate from a certificate authority. Until then the instance uses its EC2 public DNS name with its own self-signed certificate.

Connecting to your instance

Operating system SSH user
Ubuntu 24.04 ubuntu

Step 1: Launch the instance

Launch from the AWS Marketplace page (choose the hledger 1.52.4 on Ubuntu 24.04 version), or with the AWS CLI using the AMI ID shown on the listing's launch page:

aws ec2 run-instances --image-id ami-xxxxxxxxxxxxxxxxx --instance-type m5.large \
  --key-name my-key --security-group-ids sg-xxxxxxxx --subnet-id subnet-xxxxxxxx \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=hledger}]'

First boot takes a few seconds once the instance is up: it creates the certificate and the administrator password. The web interface stays offline until that has finished.

Step 2: Connect to your instance

ssh -i my-key.pem ubuntu@<public-ip>

Step 3: Check the services

systemctl is-active hledger-web.service nginx.service
sudo ss -ltnp | awk 'NR==1 || /:(22|80|443) /'
sudo ss -lxp | grep hledger-web
active
active
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=3464,fd=5),("nginx",pid=3463,fd=5),("nginx",pid=3462,fd=5))
LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=892,fd=3),("systemd",pid=1,fd=112))
LISTEN 0      511          0.0.0.0:443       0.0.0.0:*    users:(("nginx",pid=3464,fd=7),("nginx",pid=3463,fd=7),("nginx",pid=3462,fd=7))
LISTEN 0      511             [::]:80           [::]:*    users:(("nginx",pid=3464,fd=6),("nginx",pid=3463,fd=6),("nginx",pid=3462,fd=6))
LISTEN 0      4096            [::]:22           [::]:*    users:(("sshd",pid=892,fd=4),("systemd",pid=1,fd=113))
LISTEN 0      511             [::]:443          [::]:*    users:(("nginx",pid=3464,fd=8),("nginx",pid=3463,fd=8),("nginx",pid=3462,fd=8))
u_str LISTEN 0      128                  /run/hledger-web/hledger-web.sock 18935            * 0    users:(("hledger-web",pid=3449,fd=13))

Only SSH, HTTP and HTTPS listen on the network. hledger-web listens only on its Unix socket.

Step 4: Get your address and the administrator password

First boot writes everything that is unique to your instance to a file only root can read:

sudo cat /root/hledger-credentials.txt
# hledger 1.52.4 - written on first boot by hledger-firstboot.service. Unique to this instance. Keep it secret.
HLEDGER_URL=https://ec2-52-87-182-134.compute-1.amazonaws.com/
HLEDGER_TLS_SHA256=F7:11:BA:F6:EE:79:29:16:65:D2:DD:B5:76:38:24:C5:10:48:4B:CD:25:71:DC:29:AD:5B:53:93:7F:FF:90:38
HLEDGER_USER=admin
HLEDGER_PASS=<40 random hexadecimal characters>
...
  • HLEDGER_URL is your hledger address.
  • HLEDGER_TLS_SHA256 is the fingerprint of the instance's own certificate. Your browser will warn that the certificate is not trusted; compare the fingerprint it shows with this one before you continue. Replace it with a certificate from a certificate authority for your own name when you can (Step 12).
  • HLEDGER_USER / HLEDGER_PASS sign in to hledger-web. This is the only account.

Step 5: Run the self-test

The image includes an end-to-end test. It checks hledger's arithmetic on a scratch journal, checks that the live journal parses and that its balance and register agree, checks that every page refuses requests without the password or with a wrong one, signs in over HTTPS and reads the journal, and checks that a write from another web site is refused:

sudo hledger-selftest
hledger self-test passed: computed 743.25 from a fresh transaction; assets:bank:current balance = register (3126.55); health 1.52.4 via the socket; every path 401 without the credential and with a wrong one; hledger-web serves the same journal over pinned HTTPS; cross-origin POST 403; edit capability not granted
HLEDGER_SELFTEST_OK

Step 6: Sign in to the web interface

Open HLEDGER_URL in your browser, check the certificate fingerprint, and sign in as admin with the password from Step 4. The journal view lists every transaction, and the sidebar shows every account with its balance:

The hledger-web journal view titled main.journal, with a sidebar listing assets bank current at 3126.55 GBP, equity opening balances, expenses with cloudimg verification and office rent, and income consulting, and a General Journal table listing four transactions from Opening balances to the cloudimg appliance verification entry, each with its two postings and amounts

To change the password, run this on the instance and type the new password (at least 12 characters) at the prompt. It is read from the terminal, never from the command line, and nginx stores only its bcrypt hash:

# Type <new-password> at the prompt; nothing is echoed.
read -rs -p 'New hledger password: ' P; echo
printf '%s\n' "$P" | sudo hledger-set-password admin; unset P

Once you have stored the password somewhere safe, you can delete the HLEDGER_PASS line from /root/hledger-credentials.txt.

Step 7: Understand the journal file

Everything hledger reports comes from one plain text file, /var/lib/hledger/main.journal:

sudo -u hledger cat /var/lib/hledger/main.journal

The journal is owned by the hledger service account with mode 0640 inside a 0750 directory, so only that account and root can read it. A transaction looks like this. Every transaction must balance to zero, and leaving the last amount blank lets hledger infer it:

2026-01-08 * Client invoice 001
    assets:bank:current                       1200.00 GBP
    income:consulting

Accounts are created simply by naming them, and colons make a hierarchy, so assets:bank:current sits under assets:bank, which sits under assets. The sample entries exist so the instance has real data to report on; delete them when you start recording your own transactions.

You do not need to pass -f to hledger: LEDGER_FILE is set for login shells and hledger's default path (~/.hledger.journal) is linked to the journal for root and the hledger account.

Step 8: Record a transaction from the command line

Append a transaction with any editor, or from the shell. This example records a second client invoice:

sudo -u hledger tee -a /var/lib/hledger/main.journal >/dev/null <<'TXN'

2026-01-20 * Client invoice 002
    assets:bank:current                        850.00 GBP
    income:consulting
TXN
sudo -u hledger hledger check && echo "journal OK"
journal OK

hledger check prints nothing else when the journal is valid. Because the file is plain text, a typo is a parse error you can see and fix, not a corrupted database. hledger also has an interactive entry mode that suggests accounts as you type: run sudo -u hledger hledger add in your SSH session.

Step 9: Run balance and register reports

The balance report totals every account; the register report shows the postings to an account with a running total:

sudo -u hledger hledger balance
sudo -u hledger hledger register income:consulting
         3976.55 GBP  assets:bank:current
        -2500.00 GBP  equity:opening balances
          123.45 GBP  expenses:cloudimg:verification
          450.00 GBP  expenses:office:rent
        -2050.00 GBP  income:consulting
--------------------
                   0
2026-01-08 Client invoice 001   income:consulting     -1200.00 GBP  -1200.00 GBP
2026-01-20 Client invoice 002   income:consulting      -850.00 GBP  -2050.00 GBP

The new invoice is reflected immediately. Income and equity accounts show as negative under the standard double entry sign convention. Other useful reports:

sudo -u hledger hledger balancesheet
sudo -u hledger hledger incomestatement
sudo -u hledger hledger accounts
sudo -u hledger hledger stats

hledger-web re-reads the journal when the file changes, so the browser shows the new transaction on its next page load.

Step 10: Explore accounts in the browser

Select any account in the sidebar to open its register, with a running balance chart above the postings:

The hledger-web register for assets bank current, with a period total line chart stepping from 2500 to 3700 and down to 3126.55 GBP across January 2026, above a table of four postings with date, description, the other account, the amount and the running total

The search box takes hledger's query language: inacct:expenses shows transactions touching expense accounts, date:2026-01 limits to January 2026 and amt:>500 filters by size.

The hledger-web journal filtered with the query inacct:expenses, titled Transactions in expenses, with the expense accounts highlighted in the sidebar

Step 11: Add a transaction from the browser

Select Add a transaction on the journal view. The form takes a date, a description and up to four account and amount pairs; leave the last amount blank to let hledger infer it. Save appends the transaction to the same /var/lib/hledger/main.journal, so hledger balance shows it at once.

The hledger-web Add a transaction dialog over the journal view, with a date field, a description field, four pairs of account and amount fields and a Save button

The web interface is append-only on this image: it can add transactions but cannot edit or replace the journal. Make corrections in the file itself (Step 8), where every change is visible.

hledger-web also has a JSON API behind the same password, for example /accounts, /transactions and /accountnames. From your own machine, curl -u admin https://<your-domain>/accounts prompts for the password and returns the account tree.

Step 12: Use your own domain and certificate

Point a DNS name at the instance (an Elastic IP is a good idea), then:

sudo hledger-set-hostname <your-domain>

This renders nginx for the new name (the instance's other names redirect to it), sets hledger-web's base URL to it, creates a new self-signed certificate for it and updates the URL and fingerprint in /root/hledger-credentials.txt. To use a certificate from a certificate authority, replace /etc/ssl/hledger/hledger.crt and hledger.key (keep the key 0600 root) and run sudo systemctl reload nginx.

Step 13: Restrict hledger to your networks

Every page uses the access list in /etc/nginx/cloudimg/hledger-access.conf. By default it is allow all; (every page still needs the password). To allow only your own networks, replace it with your addresses and a final deny all;:

sudo tee /etc/nginx/cloudimg/hledger-access.conf >/dev/null <<'EOF'
allow <your-mgmt-cidr>;
deny all;
EOF
sudo nginx -t && sudo systemctl reload nginx

Everyone else then gets 403 Forbidden. hledger-set-hostname never overwrites this file. You can also restrict TCP 443 in the security group.

Step 14: Check the security posture

U=$(sudo sed -n 's/^HLEDGER_URL=//p' /root/hledger-credentials.txt)
for p in "" journal register accounts add; do
  curl -s --cacert /etc/ssl/hledger/hledger.crt -o /dev/null -w "GET /$p without a password: %{http_code}\n" "${U}$p"
done
WRONG="admin:$(openssl rand -hex 20)"   # a random password that is not yours
curl -s --cacert /etc/ssl/hledger/hledger.crt -o /dev/null -w 'GET /journal with a wrong password: %{http_code}\n' --user "${WRONG}" "${U}journal"
sudo ls -l /run/hledger-web/
sudo -u nobody curl -s -m 5 --unix-socket /run/hledger-web/hledger-web.sock -o /dev/null -w 'hledger-web socket as another local user: %{http_code}\n' http://localhost/version || true
systemctl show -p IPAddressDeny -p RestrictAddressFamilies -p User hledger-web.service
curl -s http://127.0.0.1/healthz; echo
GET / without a password: 401
GET /journal without a password: 401
GET /register without a password: 401
GET /accounts without a password: 401
GET /add without a password: 401
GET /journal with a wrong password: 401
total 0
srwxrwx--- 1 hledger www-data 0 Nov  7 14:44 hledger-web.sock
hledger-web socket as another local user: 000
IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 ::ffff:169.254.169.254/128 169.254.169.254/32
User=hledger
RestrictAddressFamilies=AF_UNIX
"1.52.4"
  • Every page and API call needs the password. If the password file is ever missing, nginx refuses every request rather than serving the journal.
  • The socket belongs to hledger with the www-data group (nginx), so no other local account can talk to hledger-web.
  • IPAddressDeny is the metadata-service block on hledger-web; nginx and the first-boot service have the same one. hledger-web can only use Unix sockets (RestrictAddressFamilies=AF_UNIX), so it cannot open a network connection at all.
  • http://127.0.0.1/healthz returns hledger-web's version for a monitoring agent on the instance. It is answered only on 127.0.0.1, never on 443.

Step 15: Back up your journal

Your data is the journal file. Copy it to a file only root can read, and keep copies off the instance (for example in an encrypted S3 bucket):

sudo sh -c 'umask 077; cp /var/lib/hledger/main.journal /var/backups/main.journal.$(date +%F)'
sudo ls -l /var/backups/ | grep main.journal

Plain text also works well with git, which gives you a complete, diffable history of every change to your books. EBS snapshots of the instance are a simple way to keep point-in-time copies of the whole server.

Step 16: Updates

Ubuntu security updates install automatically (unattended upgrades). New hledger releases are published as new versions of this listing: launch the new version and copy your journal across.

Step 17: Licences and source

ls /opt/hledger/
sed -n '1,3p' /opt/hledger/VERSION
LICENSE
SOURCE-OFFER.txt
VERSION
main.journal.sample
hledger_version=1.52.4
hledger_tarball_sha256=f76a2e4088e162b7658e43077502569d5a6fdacd219b19bfb9aab6588b69643a
hledger_license=GPL-3.0-or-later

hledger is licensed under the GNU General Public License version 3 or later. The image ships the unmodified upstream binaries; SOURCE-OFFER.txt says where to get the corresponding source (the upstream project at tag 1.52.4) and includes a written offer to provide it.

Security notes

  • Advisories. hledger 1.52.4 includes the fixes for the two published hledger-web advisories rated high: a cross-site scripting issue in the add form's account autocompletion (GHSA-538p-cvc4-4qjm, fixed in 1.52.2) and a cross-site scripting and journal include injection issue in the add form's error handling (GHSA-vq7r-8w52-jv84, fixed in 1.52.3).
  • Web server library. The upstream 1.52.4 binary is built with warp 3.4.15, which has a published request smuggling advisory (HSEC-2026-0013, fixed in warp 3.4.17). It is not reachable here: hledger-web listens only on its Unix socket, and its only client is nginx, which requires the password before it forwards anything, refuses the ambiguous request framings that smuggling relies on, and sends each request on its own connection.
  • Web access level. --allow=add is set in /etc/systemd/system/hledger-web.service. Use view for read-only reports. Raising it to edit would let anyone with the password replace the whole journal from the browser, so cloudimg does not recommend it.
  • Secrets. The administrator password is in /root/hledger-credentials.txt until you remove it; nginx stores only a bcrypt hash in /etc/nginx/cloudimg/hledger.htpasswd. The TLS key is in /etc/ssl/hledger/hledger.key (root only). No secret is on any command line, in a systemd unit or in a log.

Support

cloudimg provides support for this image by email at support@cloudimg.co.uk. hledger documentation is at hledger.org.