UpSnap on Ubuntu 24.04 on Azure User Guide
Overview
This image runs UpSnap 5.4.4 on Ubuntu 24.04 LTS. UpSnap is a small, fast, open source web application for powering the machines on your own network on and off. Register a device once with its MAC address, then wake it with a single click, reboot it, or shut it down with a command you define. A live dashboard pings every registered device so you always know what is up, and cron style schedules turn the whole thing into unattended automation.
Wake on LAN is built into virtually every business class network adapter, yet almost nobody uses it, because the magic packet has to originate on the same broadcast domain as the target machine. UpSnap is the piece that closes that gap: one small always on host inside your network that holds the device list and sends the packets, with a browser interface you can reach from anywhere you already trust.
What is included:
- UpSnap 5.4.4, installed from the pinned upstream release binary, verified against the SHA256 published in the release's own
checksums.txt - nginx terminating TLS on port
443, the only path to the application, with a certificate generated on your VM's first boot - An administrator account created on your VM's first boot with a 28 character password
- A dedicated Azure data disk at
/var/lib/upsnapholding the device database, schedules, permissions and users - nmap, for the optional on demand discovery pass over a subnet you supply
- An unauthenticated
/healthzendpoint for load balancer and monitoring probes upsnap.serviceandnginx.serviceas enabled systemd units- 24/7 cloudimg support
How this image is secured
A device power manager is a powerful thing to put on a network. UpSnap's shutdown feature runs a shell command that you define, and the upstream project is explicit in its README that UpSnap should not be published to the open internet. This image is built to match that advice rather than ignore it.
- The application is bound to
127.0.0.1:8090only. It is never bound to a public interface. nginx is the only way in, and the build verifies with the services running that nothing unexpected is listening on a non loopback address. - The service runs as a dedicated non root account (
upsnap) holding exactly one Linux capability,CAP_NET_RAW, which it needs for privileged ping and discovery. The shutdown command you define therefore cannot become a root shell on the VM. - Authentication is mandatory and cannot be disabled. Upstream removed the ability to turn it off in version 3.1.0. Every device, wake, shutdown and discovery route is permission checked, and this guide proves it below.
- Anonymous account creation is closed. Upstream ships the account collection open to anyone who can reach it, which would let a passer by create their own login. This image closes that on first boot, so only your administrator can add accounts. The guide proves this below too.
- Your administrator password is unique to your VM. It is generated on the first boot of your own VM, is 28 characters, and is written to a file only
rootcan read. Nothing is baked into the image and no two VMs share a credential. - The database is created on your VM, not shipped in the image, so the token signing keys behind your sessions are yours alone.
- Nothing is preloaded. The image ships with no device, no scan range, no wake or shutdown command and no schedule. It performs no action against any network until you configure one.
A note on what this product does
UpSnap sends Wake on LAN magic packets, pings devices, can run a shutdown command you define, and can run an on demand discovery pass over a CIDR you supply. Only ever point it at networks you own or are authorised to manage. The image ships with no scan range configured and will not touch a single machine until you tell it to.
Because the console controls power for real machines, treat it as management plane infrastructure: restrict the Network Security Group to addresses you trust, and prefer reaching it over a VPN or Azure ExpressRoute rather than publishing it to the internet.
Prerequisites
Before you deploy this image you need:
- An active Azure subscription with permission to deploy Marketplace VM images
- An SSH key pair for access as
azureuser - A virtual network and subnet in your target region
- A Network Security Group allowing inbound TCP 22 (SSH) and TCP 443 (the console) from the addresses you will connect from
- A route from the VM to the network whose devices you intend to manage. Magic packets are broadcast within a subnet, so the VM must sit on, or be bridged to, that broadcast domain
Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting size. UpSnap is a single small Go binary with an embedded database, so it is not demanding; size up only if you manage a very large estate with frequent ping intervals.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for UpSnap by cloudimg, and select Create.
On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and set the username to azureuser, pasting your public key. Under Inbound port rules allow SSH (22) and HTTPS (443), and restrict both to your own address rather than leaving them open to the internet.
On the Disks tab you will see the dedicated data disk that carries /var/lib/upsnap. Leave it attached: it is where your devices, schedules and users live. Then choose Review + create and Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name upsnap \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
Then open the console port to your own address only. Replace <your-address> with the address you administer from:
az vm open-port --resource-group <your-rg> --name upsnap --port 443 --priority 900
For a tighter rule that names the source address explicitly:
az network nsg rule create \
--resource-group <your-rg> \
--nsg-name upsnapNSG \
--name allow-console \
--priority 900 \
--source-address-prefixes <your-address> \
--destination-port-ranges 443 \
--access Allow --protocol Tcp
Step 3 - Connect and confirm first boot has finished
Connect over SSH as azureuser.
ssh azureuser@<vm-public-ip>
First boot creates the database, the administrator account and the TLS certificate. It normally completes within a minute of the VM being reported as running.
sudo test -f /var/lib/cloudimg/upsnap-firstboot.done \
|| { echo "FATAL: first boot has not finished yet - check: systemctl status upsnap-firstboot"; exit 1; }
echo "first boot completed at $(sudo cat /var/lib/cloudimg/upsnap-firstboot.done)"
Confirm both services are running, see what is published against where the application actually listens, and confirm the dedicated data disk is mounted:
ACT=$(systemctl is-active upsnap.service nginx.service 2>/dev/null | tr '\n' ' ')
echo "upsnap.service nginx.service -> $ACT"
case "$ACT" in
"active active "*) echo "both services are active" ;;
*) echo "FATAL: expected both services active"; exit 1 ;;
esac
sudo ss -Hltn | awk '{print $4}' | grep -E ':(80|443|8090)$' | sort -u
findmnt -no SOURCE,TARGET,FSTYPE,SIZE /var/lib/upsnap

Step 4 - Retrieve your administrator credentials
The credentials generated for your VM live in a root only file. Read it on your own VM with:
sudo cat /root/upsnap-credentials.txt
The command below confirms the file is present and correctly populated without printing your password:
sudo grep -E '^UPSNAP_(URL|ADMIN_EMAIL|APP_LISTEN)=' /root/upsnap-credentials.txt
sudo grep -qE '^UPSNAP_ADMIN_PASSWORD=.{24,}$' /root/upsnap-credentials.txt \
|| { echo "FATAL: no per-VM administrator password was generated"; exit 1; }
echo "administrator password present: 28 characters, unique to this VM"
PERMS=$(sudo stat -c '%a %U:%G' /root/upsnap-credentials.txt)
[ "$PERMS" = "600 root:root" ] \
|| { echo "FATAL: credentials file permissions are $PERMS, expected 600 root:root"; exit 1; }
echo "credentials file permissions: $PERMS"
The health endpoint is the only route that answers without a session. It exists so an Azure Load Balancer or your monitoring can probe the VM without holding a credential:
HZ=$(curl -sk -o /dev/null -w '%{http_code}' https://127.0.0.1/healthz)
[ "$HZ" = "200" ] || { echo "FATAL: /healthz returned $HZ, expected 200"; exit 1; }
echo "/healthz: HTTP $HZ (unauthenticated, for load balancer probes)"
SHELL_CODE=$(curl -sk -o /dev/null -w '%{http_code}' https://127.0.0.1/)
[ "$SHELL_CODE" = "200" ] || { echo "FATAL: the console returned $SHELL_CODE, expected 200"; exit 1; }
echo "console: HTTP $SHELL_CODE"

Step 5 - Verify the image is closed by default
These are the checks that matter most on a device power manager. Every privileged route must refuse a caller with no session, and a refusal here is the expected and correct result.
CREATE=$(curl -sk -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
--data-binary '{"name":"x","ip":"127.0.0.1","mac":"00:11:22:33:44:55","netmask":"255.255.255.0"}' \
https://127.0.0.1/api/collections/devices/records)
case "$CREATE" in
400|401|403) echo "device create without a session: HTTP $CREATE (correctly refused)" ;;
*) echo "FATAL: an unauthenticated caller could create a device (HTTP $CREATE)"; exit 1 ;;
esac
for route in wake shutdown; do
CODE=$(curl -sk -o /dev/null -w '%{http_code}' "https://127.0.0.1/api/upsnap/$route/anything")
case "$CODE" in
401|403) echo "$route without a session: HTTP $CODE (correctly refused)" ;;
*) echo "FATAL: the $route route answered an unauthenticated caller (HTTP $CODE)"; exit 1 ;;
esac
done
SCAN=$(curl -sk -o /dev/null -w '%{http_code}' https://127.0.0.1/api/upsnap/scan)
case "$SCAN" in
401|403) echo "discovery without a session: HTTP $SCAN (correctly refused)" ;;
*) echo "FATAL: the discovery route answered an unauthenticated caller (HTTP $SCAN)"; exit 1 ;;
esac
LIST=$(curl -sk https://127.0.0.1/api/collections/devices/records | jq -r '.totalItems // -1')
[ "$LIST" = "0" ] \
|| { echo "FATAL: the device list returned $LIST record(s) to a caller with no session"; exit 1; }
echo "device list without a session: 0 records"
Anonymous account creation is closed on this image. A stranger who can reach the console cannot register themselves a login:
REG=$(curl -sk -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
--data-binary '{"username":"anonymous","password":"anonanon12345","passwordConfirm":"anonanon12345"}' \
https://127.0.0.1/api/collections/users/records)
[ "$REG" != "200" ] \
|| { echo "FATAL: an unauthenticated caller created an account (HTTP $REG)"; exit 1; }
echo "anonymous self registration: HTTP $REG (correctly refused)"
The image also ships a fail closed audit of every listening socket, which rejects anything unexpectedly bound to a non loopback address, and the service identity holds exactly one Linux capability:
sudo /usr/local/sbin/upsnap-exposure-check.sh '22 80 443' \
|| { echo "FATAL: an unexpected non-loopback listener exists"; exit 1; }
PID=$(systemctl show -p MainPID --value upsnap.service)
SVCUSER=$(ps -o user= -p "$PID" | tr -d ' ')
[ "$SVCUSER" = "upsnap" ] \
|| { echo "FATAL: the service runs as $SVCUSER, expected the unprivileged upsnap account"; exit 1; }
echo "service runs as: $SVCUSER (pid $PID)"
grep -E '^CapBnd:' "/proc/$PID/status"
echo "capability bounding set is CAP_NET_RAW only (0x2000)"

The application must appear on 127.0.0.1:8090 and nowhere else. This is the property that keeps the console from being reachable without going through the TLS proxy:
BOUND=$(sudo ss -Hltn | awk '{print $4}' | grep ':8090$' | paste -sd' ' -)
echo "application bindings: ${BOUND:-none}"
[ -n "$BOUND" ] || { echo "FATAL: the application is not listening"; exit 1; }
for ep in $BOUND; do
case "$ep" in
127.*|'[::1]:8090') ;;
*) echo "FATAL: the application is bound to $ep, which is not loopback"; exit 1 ;;
esac
done
echo "confirmed: the application is reachable only from inside the VM"
Step 6 - Confirm your administrator account authenticates
This block reads your password from the root only credentials file at run time. It never prints it, and it proves both that the right password works and that a wrong one does not.
AE=$(sudo grep -m1 '^UPSNAP_ADMIN_EMAIL=' /root/upsnap-credentials.txt | cut -d= -f2-)
AP=$(sudo grep -m1 '^UPSNAP_ADMIN_PASSWORD=' /root/upsnap-credentials.txt | cut -d= -f2-)
OK=$(curl -sk -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
--data-binary "{\"identity\":\"$AE\",\"password\":\"$AP\"}" \
https://127.0.0.1/api/collections/_superusers/auth-with-password)
[ "$OK" = "200" ] || { echo "FATAL: the administrator account did not authenticate (HTTP $OK)"; exit 1; }
echo "administrator sign in: HTTP $OK"
BAD=$(curl -sk -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
--data-binary "{\"identity\":\"$AE\",\"password\":\"not-the-real-password\"}" \
https://127.0.0.1/api/collections/_superusers/auth-with-password)
[ "$BAD" = "400" ] || { echo "FATAL: a wrong password returned $BAD, expected a refusal"; exit 1; }
echo "wrong password: HTTP $BAD (correctly refused)"
Step 7 - Sign in
Open https://<vm-public-ip>/ in a browser. The certificate is generated on your VM and is self signed, so your browser will warn once; accept it, or replace the certificate with your own as described further down.
Sign in with the UPSNAP_ADMIN_EMAIL and UPSNAP_ADMIN_PASSWORD values from /root/upsnap-credentials.txt.

Change the administrator password from the account page as soon as you have signed in.
Step 8 - Add your first device
Choose New, and stay on the Manual tab. A device needs a name, an IP address, a MAC address and a netmask. The MAC address is what the magic packet is addressed to; the IP address and netmask are what UpSnap pings to decide whether the device is up.

You can also add devices from the API. Substitute your own values:
curl -sk -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
--data-binary '{"name":"Build Runner 01","ip":"<device-ip>","mac":"<device-mac>","netmask":"255.255.255.0","port":9}' \
https://127.0.0.1/api/collections/devices/records
Step 9 - The device dashboard
Every registered device gets a card showing its name, IP address, MAC address and a power button. The dashboard pings each device on a schedule, so the status you see is live: the power button is red while the device is down, and pressing it sends the magic packet and switches the card to a countdown while UpSnap waits for the machine to answer.

Prove a magic packet is actually sent
This block registers a temporary device, sends it a magic packet through the wake endpoint, then removes it again, leaving your device list exactly as it was. It proves the headline feature works end to end on your own VM.
AE=$(sudo grep -m1 '^UPSNAP_ADMIN_EMAIL=' /root/upsnap-credentials.txt | cut -d= -f2-)
AP=$(sudo grep -m1 '^UPSNAP_ADMIN_PASSWORD=' /root/upsnap-credentials.txt | cut -d= -f2-)
TOKEN=$(curl -sk -H 'Content-Type: application/json' \
--data-binary "{\"identity\":\"$AE\",\"password\":\"$AP\"}" \
https://127.0.0.1/api/collections/_superusers/auth-with-password | jq -r '.token // empty')
[ -n "$TOKEN" ] || { echo "FATAL: could not obtain a session token"; exit 1; }
DEV=$(curl -sk -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
--data-binary '{"name":"guide-check","ip":"127.0.0.1","mac":"00:11:22:33:44:55","netmask":"255.255.255.0","port":9}' \
https://127.0.0.1/api/collections/devices/records | jq -r '.id // empty')
[ -n "$DEV" ] || { echo "FATAL: could not register the temporary device"; exit 1; }
echo "temporary device registered: $DEV"
WAKE=$(curl -sk -o /dev/null -w '%{http_code}' -H "Authorization: $TOKEN" \
"https://127.0.0.1/api/upsnap/wake/$DEV")
[ "$WAKE" = "200" ] || { echo "FATAL: the wake endpoint returned $WAKE, expected 200"; exit 1; }
echo "magic packet sent: HTTP $WAKE"
DEL=$(curl -sk -o /dev/null -w '%{http_code}' -X DELETE -H "Authorization: $TOKEN" \
"https://127.0.0.1/api/collections/devices/records/$DEV")
[ "$DEL" = "204" ] || { echo "FATAL: could not remove the temporary device (HTTP $DEL)"; exit 1; }
echo "temporary device removed: HTTP $DEL"
Step 10 - Schedule power on and shutdown
Each device carries an optional wake cron and shutdown cron, plus the shutdown command to run. The shutdown command runs on this VM as the unprivileged upsnap account, so it is normally an SSH command against the target machine. Key based SSH is strongly preferred over putting a password in the command.
The ping interval that drives the live status is set globally under Settings, using cron syntax. Lazy ping stops UpSnap pinging when nobody is looking at the dashboard, which is useful on a large estate.

Step 11 - Add more people, with only the access they need
UpSnap has a per user permission model, so you can hand someone the power button for one machine without giving them the rest of the estate. Because anonymous account creation is closed on this image, accounts are created by you: open Users, add the person, then grant them read, power, update or delete rights on the specific devices they should control.
Step 12 - Discover devices on your own subnet
Rather than typing MAC addresses by hand, UpSnap can run a single on demand discovery pass over a CIDR you supply, and offer the results for you to add. Choose New, then the Network Scan tab.
The image ships with no scan range configured. You must supply the range yourself, and you must only scan networks you own or are authorised to scan. Set the range to your own subnet, for example:
<your-subnet-cidr>
Reaching the network you intend to manage
A magic packet is a broadcast frame. It only reaches devices in the same broadcast domain as the sender, so where you place this VM decides what it can wake.
- Devices in the same virtual network subnet work directly.
- Devices on your own premises need a path from the VM into that LAN: an Azure Site-to-Site VPN or ExpressRoute, with the VM's traffic routed into the target subnet.
- Directed broadcast across a router is disabled on most equipment by default and is not recommended; putting the VM inside the target broadcast domain is the reliable approach.
Confirm what this VM can route to:
ip route
Replacing the TLS certificate
First boot issues a self signed certificate naming your VM's address. To use your own certificate, replace these two files and reload nginx:
/etc/nginx/tls/upsnap.crt (certificate chain, 0644 root:root)
/etc/nginx/tls/upsnap.key (private key, 0600 root:root)
sudo install -o root -g root -m 0644 your-chain.crt /etc/nginx/tls/upsnap.crt
sudo install -o root -g root -m 0600 your-private.key /etc/nginx/tls/upsnap.key
sudo nginx -t && sudo systemctl reload nginx
Backing up
Everything that matters lives on the dedicated Azure data disk mounted at /var/lib/upsnap, so a snapshot of that disk is a complete backup of your devices, schedules, permissions and users. The disk is separate from the OS disk and can be resized and snapshotted independently.
echo "back up this disk:"
findmnt -no SOURCE,TARGET /var/lib/upsnap
sudo du -sh /var/lib/upsnap/pb_data
Troubleshooting
The console does not load. Check both services and the proxy configuration:
systemctl is-active upsnap.service nginx.service 2>/dev/null | tr '\n' ' '; echo
sudo nginx -t
First boot did not complete. Read its journal:
sudo journalctl -u upsnap-firstboot.service --no-pager -n 40
A device never shows as online. UpSnap pings it from this VM. Confirm the VM can reach the device at all, and remember that the ping is what drives status, not the magic packet. The ping interval is configured under Settings in the web interface.
A device does not wake. Wake on LAN must be enabled in the target machine's firmware and its network adapter must be configured to allow it while powered off. Confirm the MAC address is the one belonging to the wired adapter, and that this VM is in the same broadcast domain as the target.
The browser warns about the certificate. That is expected: the certificate is generated on your own VM and is self signed. Replace it with your own certificate as described above.
Support
cloudimg provides 24/7 technical support for this image by email and live chat.
- Email: support@cloudimg.co.uk
- Live chat: available 24/7 at cloudimg.co.uk
UpSnap is distributed under the MIT License. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.