Deployment guide

rqlite

Overview

rqlite is a lightweight, open source distributed relational database that uses SQLite as its storage engine and the Raft consensus protocol to replicate it. This guide covers deploying and using rqlite 10.6.4 on Ubuntu 24.04 LTS from SecureAnyCloud on Microsoft Azure.

You work with rqlite over a simple HTTP API (SQL in, JSON out), through its built-in web console or with the rqlite shell. It runs well as a single node, as this image ships it, and grows into a fault-tolerant cluster when you need one.

The version is rqlite 10.6.4, a security release. It fixes GHSA-f4f3-px5g-gm4x (critical: an unauthorized request on the Raft port could download a backup of the database). It also includes the fix for GHSA-j62w-8865-2h7x (high: ATTACH DATABASE through the query endpoint) from 10.6.3. Earlier versions of this image shipped rqlite 10.2.7, which both advisories affect.

What you get:

  • No default login, no anonymous access. Started without an authentication file, rqlite has no authentication at all. This image always starts it with one. First boot creates the only rqlite user, admin, with a 32-character password generated on your VM, and nothing is granted to anonymous requests. Every API path and the web console answer an unauthenticated or wrong-password request with 401. The password is written to the root-only file /root/rqlite-credentials.txt, never to a command line, a log or the journal.
  • HTTPS only, on localhost. The HTTP API and the web console are served over TLS with a certificate generated on your VM at first boot. There is no plaintext listener. Both the API (127.0.0.1:4001) and Raft (127.0.0.1:4002) listen on the loopback address only, so from the network only SSH (22) is reachable. You reach rqlite through an SSH tunnel (Step 8), or open it to your own network when you choose to (Step 10).
  • Built from source and scanned. rqlite 10.6.4 is compiled on the image from the unmodified upstream source at the v10.6.4 tag with Go 1.27.3, with every dependency exactly as upstream pins it. It is checked with govulncheck at build time, which finds no known vulnerabilities in the code it links. The build provenance, the scan report and the license of every linked component ship on the image.
  • Hardened service. rqlite runs as the unprivileged rqlite user with no capabilities, a read-only view of the system and no access to /root or the Azure agent's files. It and its first boot service are blocked from the Azure Instance Metadata Service and the WireServer, because rqlite's SQL engine, change-data-capture and backup clients can be pointed at an address. The Azure Linux Agent and cloud-init keep their access. A start-up check refuses to start rqlite before first boot, without its data disk or TLS, or with an authentication file that would allow anonymous access. The automatic firmware-metadata refresh in fwupd is switched off, because Azure VMs have no firmware it can update.
  • Your data on its own disk. The data directory (/var/lib/rqlite, holding the SQLite database, the Raft log and snapshots) is on a dedicated 32 GiB Azure data disk that you can grow or snapshot on its own. The image ships it empty, so each VM starts its own new cluster.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: rqlite 10.6.4 (rqlited, the server, runs as rqlited.service; rqlite, the shell), the helpers rqlite-query (runs SQL and API calls with your credentials) and rqlite-rotate-password (sets a new password), Ubuntu 24.04 LTS, cloud-init and the Azure Linux Agent. The database is empty, ready for your tables.

Default user: the administrator you choose at deployment (azureuser in the examples). Architecture: x64, Gen2. Recommended size: Standard_B2s (2 vCPU, 4 GiB) or larger.

What this is not

This is a single-node rqlite. A single node is a complete database, and reads and writes go through Raft and SQLite as usual. It is not fault tolerant, so protect it with disk snapshots and backups (Step 14), and see Step 13 to grow it into a cluster.

rqlite replicates every write to a majority of nodes, which suits read-heavy workloads and moderate write rates. It is not a drop-in replacement for a large OLTP server.

Prerequisites

  • An Azure subscription, an SSH key pair, and a virtual network and subnet in your region
  • A network security group (NSG) that allows TCP 22 (SSH) from your admin network. rqlite listens on the loopback address only, so no database port needs to be open. You reach it through an SSH tunnel.
1

Deploy from the Azure Marketplace

Sign in to the Azure portal, choose Create a resource, search the Marketplace for rqlite by SecureAnyCloud and select Create.

On Basics, pick your subscription, resource group, region and size (Standard_B2s or larger). Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) only.

The Disks tab shows the image's 32 GiB data disk (LUN 0) for /var/lib/rqlite. Make it as large as your data needs. Then choose Review + create, then Create. First boot takes a few seconds once the VM is running.

