Ot
Security Azure

OTS on Ubuntu 24.04 on Azure User Guide

| Product: OTS on Ubuntu 24.04 LTS on Azure

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 ots system 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 :80 returns a 301 redirect to HTTPS for every path except an unauthenticated /healthz endpoint (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.sh that 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 azureuser account

  • A Network Security Group allowing inbound TCP 443 (HTTPS) and 80 (redirect) from the networks that should reach the service, and 22 (SSH) from your management network only

  • A recommended size of Standard_B2s or larger

Step 1: Deploy from the Azure Portal

  1. Locate the OTS on Ubuntu 24.04 LTS by cloudimg image in the Azure Marketplace and select Create.

  2. Choose your subscription, resource group and region.

  3. Select a VM size (Standard_B2s or larger) and provide your SSH public key for the azureuser account.

  4. On the Networking tab, allow inbound 443 and 80 from your users, and restrict 22 to your management network.

  5. 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:*

systemctl reports ots, redis-server and nginx all active, the VERSION file shows the baked OTS release and artifact checksum, and ss shows OTS bound to 127.0.0.1:3000 and Redis to 127.0.0.1:6379 loopback only with nginx public on :80 and :443

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.

The OTS Create a new secret form with a password entered in the Secret data field, an attachment picker showing the appliance maximum size, and the Create the secret button ready

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.

The OTS Secret created screen showing the generated one time URL with the secret id and decryption password in the fragment, a warning not to open the link yourself, and the automatic deletion time

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.

The OTS Reading your secret panel showing the decrypted plaintext password recovered in the browser, with copy, download and QR buttons and a warning that the secret is only shown once

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.

Opening the same one time link a second time shows the This is not the secret you are looking for warning, confirming the secret was destroyed on its single view

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.

The API lifecycle on the VM, showing a secret created through the API, read once returning its plaintext, and a second read returning HTTP 404 because the secret self destructed

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.

The per instance first boot state, showing the instance URL from ots-info.txt with the Redis password redacted, the root only file permissions on every secret bearing file, and the certificate generated for this VM

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.

The store hardening checks, showing anonymous Redis commands rejected with NOAUTH, a wrong password rejected, snapshots and the append only log disabled, zero secret files on disk, plain HTTP redirecting to HTTPS, and the metrics endpoint returning 404

Step 12: How the appliance is wired

  • OTS server — a single static Go binary at /opt/ots/ots, run by ots.service as the unprivileged ots user, bound to 127.0.0.1:3000, serving both the API and the embedded web frontend.

  • Redisredis-server.service, bound to 127.0.0.1:6379, password protected, with persistence disabled so ciphertext is held in memory only. ots.service requires it.

  • nginxnginx.service, terminating TLS on :443 and reverse proxying to OTS, redirecting :80 to HTTPS, and serving an unauthenticated /healthz for load balancer probes.

  • First bootots-firstboot.service runs 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.service will 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 22 only from your management network; keep 443 (and 80 for 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.