Databases Azure

FerretDB on Ubuntu 24.04 on Azure User Guide

| Product: FerretDB on Ubuntu 24.04 LTS on Azure

Overview

FerretDB is an open source database that speaks the MongoDB wire protocol. Applications, MongoDB drivers, mongosh and tools such as MongoDB Compass connect to it as they would to MongoDB, and FerretDB stores the documents in PostgreSQL through the DocumentDB extension. This image runs FerretDB 2.7.0 on PostgreSQL 17 with DocumentDB 0.107.0 (the release FerretDB 2.7.0 is built for), on one VM.

What the cloudimg image adds:

  • No default login, a password unique to your VM. The image ships no PostgreSQL cluster and no database. First boot creates your VM's own cluster on the data disk, installs DocumentDB and creates the MongoDB administrator admin with a 32 character password generated on the VM. Only a SCRAM-SHA-256 verifier of that password is handed to PostgreSQL: the password itself never reaches PostgreSQL, a command line, a log or the journal. It is written to the root only file /root/ferretdb-credentials.txt. FerretDB and PostgreSQL refuse to start until first boot has finished.
  • No trust rule, no PostgreSQL password. PostgreSQL listens on 127.0.0.1:5432 only and every connection there must prove a password with SCRAM-SHA-256; on the local socket a PostgreSQL role signs in only as the matching operating system user (peer authentication). FerretDB, DocumentDB's background jobs and you (as the operating system's postgres user) use the socket. PostgreSQL's own postgres and ferretdb roles have no password, so there is no PostgreSQL credential to steal, and there is no trust rule anywhere in pg_hba.conf.
  • FerretDB on localhost, with TLS. FerretDB listens on 127.0.0.1:27017 and, with a certificate generated on your VM's first boot, on 127.0.0.1:27018 over TLS. Nothing but SSH is reachable from the network until you choose to open the TLS port to your virtual network (Step 9).
  • A FerretDB built for this image. The FerretDB 2.7.0 release binary was built with Go 1.25.4 and modules that now carry fixed security advisories, so cloudimg compiles FerretDB 2.7.0 from its release source with Go 1.27.3 and the fixed module releases; govulncheck reports no vulnerabilities in it. The build is reproducible and its provenance is on the VM (/usr/share/doc/ferretdb/BUILD-PROVENANCE.txt).
  • Hardened services. FerretDB runs as an unprivileged ferretdb user with no capabilities, a read only view of the system and no access to /root; FerretDB, PostgreSQL and the first boot service are blocked from the Azure Instance Metadata Service and the platform wire server. FerretDB's telemetry is off, mongosh sends no telemetry for any user and keeps no session logs, and only the open source mongosh shell is installed (not MongoDB's proprietary encryption library). pg_cron does not record DocumentDB's once a second background jobs, so its history table and the PostgreSQL log cannot grow without bound.
  • Your data on its own disk. The PostgreSQL data directory (/var/lib/ferretdb/pg17) is on a dedicated 30 GiB Azure managed data disk you can grow or snapshot on its own.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: FerretDB 2.7.0 (/usr/local/bin/ferretdb), PostgreSQL 17 with pg_cron, PostGIS, RUM and pgvector from the PostgreSQL Global Development Group (PGDG) repository, the DocumentDB extension 0.107.0 for FerretDB 2.7.0, mongosh 2.13.0 (the MongoDB Shell), Ubuntu 24.04 LTS, cloud-init and the Azure Linux agent. There is no MongoDB server on the image.

Admin user: azureuser (or the name you choose) · Recommended size: Standard_B2s to start

Prerequisites

  • An Azure subscription with permission to create virtual machines, and an SSH key pair (or a password) for the admin account.
  • A Network Security Group that allows TCP 22 (SSH) from your admin network. FerretDB listens on the loopback address only, so no database port needs to be open; you reach FerretDB through an SSH tunnel (Step 8), or open the TLS port to your application subnet later (Step 9). Never open 27017, 27018 or 5432 to the internet.

Step 1: Deploy from the Azure Portal

  1. Find FerretDB on Ubuntu 24.04 LTS by cloudimg in the Azure Marketplace and click Create.
  2. On the Basics tab choose a resource group, region and size. Standard_B2s (2 vCPU, 4 GiB RAM) is a sensible starting point; size up for larger working sets.
  3. Under Administrator account use the username azureuser with an SSH public key (recommended) or a password. The image honours whichever you choose.
  4. Under Inbound port rules allow SSH (22) only, then restrict it to your own address range in the Network Security Group after the VM is created.
  5. The Disks tab shows the image's 30 GiB data disk for /var/lib/ferretdb. Make it as large as your data needs.
  6. Click Review + create, then Create. First boot takes well under a minute after the VM is running.

