Blnk 0.15.6 on AWS User Guide
Overview
This guide covers launching and using the Blnk 0.15.6 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.
Blnk is an open source double entry ledger and financial core. It is an HTTP JSON API: your application creates ledgers and balances, records money movements as transactions between balances, and reads balances back. Every transaction debits one balance and credits another by the same amount, so the ledger always nets to zero. Blnk also supports multi currency balances, exact integer amounts with a declared precision, inflight (authorise then commit or void) transactions, balance snapshots, reconciliation, identities with field tokenization, webhooks and pre and post transaction hooks. Transactions are queued in Redis and applied to PostgreSQL by a worker, so the API answers fast under load.
Blnk has no web interface. Everything in this guide uses curl; point your own application or an HTTP client at the API.
What the cloudimg image adds:
- HTTPS from the first boot. nginx serves the API on port 443 with a certificate generated on your instance, port 80 redirects to HTTPS, and requests for a host name the instance does not serve are dropped. Upstream Blnk serves plain HTTP on port 5001 on every interface; on this image Blnk listens on
127.0.0.1only (the one change cloudimg makes to the upstream source, shipped as/usr/share/doc/blnk/CLOUDIMG-CHANGES.patch), so the master key never crosses the network in clear text. - Secure mode with a key unique to your instance. Every API route except
/healthneeds the master key in theX-Blnk-Keyheader; a request without it, or with a wrong key, gets HTTP 401. The key, the/metricsbearer token, a 32 byte tokenization secret, the PostgreSQL passwords and the Redis password are all generated on first boot. None is in the image. - Secrets in root only files. The running services read their secrets from
/etc/blnk/blnk.env(mode 0600, loaded by systemd), never from a world readable file or a command line, and you read them from/root/blnk-credentials.txt(mode 0600)./etc/blnk/blnk.jsonholds only non secret settings. - Closed until first boot has finished. The Blnk services start only after first boot has written the secrets and run the database migrations, and nginx starts only after that. Before then nothing but SSH listens.
- Hardened defaults. PostgreSQL 16 and Redis listen on
127.0.0.1only and require their passwords. Every service (Blnk, PostgreSQL, Redis, nginx and the first boot service) is blocked from the instance metadata service; the Blnk services, which deliver webhooks and hooks to URLs an API caller registers, are also blocked from the whole link local range. The AMI requires IMDSv2. nginx passes Blnk the client address from the connection and the configured host name, never values a client sends. Blnk's anonymous usage telemetry is off, and its import from URL feature allows no hosts. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Blnk 0.15.6 built from the upstream v0.15.6 source tag (commit 36ecff3e) with Go 1.26.9, PostgreSQL 16 (PGDG), Redis 7, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The Go toolchain and the source are removed after the build. The optional Typesense search engine is not included; the ledger works fully without it.
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 a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (SSH) from your admin network, and TCP 443 (the API) and TCP 80 (redirects to HTTPS) from the networks your applications call from. Never open 5001, 5004, 5432 or 6379; nothing listens on them off the instance anyway.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Blnk 0.15.6 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above. The ledger database lives on the root volume, so for production give the root volume room to grow (20 GiB or more).
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> \
--block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=20,VolumeType=gp3}' \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=blnk}]'
First boot takes well under a minute after the instance is running.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Blnk 0.15.6 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot sets the secrets, runs the database migrations and the certificate, then starts Blnk and nginx:
systemctl is-active postgresql redis-server blnk-server blnk-worker nginx blnk-firstboot
sudo test -f /var/lib/cloudimg/blnk-firstboot.done && echo FIRSTBOOT_DONE
All six units are active and the first boot marker is present:
active
active
active
active
active
active
FIRSTBOOT_DONE
blnk-server is the API and blnk-worker applies queued transactions and delivers webhooks. Only SSH and nginx listen on network ports; Blnk (5001 API, 5004 worker monitor), PostgreSQL and Redis listen on the loopback address:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | grep -v ':53 ' | sort
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
127.0.0.1:5001 users:(("blnk"))
127.0.0.1:5004 users:(("blnk"))
127.0.0.1:5432 users:(("postgres"))
127.0.0.1:6379 users:(("redis-server"))
[::1]:6379 users:(("redis-server"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
Step 4: Get your URL and master key
sudo cat /root/blnk-credentials.txt
The file is readable by root only (values shown here as placeholders):
# Blnk 0.15.6 on Ubuntu 24.04 - generated on first boot by blnk-firstboot.service.
# Every value is UNIQUE to this instance. Back up api.secret_key and tokenization.secret: data tokenized with
# the tokenization secret cannot be detokenized without it.
# --- The Blnk API: HTTPS through nginx; send the key in the X-Blnk-Key header ---
api.base=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com
api.secret_key=<BLNK_SECRET_KEY>
metrics.bearer_token=<metrics-token>
tokenization.secret=<tokenization-secret>
# --- PostgreSQL 16 (127.0.0.1:5432 only) ---
postgres.password=<postgres-password>
blnk_db.role=blnk
blnk_db.database=blnk
blnk_db.password=<blnk-db-password>
# --- Redis 7 (127.0.0.1:6379 only) ---
redis.password=<redis-password>
# The running services read these from /etc/blnk/blnk.env (0600 root). Change a value in BOTH places, then
# restart blnk-server and blnk-worker. Verify the instance any time: sudo blnk-selfcheck
api.base uses the instance's public DNS name from the EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip> works too. If you stop and start the instance without an Elastic IP, the boot service picks up the new address and issues a new certificate for it. To serve the API under your own DNS name, see Step 7.
Store api.secret_key in your application's secret store. Your application sends it on every request as X-Blnk-Key: <api.secret_key>. Blnk can also issue scoped API keys for individual services through its /api-keys endpoint; see the Blnk documentation.
Step 5: Call the API
From your workstation the calls look like this (the certificate is self signed until you install your own, so pass -k or, better, download /etc/ssl/blnk/blnk.crt and pass --cacert):
curl -k https://<api-host>/health
curl -k -H 'X-Blnk-Key: <api.secret_key>' https://<api-host>/ledgers
On the instance itself you can run the same calls against nginx. /health is open, a request without the key or with a wrong key is refused with 401, and the master key works:
B=$(sudo sed -n 's/^api\.base=//p' /root/blnk-credentials.txt); H=${B#https://}
C=(--cacert /etc/ssl/blnk/blnk.crt --resolve "$H:443:127.0.0.1" -s -o /dev/null -w '%{http_code}\n')
curl "${C[@]}" "$B/health"
curl "${C[@]}" "$B/ledgers"
curl "${C[@]}" -H 'X-Blnk-Key: not-the-key' "$B/ledgers"
curl "${C[@]}" -H "X-Blnk-Key: $(sudo sed -n 's/^api\.secret_key=//p' /root/blnk-credentials.txt)" "$B/ledgers"
200
401
401
200
Now record a deposit. This creates a ledger, a funding balance and a customer balance, then moves USD 250.75 from the funding balance to the customer. Amounts are sent with a precision (100 for cents) and stored as exact integers (25075). The transaction is queued, the worker applies it a moment later, and the balances read back as a debit on one side and a credit of the same amount on the other:
B=$(sudo sed -n 's/^api\.base=//p' /root/blnk-credentials.txt); H=${B#https://}
KEY=$(sudo sed -n 's/^api\.secret_key=//p' /root/blnk-credentials.txt)
api() { curl -s --cacert /etc/ssl/blnk/blnk.crt --resolve "$H:443:127.0.0.1" -H "X-Blnk-Key: $KEY" -H 'Content-Type: application/json' "$@"; }
LEDGER=$(api -X POST "$B/ledgers" -d '{"name":"Customer wallets"}' | jq -r .ledger_id)
FUNDING=$(api -X POST "$B/balances" -d "{\"ledger_id\":\"$LEDGER\",\"currency\":\"USD\",\"meta_data\":{\"name\":\"funding\"}}" | jq -r .balance_id)
ALICE=$(api -X POST "$B/balances" -d "{\"ledger_id\":\"$LEDGER\",\"currency\":\"USD\",\"meta_data\":{\"name\":\"alice\"}}" | jq -r .balance_id)
api -X POST "$B/transactions" -d "{\"amount\":250.75,\"precision\":100,\"currency\":\"USD\",\"source\":\"$FUNDING\",\"destination\":\"$ALICE\",\"allow_overdraft\":true,\"reference\":\"deposit-$(date +%s)\",\"description\":\"First deposit\"}" | jq '{amount, precise_amount, currency, status}'
sleep 3
api "$B/balances/$ALICE" | jq '{currency, balance, credit_balance, debit_balance}'
api "$B/balances/$FUNDING" | jq '{currency, balance, credit_balance, debit_balance}'
{
"amount": 250.75,
"precise_amount": 25075,
"currency": "USD",
"status": "QUEUED"
}
{
"currency": "USD",
"balance": 25075,
"credit_balance": 25075,
"debit_balance": 0
}
{
"currency": "USD",
"balance": -25075,
"credit_balance": 0,
"debit_balance": 25075
}
allow_overdraft lets the funding balance go negative; leave it out for customer balances so Blnk rejects a transfer that would overdraw them. The full API (inflight transactions, bulk transactions, balance monitors, reconciliation, identities, hooks) is described in the Blnk documentation at docs.blnkfinance.com.
Step 6: Verify the instance
blnk-selfcheck checks the network exposure and the authentication and runs a real double entry round trip: it creates a ledger named cloudimg-selfcheck-<time> with three balances, funds one from an overdraft balance, transfers part of it on, and checks that every balance is right and the sum is zero. Each run leaves that small ledger behind, so run it on a new instance or when you need the proof:
sudo blnk-selfcheck
self-check OK: public TCP {22, 80, 443}; Blnk, PostgreSQL and Redis on 127.0.0.1 only; Redis refuses unauthenticated clients; anonymous and wrong-key writes refused 401 over HTTPS and on the loopback API; with the per-instance key ledger ldg_2c959bb5-2cb3-420d-bf60-37471b921cec funded alice USD 1000 from an overdraft world balance and moved USD 250 alice->bob (world -100000, alice 75000, bob 25000; sum 0)
Step 7: Your own DNS name and certificate
Point a DNS record at the instance (an Elastic IP keeps the address stable), then set the name the API is served as. This mints a new self signed certificate for the name, updates nginx and api.base in /root/blnk-credentials.txt, and keeps the name across reboots:
sudo blnk-set-hostname <your-domain>
To use a certificate from a public or internal certificate authority instead, copy it over the generated pair and reload nginx (running blnk-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/ssl/blnk/blnk.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/blnk/blnk.key
sudo nginx -t && sudo systemctl reload nginx
Step 8: Security checks
Plain HTTP redirects to HTTPS on the configured name, and a request for any other host name is dropped without an answer (nginx closes the connection, shown as 000):
B=$(sudo sed -n 's/^api\.base=//p' /root/blnk-credentials.txt); H=${B#https://}
curl -s -o /dev/null -w 'http:// -> %{http_code} %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/ledgers"
curl -s -o /dev/null -w 'unknown host -> %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
http:// -> 301 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/ledgers
unknown host -> 000
The certificate was generated on this instance and names its own addresses:
B=$(sudo sed -n 's/^api\.base=//p' /root/blnk-credentials.txt); H=${B#https://}
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-<public-ip-dashed>.compute-1.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost
None of the services can reach the instance metadata service. The Blnk services and the first boot service are also blocked from the whole IPv4 and IPv6 link local ranges (systemd shows 169.254.169.254/32 folded into 169.254.0.0/16), so a webhook or hook URL can never reach instance metadata:
systemctl show blnk-server blnk-worker postgresql@16-main redis-server nginx blnk-firstboot -p Id -p IPAddressDeny
IPAddressDeny=fd00:ec2::254/128 fe80::/10 169.254.0.0/16 168.63.129.16/32
Id=blnk-server.service
IPAddressDeny=fd00:ec2::254/128 fe80::/10 168.63.129.16/32 169.254.0.0/16
Id=blnk-worker.service
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
Id=postgresql@16-main.service
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
Id=redis-server.service
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
Id=nginx.service
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 fe80::/10 169.254.0.0/16
Id=blnk-firstboot.service
Webhooks and hooks can still reach addresses in your VPC, which is usually what you want. To stop the Blnk services reaching private ranges too, add them in a drop-in, for example IPAddressDeny=10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 in /etc/systemd/system/blnk-worker.service.d/60-private.conf (and the same for blnk-server), then sudo systemctl daemon-reload and restart both services.
The secrets are only in root only files, and the Blnk settings file holds none:
sudo stat -c '%A %U %G %n' /root/blnk-credentials.txt /etc/blnk/blnk.env /etc/blnk/blnk.json
sudo cat /etc/blnk/blnk.json
-rw------- root root /root/blnk-credentials.txt
-rw------- root root /etc/blnk/blnk.env
-rw-r----- root blnk /etc/blnk/blnk.json
{
"project_name": "cloudimg Blnk",
"backup_dir": "/var/lib/blnk/backups",
"enable_telemetry": false,
"enable_observability": false,
"server": {
"port": "5001",
"secure": true,
"ssl": false,
"upload_whitelist": ""
},
"queue": { "monitoring_port": "5004" }
}
nginx hands Blnk the configured host name and the client address from the connection, never values a client sends:
grep -E 'proxy_pass|proxy_set_header (Host|X-Forwarded-For|X-Real-IP|X-Forwarded-Host) ' /etc/nginx/sites-available/cloudimg-blnk
proxy_pass http://127.0.0.1:5001;
proxy_set_header Host $blnk_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host "";
The build records exactly what is on the image, and the one source change is shipped next to the licence:
cat /usr/share/doc/blnk/BUILD_VERSIONS
grep -c '^+.*127.0.0.1' /usr/share/doc/blnk/CLOUDIMG-CHANGES.patch
blnk=v0.15.6
blnk-commit=36ecff3ec7f480c38689ad4e6fedc4b9f172a6ff
go-toolchain=go1.26.9
postgresql-16=16.15-1.pgdg24.04+2
redis-server=5:7.0.15-1ubuntu0.24.04.5
nginx=1.24.0-2ubuntu7.18
3
Step 9: Changing secrets
- Master key: generate a new value (for example
blnk_followed byopenssl rand -hex 32), put it inBLNK_SERVER_SECRET_KEYin/etc/blnk/blnk.envand inapi.secret_keyin/root/blnk-credentials.txt, then restartblnk-serverandblnk-worker. Update your applications at the same time. - Database or Redis passwords: change the password in PostgreSQL (
ALTER ROLE blnk WITH PASSWORD ...as thepostgresuser) or Redis (requirepassin/etc/redis/redis.conf), then the matchingBLNK_DATA_SOURCE_DNSorBLNK_REDIS_DNSin/etc/blnk/blnk.envand the credentials file, and restart the services. - Tokenization secret: do not change it once identities hold tokenized fields; they can only be detokenized with the secret that tokenized them. Back it up with the master key.
Step 10: Backup and maintenance
The ledger lives in the PostgreSQL database blnk (/var/lib/postgresql/16/main). Redis only holds the queue of transactions waiting for the worker. The simplest backup is an EBS snapshot of the root volume. For a consistent logical backup while Blnk is running:
sudo -u postgres pg_dump -Fc blnk > <backup-dir>/blnk-$(date +%F).dump
Blnk's own GET /backup endpoint (master key required) writes a dump to /var/lib/blnk/backups. Operating system security updates install automatically; run sudo apt-get update && sudo apt-get upgrade to apply everything else. PostgreSQL, Redis, Blnk and nginx restart cleanly and keep all of their data.
| Component | Path |
|---|---|
| Blnk binary | /usr/local/bin/blnk |
| Settings (no secrets) | /etc/blnk/blnk.json (mode 0640) |
| Secrets for the services | /etc/blnk/blnk.env (mode 0600) |
| Services | blnk-server.service, blnk-worker.service |
| Database | PostgreSQL 16 database blnk (/var/lib/postgresql/16/main) |
| nginx site | /etc/nginx/sites-available/cloudimg-blnk |
| Certificate and key | /etc/ssl/blnk/ |
| Boot service | /usr/local/sbin/blnk-firstboot.sh |
| Host name helper | /usr/local/sbin/blnk-set-hostname |
| Self check | /usr/local/sbin/blnk-selfcheck |
| Credentials | /root/blnk-credentials.txt (mode 0600) |
| Licence and source change | /usr/share/doc/blnk/ |
Step 11: Security recommendations
- Allow port 22 from your admin network only, and ports 80 and 443 only from the networks your applications call from.
- Use your own DNS name and a certificate your clients trust (Step 7), and pin it in your applications.
- Give each application its own scoped Blnk API key rather than sharing the master key, and rotate the master key if it may have leaked (Step 9).
- Keep the operating system patched, and snapshot the root volume regularly.
Support and licensing
Blnk is open source under the Apache License 2.0; there is no per user or per CPU licence fee. cloudimg builds it from the upstream source with one change (the API and worker monitor listen on 127.0.0.1), shipped as /usr/share/doc/blnk/CLOUDIMG-CHANGES.patch next to the licence. The AMI price covers cloudimg's hardening, first boot setup and support.
Blnk is a trademark of Blnk Finance. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by Blnk Finance.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA