Deployment guide

OTS

Overview

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 the link once to reveal the secret, and the secret is then destroyed, so anyone who finds the link later sees nothing.

This guide covers the deployment and configuration of OTS on Ubuntu 24.04 on Azure, using the SecureAnyCloud Azure Marketplace image. The decryption password never reaches the server, which is what sets OTS apart from a generic paste service. 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 receives the secret id but not the password, and it stores only ciphertext that 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. Nothing sensitive is written to disk, so a stopped or captured instance carries no secret material. nginx is the only network-facing surface and terminates TLS on port 443.

OTS is unauthenticated by design. It is a public secret-sharing service, with no admin login and no API key, so the image contains none. Two pieces of per-instance secret material, the TLS private key and the Redis password, are generated for your VM on first boot. The application stays shut until that has happened, so no instance ever serves with build-time settings.

What you get:

  • 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), so neither is exposed to the network
  • nginx terminating TLS on :443 and reverse proxying to OTS, with HSTS and sensible security headers
  • Port :80 returning 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, with Subject Alternative Names that include the VM address, the hostname and 127.0.0.1. Replace it with your own CA-signed certificate for production
  • Ciphertext held in memory only (no RDB snapshots and 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 behaviour end to end over TLS
  • An Ubuntu 24.04 LTS base with the latest security patches applied at build time
  • The Azure Linux Agent for cloud integration and SSH key injection
  • 24/7 SecureAnyCloud support with a 24-hour response SLA

Prerequisites

You need the following:

  • 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 that allows inbound TCP 443 (HTTPS) and 80 (redirect) from the networks that should reach the service, and 22 (SSH) from your management network only
  • A VM size of Standard_B2s or larger (recommended)
1

Deploy from the Azure Portal

Find the OTS on Ubuntu 24.04 LTS image by SecureAnyCloud 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 azureuser account.

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

Review and create the VM. When the VM is running, browse to https://<your-public-ip>/.

2

Deploy from the Azure CLI

Deploy the same image from the Azure CLI. Replace the resource group, location and image URN as needed:

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:ots-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-ots-rg \
  --name ots \
  --image sac1647283583153:ots-ubuntu-24-04:default:latest \
  --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
3

Verify the OTS, Redis and nginx services

SSH in as azureuser and confirm that the three services are active:

sudo systemctl is-active ots redis-server nginx

Expected output, three active lines:

active active active
ss -tln | grep -E ':3000 |:6379 |:80 |:443 '

This command checks that OTS and Redis are bound to loopback only and that nginx is the sole public listener. The output shows 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:*
4

Open the OTS web interface

Browse to https://<your-public-ip>/. The certificate is self-signed and generated per VM, so your browser warns about it the first time. Accept the warning, or install your own certificate (see Step 13).

The Create a new secret form opens. Type or paste your secret into Secret data, optionally attach files, and choose how long the secret should live in Expire in.

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. Everything after the # in the link is the secret id and the decryption password, and your browser never sends that part to the server.

The screen reminds you not to open the link yourself, because that would consume the single view. It also tells you when the secret is deleted if nobody collects it.

6

Reveal the secret once

When the recipient opens the link, a Reading your secret panel asks for a deliberate confirmation, because opening the secret destroys it. Selecting Show me the secret! decrypts the secret in the recipient's browser and displays the plaintext, with a clear warning that reloading the page loses it.

7

Confirm the secret self destructs

Open the same link a second time to see the one-time behaviour. The store has already deleted the secret, so OTS reports that it cannot be found. It also warns that if you expected a secret to be there, someone else may have opened the link first.

8

Verify the one time semantics from the command line

The image includes a self test that proves the self-destruct behaviour end to end over TLS. The test creates a secret through the API, reads it back once and checks that the payload matches, and reads it a second time and requires a 404. It also confirms that the store rejects unauthenticated access. Run it:

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 call the API directly. These commands create a secret, read it once, and read it again to show that 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"}

The API stores whatever you send exactly as you send it. To get the same protection as the web interface, encrypt the payload yourself before you post it.

9

Confirm TLS and inspect the per instance certificate

Confirm that plain HTTP redirects to HTTPS and that the health endpoint answers over TLS, then 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 the address and hostname of this VM:

301 -> https://127.0.0.1/ 200 subject=C = GB, O = sac, 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
10

Check the baked version and the per instance details

The VM records the exact OTS release in the image, its upstream artifact checksum and the per-VM appliance details. Read them:

cat /opt/ots/VERSION
sudo cat /root/ots-info.txt

Only root can read /root/ots-info.txt. It holds the instance URL and the Redis password that first boot generated for this VM. You do not need that password to use OTS. It lets the application and its store authenticate to each other.

11

Verify the store is locked down

Redis holds every ciphertext, so confirm that the store is closed and writes nothing to disk:

redis-cli ping

Redis rejects an unauthenticated client. This is the expected response:

NOAUTH Authentication required.
sudo find /var/lib/redis -maxdepth 1 -name '*.rdb' -o -name '*.aof' -o -name 'appendonlydir'

The command returns nothing. RDB snapshots and the append-only log are both disabled, so ciphertext never reaches the disk. As a result, secrets do not survive a reboot of the VM, which suits a service whose purpose is that secrets disappear.

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 and bound to 127.0.0.1:3000. It serves both the API and the embedded web frontend.

Redis. redis-server.service, bound to 127.0.0.1:6379 and password protected. Persistence is disabled, so ciphertext is held in memory only. ots.service requires it.

nginx. nginx.service terminates TLS on :443 and reverse proxies to OTS. It redirects :80 to HTTPS and serves an unauthenticated /healthz for load balancer probes.

First boot. ots-firstboot.service runs once. It generates the Redis password and TLS certificate for this VM, opens the bootstrap gate, starts the services, proves the one-time round trip and writes /root/ots-info.txt. ots.service does not start until the gate opens, so an instance never serves 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. The size the interface shows is the size the whole stack accepts.

13

Use your own domain and a trusted certificate (production)

The image ships with a self-signed certificate, so traffic is encrypted from the first request, but browsers warn about 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 that the browser used, so you do not need to change the application configuration when you move to your own domain.

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. Set the maximum secret lifetime there with --secret-expiry, in seconds. Appearance and limits (attachment size, accepted file types, expiry choices and footer links) live in /etc/ots/customize.yaml. After you change either file, run sudo systemctl daemon-reload && sudo systemctl restart ots.

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 that recipients are not trained to click through warnings.
  • Keep secrets short-lived. Choose the shortest expiry that works for your recipient. That expiry is the only window in which an uncollected secret exists.
  • Do not open the link yourself. Opening it consumes the single view, and the recipient then sees the not-found warning and rightly suspects interception.
  • Treat a missing secret as a possible compromise. If a recipient reports that the link shows nothing and they never opened it, assume that someone else did and rotate the credential.
  • Keep the image updated. Unattended security upgrades stay enabled, so the OS keeps receiving patches.
16

Support and Licensing

OTS is open-source software distributed under the Apache License 2.0. This image bundles the upstream OTS release unmodified, and the licence file is retained on the image at /opt/ots/LICENSE. SecureAnyCloud provides the packaging, hardening and support.

Deploy on Azure

Find this image on the Azure Marketplace and deploy it in minutes. See secureanycloud.com for the full catalogue.

Need Help?

SecureAnyCloud provides 24/7 support with a 24-hour response SLA. Contact us through secureanycloud.com for deployment assistance, configuration help or any questions about this image.

Deploy on Azure

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