Step 2: Deploy with the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn cloudimg1647283583153:ferretdb-ubuntu-24-04:default:latest

az vm create \
  --resource-group <your-resource-group> \
  --name ferretdb \
  --image cloudimg1647283583153:ferretdb-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_rsa.pub \
  --public-ip-sku Standard \
  --nsg-rule SSH

--nsg-rule SSH creates a Network Security Group that allows SSH only, which is all FerretDB needs. Do not run az vm open-port for 27017, 27018 or 5432: that opens the port to every address on the internet.

Step 3: Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4: Check the services

First boot records the VM's public address, creates the PostgreSQL cluster, the administrator and the certificate; then PostgreSQL and FerretDB start:

systemctl is-active ferretdb-firstboot postgresql@17-main ferretdb
sudo test -f /var/lib/cloudimg/ferretdb-firstboot.done && echo FIRSTBOOT_DONE
ferretdb --version | head -1

On the first boot all three units are active and the first boot marker is present (on later boots ferretdb-firstboot shows inactive: it runs once):

active
active
active
FIRSTBOOT_DONE
version: v2.7.0

Only SSH listens on a network address. FerretDB listens on the loopback address: 27017 (MongoDB protocol), 27018 (MongoDB protocol over TLS) and 8088 (its health and metrics handler). PostgreSQL listens on 127.0.0.1:5432 and on its local socket:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | grep -v -E '^127\.0\.0\.5[34]' | sort
0.0.0.0:22 users:(("sshd"),("systemd"))
127.0.0.1:27017 users:(("ferretdb"))
127.0.0.1:27018 users:(("ferretdb"))
127.0.0.1:5432 users:(("postgres"))
127.0.0.1:8088 users:(("ferretdb"))
[::]:22 users:(("sshd"),("systemd"))

The data is on the dedicated data disk, and FerretDB reports ready once PostgreSQL and DocumentDB answer:

findmnt -no FSTYPE,TARGET /var/lib/ferretdb
sudo du -sh /var/lib/ferretdb/pg17
curl -s -o /dev/null -w 'readyz HTTP %{http_code}\n' http://127.0.0.1:8088/debug/readyz
ext4   /var/lib/ferretdb
65M /var/lib/ferretdb/pg17
readyz HTTP 200

FerretDB first boot complete, the units active, the loopback listeners and the data disk

Step 5: Get your administrator password

sudo cat /root/ferretdb-credentials.txt

The file is readable by root only:

# FerretDB 2.7.0 on Ubuntu 24.04 - generated on first boot by ferretdb-firstboot.service
# This password is unique to this VM; none was baked into the image. Keep this file secret.

FERRETDB_USERNAME=admin
FERRETDB_PASSWORD=<FERRETDB_PASSWORD>
FERRETDB_HOST=127.0.0.1
FERRETDB_PORT=27017
FERRETDB_TLS_PORT=27018
FERRETDB_TLS_CA=/etc/ferretdb/tls/ferretdb.crt

# On the VM (mongosh signed in as admin; the password never goes on a command line):  sudo ferretdb-shell
# Connection string, on the VM or through an SSH tunnel to 127.0.0.1:27017 (replace <password>):
#   mongodb://admin:<password>@127.0.0.1:27017/?authMechanism=SCRAM-SHA-256
# TLS listener (127.0.0.1:27018 by default; give clients /etc/ferretdb/tls/ferretdb.crt as the CA file):
#   mongodb://admin:<password>@<host>:27018/?authMechanism=SCRAM-SHA-256&tls=true&tlsCAFile=ferretdb.crt
# FerretDB listens on the loopback interface only; ports 27017 and 27018 are not open to the network. To let clients
# in your virtual network connect, use the TLS listener (27018) and allow it in your NSG from their subnet only.
# User guide: https://www.cloudimg.co.uk/guides/ferretdb-on-ubuntu-24-04-azure/

Store the password in your secrets manager. In the commands below, <FERRETDB_PASSWORD> stands for the value of FERRETDB_PASSWORD.

The root only credentials file and mongosh signed in as admin

Step 6: Use the MongoDB shell on the VM

sudo ferretdb-shell opens mongosh signed in as admin. It reads the password from the credentials file itself, so the password never appears on a command line or in your shell history. With no arguments it is an interactive shell; with --eval it runs a command and exits:

sudo ferretdb-shell --eval 'db.runCommand({ping: 1})'
{ ok: 1 }

