Fava for Beancount 1.30.16
Overview
This guide covers launching and using the Fava for Beancount 1.30.16 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud 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 that you can read, diff and keep in version control.
Fava is the web interface for Beancount. It gives you an income statement, a balance sheet and a trial balance with charts, plus net worth, holdings and commodities. You also get an interactive journal that you can filter by time, account, tag and payee, and a query editor for Beancount's SQL-like query language (BQL). A source editor with syntax checking and documents attached to accounts complete the set.
Fava has no login of its own, so anyone who can reach it can read and edit the ledger. This image makes it safe to run on EC2.
What you get:
- 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 the password at any time with
fava-set-password. - Rate limiting 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. This 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). It runs as its own unprivilegedfavauser with no Linux capabilities, and it 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. Every change must come from Fava's own pages.
- No outbound reach inside AWS. On every boot, the kernel refuses Fava and nginx any connection to the EC2 instance metadata service. The same applies to link-local, private (RFC 1918), carrier-grade NAT and reserved addresses, and to 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.
The image contains:
- Fava 1.30.16 with Beancount 3.2.3, beanquery 0.2.0 and beangulp 0.2.0, in
/opt/fava/venv - The command-line tools
bean-check,bean-query,bean-format,bean-doctorandbean-example - The ledger directory
/var/lib/fava/ledger - nginx
- The tools
fava-set-ledger,fava-set-password,fava-set-domainandfava-selftest - Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent
Defaults:
- 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. Also allow TCP 80, which redirects to HTTPS, if you want plain
http://links to work. - Restricted access to port 443. Your ledger is your financial history, so open 443 only to the addresses you use, not to the whole internet, unless you need to.
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, choose your Region, and select Launch through EC2.
Choose m5.large, your key pair, a subnet and a security group as above. A personal ledger needs very little, so a smaller instance such as t3.small is enough for most. 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}]'Connect to your instance
Connect over SSH. On Fava for Beancount 1.30.16 on Ubuntu 24.04, the SSH user is ubuntu.
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot reads the instance's public address from EC2 instance metadata. It generates the password and the certificate and writes the credentials file. The egress backstop then loads, and Fava and nginx start:
systemctl is-active sac-fava-egress fava nginx
sudo test -f /var/lib/sac/data/fava-firstboot.done && echo FIRSTBOOT_DONEAll three units are active, and the first-boot marker is present:
active active active FIRSTBOOT_DONEOnly 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 -u0.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 nginxGet your URL and password
sudo cat /root/fava-credentials.txtOnly root can read the file. It holds the URL, the user name, the password and the certificate's fingerprint:
# Fava 1.30.16 (sac) 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://secureanycloud.com/stacks/fava-for-beancount-1-30-16/ FAVA_PASSWORD=<FAVA_PASSWORD>FAVA_ADDRESS_SOURCE says where the address came from. It is the public IPv4 address from instance metadata, or the private address when the instance has no public one.
The image includes a self-test. It runs through nginx and is pinned to this instance's certificate. It checks that six paths, including the JSON API, refuse a request with no password and a request with a wrong password. It also checks that the password opens the income statement and its API:
sudo fava-selftestfava-selftest: 6 paths x (no credential, wrong password) -> 401; the password opens /sac-demo-ledger/income_statement/ and its API FAVA_SELFTEST_OKSign in
Open FAVA_URL in your browser. The certificate is self-signed for your instance, so the browser warns once. To check that you are talking to your instance, open the certificate details and compare its SHA-256 fingerprint with FAVA_TLS_SHA256. Then continue.
The browser 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. The bar at the top filters every page by time (for example 2026 or month), account, and tag or payee:
Query, edit and attach documents
Query runs Beancount's query language over the ledger and charts the result. You can download every result as CSV:
SELECT account, sum(position) AS total WHERE account ~ '^Expenses' GROUP BY account ORDER BY accountEditor 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, such as a receipt or a statement, 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. Fava refuses files over 16 MiB.
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 errorsbean-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}Serve your own ledger
The demo ledger is /var/lib/fava/ledger/main.beancount. Fava reads the file named in /etc/sac/fava-ledger.env:
cat /etc/sac/fava-ledger.env# sac: the ledger Fava serves (Fava reads BEANCOUNT_FILE). Change it with:
sudo fava-set-ledger FILE BEANCOUNT_FILE=/var/lib/fava/ledger/main.beancountFava may read and write only /var/lib/fava/ledger. 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-ledgerfirst checks the file withbean-checkas thefavauser. It keeps serving the previous ledger if it finds errors:
sudo fava-set-ledger /var/lib/fava/ledger/<your-ledger-folder>/<your-ledger>.beancountfava-set-ledger: Fava now serves /var/lib/fava/ledger/<your-ledger-folder>/<your-ledger>.beancountFava's address for a ledger comes from its option "title" line. For the demo, it is /sac-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 in the same directory. It needs an option "title" "<your title>" line and your open directives. Set its owner to fava and switch to it the same way.
Change the password
To generate a new random password, which is written to the credentials file as FAVA_PASSWORD:
sudo fava-set-passwordTo choose your own password, use 16 characters or more. The command reads it from standard input and does not store it on the instance:
sudo fava-set-password --stdinThe new password takes effect at once, and your browser asks for it again. nginx stores the password as a bcrypt hash in /etc/nginx/sac/fava.htpasswd.
Use your own domain, or a new address
The certificate and the host names that 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 run:
sudo fava-set-domain <your-domain>The command sets the name that nginx accepts and generates a new self-signed certificate for it. It also updates FAVA_URL, FAVA_HOST and FAVA_TLS_SHA256 in the credentials file. To serve more names or addresses, add them after the first (sudo fava-set-domain <your-domain> <public-ip>).
For a certificate that 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). Then run sudo systemctl reload nginx.
Alternatively, put the instance behind an Application Load Balancer with an AWS Certificate Manager certificate that forwards to port 443 on the instance.
Outbound connections and prices
Fava, nginx and the first-boot service are refused any connection to the instance metadata service. 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, which are read from instance metadata on every boot. They are also refused 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/sac/fava-egress-allow# sac 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 sac-fava-egressTo allow a network, add its CIDR as a line with sudo nano /etc/sac/fava-egress-allow. Then run sudo systemctl reload sac-fava-egress. You can never allow the instance metadata service.
Prices. bean-price is Beancount's tool that downloads prices from online sources, and it 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. Alternatively, fetch them on your own computer and copy the lines in.
Security checks
The password guards every page and the API. The API refuses a request that 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/sac-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/sac-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/sac-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/ || trueno 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: 000Fava 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/ || truefava 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: 000The egress backstop refuses the private ranges and your VPC. It reads the VPC's ranges on every boot:
sudo iptables -S SAC-FAVA-EGRESS | sed -n '1,4p' cat /run/sac/fava-vpc-cidrs-N SAC-FAVA-EGRESS -A SAC-FAVA-EGRESS -d 127.0.0.0/8 -o lo -j RETURN -A SAC-FAVA-EGRESS -d 169.254.0.0/16 -p tcp -j REJECT --reject-with tcp-reset -A SAC-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"/statusfava /opt/fava/venv/bin/python /opt/fava/venv/bin/fava --host 127.0.0.1 --port 5000 CapEff: 0000000000000000 CapBnd: 0000000000000000 CapAmb: 0000000000000000 NoNewPrivs: 1Only root can read the credentials, the password hash, the TLS key and the ledger. In addition, nginx can read the hash and the fava user can read the ledger:
sudo stat -c '%a %U:%G %n' /root/fava-credentials.txt /etc/nginx/sac/fava.htpasswd /etc/ssl/fava/fava.key /var/lib/fava/ledger /var/lib/fava/ledger/main.beancount /etc/sac/fava-ledger.env600 root:root /root/fava-credentials.txt 640 root:www-data /etc/nginx/sac/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/sac/fava-ledger.envOther 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).
Back up your ledger
Everything Fava keeps is in /var/lib/fava/ledger. That is 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.gzA 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.
Keep it up to date
To apply Ubuntu updates now:
sudo apt-get update && sudo apt-get upgrade -yNew 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) and launch the new version. Then 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 license. Beancount, beanquery and beangulp are open source under the GNU General Public License version 2. They are installed unmodified.
Their license texts, the source packages they were installed from, and the license of every other Python package (all permissive) are in /usr/share/doc/fava:
ls /usr/share/doc/favaCOPYING.beancount COPYING.beangulp COPYING.beanquery LICENSE.fava README.sac dependency-licences.txt requirements.lock sourceThe source code is at github.com/beancount/fava and github.com/beancount/beancount. Fava and Beancount are not endorsed by or affiliated with SecureAnyCloud. There is no per-user license fee. The AMI price covers SecureAnyCloud's build, hardening, first-boot setup and support.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
- Support: 24/7, with a 24-hour response SLA
The other versions on this listing are on the accounting, HR and finance guide.
Deploy on AWS
Launch Fava for Beancount 1.30.16 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com