Deployment guide

Headwind MDM

Overview

Headwind MDM is an open-source Mobile Device Management server for Android. It gives you one admin console and a REST API to enrol company devices, push configurations, applications and policies over the air, and monitor the state of your estate.

The SecureAnyCloud image installs the official open-source Headwind MDM 5.40.1 web application on Apache Tomcat 9 with a bundled PostgreSQL 16 database. Both run on a private container network, fronted by an nginx reverse proxy on port 80.

The image removes the two things that make a self-deployed Headwind MDM risky. Upstream seeds a default admin / admin administrator account and a static device shared secret when it first creates its database. On this image, both are replaced with unique random values generated on the first boot of your VM. The reverse proxy refuses the console until that replacement has completed, so the default credential is never reachable over the network.

What you get:

  • The official open-source Headwind MDM 5.40.1 web application on Apache Tomcat 9, pinned so that the appliance never silently upgrades
  • A bundled PostgreSQL 16 database, pinned by digest and published to no host port at all, that holds your fleet, configurations and application catalogue
  • An nginx reverse proxy on port 80 serving the admin console, the REST API and an unauthenticated /healthz endpoint for load balancer probes
  • A unique administrator password, device shared secret and database password generated on the first boot of every VM and stored in a root-only file
  • A dedicated, independently resizable Azure data disk mounted at /var/lib/hmdm-data that holds the database and your uploaded application files
  • A public enrolment base URL that resolves to your VM's own address at first boot and that you can change afterwards
  • The whole stack managed as one hmdm.service systemd unit alongside docker.service and nginx.service, all enabled
  • 24/7 SecureAnyCloud support

Key facts:

  • Platform: Ubuntu 24.04 LTS
  • Default login user: azureuser
  • Admin console: http://<vm-public-ip>/
  • Health endpoint: http://<vm-public-ip>/healthz
  • Credentials file: /root/headwind-mdm-credentials.txt
  • Per-VM configuration: /etc/hmdm/hmdm.env
  • Data disk: /var/lib/hmdm-data

Prerequisites

  • An active Azure subscription with permission to create virtual machines.
  • An SSH key pair for the azureuser account.
  • A virtual network and subnet in your target region.
  • A network security group that allows inbound 22/tcp from your management network and 80/tcp from the clients and Android devices that reach the server.

Recommended VM size: Standard_B2s (2 vCPU / 4 GiB RAM). The Tomcat application, the bundled PostgreSQL database and the nginx proxy run comfortably in that footprint, with roughly 2.8 GiB free.

Choose a larger size such as Standard_D2s_v3 if you plan to manage a large estate or upload a big application catalogue.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, and search the Marketplace for Headwind MDM by SecureAnyCloud. Select Create.

On Basics, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22), then add 80 after deployment.

Select Review + create, then Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:headwind-mdm-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name headwind-mdm \
  --image sac1647283583153:headwind-mdm-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 open-port --resource-group <your-rg> --name headwind-mdm --port 80 --priority 1010

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

You can look up the address at any time:

az vm show --resource-group <your-rg> --name headwind-mdm --show-details --query publicIps --output tsv

Step 4 - Confirm the stack is running

Headwind MDM runs as a two-container Docker Compose stack managed by a single systemd unit. On the first boot of your VM, the unit generates every secret, re-rolls the database and rotates the administrator password before the console is opened up. Allow a few minutes after the VM is created.

systemctl is-active hmdm.service

The unit reports active. List the containers to see the application and its bundled database:

sudo docker compose --env-file /etc/hmdm/hmdm.env -f /etc/hmdm/compose.yaml ps --format 'table {{.Service}}\t{{.Status}}'

Expected output:

SERVICE STATUS hmdm Up 12 minutes (healthy) postgresql Up 13 minutes (healthy)

You control the whole stack as one unit with sudo systemctl status hmdm and sudo systemctl restart hmdm.

Step 5 - Retrieve your per-VM administrator credentials

The first boot of your VM generates a unique administrator password and writes it to a root-only file. Confirm that the file exists and is 0600:

sudo ls -l /root/headwind-mdm-credentials.txt

Expected output:

-rw------- 1 root root 1058 Jul 25 15:18 /root/headwind-mdm-credentials.txt

Then read the file to get your credentials:

sudo cat /root/headwind-mdm-credentials.txt

The file contains HMDM_ADMIN_USERNAME (admin), HMDM_ADMIN_PASSWORD (your unique password), HMDM_URL (the console address) and HMDM_ENROLLMENT_BASE_URL (the address your Android devices enrol against). Store them somewhere safe.

Step 6 - Confirm the endpoints are healthy

The nginx proxy serves an unauthenticated health endpoint that suits a load balancer probe:

curl -s -o /dev/null -w 'health -> HTTP %{http_code}\n' http://127.0.0.1/healthz