Write some documents, add an index and query them, as you would on MongoDB:

sudo ferretdb-shell --eval 'const inv = db.getSiblingDB("inventory"); inv.items.insertMany([{sku: "A-100", qty: 25}, {sku: "B-200", qty: 40}, {sku: "C-300", qty: 75}]); inv.items.createIndex({sku: 1}, {unique: true}); printjson(inv.items.find({qty: {$gt: 30}}, {_id: 0}).sort({qty: 1}).toArray())'
[
  {
    sku: 'B-200',
    qty: 40
  },
  {
    sku: 'C-300',
    qty: 75
  }
]

FerretDB identifies itself in buildInfo:

sudo ferretdb-shell --eval 'printjson(db.adminCommand({buildInfo: 1}).ferretdb)'
{
  version: 'v2.7.0',
  package: 'cloudimg'
}

Remove the example database when you have finished with it:

sudo ferretdb-shell --eval 'db.getSiblingDB("inventory").dropDatabase()'
{ ok: 1, dropped: 'inventory' }

Documents inserted, indexed and queried through mongosh, and the TLS listener verified

FerretDB covers the MongoDB features most applications use (CRUD, indexes including text indexes, aggregation pipelines, cursors and user management). Check the FerretDB compatibility documentation for the commands and operators your application relies on before you migrate.

Step 7: Create a user for your application

Give each application its own user, so you can change or revoke its password without touching admin. Run this in sudo ferretdb-shell (passwordPrompt() asks for the new password, so it is never written on a command line):

use admin
db.createUser({user: "appuser", pwd: passwordPrompt(), roles: [{role: "clusterAdmin", db: "admin"}, {role: "readWriteAnyDatabase", db: "admin"}]})

FerretDB 2.7.0 stores every user as a PostgreSQL role and accepts two role sets: clusterAdmin with readWriteAnyDatabase, and readAnyDatabase. FerretDB does not yet enforce MongoDB roles: every user it authenticates can read and write every database. Use separate users to separate credentials, and keep network access to FerretDB as narrow as you can (Steps 8 and 9). Change a user's password with db.getSiblingDB("admin").updateUser("appuser", {pwd: passwordPrompt()}), and remove one with db.getSiblingDB("admin").dropUser("appuser").

Step 8: Connect from your own computer through an SSH tunnel

FerretDB is not exposed to the network. From your computer, open an SSH tunnel and point mongosh, MongoDB Compass or your application at the local end of it (the tunnel encrypts the connection):

ssh -N -L 27017:127.0.0.1:27017 azureuser@<vm-public-ip>
mongosh "mongodb://admin@127.0.0.1:27017/?authMechanism=SCRAM-SHA-256" -p

-p with no value makes mongosh prompt for the password. In MongoDB Compass use the connection string mongodb://admin:<password>@127.0.0.1:27017/?authMechanism=SCRAM-SHA-256&directConnection=true.

Step 9: Allow TLS connections from your virtual network

To let application servers in your virtual network connect directly, use FerretDB's TLS listener rather than the plain one. The certificate generated on first boot names localhost, 127.0.0.1 and the VM's host name and private and public addresses; add your own DNS name if clients use one, open the TLS listener to the network, and restart FerretDB:

sudo ferretdb-tls-cert --renew db.example.internal
sudo sed -i 's/^FERRETDB_LISTEN_TLS=127.0.0.1:27018$/FERRETDB_LISTEN_TLS=0.0.0.0:27018/' /etc/ferretdb/ferretdb.env
sudo systemctl restart ferretdb

Then allow TCP 27018 in the VM's Network Security Group from your application subnet only (replace <your-app-subnet-cidr>, for example 10.0.1.0/24, and the NSG name):

az network nsg rule create --resource-group <your-resource-group> --nsg-name ferretdbNSG \
  --name allow-ferretdb-tls --priority 1010 --access Allow --protocol Tcp --direction Inbound \
  --source-address-prefixes <your-app-subnet-cidr> --destination-port-ranges 27018

Never open 27018, or 27017, to the internet; the plain listener on 27017 stays on the loopback address. Copy /etc/ferretdb/tls/ferretdb.crt to the clients and use it as their CA file:

mongodb://appuser:<password>@<private-ip>:27018/?authMechanism=SCRAM-SHA-256&tls=true&tlsCAFile=/path/to/ferretdb.crt

To use a certificate from your own certificate authority instead, replace /etc/ferretdb/tls/ferretdb.crt and ferretdb.key (keep the key 0640, owner root, group ferretdb) and restart FerretDB.

Step 10: Security checks

