Headwind MDM 5.41.1 on AWS User Guide
Overview
This guide covers launching and using the Headwind MDM 5.41.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg identity, secrets and PKI listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Headwind MDM is an open source mobile device management (MDM) server for Android. You enrol devices by scanning a QR code during device setup, then manage them from a web console: group them into configurations, push apps and files, lock devices into a kiosk launcher, send messages, and see each device's status. The devices run the free Headwind MDM launcher app, which this server hands out itself.
The cloudimg image is a hardened, ready-to-use Headwind MDM server (the open source Community Edition):
- nginx in front, Headwind behind it. nginx is the only public web listener (ports 80 and 443). Headwind runs in Apache Tomcat on
127.0.0.1:8080and its PostgreSQL database on127.0.0.1:5432, the loopback address only. - HTTPS from the first boot. nginx serves the console with a certificate generated on your instance's first boot (an end-entity certificate, not a certificate authority), and port 80 redirects to HTTPS. The instance's name is set from its EC2 public DNS name at first boot; you change it to your own domain with one command.
- No default credentials. Upstream Headwind MDM ships the console login
admin/admin, the device settings password12345678in every configuration, fixed QR enrolment keys, a demo device and a hard-coded password for its push broker's administrator. On this image every one of them is replaced on first boot by a value unique to your instance (the demo device is removed), before anything is reachable, and the upstream defaults are refused. - No self-registration. Upstream's sign-up routes do not check whether sign-up is switched on; this image refuses them.
- Device push over HTTPS. Devices receive push notifications by HTTPS long polling. Headwind's MQTT broker listens on the loopback address only (see Step 8 before you open it).
- Browser protections. The session cookie is
Secure,HttpOnlyandSameSite=Strict; cross-site requests that change state are refused; uploaded files are served as downloads in a sandbox, never as pages of the console. - Real client addresses, fixed host name. nginx passes the real TCP peer to Headwind, so its audit log cannot be fooled by a forged
X-Forwarded-For; requests for any other host name are dropped; the enrolment data always names the configured host. - Metadata access blocked. Headwind fetches some URLs itself (for example the launcher file named in a configuration). Tomcat, PostgreSQL, nginx and the first-boot service are blocked from the instance metadata service, and the AMI requires IMDSv2.
- Patched libraries. The official Headwind MDM 5.41.1 web application is deployed unchanged except that 26 of its bundled Java libraries are raised to fixed releases (Step 12).
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Headwind MDM 5.41.1 (Community Edition) on Apache Tomcat 9.0.123, OpenJDK 17, PostgreSQL 16, nginx, the Headwind MDM launcher 6.40 for devices, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet.
- A security group that allows TCP 22 (SSH) from your own networks only, TCP 443 (the console and the devices) and TCP 80 (redirects to HTTPS). Do not open 8080, 5432 or 31000: they listen on the loopback address only.
- For enrolling real devices: a DNS name you control (for example
mdm.example.com) and a certificate for it from a certificate authority. Android devices only talk to a server whose certificate they trust; the self-signed certificate the image starts with is fine for the console but devices will not enrol against it (Step 6). - Disk: uploaded apps and files are stored on the instance (
/var/lib/hmdm/data/files). Give the root volume room for the APKs you plan to distribute.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Headwind MDM 5.41.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair, and a security group as above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --image-id <ami-id-from-the-listing> --instance-type m5.large \
--key-name <your-key-pair> --security-group-ids <your-security-group> --subnet-id <your-subnet> \
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":30,"VolumeType":"gp3"}}]' \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=headwind-mdm}]'
First boot creates the database and takes about half a minute after the instance is running. If you want the instance to use your own name from the start, put it in /etc/cloudimg/hmdm-domain with user data (for example a cloud-init write_files entry); otherwise first boot uses the instance's public DNS name, or its public IP address if it has no DNS name.
Step 2: Connect to your instance
| Operating system | SSH user |
|---|---|
| Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
systemctl is-active hmdm-firstboot hmdm postgresql@16-main nginx
active
active
active
active
Only SSH, HTTP and HTTPS are public. Tomcat, PostgreSQL and the MQTT broker listen on the loopback address:
sudo ss -tln | awk 'NR>1{print $4}' | sed 's/^\[::ffff:\(.*\)\]/\1/' | grep -E ':(22|80|443|5432|8080|31000)$' | sort -u
0.0.0.0:22
0.0.0.0:443
0.0.0.0:80
127.0.0.1:31000
127.0.0.1:5432
127.0.0.1:8080
[::]:22
[::]:443
[::]:80
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
200
Step 4: Get your sign-in and the certificate fingerprint
sudo cat /root/headwind-mdm-credentials.txt
The file (root only, mode 0600) gives the console address, the administrator login, its password and the device settings password (both shown here as <redacted>), and the SHA-256 fingerprint of the instance's certificate, followed by a short how-to:
# Headwind MDM 5.41.1 - generated on first boot by hmdm-firstboot.service. Unique to this instance. Store it safely.
hmdm.url=https://ec2-34-203-204-87.compute-1.amazonaws.com/
hmdm.admin_login=admin
hmdm.admin_password=<redacted>
hmdm.device_settings_password=<redacted>
hmdm.tls.sha256_fingerprint=A0:B5:99:ED:C8:E5:F0:36:46:94:5A:70:6D:10:7E:6C:33:F8:04:96:8E:BA:56:91:4E:86:2C:E1:B9:FE:DF:EE
# Sign in at hmdm.url with hmdm.admin_login / hmdm.admin_password.
# hmdm.device_settings_password unlocks the launcher's settings on enrolled devices (every configuration starts with it).
# The certificate is self-signed and unique to this instance: compare its SHA-256 fingerprint with the one your browser
# shows. Android devices only enrol against a certificate they trust: install a CA-issued certificate for your own name.
# Your own name (point DNS at this instance first): sudo hmdm-set-domain mdm.example.com
# New administrator password: sudo hmdm-admin-password rotate
# Device push runs over HTTPS (long polling). The MQTT broker (31000) is bound to loopback; its device credential is
# PUBLIC (built into the stock launcher): read the guide before you run sudo hmdm-mqtt enable.
# User guide: https://www.cloudimg.co.uk/guides/headwind-mdm-5-41-1-on-ubuntu-24-04-aws/
The name, every address the instance answers to and the certificate fingerprint are also shown at login and by:
hmdm-set-domain --show
domain: ec2-34-203-204-87.compute-1.amazonaws.com
console: https://ec2-34-203-204-87.compute-1.amazonaws.com/
also served: ec2-34-203-204-87.compute-1.amazonaws.com 34.203.204.87 172.31.92.49
certificate: SHA-256 A0:B5:99:ED:C8:E5:F0:36:46:94:5A:70:6D:10:7E:6C:33:F8:04:96:8E:BA:56:91:4E:86:2C:E1:B9:FE:DF:EE
The certificate is self-signed and unique to your instance, so your browser warns about it. Open the certificate details in the browser, check that its SHA-256 fingerprint matches hmdm.tls.sha256_fingerprint, and only then continue. Do not add the certificate to a trust store; for devices and production use, install a certificate from a certificate authority (Step 6).
Step 5: Sign in to the console
Open hmdm.url from the credentials file in your browser. Check the certificate fingerprint as above, then sign in as admin with hmdm.admin_password.

