Headwind MDM 5.41.1
Overview
Headwind MDM is an open-source mobile device management (MDM) server for Android. You enrol devices by scanning a QR code during device setup. You 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 SecureAnyCloud image is a hardened, ready-to-use Headwind MDM server (the open-source Community Edition).
This guide covers launching and using the Headwind MDM 5.41.1 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud identity, secrets and PKI listing on AWS Marketplace. You pick it as the software version when you subscribe and launch.
What you get:
- 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:8080, and its PostgreSQL database is on127.0.0.1:5432. Both use 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. First boot sets the instance's name from its EC2 public DNS name. 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, first boot replaces every one of them with a value unique to your instance, before anything is reachable. The demo device is removed, 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 a forged
X-Forwarded-Forcannot fool its audit log. Requests for any other host name are dropped, and 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.
- 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.
- Defaults: default user
ubuntu, architecture x86_64, recommended instance type m5.large. - 24/7 SecureAnyCloud support
Prerequisites
You need:
- 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, plus TCP 443 (the console and the devices) and TCP 80 (redirects to HTTPS). Do not open 8080, 5432 or 31000, because 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 talk only to a server whose certificate they trust. The self-signed certificate that the image starts with is fine for the console, but devices will not enrol against it (Step 6). - Disk space: the instance stores uploaded apps and files in
/var/lib/hmdm/data/files. Give the root volume room for the APKs you plan to distribute.
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, choose your Region, then choose Launch through EC2.
Choose m5.large, your key pair and a security group as described above.
To launch with the AWS CLI instead, 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.
To make the instance use your own name from the start, put it in /etc/sac/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.
Connect to your instance
On Ubuntu 24.04, the SSH user is ubuntu. Connect over SSH:
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
systemctl is-active hmdm-firstboot hmdm postgresql@16-main nginxactive active active activeOnly SSH, HTTP and HTTPS are public. Tomcat, PostgreSQL and the MQTT broker listen on the loopback address.
Check the listeners:
sudo ss -tln | awk 'NR>1{print $4}' | sed 's/^\[::ffff:\(.*\)\]/\1/' | grep -E ':(22|80|443|5432|8080|31000)$' | sort -u0.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 [::]:80curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/200Get your sign-in and the certificate fingerprint
sudo cat /root/headwind-mdm-credentials.txtThe file (root only, mode 0600) holds the console address, the administrator login and its password, the device settings password, and the SHA-256 fingerprint of the instance's certificate. A short how-to follows. This example shows both passwords as <redacted>:
# 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://secureanycloud.com/stacks/headwind-mdm-5-41-1/The login banner also shows the name, every address the instance answers to and the certificate fingerprint. You can print them again with:
hmdm-set-domain --showdomain: 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:EEThe certificate is self-signed and unique to your instance, so your browser warns about it. Open the certificate details in the browser and check that its SHA-256 fingerprint matches hmdm.tls.sha256_fingerprint. 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).
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 you sign in, the device list appears:
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. This instance serves the Headwind MDM launcher 6.40:
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 the name.
This command 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.comThen 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, with the key readable by root only. Then 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 nginxhmdm-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.
Add a device and enrol it
In the console, open Devices and choose Add. Give the device a number and pick a configuration.
Use a device number 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.
Open the device's QR code and 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.
You can do the same through Headwind's REST API. This example signs in with the credentials file, so the password never appears on a command line or on screen. It adds a device to the Managed Launcher configuration and reads the enrolment data, which names the configured host:
sudo bash <<'EOF'
D=$(cat /etc/sac/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" EOFadd device: OK enrolment base URL: https://ec2-34-203-204-87.compute-1.amazonaws.comDevice 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:31000Read this before you open it:
- The MQTT password that 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, because anyone who downloads the launcher has it. Headwind's broker gives that one shared credential read access to every device topic. This access rule is built into Headwind and cannot be narrowed per device. Only the server can publish, with a password unique to your instance, so devices cannot be sent commands. A third party who can reach port 31000 can still 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 that 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. Then switch the configurations that should use it to MQTT push in their settings:
sudo hmdm-mqtt enablesudo hmdm-mqtt disable puts the broker back on the loopback address.
Check the security posture
One command proves the whole chain on the instance. It checks that:
- The console answers over HTTPS.
- A private API call without a session is refused.
- The default
admin/adminis refused. - Your administrator signs in and reads the configurations from PostgreSQL.
- A wrong password is refused.
- Self-registration is refused.
Run it:
sudo /usr/local/sbin/hmdm-cred-roundtrip.shHMDM_ROUNDTRIP_OK console=200 no-session=403 default-admin=refused admin=OK configurations=read wrong-password=refused signup=403The certificate is an end-entity certificate for the instance's name and addresses. It cannot sign other certificates:
openssl x509 -in /etc/ssl/hmdm/hmdm.crt -noout -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltNamesubject=CN = ec2-34-203-204-87.compute-1.amazonaws.com, O = sac 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 Authenticationnginx passes the real client address to Headwind and never a client-supplied one. It also refuses cross-site sign-up and drops CORS headers:
grep -E 'proxy_set_header +X-(Real-IP|Forwarded-For)' /etc/nginx/sac/hmdm-proxy.conf
D=$(cat /etc/sac/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: 0The services are blocked from the instance metadata service. The first command shows the systemd rule on each service. The second command 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: 000The 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. The server's fetch never connects. A loopback address is the control that does connect.
Change the administrator password
sudo hmdm-admin-password rotateThis sets a new random password for admin and writes it to /root/headwind-mdm-credentials.txt. It also 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.
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.apkThe dump holds your configurations, users (password hashes) and device data. Copy it off the instance to encrypted storage, for example an encrypted S3 bucket, 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 and restore the dump into the empty hmdm database. Copy the files back, owned by hmdm, then start Headwind.
Licences, sources and security notes
Headwind MDM Community Edition, the launcher and Apache Tomcat are licensed under the Apache License 2.0. The instance holds the licences, the notices, the source locations and the list of raised libraries:
ls /usr/share/doc/sac-headwind-mdm/ head -3 /usr/share/doc/sac-headwind-mdm/raised-libraries.txt | cut -d' ' -f1-3LICENSE-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.jarNotes 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 that 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 that 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). They 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, because the application files are read-only. New Headwind releases arrive as new versions of this listing.
Support
SecureAnyCloud provides 24/7 support for this image by email at sales@secureanycloud.com. Include your instance ID and the version name from the listing.
Headwind MDM is a trademark of its respective owners. SecureAnyCloud is not affiliated with or endorsed by them.
Deploy on AWS
Launch Headwind MDM 5.41.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com