PostgreSQL accepts local socket connections with peer authentication and loopback TCP connections with SCRAM-SHA-256 only; there is no trust rule:

sudo -u postgres psql -X -At -c "SELECT type, array_to_string(user_name, ','), coalesce(address, ''), auth_method, coalesce(array_to_string(options, ','), '') FROM pg_hba_file_rules ORDER BY line_number"
local|postgres||peer|
local|all||peer|map=cloudimg
host|all|127.0.0.1|scram-sha-256|
host|all|::1|scram-sha-256|

A connection without a password is refused, on the TCP port and as the wrong operating system user on the socket:

R="$(psql -X -w -h 127.0.0.1 -U postgres -d postgres -c 'SELECT 1' 2>&1)"
case "${R}" in *"no password supplied"*) echo "127.0.0.1:5432 as postgres, no password: refused (SCRAM-SHA-256 needs a password)" ;; *) echo "UNEXPECTED: ${R}"; exit 1 ;; esac
R="$(sudo -u nobody psql -X -w -h /var/run/postgresql -U postgres -d postgres -c 'SELECT 1' 2>&1)"
case "${R}" in *"Peer authentication"*) echo "socket as postgres from the OS user nobody: refused (peer authentication)" ;; *) echo "UNEXPECTED: ${R}"; exit 1 ;; esac
127.0.0.1:5432 as postgres, no password: refused (SCRAM-SHA-256 needs a password)
socket as postgres from the OS user nobody: refused (peer authentication)

Only admin, the MongoDB user, has a password, stored as a SCRAM-SHA-256 verifier; PostgreSQL's own roles have none:

sudo -u postgres psql -X -c "SELECT rolname, rolsuper, left(rolpassword, 13) AS password FROM pg_authid WHERE rolcanlogin ORDER BY 1"
          rolname          | rolsuper |   password
---------------------------+----------+---------------
 admin                     | f        | SCRAM-SHA-256
 documentdb_bg_worker_role | f        |
 ferretdb                  | t        |
 postgres                  | t        |
(4 rows)

The image's own self test signs in as admin on both listeners (verifying this VM's certificate on the TLS one), writes, reads, indexes and aggregates data, and proves that wrong and empty passwords, anonymous reads, the password-less PostgreSQL roles, TLS without the VM's certificate and plain text on the TLS port are all refused:

sudo /usr/local/sbin/ferretdb-roundtrip.sh
OK-ferretdb-scram-tls-roundtrip

The certificate was generated on this VM and names its host name and its own addresses (the common name is ferretdb when the host name is longer than 64 characters); the key is readable by root and the ferretdb group only:

openssl x509 -in /etc/ferretdb/tls/ferretdb.crt -noout -subject -ext subjectAltName
ls -l /etc/ferretdb/tls | awk 'NR>1{print $1, $3, $4, $NF}'
subject=CN = ferretdb.<dns-suffix>.bx.internal.cloudapp.net, O = cloudimg FerretDB
X509v3 Subject Alternative Name: 
    DNS:ferretdb.<dns-suffix>.bx.internal.cloudapp.net, DNS:ferretdb, IP Address:<private-ip>, IP Address:<public-ip>, DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
-rw-r--r-- root root ferretdb.crt
-rw-r----- root ferretdb ferretdb.key

FerretDB, PostgreSQL and the first boot service cannot reach the Azure metadata service or the wire server, and FerretDB runs sandboxed as an unprivileged user:

systemctl show ferretdb postgresql@17-main ferretdb-firstboot -p Id -p IPAddressDeny
systemctl show ferretdb -p User -p ProtectSystem -p ProtectHome -p NoNewPrivileges -p ReadWritePaths -p CapabilityBoundingSet
IPAddressDeny=169.254.169.254/32 168.63.129.16/32
Id=ferretdb.service

IPAddressDeny=169.254.169.254/32 168.63.129.16/32
Id=postgresql@17-main.service

IPAddressDeny=169.254.169.254/32 168.63.129.16/32
Id=ferretdb-firstboot.service
CapabilityBoundingSet=
User=ferretdb
ReadWritePaths=/var/lib/ferretdb/state
ProtectHome=yes
ProtectSystem=strict
NoNewPrivileges=yes

How FerretDB on this image was built:

grep -E '^(toolchain |binary sha256|govulncheck)' /usr/share/doc/ferretdb/BUILD-PROVENANCE.txt
toolchain         go1.27.3 linux/amd64, official tarball https://go.dev/dl/go1.27.3.linux-amd64.tar.gz
toolchain sha256  65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40
binary sha256     f554a7edf6280a53ecc1a4ad1905b16d84c52da31b0bdb4fb42b85da2bcdc068 (reproducible: an independent out-of-band build of the same inputs gives the same bytes)
govulncheck       v1.9.0, source: No vulnerabilities found.; binary: No vulnerabilities found.

