Deployment guide

UpSnap

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. 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. The magic packet has to originate on the same broadcast domain as the target machine.

UpSnap closes that gap. It is one small always-on host inside your network that holds the device list and sends the packets. You reach its browser interface from anywhere you already trust.

What you get:

  • 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 SecureAnyCloud 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 and choose Create a resource. Search the Marketplace for UpSnap by SecureAnyCloud, and select Create.

On Basics, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key, set the username to azureuser, and paste your public key.

Under Inbound port rules, allow SSH (22) and HTTPS (443). 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, because it is where your devices, schedules and users live. Then choose Review + create and Create.

Step 2 - Deploy from the Azure CLI

Accept the Marketplace terms once per subscription, then create the VM. Use your own resource group name:

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:upsnap:default:latest
az vm create \
  --resource-group <your-rg> \
  --name upsnap \
  --image sac1647283583153:upsnap:default:latest \
  --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/sac/data/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/sac/data/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. It rejects anything unexpectedly bound to a non-loopback address. Run the audit:

sudo /usr/local/sbin/upsnap-exposure-check.sh '22 80 443' \
  || { echo "FATAL: an unexpected non-loopback listener exists"; exit 1; }

Then confirm the service identity holds exactly one Linux capability:

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 the warning, 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. Press it to send the magic packet. The card then switches 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. You can hand someone the power button for one machine without giving them the rest of the estate.

Anonymous account creation is closed on this image, so you create the accounts. 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

SecureAnyCloud provides 24/7 technical support for this image by email and live chat.

  • Email: sales@secureanycloud.com
  • Live chat: available 24/7 at secureanycloud.com

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.

Deploy on Azure

Launch UpSnap 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