OTS on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of OTS on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. OTS is an open source, self hosted platform for handing someone a secret exactly once. A sender pastes a password, key or short note into the web page; OTS encrypts it in the browser with symmetric 256 bit AES and returns a single link. The recipient opens that link once to reveal the secret, after which it is destroyed, so anyone who finds the link later sees nothing.
The property that makes OTS different from a generic paste service is where the decryption password lives. The link OTS gives you has the form https://<host>/#<secret-id>|<password>, and everything after the # is a URL fragment, which browsers never transmit to the server. The server therefore receives the secret id but never the password, and stores only ciphertext it has no practical way to decrypt. Encryption and decryption both happen in the browser.
The image installs the latest stable OTS release, verified against the upstream SHA256SUMS manifest at build time (the exact version and artifact checksum are recorded in /opt/ots/VERSION). OTS runs as a single static Go binary under systemd as the unprivileged ots system user and binds to loopback only (127.0.0.1:3000). Ciphertext is held in Redis, also bound to loopback (127.0.0.1:6379), with disk persistence switched off, so nothing sensitive is ever written to the disk and a stopped or captured instance carries no secret material. nginx is the sole network facing surface and terminates TLS on port 443.
OTS is unauthenticated by design. It is a public secret sharing service: there is no admin login and no API key, so none is baked into the image. The two pieces of per instance secret material — the TLS private key and the internal Redis password — are both generated for this specific VM on first boot, and the application is held shut until that has happened, so no instance ever serves with build time settings.
What is included:
-
The latest stable OTS release, checksum verified at build time and run under systemd as the unprivileged
otssystem user -
The OTS server bound to loopback only (
127.0.0.1:3000) and Redis bound to loopback (127.0.0.1:6379) — neither is network exposed -
nginx terminating TLS on :443 and reverse proxying to OTS, with HSTS and sensible security headers
-
Port
:80returns a301redirect to HTTPS for every path except an unauthenticated/healthzendpoint (HTTP 200) for load balancer probes -
A TLS certificate generated per VM on first boot (its Subject Alternative Names include the VM address, hostname and
127.0.0.1) — replace it with your own CA signed certificate for production -
Ciphertext held in memory only (no RDB snapshots, no append only log) with a hard maximum secret lifetime of 7 days
-
A built in self test at
/opt/ots/ots-selftest.shthat proves the self destruct semantics end to end over TLS -
Ubuntu 24.04 LTS base with latest security patches applied at build time
-
Azure Linux Agent for seamless cloud integration and SSH key injection
-
24/7 cloudimg support with guaranteed 24 hour response SLA
Prerequisites
-
An Azure subscription with permission to deploy virtual machines
-
An SSH key pair for administrative access to the VM as the
azureuseraccount -
A Network Security Group allowing inbound TCP
443(HTTPS) and80(redirect) from the networks that should reach the service, and22(SSH) from your management network only -
A recommended size of Standard_B2s or larger
Step 1: Deploy from the Azure Portal
-
Locate the OTS on Ubuntu 24.04 LTS by cloudimg image in the Azure Marketplace and select Create.
-
Choose your subscription, resource group and region.
-
Select a VM size (Standard_B2s or larger) and provide your SSH public key for the
azureuseraccount. -
On the Networking tab, allow inbound
443and80from your users, and restrict22to your management network. -
Review and create. When the VM is running, browse to
https://<your-public-ip>/.
Step 2: Deploy from the Azure CLI
You can deploy the same image from the Azure CLI. Replace the resource group, location and image URN as needed:
az vm create \
--resource-group my-ots-rg \
--name ots \
--image <cloudimg-ots-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# then open the required ports
az vm open-port --resource-group my-ots-rg --name ots --port 443 --priority 1001
az vm open-port --resource-group my-ots-rg --name ots --port 80 --priority 1002
Step 3: Verify the OTS, Redis and nginx services
SSH in as azureuser and confirm the three services are active, and that OTS and Redis are bound to loopback only with nginx the sole public listener:
sudo systemctl is-active ots redis-server nginx
Expected output — three active lines:
active
active
active
ss -tln | grep -E ':3000 |:6379 |:80 |:443 '
You should see the OTS server on 127.0.0.1:3000 and Redis on 127.0.0.1:6379, both loopback only, while nginx listens on 0.0.0.0:80 and 0.0.0.0:443:
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:*
LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*