Then check the admin console itself:

curl -s -o /dev/null -w 'console -> HTTP %{http_code}\n' http://127.0.0.1/

Both return HTTP 200.

Next, confirm which ports face the network. Only SSH and the nginx proxy are exposed. The Tomcat application is bound to loopback, and the bundled PostgreSQL is on no host port at all:

sudo ss -tln | awk 'NR==1 || /:80 |:8080 /{print $1, $4}'

Expected output:

State Local LISTEN 0.0.0.0:80 LISTEN 127.0.0.1:8080 LISTEN [::]:80

Your database and uploaded application files live on the dedicated data disk. Confirm it with:

df -h /var/lib/hmdm-data

Step 7 - Confirm the pinned release

The application and database images are pinned and baked into the image. Your VM never downloads anything at first boot and never silently upgrades. List the images with:

sudo docker compose --env-file /etc/hmdm/hmdm.env -f /etc/hmdm/compose.yaml images

The hmdm service reports tag 5.40.1. The postgresql service is pinned by image digest rather than a tag, so its tag column shows <none> by design.

Step 8 - Confirm the default credential is gone

This is the security property that makes the image safe to expose. Upstream Headwind MDM seeds an admin / admin account when it creates its database. Your VM rotated that account on first boot.

The console sends the uppercase MD5 of the password. Reproduce that and try the default:

DEFAULT_MD5=$(printf 'admin' | md5sum | awk '{print toupper($1)}')
curl -s -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d "{\"login\":\"admin\",\"password\":\"$DEFAULT_MD5\"}" \
  http://127.0.0.1/rest/public/auth/login

Expected output. The default credential is rejected:

{"status":"ERROR","message":null,"data":null}

This endpoint answers HTTP 200 even when authentication fails. Read the status field for the result, not the HTTP code.

A private endpoint reached without a session is refused outright:

curl -s -o /dev/null -w 'private (no session) -> HTTP %{http_code}\n' http://127.0.0.1/rest/private/configurations/search

It returns HTTP 403.

Step 9 - Sign in to the admin console

Browse to http://<vm-public-ip>/ and sign in with the username admin and the password from Step 5.

The Devices tab is the console's main view. It lists every enrolled Android device with its status, the configuration applied to it, and its permission and installation state. It also shows the per-device actions you can take.

Step 10 - Review configurations and applications

A configuration is the policy and application set that applies to a group of devices. The image ships two ready-made configurations that you can edit, copy or extend.

The Applications tab is your application catalogue. Add your own APK files or web links here. Assign them to a configuration so that every device in that group installs and updates them over the air.

Step 11 - Set your public enrolment URL

Android devices enrol against a public base URL. On first boot, your VM sets that URL to its own address so that the server works immediately:

sudo grep -E '^BASE_DOMAIN|^PROTOCOL' /etc/hmdm/hmdm.env

To enrol devices against your own domain name instead, edit BASE_DOMAIN in /etc/hmdm/hmdm.env. Then restart the stack so that the application picks up the change:

sudo sed -i 's/^BASE_DOMAIN=.*/BASE_DOMAIN=mdm.example.com/' /etc/hmdm/hmdm.env
sudo systemctl restart hmdm

The application regenerates its Tomcat context from that file on every start, so nothing else needs editing.

Point the DNS name at your VM before you enrol devices against it. Enrolled devices keep using the URL they were enrolled with.

Step 12 - Enrol your first Android device

Install the Headwind MDM launcher on the device and point it at your server. Then approve the device in the console:

  • In the console, open Devices and select Add to create a device entry. Alternatively, allow automatic creation from Settings.
  • Assign the device to a configuration, for example Common - Minimal.
  • On the Android device, install the Headwind MDM launcher from the Headwind MDM downloads page and enter your enrolment URL from Step 11.
  • The device appears in the Devices list and receives its configuration and applications over the air.

See the Headwind MDM documentation for the full enrolment, kiosk and REST API reference.

Server components

  • Headwind MDM 5.40.1: open-source Android Mobile Device Management server, admin console and REST API
  • Apache Tomcat 9: servlet container that runs the application, bound to 127.0.0.1:8080
  • PostgreSQL 16: bundled database that holds devices, configurations and the application catalogue
  • nginx 1.24: reverse proxy on port 80 and the /healthz endpoint
  • Docker Engine (Docker CE): container runtime for the two application containers

Filesystem layout

  • /: root filesystem on the OS disk
  • /boot: operating system kernel files
  • /boot/efi: UEFI boot partition (Gen2 Hyper-V)
  • /mnt: Azure temporary resource disk
  • /var/lib/hmdm-data: dedicated 30 GiB data disk that holds the database and uploaded files

