Whisper · Docs
Containers & cloud

DigitalOcean Marketplace

Every DigitalOcean tutorial for "give your droplet an outbound identity" ends the same way: hand-roll a WireGuard config, hard-code a private key into a file, and hope nobody snapshots the image and leaks it.

The Whisper Agent Node droplet skips all of it — it boots identity-less, takes your API key as ordinary cloud-init user data, and by the time SSH is up it already has a routable IPv6 /128 and a persistent WireGuard tunnel sourcing its egress.

DigitalOcean is the second of Whisper's two Marketplace 1-Click apps (the other is Vultr), and the two are deliberately built to different mechanics because the platforms are different: Vultr's Marketplace is imageless — the vendor-portal build is a cloud-init script run against a stock image at every deploy, so a CLI release needs no rebuild at all. DigitalOcean's Marketplace is snapshot-based — the listing is a pre-built disk image, so getting the whisper CLI baked in means running a real image-build pipeline (Packer) ahead of time, then letting cloud-init handle only the per-deploy activation. This page covers that pipeline end to end, from the Packer template to the systemd units it bakes to the exact bytes a droplet exchanges with 169.254.169.254 on first boot.

What you get

A Droplet created from the "Whisper Agent Node" 1-Click app has the whisper CLI installed system-wide from Whisper's signed apt/dnf repo (get.whisper.online) the moment it boots. The image itself carries no secret — no API key, no identity, nothing tenant-specific is baked into the snapshot. What happens next depends entirely on whether you supplied a key at deploy time:

That two-tier shape isn't incidental — see Integrations for why every Whisper integration is built keyless-works / key-unlocks-more, never gated behind an account just to prove the standard works.

The mechanism: Packer snapshot + cloud-init activation

Build time — one shell recipe, run by Packer

The entire image build is docs/packaging/digitalocean/template.pkr.hcl plus a single POSIX-sh script, firstboot.sh, that is shared — recipe-for-recipe — with the Vultr channel. It has two phases selected by argv, because the same file has to work two different ways on two different platforms:

firstboot.sh install     # image build (Packer): CLI + guide + systemd units, no secret
firstboot.sh firstboot   # every boot: discover an operator key, activate once
firstboot.sh             # both — lets the same file double as plain user data
                          # on a stock Ubuntu/Debian droplet, no snapshot needed

install runs once, during the Packer build, against a temporary ubuntu-24-04-x64 droplet:

  1. Installs whisper from the signed apt repo (deb [signed-by=/usr/share/keyrings/whisper.gpg] https://get.whisper.online/deb stable main), falling back to the signed dnf repo, then to the signed curl https://get.whisper.online | sh installer if neither package manager is present.
  2. Writes /etc/motd and /etc/whisper/README.md with the on-box guide.
  3. Bakes two systemd units: - whisper-firstboot.service — a Type=oneshot, RemainAfterExit=yes unit ordered After=network-online.target cloud-init.service (deliberately not cloud-final.service, which would create an ordering cycle against its own WantedBy=multi-user.target). It runs the same script with firstboot on every boot, but is idempotent past the first. - whisper-connect.serviceConditionPathExists=/etc/whisper/connect.env, so it simply never starts on a keyless droplet. It execs whisper connect --tier ${WHISPER_TIER} --port ${WHISPER_PROXY_PORT} and is Restart=always.

Then Packer hands off to DigitalOcean's own mandatory Marketplace scripts, pinned to a fixed commit of digitalocean/marketplace-partners for reproducibility: 90-cleanup.sh (purges droplet-agent, wipes SSH host keys, shell history, logs, and the machine-id) and 99-img-check.sh (DO's own listing validator). Only after both pass does Packer power off and snapshot:

export DIGITALOCEAN_TOKEN=…                 # never committed
packer init  docs/packaging/digitalocean
packer build docs/packaging/digitalocean
# -> snapshot "whisper-agent-node-<ts>"; the printed image ID is what the
#    Vendor Portal listing attaches to the 1-Click app

Deploy time — the Metadata API, once

Every DigitalOcean droplet can read the user data it was created with from the local, link-local Metadata API — no auth, no network hop off-box:

curl -fsS http://169.254.169.254/metadata/v1/user-data

whisper-firstboot.service reads exactly that URL on boot. The parser is deliberately liberal (Postel's law in miniature): it accepts either plain key: value lines or KEY=value env-style, tolerates CRLF and quoting, and also checks /etc/whisper/key first in case you wrote the key via a proper #cloud-config write_files block instead of free-form user data:

# cloud-config.yaml — for operators who already manage droplets with cloud-init
write_files:
  - path: /etc/whisper/key
    permissions: "0600"
    content: "<your-whisper-key>"

or the plainer, Marketplace-native form (pasted into "Add Initialization scripts" in the console, or user_data in the API call below):

whisper_api_key: <your-whisper-key>
whisper_tier: wireguard        # optional: wireguard (default) | socks5
whisper_agent_name: my-node    # optional: defaults to do-<hostname>

Once a key is found, the activation is a fixed, five-step sequence, logged to /var/log/whisper-agent-node.log:

whisper login <key>                         # sign in with the operator's key
whisper create --name do-<hostname>         # mint this droplet's own /128
whisper use <address>                       # select it for this shell/service
systemctl enable --now whisper-connect      # bring up the persistent tunnel
curl -x socks5h://127.0.0.1:1080 https://rdap.whisper.online/egress-ip
                                             # verify egress THROUGH the running
                                             # service (never `whisper ip` here —
                                             # it opens its own tunnel and would
                                             # orphan the service's)

The key file is deleted the moment login succeeds — the CLI keeps its own mode-0600 copy, and nothing in /etc/whisper after that point can reconstruct the plaintext key.

Prove it — with stock tools, and with Whisper

With stock tools (the exact live e2e the image is tested with — DigitalOcean's public API, curl, dig, ssh; no Whisper software on your side of the wire):

T="Authorization: Bearer $DIGITALOCEAN_TOKEN"
UD='whisper_api_key: whisper_live_...'          # your real key — redact in any transcript
DID=$(curl -s -X POST https://api.digitalocean.com/v2/droplets -H "$T" \
  -H 'Content-Type: application/json' -d "{\"name\":\"wan-e2e\",\"region\":\"ams3\",
  \"size\":\"s-1vcpu-1gb\",\"image\":<snapshot-image-id>,
  \"ssh_keys\":[<your-key-id>],\"user_data\":\"$UD\"}" | jq -r .droplet.id)

# poll until status=active, grab the public IPv4, then:
ssh root@<droplet-ip> 'cat /etc/whisper/address; systemctl is-active whisper-connect;
  curl -s -x socks5h://127.0.0.1:1080 https://rdap.whisper.online/egress-ip'
# -> {"ip":"2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"}   (inside 2a04:2a01::/32)

dig -x 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4 +short
# -> cef2002a323d40d4.<tenant>.agents.whisper.online.

curl -s -X DELETE https://api.digitalocean.com/v2/droplets/$DID -H "$T"   # always destroy

With Whisper — the same proof from inside the droplet, once it's up:

whisper status                    # key state, selected agent, connection state
cat /etc/whisper/address          # the /128 this node activated with
whisper verify 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4   # DANE + DNSSEC + reverse-DNS + transparency log

Or drive the entire droplet lifecycle from the control plane instead of the DO console:

CALL whisper.agents({op:'create', args:{name:'do-wan-e2e'}})
CALL whisper.agents({op:'identity', args:{agent:'do-wan-e2e'}})
  -> address, TLSA "3 1 1 b653a4ef…fcb82d1d", the reverse name

Two things worth knowing before you deploy

Rebuild triggers

Because the image is a snapshot, a CLI release does not automatically reach droplets already deployed — but new deploys from a stale snapshot still install the current CLI at first boot only if the recipe re-runs install. In practice: rebuild + resubmit the snapshot on any firstboot.sh change (keep it in sync with the Vultr channel's copy), and on every minor/major CLI release so fresh droplets start current. The public build recipe lives at docs/packaging/digitalocean/ in whisper-examples.

Next