2

Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deployment)
az vm image terms accept --urn sac1647283583153:rqlite-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name rqlite \
  --image sac1647283583153:rqlite-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm create opens SSH (22) and nothing else. To make the data disk larger, resize it while the VM is deallocated. Then grow the file system once the VM is running again:

az vm deallocate --resource-group <your-rg> --name rqlite
az disk update --resource-group <your-rg> --name <data-disk-name> --size-gb 128
az vm start --resource-group <your-rg> --name rqlite
sudo resize2fs "$(findmnt -no SOURCE /var/lib/rqlite)"
3

Connect to your VM

Connect over SSH as the administrator you chose at deployment:

ssh azureuser@<vm-public-ip>
4

Check the services

Confirm that the first boot service and rqlite are both active:

systemctl is-active rqlite-firstboot.service rqlited.service
active active

rqlite listens on the loopback address only, so from the network only SSH is reachable. Check the listeners:

sudo ss -Htln | awk '{print $4}' | sort -V
0.0.0.0:22 127.0.0.1:4001 127.0.0.1:4002 127.0.0.53%lo:53 127.0.0.54:53 [::]:22

Port 4001 is the HTTPS API and web console, and port 4002 is Raft. The port 53 lines are Ubuntu's local DNS resolver, also loopback only. The data directory lives on the dedicated data disk:

lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS /dev/disk/azure/scsi1/lun0
NAME SIZE FSTYPE LABEL MOUNTPOINTS sda 32G ext4 rqlitedata /var/lib/rqlite

The kernel names disks in the order it finds them, so the data disk's device name (sda here, sdc on other VMs) can differ from VM to VM. /dev/disk/azure/scsi1/lun0 always points at it, and /etc/fstab mounts it by its file system UUID.

5

Get your password

Your connection details are in /root/rqlite-credentials.txt. Show them:

sudo grep -E '^RQLITE_(USER|URL|CONSOLE_URL|TLS_CERT|TLS_SHA256)=' /root/rqlite-credentials.txt
RQLITE_USER=admin RQLITE_URL=https://127.0.0.1:4001 RQLITE_CONSOLE_URL=https://127.0.0.1:4001/console/ RQLITE_TLS_CERT=/etc/rqlite/tls/server.crt RQLITE_TLS_SHA256=CC:87:14:E0:A0:DE:93:27:BC:91:DD:11:19:B0:2D:8B:34:90:FA:4F:96:81:AE:01:D4:CC:3D:70:A0:02:B4:11

Show the password itself when you need it (for the web console or a client on your own computer):

sudo grep '^RQLITE_PASSWORD=' /root/rqlite-credentials.txt

RQLITE_TLS_SHA256 is the SHA-256 fingerprint of this VM's TLS certificate (RQLITE_TLS_CERT). Compare it with what your browser or client shows the first time it connects. It is different on every VM.

6

Run SQL

rqlite-query sends SQL to rqlite over HTTPS as admin and trusts this VM's certificate. It hands the password from the credentials file to curl in a configuration file rather than on a command line. Each argument is one statement, and --tx runs them in one transaction.

Create a table and add some rows:

sudo rqlite-query --tx "CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT NOT NULL, region TEXT NOT NULL, cents INTEGER NOT NULL)" "INSERT INTO orders (customer, region, cents) VALUES ('Ada', 'EMEA', 25025), ('Grace', 'AMER', 99999), ('Alan', 'EMEA', 44780), ('Hedy', 'APAC', 12000), ('Linus', 'APAC', 8050)"
{ "results": [ { "rows": null }, { "last_insert_id": 5, "rows_affected": 5, "rows": null } ] }

Query them:

sudo rqlite-query "SELECT region, COUNT(*) AS orders, SUM(cents) / 100.0 AS total FROM orders GROUP BY region ORDER BY total DESC"
{ "results": [ { "types": { "orders": "integer", "region": "text", "total": "real" }, "rows": [ { "orders": 1, "region": "AMER", "total": 999.99 }, { "orders": 2, "region": "EMEA", "total": 698.05 }, { "orders": 2, "region": "APAC", "total": 200.5 } ] } ] }

Every write is committed through Raft before rqlite answers. For applications, prefer parameterized statements, which the HTTP API supports directly. For example, post [["INSERT INTO orders (customer, region, cents) VALUES (?, ?, ?)", "Ada", "EMEA", 25025]] to /db/execute. See the rqlite API documentation for details.

7

Check the node

rqlite-query --get calls any API path. The /readyz path reports whether the node, the Raft leader and the store are ready:

sudo rqlite-query --get /readyz
[+]node ok [+]leader ok [+]store ok [+]db ok

The /status path returns everything about the node as JSON. This example prints the version, the Raft state and the API's security settings:

sudo rqlite-query --get /status | python3 -c 'import json, sys; d = json.load(sys.stdin); print(d["build"]["version"], d["store"]["raft"]["state"], "auth", d["http"]["auth"], "tls", d["http"]["tls"]["enabled"], d["http"]["bind_addr"])'
v10.6.4 Leader auth enabled tls true 127.0.0.1:4001
8

Open the web console through an SSH tunnel

rqlite's built-in web console shows the cluster and your schema, runs queries and takes backups. rqlite serves it at /console/ on the same HTTPS port, so you reach it through an SSH tunnel from your computer. Open the tunnel:

ssh -N -L 4001:127.0.0.1:4001 azureuser@<vm-public-ip>

Then open https://127.0.0.1:4001/console/ in your browser. You can also use https://localhost:4001/console/, because the certificate names both.

The certificate is self-signed and unique to your VM, so the browser warns about it the first time. Check that the SHA-256 fingerprint it shows matches RQLITE_TLS_SHA256 from Step 5, then continue. To avoid the warning, copy /etc/rqlite/tls/server.crt to your computer and trust it for localhost.

The browser asks for a user name and password. Sign in as admin with the password from Step 5.

The Home page shows the cluster. This node is the leader of a one-node cluster, its API address is HTTPS and Raft is on the loopback address. The HTTP and Raft panels on Home show how the API is exposed: bound to 127.0.0.1:4001, with authentication enabled and TLS on.

Query runs SQL, for example the query from Step 6. Schema shows every table and its columns.

Backup downloads a copy of the database as a SQLite file, and Restore loads one. Both act on the whole database, so use them with care.

9

Connect applications from your computer

With the tunnel from Step 8 open, applications on your computer connect to https://localhost:4001 with HTTP basic authentication as admin. First copy the VM's certificate so that your client can verify it:

ssh azureuser@<vm-public-ip> cat /etc/rqlite/tls/server.crt > rqlite-vm.crt curl --cacert rqlite-vm.crt -u admin -G https://localhost:4001/db/query --data-urlencode 'q=SELECT COUNT(*) FROM orders'

curl -u admin asks for the password, so it does not end up on a command line or in your shell history. rqlite's client libraries take an HTTPS URL, a user and a password the same way. Examples are pyrqlite for Python, gorqlite for Go and rqlite-js for JavaScript. Give them the password from a file or a secret store, not as a command-line argument.

The rqlite shell is also on the VM and on rqlite's release downloads. It accepts credentials only as -u admin:<password> on its command line, where other local users can see them while it runs and your shell may record them. Prefer rqlite-query, the console or a client library. If you use the shell, connect like this: rqlite -s https -H localhost -p 4001 -c rqlite-vm.crt -u admin:<password>.

10

Open rqlite to your virtual network (optional)

For applications inside your Azure virtual network, the SSH tunnel (or a bastion host) keeps the database ports closed. If you decide to expose the API to your network instead, do the following:

  • Listen on the VM's private address as well. Override the ExecStart line of rqlited.service (sudo systemctl edit --full rqlited), for example with -http-addr 0.0.0.0:4001 -http-adv-addr <vm-private-ip>:4001 in place of -http-addr 127.0.0.1:4001. Keep -auth, -http-cert and -http-key. The certificate already names the VM's private address. Then run sudo systemctl restart rqlited.
  • Allow TCP 4001 in the NSG from one CIDR only (your application subnet), never from * or Internet:
az network nsg rule create --resource-group <your-rg> --nsg-name <your-nsg> \
  --name allow-rqlite-api-from-app-subnet --priority 310 --direction Inbound --access Allow \
  --protocol Tcp --source-address-prefixes <your-cidr> --destination-port-ranges 4001

Leave Raft (4002) on the loopback address unless you build a cluster (Step 13).

11

Security checks

rqlite and its first boot service cannot reach the Azure Instance Metadata Service or the WireServer. Check the blocked addresses:

systemctl show -p IPAddressDeny --value rqlited.service | tr ' ' '\n' | sort
168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128

Run the image's egress probe inside the control group of rqlited.service as the rqlite user. It shows both endpoints blocked while loopback still works:

sudo bash -c 'echo $$ > /sys/fs/cgroup$(systemctl show -p ControlGroup --value rqlited.service)/cgroup.procs; exec setpriv --reuid=rqlite --regid=rqlite --init-groups /usr/local/sbin/sac-egress-probe rqlited.service'
EGRESS-PROBE rqlited.service loopback=REACHABLE imds4=TimeoutError imds4mapped=TimeoutError imds6=ENETUNREACH wireserver=TimeoutError wireservermapped=TimeoutError verdict=BLOCKED

A request without credentials is refused, plaintext HTTP is refused, and the self-signed certificate is not trusted unless you name it. Curl exit code 60 is a certificate error:

curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/rqlite/tls/server.crt https://localhost:4001/status
curl -s http://127.0.0.1:4001/status; echo
curl -s -o /dev/null https://localhost:4001/status; echo "curl exit code $?"
401 Client sent an HTTP request to an HTTPS server. curl exit code 60

Only root can read the password. Only root and the rqlite service group can read the authentication file and the TLS key. rqlite itself runs unprivileged:

sudo stat -c '%a %U:%G %n' /etc/rqlite/auth.json /etc/rqlite/tls/server.key /root/rqlite-credentials.txt ps -o user:10=,comm= -u rqlite
sudo grep -E '^(CapEff|NoNewPrivs)' /proc/$(systemctl show -p MainPID --value rqlited.service)/status
640 root:rqlite /etc/rqlite/auth.json 640 root:rqlite /etc/rqlite/tls/server.key 600 root:root /root/rqlite-credentials.txt rqlite rqlited CapEff: 0000000000000000 NoNewPrivs: 1

Check how this rqlite was built, and read the build-time vulnerability scan:

rqlited -version sed -n '1,4p' /usr/share/doc/sac/rqlite-BUILD-PROVENANCE.txt grep -c 'No vulnerabilities found' /usr/share/doc/sac/rqlite-govulncheck.txt
rqlited v10.6.4 linux amd64 go1.27.3 sqlite3.53.4 (commit 64145717224d620809174cb2235ac86ec604aac5, compiler gc) rqlite v10.6.4 on this image was compiled by sac from the unmodified upstream source: repository https://github.com/rqlite/rqlite (MIT) tag v10.6.4 (commit 64145717224d620809174cb2235ac86ec604aac5; go.mod sha256 d4187d82ca17de2cce2c2bdf64cf70211617dcfdecfe7279f5248337cc9c640a; go.sum sha256 b200367b9ee837f5e05a0b2fcc69a64b2fb89f9e5ab44c5689b43bf518477788) toolchain go1.27.3 linux/amd64, CGO_ENABLED=1 (SQLite 3.53.4, vendored by github.com/rqlite/go-sqlite3) 2

govulncheck checks the code each binary actually links. For the rqlite shell, it notes one advisory in a module the build requires, GO-2026-5932 (golang.org/x/crypto/openpgp, unmaintained, no fix). It reports that the code does not call it, because the shell links only x/crypto/ssh/terminal from that module. rqlited, the server, has no finding at all.

The image also ships its own round-trip check. The per-VM password reads and writes through Raft, using a transactional, parameterized write and an aggregate read on a scratch table that the check drops again. Anonymous, wrong, blank, common and unknown-user credentials are refused with 401. The check also confirms that plaintext is refused, that the certificate is needed and that both ports are loopback only:

sudo /usr/local/sbin/rqlite-roundtrip.sh | tail -1
OK-rqlite-roundtrip
12

Change the password

rqlite-rotate-password gives admin a new password. It writes the password to /etc/rqlite/auth.json and the credentials file, restarts rqlite, and checks that the new password works and the old one is refused. The password never appears on a command line.

To set a new random 32-character password, run:

sudo rqlite-rotate-password
rqlite-rotate-password: the password of 'admin' was changed; the new one reads (200) and the old one is refused (401). It is in /root/rqlite-credentials.txt.

To choose the password yourself, type it at a prompt that does not echo and pass it to the helper on standard input. Use 16 to 128 printable characters, with no space, double quote or backslash:

read -rs -p 'New rqlite password: ' NEWPW; echo printf '%s\n' "$NEWPW" | sudo rqlite-rotate-password --stdin; unset NEWPW

Avoid passing a password to sudo on a command line, because sudo records its command line in /var/log/auth.log. rqlite 10.6.4 stores passwords in auth.json in plain text, which is why only root and the rqlite group can read the file.

You can add further users with narrower permissions by editing auth.json. Use ["query", "execute"] for an application, or ["query"] for read-only reporting. See rqlite's security documentation for the permission names.