Key directories:

  • /etc/hmdm/hmdm.env: per-VM configuration and secrets, 0600, root only
  • /etc/hmdm/compose.yaml: the Docker Compose definition of the stack
  • /var/lib/hmdm-data/pgdata: PostgreSQL data directory
  • /var/lib/hmdm-data/hmdm-work: application working directory and uploaded application files
  • /root/headwind-mdm-credentials.txt: your per-VM administrator credentials, 0600, root only
  • /etc/nginx/sites-available/hmdm: the reverse proxy configuration

Managing the service

  • Status: sudo systemctl status hmdm
  • Restart the whole stack: sudo systemctl restart hmdm
  • Stop the stack: sudo systemctl stop hmdm
  • Container status: sudo docker compose --env-file /etc/hmdm/hmdm.env -f /etc/hmdm/compose.yaml ps
  • Application logs: sudo docker compose --env-file /etc/hmdm/hmdm.env -f /etc/hmdm/compose.yaml logs --tail 100 hmdm
  • Database logs: sudo docker compose --env-file /etc/hmdm/hmdm.env -f /etc/hmdm/compose.yaml logs --tail 100 postgresql
  • Proxy status: sudo systemctl status nginx

Scripts and log files

  • /usr/local/sbin/hmdm-firstboot.sh: generates the per-VM secrets and enrolment base URL on first boot
  • /usr/local/sbin/hmdm-admininit.sh: rotates the seeded administrator credential on first boot and opens the console
  • /usr/local/sbin/hmdm-roundtrip.sh: proves the security model and a full database round trip
  • /var/lib/sac/data/hmdm-firstboot.done: marker written once the per-VM secrets exist
  • /var/lib/sac/data/hmdm-admininit.done: marker written once the administrator credential has been rotated
  • journalctl -u hmdm-firstboot: first-boot secret generation log
  • journalctl -u hmdm-admininit: administrator rotation log

On startup

The first boot of your VM runs two one-shot units in order.

  • hmdm-firstboot.service generates a unique administrator password, device shared secret and database password. It resolves your VM's public address into the enrolment base URL and writes /etc/hmdm/hmdm.env and /root/headwind-mdm-credentials.txt.
  • hmdm.service then starts the application, which creates its database schema.
  • hmdm-admininit.service finally replaces the seeded admin / admin credential with your unique password. It verifies that the default is rejected and that your password works, then writes its marker.

Until that last marker exists, nginx answers every console route with HTTP 503 and a short explanatory message. This is deliberate. The upstream default credential is never reachable from the network, not even during the two to five minutes that your VM takes to initialise.

Both markers are removed before the image is captured, so every VM you launch repeats this process from a clean database.

Troubleshooting

  • The console returns 503 with an initialising message: first boot has not finished. Wait a few minutes, then check journalctl -u hmdm-admininit -n 50.
  • The console returns 502: the application container is still starting. Check sudo docker compose --env-file /etc/hmdm/hmdm.env -f /etc/hmdm/compose.yaml logs --tail 100 hmdm.
  • You cannot reach the console from your browser: confirm that the network security group allows inbound 80/tcp. Then confirm that curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/ returns 200 on the VM.
  • Your password is rejected: confirm that you are using the value from /root/headwind-mdm-credentials.txt. Headwind MDM applies a brief lockout after repeated failures, so wait a minute before retrying.
  • Devices enrol against the wrong address: check BASE_DOMAIN in /etc/hmdm/hmdm.env, correct it and restart the stack. Devices keep the URL they were enrolled with.
  • The stack will not start: confirm that the data disk is mounted with df -h /var/lib/hmdm-data, then check journalctl -u hmdm -n 80.

Security recommendations

  • Change the administrator password from Settings in the console after your first sign in. Remove /root/headwind-mdm-credentials.txt once you have stored the value in your password manager.
  • Terminate TLS in front of the server. The image serves plain HTTP on port 80. Before you enrol production devices, put an Azure Application Gateway, an Azure Front Door or your own reverse proxy with a certificate in front of it. Set BASE_DOMAIN to that public name.
  • Restrict the network security group. Allow 22/tcp only from your management network. Allow 80/tcp only from the networks your devices and administrators use.
  • Keep the device shared secret private. It is generated per VM and lives in /etc/hmdm/hmdm.env, which is root only. Anything that can read it can impersonate a device.
  • Back up the data disk. Your fleet, configurations and uploaded applications live on /var/lib/hmdm-data. Take Azure snapshots of that disk, or run pg_dump against the database container.
  • Security patches continue to arrive automatically. Unattended upgrades remains enabled on the image.

Support

SecureAnyCloud provides 24/7 expert support for this image. Contact sales@secureanycloud.com.

Headwind MDM is open-source software licensed under the Apache License 2.0. SecureAnyCloud is not affiliated with or endorsed by Headwind MDM or h-mdm.com. The name is used only to identify the software included in this image.

Deploy on Azure

Launch Headwind MDM 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