Step 4: Open the OTS web interface
Browse to https://<your-public-ip>/. Because the certificate is self signed and generated per VM, your browser will warn about it the first time. Accept it (or install your own certificate, see Step 11).
You are presented with the Create a new secret form. Type or paste your secret into Secret data, optionally attach files, and choose how long the secret should live in Expire in.

Step 5: Create the secret and get the one time link
Select Create the secret!. OTS encrypts the secret in your browser, sends only the ciphertext to the server, and shows the link to pass to your recipient. Note the # in the link: everything after it is the secret id and the decryption password, and your browser never sends that part to the server.
The screen also reminds you not to open the link yourself, because doing so would consume the single view, and tells you when the secret will be deleted if nobody collects it.

Step 6: Reveal the secret once
When the recipient opens the link they see a Reading your secret panel with a deliberate confirmation step, because opening the secret destroys it. Selecting Show me the secret! decrypts it in their browser and displays the plaintext, with a clear warning that reloading the page will lose it.

Step 7: Confirm the secret self destructs
Opening the same link a second time proves the one time guarantee. The secret has already been consumed and deleted from the store, so OTS reports that it cannot be found and warns that if you expected a secret to be there, someone else may have opened the link first.

Step 8: Verify the one time semantics from the command line
The image ships a self test that proves the self destruct behaviour end to end over TLS. It creates a secret through the API, reads it back once and checks the payload matches, reads it a second time and requires a 404, and confirms the store rejects unauthenticated access:
sudo /opt/ots/ots-selftest.sh
Expected output:
round-trip OK: created=c32155ac-47ca-4ed1-9a68-15dd10c2767b firstGET=200 payload-matched secondGET=404 redis=NOAUTH (one-time self-destruct proven over TLS)
You can also drive the API directly. This creates a secret, reads it once, then reads it again to show it is gone:
CA=/etc/nginx/tls/ots.crt
ID=$(curl -sS --cacert $CA -H 'Content-Type: application/json' \
-X POST https://127.0.0.1/api/create -d '{"secret":"example-api-secret"}' | jq -r .secret_id)
echo "secret_id=$ID"
curl -sS --cacert $CA https://127.0.0.1/api/get/$ID; echo
curl -sS --cacert $CA https://127.0.0.1/api/get/$ID; echo
The first read returns the secret and the second reports that it no longer exists:
secret_id=293ccdbd-8930-4a8d-9942-17172e7418ee
{"success":true,"secret":"example-api-secret"}
{"success":false,"error":"362e1187-d6d4-4874-8ac8-b7c99db84a84"}
Anything you send through the API this way is stored as you send it, so encrypt the payload yourself before posting it if you want the same guarantee the web interface gives you.

Step 9: Confirm TLS and inspect the per instance certificate
Confirm that plain HTTP redirects to HTTPS, that the health endpoint answers over TLS, and inspect the certificate generated for this VM:
curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' http://127.0.0.1/
curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1/healthz
sudo openssl x509 -in /etc/nginx/tls/ots.crt -noout -subject -ext subjectAltName
The redirect returns 301, the health endpoint returns 200, and the certificate subject and Subject Alternative Names carry this VM's own address and hostname:
301 -> https://127.0.0.1/
200
subject=C = GB, O = cloudimg, CN = 10.0.0.16
X509v3 Subject Alternative Name:
IP Address:127.0.0.1, DNS:localhost, DNS:ots, IP Address:10.0.0.16
Step 10: Check the baked version and the per instance details
The exact OTS release baked into the image, its upstream artifact checksum, and the per VM appliance details are recorded on the VM:
cat /opt/ots/VERSION
sudo cat /root/ots-info.txt
/root/ots-info.txt is readable by root only and holds the instance URL and the internal Redis password that first boot generated for this VM. You do not need that password to use OTS; it exists so the application and its store authenticate to each other.

Step 11: Verify the store is locked down
Every ciphertext lives in Redis, so it is worth confirming for yourself that the store is closed and holds nothing on disk:
redis-cli ping
An unauthenticated client is rejected, which is the expected and correct response:
NOAUTH Authentication required.
sudo find /var/lib/redis -maxdepth 1 -name '*.rdb' -o -name '*.aof' -o -name 'appendonlydir'
This returns nothing: RDB snapshots and the append only log are both disabled, so ciphertext never reaches the disk. It also means secrets do not survive a reboot of the VM, which is the right trade for a service whose entire purpose is that secrets disappear.

Step 12: How the appliance is wired
-
OTS server — a single static Go binary at
/opt/ots/ots, run byots.serviceas the unprivilegedotsuser, bound to127.0.0.1:3000, serving both the API and the embedded web frontend. -
Redis —
redis-server.service, bound to127.0.0.1:6379, password protected, with persistence disabled so ciphertext is held in memory only.ots.servicerequires it. -
nginx —
nginx.service, terminating TLS on:443and reverse proxying to OTS, redirecting:80to HTTPS, and serving an unauthenticated/healthzfor load balancer probes. -
First boot —
ots-firstboot.serviceruns once: it generates this VM's Redis password and TLS certificate, opens the bootstrap gate, starts the services, proves the one time round trip, and writes/root/ots-info.txt. Until that gate opens,ots.servicewill not start, so an instance can never serve with build time settings. -
Sizing — the appliance advertises and enforces a 16 MiB attachment limit, with a 32 MiB server side secret ceiling and a 48 MiB reverse proxy body limit, so the size the interface shows you is the size the whole stack actually accepts.
Step 13: Use your own domain and a trusted certificate (production)
The image ships a self signed certificate so it is secure out of the box, but browsers will warn on it. For production, point a DNS name at the VM and install a CA signed certificate. With your own domain you can obtain a free certificate from Let's Encrypt:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain>
Certbot installs the certificate into nginx and sets up automatic renewal. OTS builds each link from the host the browser used, so no application configuration change is needed when you move to your own domain.
Step 14: Managing the OTS service
sudo systemctl restart ots
sudo journalctl -u ots.service --no-pager | tail -20
Server options live on the ExecStart line in /etc/systemd/system/ots.service — the maximum secret lifetime is set there with --secret-expiry, in seconds. Appearance and limits (attachment size, accepted file types, expiry choices, footer links) live in /etc/ots/customize.yaml. After changing either, run sudo systemctl daemon-reload && sudo systemctl restart ots.
Step 15: Security recommendations
-
Restrict SSH. Allow inbound
22only from your management network; keep443(and80for the redirect) open to your users. -
Install a trusted certificate. Replace the self signed certificate with a CA signed one (Step 13) so recipients are not trained to click through warnings.
-
Keep secrets short lived. Choose the shortest expiry that still works for your recipient; that is the window in which an uncollected secret exists at all.
-
Do not open the link yourself. Opening it consumes the single view, and the recipient will then see the not found warning and rightly suspect interception.
-
Treat a missing secret as a possible compromise. If a recipient reports that the link shows nothing and they never opened it, assume someone else did and rotate the credential.
-
Keep the image updated. Unattended security upgrades remain enabled so the OS keeps receiving patches.
Step 16: Support and Licensing
OTS is open source software distributed under the Apache License 2.0. This image bundles the upstream OTS release unmodified; the licence file is retained on the image at /opt/ots/LICENSE. cloudimg packaging, hardening and support are provided by cloudimg.
Deploy on Azure
Find this image on the Azure Marketplace and deploy it in minutes. See www.cloudimg.co.uk for the full catalogue.
Need Help?
cloudimg provides 24/7 support with a guaranteed 24 hour response SLA. Contact us through www.cloudimg.co.uk for deployment assistance, configuration help or any questions about this image.