Never add a user named *. rqlite grants that user's permissions to anonymous requests, and the image's start-up check refuses to start rqlite with one. The check also expects exactly one user, so if you add users, remove the check from rqlited.service with sudo systemctl edit rqlited (an empty ExecStartPre= line) and keep the file's permissions.

13

Growing into a cluster

A fault-tolerant rqlite cluster has three or more nodes, and a majority must be up to accept writes. The image is configured as a single node. To build a cluster from several VMs of this image, follow this outline:

  • Put the nodes in one subnet and allow TCP 4001 and 4002 in the NSG from that subnet only, never from * or Internet.
  • Create your own certificate authority and a certificate for each node that names its private IP address. This encrypts and mutually authenticates Raft traffic between nodes. Give each node the CA so that the HTTPS API can verify its peers.
  • On each node, change the ExecStart line of rqlited.service (sudo systemctl edit --full rqlited) to listen on the private address and join the first node, for example:
/usr/local/bin/rqlited -http-addr 0.0.0.0:4001 -http-adv-addr <vm-private-ip>:4001 \
  -raft-addr 0.0.0.0:4002 -raft-adv-addr <vm-private-ip>:4002 \
  -auth /etc/rqlite/auth.json -join-as admin -join <first-node-private-ip>:4002 \
  -http-cert /etc/rqlite/tls/server.crt -http-key /etc/rqlite/tls/server.key -http-ca-cert /etc/rqlite/tls/ca.crt \
  -node-cert /etc/rqlite/tls/node.crt -node-key /etc/rqlite/tls/node.key -node-ca-cert /etc/rqlite/tls/ca.crt -node-verify-client \
  /var/lib/rqlite/data
  • Use the same auth.json on every node, and join nodes that start with an empty data directory. On a fresh VM, stop rqlite and empty /var/lib/rqlite/data before its first join.

rqlite's clustering and security documentation describes each flag, automatic bootstrapping (-bootstrap-expect) and read-only nodes.

14

Backup, updates and maintenance

Back up by snapshotting the data disk. Stop rqlite first for a fully consistent copy with sudo systemctl stop rqlited. For example, run az snapshot create --resource-group <your-rg> --name rqlite-data-snap --source <data-disk-name>.

Alternatively, download a consistent SQLite copy of the database while rqlite runs, from the console's Backup page or the API. In this example, the password goes to curl on standard input, not on its command line:

sudo sh -c 'printf "user = \"admin:%s\"\n" "$(sed -n "s/^RQLITE_PASSWORD=//p" /root/rqlite-credentials.txt)" | curl -sS -K - --cacert /etc/rqlite/tls/server.crt -o /root/rqlite-backup.sqlite https://localhost:4001/db/backup'
sudo head -c 15 /root/rqlite-backup.sqlite; echo
SQLite format 3

rqlite can also back up automatically to S3-compatible object storage (-auto-backup, see rqlite's backup documentation). This image blocks rqlite from the Azure metadata service, so that feature needs an access key in its configuration file rather than a managed identity. Disk snapshots need no credentials on the VM.

  • Logs: run sudo journalctl -u rqlited.service -n 50.
  • Updates: Ubuntu security updates install automatically. rqlite is not an apt package. To move to a newer rqlite, snapshot the data disk. Then replace /usr/local/bin/rqlited and /usr/local/bin/rqlite with the binaries from the new release on rqlite's GitHub releases page, after you check the release's SHA-256 digest. Restart by running sudo systemctl stop rqlited, installing the two files with mode 0755, and running sudo systemctl start rqlited.
  • Read the release notes first. SecureAnyCloud also publishes new images as rqlite releases.
  • The build provenance of the shipped binaries is in /usr/share/doc/sac/rqlite-BUILD-PROVENANCE.txt.

Support and licensing

rqlite is open source software under the MIT license, and SQLite is in the public domain. The license of rqlite and of every Go module linked into it (MIT, Apache-2.0, BSD and MPL-2.0 licenses) is in /usr/share/doc/sac/rqlite-third-party-licences/, with a notice saying where to obtain the source of each module. The rqlite on this image is built from the unmodified upstream source.

SecureAnyCloud provides 24/7 technical support for this image by email and live chat: sales@secureanycloud.com.

rqlite and SQLite are trademarks of their respective owners. SecureAnyCloud is not affiliated with or endorsed by the rqlite project or the SQLite developers.

Deploy on Azure

Launch rqlite 10.6.4 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure