Mc
Security Azure

MWDB Core on Ubuntu 24.04 on Azure User Guide

| Product: MWDB Core on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of MWDB Core on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. MWDB Core is the open source malware repository developed by CERT Polska and the engine behind the mwdb.cert.pl service. It stores files, extracted static configurations and text blobs as content addressed objects, links them with parent and child relations, and lets analysts enrich every object with tags, attributes and comments. A Lucene style query language searches across hashes, names, tags, attributes and configuration fields, and a capability based permission model with users, groups and sharing rules controls exactly who can see and do what. Everything is available through the web interface and a documented REST API, so automated pipelines and analysts work against the same database.

The cloudimg image ships the free and open source, AGPL-3.0 licensed MWDB Core 2.19.0 release, installed the officially supported way from the project's PyPI package (pinned by SHA-256, with the pre built web application included), running under gunicorn with on box PostgreSQL 16 and Redis 7. The API, PostgreSQL and Redis bind to the loopback interface only, and nginx terminates TLS as the single public listener. MWDB creates its built in administrator only on a database with no users, so this image ships an empty database and an empty store, and mints everything unique per VM on first boot: the PostgreSQL password, the application secret key that signs every session and API key, the Redis password, a self signed TLS certificate and the administrator account with a random password. Self registration is disabled. Backed by 24/7 cloudimg support.

MWDB and CERT Polska are names of their respective owners. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by CERT Polska or NASK. It ships the free and open source AGPL-3.0 licensed software, unmodified; the corresponding source is available at https://github.com/CERT-Polska/mwdb-core.

The postgresql, redis-server, nginx and mwdb-core services active, the two one shot units completed, and the API reporting version 2.19.0 with registration disabled

What is included:

  • MWDB Core 2.19.0 — the official PyPI release, pinned by SHA-256, run unmodified under gunicorn as an unprivileged service account
  • The MWDB web application — the pre built bundle from the same release, served by nginx
  • PostgreSQL 16 — the object database, bound to loopback with no host port published
  • Redis 7 — the rate limiting backend, bound to loopback and password protected
  • nginx — the single public listener, terminating TLS, with the API reachable only through it
  • First boot secret generation — database password, secret key, Redis password, TLS certificate and administrator account, all unique to each VM
  • A bundled self test — signs in as the per instance administrator, uploads a benign text blob, reads it back, finds it by query and deletes it, proving the repository really works end to end

Prerequisites

  • An Azure subscription with permission to create virtual machines
  • An SSH key pair for administrative access
  • A network security group allowing inbound TCP 22 (SSH) and TCP 443 (HTTPS) from your own address ranges
  • Standard_B2s (2 vCPU, 4 GB RAM) or larger; choose more memory and a larger disk once you store real sample volumes

Step 1: Deploy from the Azure Portal

  1. Open the Azure Marketplace and search for MWDB Core on Ubuntu 24.04 LTS by cloudimg.
  2. Select Create, then choose your subscription, resource group and region.
  3. Pick a VM size of Standard_B2s or larger.
  4. Under Administrator account, select SSH public key and supply your public key.
  5. Under Inbound port rules, allow SSH (22) and HTTPS (443).
  6. Select Review + create, then Create.

Step 2: Deploy from the Azure CLI

az group create --name mwdb-rg --location eastus
az vm create \
  --resource-group mwdb-rg \
  --name mwdb-vm \
  --image cloudimg:mwdb-core-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

az vm open-port --resource-group mwdb-rg --name mwdb-vm --port 443 --priority 1010

Step 3: Connect to your VM

ssh azureuser@<vm-ip>

Step 4: Confirm the services are running

MWDB Core runs as a systemd service behind nginx, with PostgreSQL and Redis on box. Two one shot units run on boot: mwdb-core-tls mints the per VM TLS certificate before nginx starts, and mwdb-core-firstboot (first boot only) generates the secrets, migrates the database schema and creates the administrator before the API is allowed to start.

systemctl is-active postgresql redis-server nginx mwdb-core

Expected output:

active
active
active
active

The one shot units report active (exited) after a successful run, and the first boot unit disables itself afterwards so it can never run twice:

systemctl --no-pager --plain list-units 'mwdb-core*'

Expected output:

UNIT                        LOAD   ACTIVE SUB     DESCRIPTION
mwdb-core-firstboot.service loaded active exited  MWDB Core first-boot setup (per-VM secrets, schema, admin account) (cloudimg)
mwdb-core-tls.service       loaded active exited  Mint the per-VM self-signed TLS certificate for the MWDB Core front door (cloudimg)
mwdb-core.service           loaded active running MWDB Core malware repository API (gunicorn, loopback :8080) (cloudimg)

The API listens only on the loopback interface, and nginx is the single public listener on ports 80 (which redirects to HTTPS) and 443. Rather than grepping for the ports you expect, list every socket that is bound to something other than loopback, so anything unexpected has nowhere to hide:

sudo ss -ltn | awk 'NR>1{print $4}' | grep -vE '^(127\.|\[::1\])' | sort -u

Expected output:

0.0.0.0:22
0.0.0.0:443
0.0.0.0:80
[::]:22
[::]:443
[::]:80

The only sockets bound off loopback are SSH on 22 and nginx on 80 and 443, the API answering on HTTPS, plain HTTP redirecting, and self registration refused

The only entries are 22 (SSH) and 80 and 443 (nginx), on both IPv4 and IPv6. Everything else the image runs, the API on 127.0.0.1:8080, PostgreSQL on 127.0.0.1:5432 and Redis on 127.0.0.1:6379, is bound to loopback and is not reachable from off the box. Because this lists the full set rather than a filtered subset, it is also the check to re run after you install anything else on the VM.

The API itself answers over HTTPS, and confirms which release you are running:

curl -sk https://127.0.0.1/api/server | jq '{server_version, instance_name, is_registration_enabled, is_password_auth_enabled}'

Expected output:

{
  "server_version": "2.19.0",
  "instance_name": "mwdb",
  "is_registration_enabled": false,
  "is_password_auth_enabled": true
}

Step 5: Retrieve your per instance credentials

Every VM generates its own secrets and administrator on first boot. They are written to a root only file:

sudo cat /root/mwdb-core-credentials.txt

This file contains:

  • MWDB_ADMIN_USER / MWDB_ADMIN_PASSWORD — the per instance administrator account
  • MWDB_URL — the HTTPS URL of your instance

The per VM credentials file, root only, showing the administrator user, its password and the instance URL, all unique to this VM

The PostgreSQL password, the application secret key and the Redis password live in /etc/mwdb-core/mwdb.ini, readable only by root and the service account. Keep both files safe, and change the administrator password from the profile page once you have signed in.

Step 6: Understand the security model

MWDB's built in admin account is created by its configure step, and only ever on a database with no users. The image therefore ships with an empty database, so no administrator, no session secret and no API key exists until your VM boots. Everything security sensitive is generated uniquely on the first boot of every VM, before the API is allowed to start:

  • the PostgreSQL password for the mwdb role
  • the application secret key, which signs every session cookie and every API key
  • the Redis password for the rate limiter
  • a per instance self signed TLS certificate
  • the administrator account and its 24 character password

On top of that, the image is configured the way upstream recommends for a production deployment: the API sits behind nginx with X-Forwarded-For handling enabled, self registration is disabled, password login is on, and rate limiting is on with Redis as its store. You can verify the whole model end to end with the bundled self test, which proves the front door works over TLS, that registration is refused, that a wrong password is rejected, that the per instance administrator can sign in, and that an object uploaded through the real API is read back, found by query and deleted again, leaving the store empty:

sudo /usr/local/bin/mwdb-core-selftest

Expected output:

OK mwdb-core self-test passed: healthz 200, :80 301, SPA + text/css served, /api/ping ok, registration disabled, rate limit on, register 403, wrong password 403, admin login + profile OK, benign blob upload/read/search/delete round-trip OK, store empty, exposed ports exactly {22,80,443}, gunicorn/PostgreSQL/Redis loopback-only, Redis requires auth

The bundled self test proving the TLS front door, refused registration, a rejected wrong password, the administrator sign in and a full upload, read back, search and delete round trip

Registration really is closed. An attempt to self register is refused with 403:

curl -sk -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/json' \
  -d '{"login":"someone","email":"someone@example.invalid","additional_info":"x","recaptcha":""}' \
  https://127.0.0.1/api/auth/register

Expected output:

403

Step 7: Sign in to the web interface

Open your instance in a browser:

https://<vm-ip>/

Because the TLS certificate is self signed and per VM, your browser will warn once. Accept it to continue, or install a CA issued certificate as described below to remove the warning. Sign in as MWDB_ADMIN_USER with MWDB_ADMIN_PASSWORD from your credentials file.

