Fava for Beancount 1.30.16 on AWS User Guide
Overview
This guide covers launching and using the Fava for Beancount 1.30.16 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg accounting, HR and finance listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Beancount is double entry accounting kept in a plain text file: every account, transaction, balance assertion and price is a line you can read, diff and keep in version control. Fava is its web interface: an income statement, a balance sheet and a trial balance with charts, net worth, holdings and commodities, an interactive journal you can filter by time, account, tag and payee, a query editor for Beancount's SQL like query language (BQL), a source editor with syntax checking, and documents attached to accounts.

Fava has no login of its own: anyone who can reach it can read and edit the ledger. The cloudimg image makes it safe to run on EC2:
- A password on every page, unique to your instance. nginx asks for a user name and password (HTTP basic authentication) on every page and every API call. The password is generated on your instance's first boot into a root only file; no password, certificate or key is baked into the image. Change it at any time with
fava-set-password. - Rate limited per client. Every request carries the password, so nginx limits every request per client address (10 a second, bursts of 60) and answers
429beyond that, which turns password guessing into a crawl. ForgedX-Forwarded-Forheaders do not move the limit. - HTTPS from the first boot. nginx serves Fava on port 443 with a certificate generated on your instance's first boot for its public address; port 80 redirects to HTTPS. Requests for a host name the instance does not serve get no answer.
- Fava listens on localhost only (
127.0.0.1:5000), runs as its own unprivilegedfavauser with no Linux capabilities and can write only its ledger directory,/var/lib/fava/ledger. Other local accounts (includingubuntu) cannot connect to it directly. - Uploads are capped and confined. Documents you attach in the browser land only in the ledger's
documentsfolder, and nginx refuses anything over 16 MiB. - Other web sites cannot use your session. The JSON API, downloads, documents and statements refuse requests that another site's page sends from your browser, and every change must come from Fava's own pages.
- No outbound reach inside AWS. Fava and nginx are refused, at the kernel level and on every boot, any connection to the EC2 instance metadata service, link-local, private (RFC 1918), carrier grade NAT and reserved addresses and your VPC's own address ranges. Fava fetches nothing from the internet:
bean-price(which downloads prices) is not installed and no importer or plugin that fetches URLs is configured. - Pinned and checked. Fava 1.30.16 and Beancount 3.2.3 are installed from a lock file in which every one of the 35 Python packages is pinned by version and SHA-256 hash, into a root owned virtual environment; no compiler ships.
- Fully patched Ubuntu 24.04 LTS at build time.
The image ships with a fictional demo ledger so you can try Fava straight away. Step 7 shows how to serve your own.
What is on the image: Fava 1.30.16 with Beancount 3.2.3, beanquery 0.2.0 and beangulp 0.2.0 (/opt/fava/venv, with the command line tools bean-check, bean-query, bean-format, bean-doctor and bean-example), the ledger directory /var/lib/fava/ledger, nginx, the tools fava-set-ledger, fava-set-password, fava-set-domain and fava-selftest, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
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 with internet access.
- A security group that allows TCP 22 (SSH) and TCP 443 (Fava) from your own network, and TCP 80 (redirects to HTTPS) if you want plain
http://links to work. Your ledger is your financial history: open 443 to the addresses you use, not to the whole internet, unless you need to.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Fava for Beancount 1.30.16 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a personal ledger needs very little, so a smaller instance such as t3.small is enough for most), your key pair, a subnet and a security group as above. An Elastic IP keeps the address, and so the certificate's name, stable across a stop and start.
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=fava}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Fava for Beancount 1.30.16 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot reads the instance's public address from EC2 instance metadata, generates the password and the certificate and writes the credentials file; the egress backstop loads; then Fava and nginx start:
systemctl is-active cloudimg-fava-egress fava nginx
sudo test -f /var/lib/cloudimg/fava-firstboot.done && echo FIRSTBOOT_DONE
All three units are active and the first boot marker is present:
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. Fava listens on localhost only:
sudo ss -lntpH | awk '{print $4, $6}' | sed -E 's/users:\(\("([^"]+)".*/\1/' | grep -v '127.0.0.5' | sort -u
0.0.0.0:22 sshd
0.0.0.0:443 nginx
0.0.0.0:80 nginx
127.0.0.1:5000 fava
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx
Step 4: Get your URL and password
sudo cat /root/fava-credentials.txt
The file is readable by root only and holds the URL, the user name, the password and the certificate's fingerprint:
# Fava 1.30.16 (cloudimg) on this instance. Open FAVA_URL in a browser and sign in as FAVA_USERNAME / FAVA_PASSWORD.
# The password is unique to this instance. Change it: sudo fava-set-password (or --stdin to choose one).
# The certificate is self-signed for this instance: compare its SHA-256 fingerprint with FAVA_TLS_SHA256.
# The ledger is a DEMO; serve your own: copy it into /var/lib/fava/ledger and run sudo fava-set-ledger FILE.
FAVA_URL=https://<public-ip>/
FAVA_HOST=<public-ip>
FAVA_USERNAME=admin
FAVA_TLS_SHA256=<64 hex digits, colon separated>
FAVA_ADDRESS_SOURCE=imdsv2-public-ipv4
GUIDE=https://www.cloudimg.co.uk/guides/fava-for-beancount-1-30-16-on-ubuntu-24-04-aws/
FAVA_PASSWORD=<FAVA_PASSWORD>
FAVA_ADDRESS_SOURCE says where the address came from: the public IPv4 address from instance metadata, or the private address when the instance has no public one.
The image includes a self test. Through nginx and pinned to this instance's certificate it checks that six paths, including the JSON API, refuse a request with no password and one with a wrong password, and that the password opens the income statement and its API:
sudo fava-selftest
fava-selftest: 6 paths x (no credential, wrong password) -> 401; the password opens /cloudimg-demo-ledger/income_statement/ and its API
FAVA_SELFTEST_OK
Step 5: Sign in
Open FAVA_URL in your browser. The certificate is self signed for your instance, so the browser warns once. To check you are talking to your instance, open the certificate details and compare its SHA-256 fingerprint with FAVA_TLS_SHA256, then continue. The browser then asks for a user name and password: enter admin and FAVA_PASSWORD. The browser remembers them until you close it.
Fava opens on the demo ledger's income statement. The Balance Sheet shows your assets, liabilities and equity with net worth over time:

The Journal lists every entry, newest first. The buttons along the top show or hide each kind of entry, and the bar at the top filters every page by time (for example 2026 or month), account and tag or payee:

Step 6: Query, edit and attach documents
Query runs Beancount's query language over the ledger and charts the result. Every result can be downloaded as CSV:
SELECT account, sum(position) AS total WHERE account ~ '^Expenses' GROUP BY account ORDER BY account

Editor edits the ledger file itself, with syntax highlighting and completion. Fava checks the ledger whenever the file changes and lists any errors on the Errors page. Changes you make from the Journal (and Save in the editor) are written straight to the ledger file:

Documents. Drop a file (a receipt or a statement, for example) onto an account name on any report page to attach it. Fava stores it under /var/lib/fava/ledger/documents/<Account>/ and lists it on the Documents page. Files over 16 MiB are refused.
The same tools work from the command line, as the fava user. bean-check checks a ledger:
sudo -u fava /opt/fava/venv/bin/bean-check /var/lib/fava/ledger/main.beancount && echo "no errors"
no errors
bean-query runs the same queries as the Query page:
sudo -u fava /opt/fava/venv/bin/bean-query /var/lib/fava/ledger/main.beancount "SELECT account, sum(position) AS balance WHERE account ~ '^Assets' GROUP BY account ORDER BY account"
account balance
---------------------------- -------------------------------------------------------------
Assets:Bank:Checking 21831.37 USD
Assets:Bank:Savings 15584.75 USD
Assets:Cash:Wallet 1500.00 USD
Assets:Investments:Brokerage 20 EXMPL { 50.00 USD} 15 EXMPL { 54.00 USD}
Step 7: Serve your own ledger
The demo ledger is /var/lib/fava/ledger/main.beancount, and Fava reads the file named in /etc/cloudimg/fava-ledger.env:
cat /etc/cloudimg/fava-ledger.env
# cloudimg: the ledger Fava serves (Fava reads BEANCOUNT_FILE). Change it with: sudo fava-set-ledger FILE
BEANCOUNT_FILE=/var/lib/fava/ledger/main.beancount
Fava may read and write only /var/lib/fava/ledger, so your ledger, any files it includes and its documents folder go there, owned by the fava user.
- From your computer, copy your ledger (or the folder that holds it and its include files) to the instance:
scp -i <your-key.pem> -r <your-ledger-folder> ubuntu@<public-ip>:/tmp/
- On the instance, move it into the ledger directory and give it to
fava:
sudo cp -r /tmp/<your-ledger-folder> /var/lib/fava/ledger/
sudo chown -R fava:fava /var/lib/fava/ledger
sudo chmod -R u=rwX,g=rX,o= /var/lib/fava/ledger
rm -rf /tmp/<your-ledger-folder>
- Switch Fava to it.
fava-set-ledgerchecks the file withbean-checkas thefavauser first and keeps serving the previous ledger if it finds errors:
sudo fava-set-ledger /var/lib/fava/ledger/<your-ledger-folder>/<your-ledger>.beancount
fava-set-ledger: Fava now serves /var/lib/fava/ledger/<your-ledger-folder>/<your-ledger>.beancount
Fava's address for a ledger comes from its option "title" line (the demo is /cloudimg-demo-ledger/). For documents, add option "documents" "documents" to your ledger; the folder is relative to the ledger file. When you no longer need the demo ledger, delete it with sudo rm /var/lib/fava/ledger/main.beancount after switching.
To start a new ledger from scratch, create a file that holds an option "title" "<your title>" line and your open directives in the same directory, set its owner to fava and switch to it the same way.
Step 8: Change the password
To generate a new random password (written to the credentials file as FAVA_PASSWORD):
sudo fava-set-password
To choose your own (16 characters or more, read from standard input and not stored on the instance):
sudo fava-set-password --stdin
The new password takes effect at once; your browser asks for it again. The password is stored for nginx as a bcrypt hash in /etc/nginx/cloudimg/fava.htpasswd.
Step 9: Use your own domain, or a new address
The certificate and the host names nginx answers are set for the address the instance had on first boot. If you stop and start an instance that has no Elastic IP, its public address changes; run fava-set-domain with the new address. To use a domain, point an A record (for example ledger.example.com) at the instance's public IP address, then:
sudo fava-set-domain <your-domain>
This sets the name nginx accepts, generates a new self signed certificate for it and updates FAVA_URL, FAVA_HOST and FAVA_TLS_SHA256 in the credentials file. Add more names or addresses after the first to serve them too (sudo fava-set-domain <your-domain> <public-ip>).
For a certificate your browser trusts, put the certificate chain for <your-domain> in /etc/ssl/fava/fava.crt and its private key in /etc/ssl/fava/fava.key (root only, mode 600) and run sudo systemctl reload nginx, or put the instance behind an Application Load Balancer with an AWS Certificate Manager certificate that forwards to port 443 on the instance.
Step 10: Outbound connections and prices
Fava, nginx and the first boot service are refused any connection to the instance metadata service, and the fava and www-data accounts are refused link-local, private, carrier grade NAT, reserved and multicast addresses and your VPC's own address ranges (read from instance metadata on every boot), and all IPv6 except localhost. Fava itself needs no connection to anything: it reads and writes only its ledger. The trade-off is that a Beancount plugin or importer you add cannot reach a service inside your VPC unless you allow its network.
The allow list is empty by default:
sed -n '1,5p' /etc/cloudimg/fava-egress-allow
# cloudimg Fava egress allow-list (empty by default).
# Fava and nginx never need an internal address. If a Beancount plugin or importer you add must reach a host inside
# your VPC or a private network, list its network here, one CIDR per line - it must lie inside 10.0.0.0/8,
# 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 or fc00::/7 (metadata and link-local addresses can never be allowed) -
# then run: sudo systemctl reload cloudimg-fava-egress
To allow one, add its CIDR as a line with sudo nano /etc/cloudimg/fava-egress-allow and run sudo systemctl reload cloudimg-fava-egress. The instance metadata service can never be allowed.
Prices. bean-price, Beancount's tool that downloads prices from online sources, is not installed. Record prices in your ledger as price entries (for example 2026-11-02 price EXMPL 62.18 USD, as the demo does), or fetch them on your own computer and copy the lines in.
Step 11: Security checks
The password guards every page and the API; the API refuses a request another site's page sends; the health check needs no password; a host name the instance does not serve gets no answer; plain HTTP redirects; and other local accounts cannot reach Fava directly:
H=$(sudo sed -n 's/^FAVA_HOST=//p' /root/fava-credentials.txt); WRONG="admin:not-the-password"
curl -sk -o /dev/null -w 'no password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Sec-Fetch-Site: same-origin' "https://$H/cloudimg-demo-ledger/income_statement/"
curl -sk -o /dev/null -w 'API without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Sec-Fetch-Site: same-origin' "https://$H/cloudimg-demo-ledger/api/changed"
curl -sk -o /dev/null -w 'wrong password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Sec-Fetch-Site: same-origin' -u "$WRONG" "https://$H/"
curl -sk -o /dev/null -w 'API called from another site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Sec-Fetch-Site: cross-site' "https://$H/cloudimg-demo-ledger/api/changed"
curl -sk -o /dev/null -w 'health check: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/healthz"
curl -sk -o /dev/null -w 'unknown host name: %{http_code}\n' --resolve evil.example:443:127.0.0.1 https://evil.example/ || true
curl -s -o /dev/null -w 'http: %{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -s -o /dev/null -w 'Fava directly, as ubuntu: %{http_code}\n' http://127.0.0.1:5000/ || true
no password: 401
API without a password: 401
wrong password: 401
API called from another site: 403
health check: 200
unknown host name: 000
http: 301
Fava directly, as ubuntu: 000
Fava and nginx are blocked from the instance metadata service and reserved addresses:
for u in fava nginx; do printf '%s ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
sudo -u fava curl -s -m 5 -o /dev/null -w 'metadata from the fava account: %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true
fava 168.63.129.16/32 169.254.169.254/32 192.0.0.0/24 198.18.0.0/15 2001::/32 2002::/16 224.0.0.0/4 240.0.0.0/4 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
nginx 168.63.129.16/32 169.254.169.254/32 192.0.0.0/24 198.18.0.0/15 2001::/32 2002::/16 224.0.0.0/4 240.0.0.0/4 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
metadata from the fava account: 000
The private ranges and your VPC are refused by the egress backstop, which reads the VPC's ranges on every boot:
sudo iptables -S CLOUDIMG-FAVA-EGRESS | sed -n '1,4p'
cat /run/cloudimg/fava-vpc-cidrs
-N CLOUDIMG-FAVA-EGRESS
-A CLOUDIMG-FAVA-EGRESS -d 127.0.0.0/8 -o lo -j RETURN
-A CLOUDIMG-FAVA-EGRESS -d 169.254.0.0/16 -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-FAVA-EGRESS -d 169.254.0.0/16 -j REJECT --reject-with icmp-admin-prohibited
<your VPC CIDR, for example 172.31.0.0/16>
Fava runs as fava, on localhost, with no Linux capabilities, and cannot gain any:
P=$(systemctl show -p MainPID --value fava)
ps -o user=,args= -p "$P"
sudo grep -E '^(CapEff|CapBnd|CapAmb|NoNewPrivs)' /proc/"$P"/status
fava /opt/fava/venv/bin/python /opt/fava/venv/bin/fava --host 127.0.0.1 --port 5000
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
NoNewPrivs: 1
The credentials, the password hash, the TLS key and the ledger are readable by root, nginx (the hash) and the fava user (the ledger) only:
sudo stat -c '%a %U:%G %n' /root/fava-credentials.txt /etc/nginx/cloudimg/fava.htpasswd /etc/ssl/fava/fava.key /var/lib/fava/ledger /var/lib/fava/ledger/main.beancount /etc/cloudimg/fava-ledger.env
600 root:root /root/fava-credentials.txt
640 root:www-data /etc/nginx/cloudimg/fava.htpasswd
600 root:root /etc/ssl/fava/fava.key
750 fava:fava /var/lib/fava/ledger
640 fava:fava /var/lib/fava/ledger/main.beancount
644 root:root /etc/cloudimg/fava-ledger.env
Other notes:
- Rate limits. Fava's pages make several API calls each, and the burst of 60 covers normal use. Scripts that call the API in a tight loop from one address get
429; slow them to 10 requests a second. - Client addresses. nginx replaces any
X-Forwarded-FororX-Real-IPa client sends with the address it connected from, and keeps no query strings in its access log. - Response headers. nginx sends HSTS, a content security policy,
X-Content-Type-Options: nosniffandX-Frame-Options: DENY, and no version number. - NVMe host identity. No NVMe-oF host identity ships in the image. If you install
nvme-cli, its host NQN and host ID are your instance's own. - Who can reach it. Keep ports 22 and 443 open only to your own networks.
- Logs. nginx logs are in
/var/log/nginx; Fava logs to the journal (journalctl -u fava).
Step 12: Back up your ledger
Everything Fava keeps is in /var/lib/fava/ledger: your plain text ledger files and the documents folder. Copy it to your computer:
ssh -i <your-key.pem> ubuntu@<public-ip> 'sudo tar -C /var/lib/fava -czf - ledger' > fava-ledger-backup.tar.gz
A plain text ledger also works well in git: commit it on your computer and copy it in (Step 7) after each change, or keep the copy on the instance under version control. An EBS snapshot of the instance's volume is also a complete backup.
Step 13: Keep it up to date
To apply Ubuntu updates now:
sudo apt-get update && sudo apt-get upgrade -y
New Fava releases arrive as new versions of this listing, built and checked the same way. To move to one, back up your ledger (Step 12), launch the new version, copy the ledger in and switch to it (Step 7). Read the Fava changelog before you move.
Support and licensing
Fava is open source under the MIT licence; Beancount, beanquery and beangulp are open source under the GNU General Public License version 2. They are installed unmodified; their licence texts, the source packages they were installed from and the licence of every other Python package (all permissive) are in /usr/share/doc/fava:
ls /usr/share/doc/fava
COPYING.beancount
COPYING.beangulp
COPYING.beanquery
LICENSE.fava
README.cloudimg
dependency-licences.txt
requirements.lock
source
The source code is at github.com/beancount/fava and github.com/beancount/beancount. Fava and Beancount are not endorsed by or affiliated with cloudimg. There is no per user licence fee. The AMI price covers cloudimg's build, hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA
The other versions on this listing are on the accounting, HR and finance guide.