Networking Azure

UpSnap on Ubuntu 24.04 on Azure User Guide

| Product: UpSnap on Ubuntu 24.04 LTS on Azure

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/upsnap holding the device database, schedules, permissions and users
  • nmap, for the optional on demand discovery pass over a subnet you supply
  • An unauthenticated /healthz endpoint for load balancer and monitoring probes
  • upsnap.service and nginx.service as 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:8090 only. 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 root can 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

The upsnap and nginx services active, ports 80 and 443 published by the proxy while the application itself listens only on 127.0.0.1:8090, and the dedicated ext4 Azure data disk mounted at /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"

The root only credentials file showing the administrator account, the console URL and the loopback application listener with the password redacted, and the unauthenticated health probe returning HTTP 200

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)"

Every privileged route refusing an unauthenticated caller, anonymous self registration refused with HTTP 403, the service running as the unprivileged upsnap account with CAP_NET_RAW as its only capability, and no device or scan range preloaded

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.

The UpSnap sign in page, asking for the email or username and the password generated on this VM's first boot

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.

The UpSnap new device form on the Manual tab, with the name, IP, MAC and netmask fields filled in, plus the optional port checks and device link sections

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.

The UpSnap device dashboard showing a registered device card with its description, IP address, MAC address and the power button that sends the magic packet

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.

The UpSnap settings page showing the cron based ping interval with its syntax reference and the lazy ping option

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.

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.