The upstream default admin / admin is refused:

After signing in you see the device list:

Configurations hold the settings and apps that devices receive. Three upstream configurations are ready to use (Managed Launcher, MIUI and Background mode); each starts with the device settings password from your credentials file:


Applications lists the apps you can assign; the Headwind MDM launcher 6.40 is served by this instance:

Step 6: Use your own name and a trusted certificate
Point a DNS name at the instance (an Elastic IP keeps the address stable), then tell the image to use it. This pins nginx to the name, moves Headwind's base URL and the download address of the launcher served by this instance to it, and issues a new self-signed certificate for it:
sudo hmdm-set-domain mdm.example.com
Then install a certificate from a certificate authority for that name, so that browsers and Android devices trust the server. Copy the full chain and its private key into place (the key readable by root only) and reload nginx:
sudo install -m 0644 fullchain.pem /etc/ssl/hmdm/hmdm.crt
sudo install -m 0600 privkey.pem /etc/ssl/hmdm/hmdm.key
sudo nginx -t && sudo systemctl reload nginx
hmdm-set-domain never replaces a certificate that is not the image's own self-signed one. Do not add the self-signed certificate to a trust store on your computers or devices.
Step 7: Add a device and enrol it
In the console, open Devices, choose Add, give the device a number (use an identifier that is hard to guess: anyone who knows a device number can read that device's configuration from the public device API, which is how Headwind MDM works) and pick a configuration. Then open the device's QR code: scan it on a factory-reset Android device (tap the welcome screen six times to start QR provisioning). The device downloads the launcher from this server and enrols.
The same can be done through Headwind's REST API. This example signs in with the credentials file (the password never appears on a command line or on screen), adds a device to the Managed Launcher configuration, and reads the enrolment data, which names the configured host:
sudo bash <<'EOF'
D=$(cat /etc/cloudimg/hmdm-domain); J=$(mktemp -d)
sed -n 's/^hmdm.admin_password=//p' /root/headwind-mdm-credentials.txt | tr -d '\n' | md5sum \
| awk '{printf "{\"login\":\"admin\",\"password\":\"%s\"}", toupper($1)}' > "$J/login.json"
curl -sk -c "$J/jar" -o /dev/null -H 'Content-Type: application/json' --data @"$J/login.json" "https://$D/rest/public/auth/login"
printf '{"number":"%s","description":"warehouse scanner 1","configurationId":1}' "scanner-$(openssl rand -hex 4)" > "$J/dev.json"
curl -sk -b "$J/jar" -H 'Content-Type: application/json' -H "Origin: https://$D" -X PUT --data @"$J/dev.json" \
"https://$D/rest/private/devices" | python3 -c 'import json,sys; print("add device:", json.load(sys.stdin)["status"])'
KEY=$(curl -sk -b "$J/jar" "https://$D/rest/private/configurations/search" \
| python3 -c 'import json,sys; print([c["qrCodeKey"] for c in json.load(sys.stdin)["data"] if c["id"] == 1][0])')
curl -sk "https://$D/rest/public/qr/json/$KEY" | python3 -c 'import json,sys; print("enrolment base URL:", json.load(sys.stdin)["com.hmdm.BASE_URL"])'
rm -rf "$J"
EOF
add device: OK
enrolment base URL: https://ec2-34-203-204-87.compute-1.amazonaws.com

Step 8: Device push and the MQTT broker
Headwind MDM tells devices about changes by push notifications. On this image every configuration uses HTTPS long polling: devices keep a request open to https://<your name>/rest/notification/polling/<device> over port 443, so nothing else needs to be open.
Headwind can also push over MQTT (port 31000). On this image the broker listens on the loopback address only:
sudo hmdm-mqtt status
sudo ss -tln | awk 'NR>1{print $4}' | grep ':31000$' | sed 's/^\[::ffff:\(.*\)\]/\1/'
MQTT broker: 127.0.0.1:31000
127.0.0.1:31000
Read this before you open it:
- The MQTT password devices use is derived from the request-signature secret compiled into the stock Headwind MDM launcher app (
changeme-C3z9vi54). That value is public, not a secret: anyone who downloads the launcher has it. Headwind's broker gives that one shared credential read access to every device topic, and this access rule is built into Headwind (it cannot be narrowed per device). Only the server can publish (with a password unique to your instance), so devices cannot be sent commands, but a third party who can reach port 31000 can watch your push traffic. - MQTT on port 31000 is not encrypted.
- The same public value signs every server response to devices. The stock launcher checks it, so it cannot be changed per instance without breaking every device; a private value needs a launcher you build yourself with the same value (see the Headwind MDM documentation on building the launcher).
If you still want MQTT push, enable the broker, open TCP 31000 in the security group only to your devices' networks (never 0.0.0.0/0), and switch the configurations that should use it to MQTT push in their settings:
sudo hmdm-mqtt enable
sudo hmdm-mqtt disable puts the broker back on the loopback address.
Step 9: Check the security posture
One command proves the chain on the instance: the console answers over HTTPS, a private API call without a session is refused, the default admin / admin is refused, your administrator signs in and reads the configurations from PostgreSQL, a wrong password is refused and self-registration is refused:
sudo /usr/local/sbin/hmdm-cred-roundtrip.sh
HMDM_ROUNDTRIP_OK console=200 no-session=403 default-admin=refused admin=OK configurations=read wrong-password=refused signup=403
The certificate is an end-entity certificate for the instance's name and addresses, which cannot sign other certificates:
openssl x509 -in /etc/ssl/hmdm/hmdm.crt -noout -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltName
subject=CN = ec2-34-203-204-87.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-34-203-204-87.compute-1.amazonaws.com, IP Address:34.203.204.87, IP Address:172.31.92.49, DNS:localhost, IP Address:127.0.0.1
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
nginx passes the real client address to Headwind and never a client-supplied one, refuses cross-site sign-up and drops CORS headers:
grep -E 'proxy_set_header +X-(Real-IP|Forwarded-For)' /etc/nginx/cloudimg/hmdm-proxy.conf
D=$(cat /etc/cloudimg/hmdm-domain)
curl -sk -o /dev/null -w 'self-registration: %{http_code}\n' -H 'Content-Type: application/json' --data '{}' "https://$D/rest/public/signup/verifyEmail"
echo "CORS headers: $(curl -sk -D - -o /dev/null -H 'Origin: https://evil.example' "https://$D/rest/public/auth/options" | grep -ci '^access-control-' || true)"
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
self-registration: 403
CORS headers: 0
The services are blocked from the instance metadata service. The first command shows the systemd rule on each service; the second makes a metadata request from inside Headwind's own service, as its user, and gets no connection (000):
systemctl show -p IPAddressDeny hmdm nginx postgresql@16-main hmdm-firstboot
sudo bash -c 'cg=$(systemctl show -p ControlGroup --value hmdm.service); echo $$ > /sys/fs/cgroup$cg/cgroup.procs; cd /; setpriv --reuid=hmdm --regid=hmdm --init-groups curl -s -m 5 -o /dev/null -w "imds from hmdm: %{http_code}\n" -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 60" http://169.254.169.254/latest/api/token; true'
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 ::ffff:169.254.169.254/128
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 ::ffff:169.254.169.254/128
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 ::ffff:169.254.169.254/128
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 ::ffff:169.254.169.254/128
imds from hmdm: 000
The same block applies to what Headwind fetches on your behalf: the image's own validation points the launcher URL of a configuration at the metadata service and requests its QR code, and the server's fetch never connects (a loopback address is the control that does).
Step 10: Change the administrator password
sudo hmdm-admin-password rotate
This sets a new random password for admin, writes it to /root/headwind-mdm-credentials.txt, signs out every session and voids every API token issued before. To add more console users, use Users in the console with long, unique passwords (Headwind stores console passwords as salted SHA-1 hashes of their MD5, its own scheme, so strong passwords matter).
Step 11: Back up and restore
Back up the database and the uploaded files together:
sudo install -d -m 0700 /var/backups/hmdm
sudo -u postgres pg_dump -Fc hmdm | sudo tee /var/backups/hmdm/hmdm-db.dump > /dev/null
sudo tar -C /var/lib/hmdm/data -czf /var/backups/hmdm/hmdm-files.tgz files
sudo pg_restore -l /var/backups/hmdm/hmdm-db.dump | grep -c 'TABLE DATA' | sed 's/^/tables in the dump: /'
sudo tar -tzf /var/backups/hmdm/hmdm-files.tgz | sed -n '2p'
tables in the dump: 51
files/hmdm-6.40-os.apk
The dump holds your configurations, users (password hashes) and device data: copy it off the instance to encrypted storage (for example an S3 bucket with encryption) and keep it private. EBS snapshots of the instance are an alternative.
To restore on a new instance launched from the same version, stop Headwind, restore the dump into the empty hmdm database and copy the files back (owned by hmdm), then start Headwind.
Step 12: Licences, sources and security notes
Headwind MDM Community Edition, the launcher and Apache Tomcat are licensed under the Apache License 2.0. The licences, notices, the source locations and the list of raised libraries are on the instance:
ls /usr/share/doc/cloudimg-headwind-mdm/
head -3 /usr/share/doc/cloudimg-headwind-mdm/raised-libraries.txt | cut -d' ' -f1-3
LICENSE-apache-tomcat.txt
LICENSE-headwind-mdm-Apache-2.0.txt
NOTICE-apache-tomcat.txt
NOTICE-headwind-mdm.txt
SOURCE.txt
raised-libraries.txt
jackson-core-2.10.0.jar -> jackson-core-2.18.12.jar
jackson-databind-2.10.0.jar -> jackson-databind-2.18.12.jar
jackson-annotations-2.10.0.jar -> jackson-annotations-2.18.12.jar
Notes on what is shipped:
- Raised libraries. The official
hmdm-5.41.1-os.warbundles Java libraries with published advisories (Jackson, ActiveMQ, the PostgreSQL JDBC driver, Guava, MyBatis, SnakeYAML, swagger, org.json, Commons BeanUtils and others). Each is replaced by a fixed release from Maven Central (raised-libraries.txt); Headwind's own code is unchanged. - Remaining advisories, not reachable here. Liquibase 3.6.3 (CVE-2022-0839, XML external entities) reads only the database change logs shipped inside the application; the fix is in Liquibase 4, a new major version Headwind 5.41.1 is not built for. ClassGraph (CVE-2021-47621) scans only the application's own classes. OGNL inside MyBatis (CVE-2026-113730) evaluates only the application's own mapper expressions.
- AngularJS 1.8.3. The console is built on AngularJS, which reached end of life in 2022 and receives no fixes. Its open advisories affect the browser, not the server: the high-severity ones (CVE-2024-21490,
ng-srcset; CVE-2026-11998, SCE URL lists) need features the console does not use; the moderate and low ones (CVE-2022-25844, CVE-2023-26116/26117/26118, CVE-2024-8372/8373, CVE-2025-0716, CVE-2025-2336) concern text that only signed-in console users and enrolled devices can supply. - Optional helper apps. The default configurations refer to three optional Headwind helper apps (Pager, Dialer Helper, update helper) that are downloaded from h-mdm.com when you assign them; their sources carry no licence, so they are not included in the image.
- Updates. The console's Check for updates contacts h-mdm.com; it cannot replace the application on this image (the application files are read-only). New Headwind releases arrive as new versions of this listing.
Support
cloudimg provides support for this image by email at support@cloudimg.co.uk. Please include your instance ID and the version name from the listing.
Headwind MDM is a trademark of its respective owners. cloudimg is not affiliated with or endorsed by them.