The PostgreSQL access rules, the refused password-less logins and the self test

Step 11: Changing the administrator password

Set a new password for admin from the MongoDB shell (passwordPrompt() asks for it), then update FERRETDB_PASSWORD in /root/ferretdb-credentials.txt, which ferretdb-shell and the self test read (or delete the file if you keep the password elsewhere):

sudo ferretdb-shell
db.getSiblingDB("admin").updateUser("admin", {pwd: passwordPrompt()})

Step 12: Backup, updates and maintenance

All data is in the PostgreSQL cluster on the data disk, /var/lib/ferretdb/pg17. The simplest backup is an Azure snapshot of that disk, taken with FerretDB and PostgreSQL stopped for a consistent copy (sudo systemctl stop ferretdb postgresql@17-main). For a backup while it runs, dump the PostgreSQL database that holds every FerretDB database:

sudo -u postgres pg_dump -Fc postgres > <backup-dir>/ferretdb-$(date +%F).dump

MongoDB's mongodump and mongorestore (MongoDB Database Tools) also work against FerretDB if you install them.

If you made the data disk larger, grow the file system once with sudo resize2fs "$(findmnt -no SOURCE /var/lib/ferretdb)".

Ubuntu security updates install automatically. PostgreSQL comes from the PGDG repository: apply its point releases when you choose, because they restart the database (sudo apt-get update && sudo apt-get install --only-upgrade postgresql-17 postgresql-client-17). FerretDB and the DocumentDB extension are a matched pair and are updated together by a new image version.

FerretDB's settings are in /etc/ferretdb/ferretdb.env (restart FerretDB after a change). PostgreSQL's are in /etc/postgresql/17/main/; the default shared_buffers is 128 MB, and on a Standard_B2s (4 GiB) about 1 GB is a good start (sudo -u postgres psql -c "ALTER SYSTEM SET shared_buffers = '1GB'" and restart PostgreSQL).

Component Path
PostgreSQL data directory /var/lib/ferretdb/pg17 (dedicated data disk)
PostgreSQL settings /etc/postgresql/17/main/
FerretDB binary and settings /usr/local/bin/ferretdb, /etc/ferretdb/ferretdb.env
FerretDB state /var/lib/ferretdb/state
Certificate and key /etc/ferretdb/tls/ferretdb.crt, /etc/ferretdb/tls/ferretdb.key
Helpers /usr/local/bin/ferretdb-shell, /usr/local/sbin/ferretdb-tls-cert, /usr/local/sbin/ferretdb-roundtrip.sh
First boot services cloudimg-public-ip.service, ferretdb-firstboot.service
Logs journalctl -u ferretdb, /var/log/postgresql/postgresql-17-main.log
Credentials /root/ferretdb-credentials.txt (mode 0600)
Build provenance and licences /usr/share/doc/ferretdb/, /usr/share/doc/postgresql-17-documentdb/LICENSE, /opt/mongosh-2.13.0/

Step 13: Security recommendations

  • Allow port 22 from your admin network only. Keep 27017 on the loopback address; if applications need to connect, open the TLS port 27018 to their subnet only (Step 9), never to the internet. Never open 5432.
  • Remember that FerretDB authenticates users but does not yet enforce their roles: anyone who signs in can read and write everything. Treat every FerretDB password as an administrator password.
  • Give clients the VM's certificate as their CA file, or issue a certificate from your own CA.
  • Change the administrator password if it may have been shared (Step 11), and snapshot the data disk regularly.

Support and licensing

cloudimg provides 24/7 expert support for this image by email at support@cloudimg.co.uk. Include the VM name, the image (FerretDB on Ubuntu 24.04 LTS) and what you were doing.

FerretDB is open source software under the Apache License 2.0, the DocumentDB extension under the MIT License, PostgreSQL under the PostgreSQL License and mongosh under the Apache License 2.0; the licences are on the VM. There is no per user or per CPU licence fee. The cloudimg charge covers the hardening, first boot setup, image maintenance and support.

FerretDB is a trademark of FerretDB Inc. MongoDB and MongoDB Compass are trademarks of MongoDB, Inc. PostgreSQL is a trademark of the PostgreSQL Community Association of Canada. Ubuntu is a trademark of Canonical Ltd. All other names are trademarks of their respective owners. cloudimg is not affiliated with or endorsed by these organisations.