The MWDB sign in page served over TLS

Once signed in you land on the recent objects view. It is empty: this image ships no samples, no rule sets and no signature databases of any kind, so what goes in is entirely yours.

The signed in recent objects view of a freshly deployed, empty repository, with the search bar and quick query buttons

Step 8: Upload, tag and find an object

MWDB stores three kinds of object: files (samples), configs (extracted static configuration as JSON) and blobs (text, such as decoded strings or a note). The quickest way to see the workflow is to upload a text blob from Upload → Upload blob: paste some content, give it a name and a type, and submit.

Uploading a benign text blob from the web form, with a name, a type and the sharing options

You land on the object page. The Details tab shows the name, size, type and timestamps; Preview renders the content; Relations shows parents and children. Add a tag from the Tags panel on the right, attributes from the Attributes panel, and comments at the bottom. Sharing and inherited shares are listed on the same page.

The uploaded blob's object page with its details and hashes, the cloudimg tag applied, and the attribute, relation, share and comment panels

Every field you see is searchable. The search bar accepts Lucene style queries such as tag:cloudimg, blob.name:*.txt, file.size:>1000000 or config.family:emotet, and the quick query buttons store the ones you use most.

A Lucene style query for tag:cloudimg listing the uploaded blob with its type, tag and timestamps

The same round trip is available from the command line. This example signs in with the per instance administrator, uploads a small benign blob, finds it by name, reads it back and removes it again, leaving the repository empty:

ADMIN_PW=$(sudo awk -F= '/^MWDB_ADMIN_PASSWORD=/{print $2}' /root/mwdb-core-credentials.txt)
TOKEN=$(curl -sk -H 'Content-Type: application/json' \
  -d "{\"login\":\"admin\",\"password\":\"${ADMIN_PW}\"}" https://127.0.0.1/api/auth/login | jq -r .token)
ID=$(curl -sk -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"blob_name":"guide-example.txt","blob_type":"note","content":"hello from the deployment guide","tags":[{"tag":"guide"}]}' \
  https://127.0.0.1/api/blob | jq -r .id)
curl -sk -G -H "Authorization: Bearer ${TOKEN}" --data-urlencode 'query=blob.name:"guide-example.txt"' https://127.0.0.1/api/blob | jq -r '.blobs[].blob_name'
curl -sk -H "Authorization: Bearer ${TOKEN}" "https://127.0.0.1/api/blob/${ID}" | jq -r .content
curl -sk -o /dev/null -w 'delete -> %{http_code}\n' -X DELETE -H "Authorization: Bearer ${TOKEN}" "https://127.0.0.1/api/object/${ID}"

Expected output:

guide-example.txt
hello from the deployment guide
delete -> 200

From your workstation, use https://<vm-ip> in place of https://127.0.0.1 (the MWDB_URL value from your credentials file) and keep -k until you have installed a CA issued certificate. Files are uploaded with a multipart POST /api/file, and the full API is documented on your own instance at https://<vm-ip>/docs once you are signed in. The mwdblib Python client and its mwdb command line tool speak the same API.

Step 9: Manage users, groups and API keys

Self registration is closed by default on this image. Create analysts from Settings → Users → Register user, put them in groups from Settings → Groups, and control what each group may do with capabilities such as adding files, adding tags, sharing objects or removing objects. Objects are visible to a user only through a group that has been given access, so groups are how you segment a repository between teams or partner organisations.

Settings: user management, listing the per instance administrator and the Register user action, with access control, groups and attributes in the sidebar

Each user can issue API keys from their profile page (API keys). An API key is a bearer token for scripts and pipelines, and can be revoked without changing the password. The administrator can see the server flags that matter for security from the API:

ADMIN_PW=$(sudo awk -F= '/^MWDB_ADMIN_PASSWORD=/{print $2}' /root/mwdb-core-credentials.txt)
TOKEN=$(curl -sk -H 'Content-Type: application/json' \
  -d "{\"login\":\"admin\",\"password\":\"${ADMIN_PW}\"}" https://127.0.0.1/api/auth/login | jq -r .token)
curl -sk -H "Authorization: Bearer ${TOKEN}" https://127.0.0.1/api/server/admin | jq '{rate_limit_enabled, plugins_enabled, active_plugins}'

Expected output:

{
  "rate_limit_enabled": true,
  "plugins_enabled": true,
  "active_plugins": {}
}

Plugins are enabled as a mechanism but none is installed. If you add one, do it in the /opt/mwdb-core/venv virtual environment, declare it under plugins = in /etc/mwdb-core/mwdb.ini, and restart the API.

Step 10: Configuration and key paths

The live configuration is /etc/mwdb-core/mwdb.ini, generated on first boot. Any key from the upstream configuration reference can be added to its [mwdb] section. Common changes:

  • max_upload_size — the upload ceiling in bytes (256 MiB as shipped; also raise client_max_body_size in the nginx site)
  • enable_registration = 1 plus the mail_* keys — open self registration with e mail confirmation
  • storage_provider = s3 plus the s3_storage_* keys — keep samples in Azure Blob Storage through its S3 compatible gateway, or in any S3 compatible store, instead of the local uploads folder
  • enable_karton = 1 — hook a Karton analysis pipeline to every upload

After editing, restart the API:

$ sudo systemctl restart mwdb-core
Path Purpose
/etc/mwdb-core/mwdb.ini MWDB configuration: database, secret key, uploads folder, feature flags
/etc/mwdb-core/gunicorn.conf.py gunicorn settings: bind address, workers, timeouts
/root/mwdb-core-credentials.txt Per VM administrator credential and URL, root only
/var/lib/mwdb-core/uploads Uploaded samples (empty as shipped)
/opt/mwdb-core/venv The Python virtual environment holding the pinned release
/opt/mwdb-core/web The pre built web application served by nginx
/etc/nginx/sites-available/cloudimg-mwdb-core The TLS front door
/etc/nginx/tls/mwdb-core.crt and .key The per VM TLS certificate and key
/usr/local/bin/mwdb-core-selftest End to end appliance self test
/var/log/cloudimg-firstboot.log First boot log

Confirm the baked version at any time:

/opt/mwdb-core/venv/bin/pip show mwdb-core | grep -E '^(Name|Version):'

Expected output:

Name: mwdb-core
Version: 2.19.0

Step 11: Replace the certificate for production

The per VM certificate is self signed. For a real deployment, point a DNS name at the VM, obtain a CA issued certificate for it, install the certificate and key over /etc/nginx/tls/mwdb-core.crt and /etc/nginx/tls/mwdb-core.key, set base_url = https://<your-domain> in /etc/mwdb-core/mwdb.ini, and restart:

$ sudo systemctl restart nginx mwdb-core

The mwdb-core-tls unit only mints a certificate when none exists, so a certificate you install is never overwritten.

Backups

All state lives in PostgreSQL and in the uploads folder. To back up:

$ sudo -u postgres pg_dump mwdb | gzip > mwdb-$(date +%F).sql.gz
$ sudo tar czf mwdb-uploads-$(date +%F).tar.gz -C /var/lib/mwdb-core uploads

Store both off the VM, together with a copy of /etc/mwdb-core/mwdb.ini (the secret key inside it is what keeps existing sessions and API keys valid after a restore).

Security notes

  • Nothing is reachable without a credential. The API, PostgreSQL and Redis bind to loopback; nginx on 443 is the only network facing surface and it serves MWDB's own login. Self registration is off and login is rate limited.

  • Every secret is per virtual machine. The administrator password, PostgreSQL password, secret key, Redis password and TLS certificate are all generated on first boot. Nothing usable is baked into the image, and the API is prevented from starting until first boot has produced them.

  • The image ships empty. No sample, plugin, rule set or signature database of any kind is present. What you upload is malware by definition, so keep the VM's disk encrypted at rest (Azure managed disks are), restrict who may download from the repository with capabilities, and note that MWDB serves downloads in password protected archives when you use the zipped download option.

  • Restrict SSH. Allow port 22 only from your management network, and keep key based authentication.

  • The base image ships fully patched with unattended security upgrades enabled. MWDB itself is installed from PyPI and is never upgraded automatically; take a newer release deliberately with sudo /opt/mwdb-core/venv/bin/pip install -U mwdb-core, then run mwdb-core configure as the mwdb user to apply database migrations, as upstream documents.

Support

cloudimg provides 24/7 support with a guaranteed 24 hour response SLA for this image. MWDB Core itself is open source under the GNU Affero General Public License v3 with the upstream plugin exception and is shipped unmodified; cloudimg is not affiliated with CERT Polska or NASK. For questions about the image, its configuration or its security posture, contact cloudimg support.