B0
Application Infrastructure AWS

Blnk 0.15.6 on AWS User Guide

| Product: Blnk 0.15.6 on Ubuntu 24.04 on AWS

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.1 only (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 /health needs the master key in the X-Blnk-Key header; a request without it, or with a wrong key, gets HTTP 401. The key, the /metrics bearer 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.json holds 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.1 only 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 by openssl rand -hex 32), put it in BLNK_SERVER_SECRET_KEY in /etc/blnk/blnk.env and in api.secret_key in /root/blnk-credentials.txt, then restart blnk-server and blnk-worker. Update your applications at the same time.
  • Database or Redis passwords: change the password in PostgreSQL (ALTER ROLE blnk WITH PASSWORD ... as the postgres user) or Redis (requirepass in /etc/redis/redis.conf), then the matching BLNK_DATA_SOURCE_DNS or BLNK_REDIS_DNS in /etc/blnk/blnk.env and 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