Deployment guide

FerretDB

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. 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 that FerretDB 2.7.0 is built for, on one VM. The SecureAnyCloud image adds a password that is unique to your VM, TLS, hardened services and a dedicated data disk.

What you get:

  • 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. 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. SecureAnyCloud therefore compiles FerretDB 2.7.0 from its release source with Go 1.27.3 and the fixed module releases, and 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, and mongosh sends no telemetry for any user and keeps no session logs. 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 that 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 you open the TLS port to your application subnet later (Step 9). Never open 27017, 27018 or 5432 to the internet.
1

Deploy from the Azure Portal

  • Find FerretDB on Ubuntu 24.04 LTS by SecureAnyCloud in the Azure Marketplace and choose Create.
  • 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.
  • Under Administrator account, use the username azureuser with an SSH public key (recommended) or a password. The image honours whichever you choose.
  • Under Inbound port rules, allow SSH (22) only. After the VM is created, restrict it to your own address range in the Network Security Group.
  • On the Disks tab, check the image's 30 GiB data disk for /var/lib/ferretdb. Make it as large as your data needs.
  • Choose Review + create, then Create. First boot takes well under a minute after the VM is running.
2

Deploy with the Azure CLI

Accept the Marketplace terms once per subscription, then create the VM:

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:ferretdb-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-resource-group> \
  --name ferretdb \
  --image sac1647283583153: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, because that opens the port to every address on the internet.

3

Connect to your VM

Connect over SSH as the admin user:

ssh azureuser@<vm-public-ip>
4

Check the services

First boot records the VM's public address and creates the PostgreSQL cluster, the administrator and the certificate. Then PostgreSQL and FerretDB start. Check the units and the first boot marker:

systemctl is-active ferretdb-firstboot postgresql@17-main ferretdb
sudo test -f /var/lib/sac/data/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 because 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 on port 27017 (MongoDB protocol), port 27018 (MongoDB protocol over TLS) and port 8088 (its health and metrics handler). PostgreSQL listens on 127.0.0.1:5432 and on its local socket. Confirm the listeners:

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. Check both:

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
5

Get your administrator password

sudo cat /root/ferretdb-credentials.txt

Only root can read the file:

# 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://secureanycloud.com/stacks/ferretdb/

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

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 opens 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: 'sac' }

Remove the example database when you have finished with it:

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

FerretDB covers the MongoDB features that most applications use: CRUD, indexes including text indexes, aggregation pipelines, cursors and user management. Before you migrate, check the FerretDB compatibility documentation for the commands and operators that your application relies on.

7

Create a user for your application

Give each application its own user, so that 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()}). Remove a user with db.getSiblingDB("admin").dropUser("appuser").

8

Connect from your own computer through an SSH tunnel

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

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.

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, and restart FerretDB. Keep the key at mode 0640, owner root, group ferretdb.

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=sac host|all|127.0.0.1|scram-sha-256| host|all|::1|scram-sha-256|

PostgreSQL refuses a connection without a password on the TCP port, and it refuses 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, which is 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. It writes, reads, indexes and aggregates data. It also proves that FerretDB refuses all of the following:

  • Wrong and empty passwords.
  • Anonymous reads.
  • The password-less PostgreSQL roles.
  • TLS without the VM's certificate.
  • Plain text on the TLS port.

Run it:

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. Only root and the ferretdb group can read the key:

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 = sac 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. FerretDB runs sandboxed as an unprivileged user. Confirm both:

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

Confirm 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.
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. If you keep the password elsewhere, delete the file instead:

sudo ferretdb-shell db.getSiblingDB("admin").updateUser("admin", {pwd: passwordPrompt()})
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. For a consistent copy, take the snapshot with FerretDB and PostgreSQL stopped (sudo systemctl stop ferretdb postgresql@17-main). For a backup while the services run, 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 a new image version updates them together.

FerretDB's settings are in /etc/ferretdb/ferretdb.env. Restart FerretDB after a change. PostgreSQL's settings 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).

Key paths on the image (component, then 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 sac-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/

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, so 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

SecureAnyCloud provides 24/7 expert support for this image by email at sales@secureanycloud.com. 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 is under the MIT License, PostgreSQL is under the PostgreSQL License and mongosh is under the Apache License 2.0. The licences are on the VM. There is no per-user or per-CPU licence fee. The SecureAnyCloud 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. SecureAnyCloud is not affiliated with or endorsed by these organisations.

Deploy on Azure

Launch FerretDB 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