OTS 1.23.1 on AWS User Guide
Overview
This guide covers launching and using the OTS 1.23.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg identity, secrets and PKI listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
OTS (One-Time Secrets) is an open source service for sharing a secret once. Paste a password, an API key or a short note, optionally attach files, and send the link. The secret is encrypted in the sender's browser (AES-256, with a password generated on that device). The secret's id and its password are both in the part of the link after the #, which browsers never send to a server, so the instance only ever receives and stores ciphertext. The first person to open the link reads the secret, and the server deletes it at that moment; an unread secret is deleted when it expires.
The cloudimg image is a hardened, ready-to-use OTS server:
- nginx in front, OTS behind it. nginx is the only public listener (ports 80 and 443). OTS listens on
127.0.0.1:3000, the loopback address only, and systemd stops the OTS process from talking to any other address at all. - HTTPS from the first boot. Browsers only allow the Web Crypto functions OTS needs on a secure (HTTPS) page, so the image serves HTTPS from the first boot with a certificate generated on your instance (an end-entity certificate, not a certificate authority). Port 80 redirects to HTTPS. The instance's name is set from its own public address at first boot; you can change it to your own domain.
- Secrets in memory only. Secrets are stored by Valkey, the Redis-compatible key-value store from Ubuntu 24.04, with snapshots and the append-only file switched off: a secret never reaches a disk, a snapshot or a backup. Valkey has no network listener at all; it listens on a local socket that only Valkey and OTS can open.
- Per-instance store passwords, nothing to share. First boot generates two random Valkey passwords, one for OTS and one for the administrator helper. Valkey keeps only their SHA-256 hashes, its default account is switched off, and each account may run only the commands it needs. Valkey does not start at all until first boot has created those accounts.
- Abuse limits. There are no accounts: anyone who can reach the address can create secrets, which is how OTS is designed. The image bounds what one client can do: nginx allows 12 new secrets per minute per client address (with a burst of 20), a secret request is at most 48 MiB at nginx and 32 MiB at OTS, attachments are limited to 16 MiB in total, a secret lives at most 7 days, and a page on another website cannot create secrets here. You can also restrict who may create secrets (Step 8).
- Real client addresses, fixed host name. The rate limit uses the real TCP peer address, never a header a client can forge. Requests for any host name other than the instance's configured names are dropped.
- Browser hardening. OTS's own Content-Security-Policy allows only its own scripts and styles (with a per-response nonce), and nginx adds a second policy that refuses framing; the server sends no referrer, so the link never leaks through a
Refererheader. The nginx access log records/api/get/<id-redacted>instead of secret ids, and OTS's own request log is off. - Locked down. TLS 1.2 and 1.3 only; OTS, nginx, Valkey and the first-boot service are blocked from the instance metadata service, and the AMI requires IMDSv2.
- The upstream release, verified, on a fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on. See Step 12 for exactly what is installed.
What is on the image: OTS 1.23.1 (the official upstream release binary, /usr/local/bin/ots, with its web app built in), Valkey 7.2.13 and nginx from Ubuntu 24.04, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
- A security group that allows TCP 22 (SSH) from your own networks only, and TCP 443 (the web app) and TCP 80 (redirects to HTTPS) only from the networks whose people should use this service: your office, your VPN, or your VPC CIDR. Anyone who can reach port 443 can create secrets, so do not open it to
0.0.0.0/0unless you want a public service. Do not open 3000 or 6379: OTS listens on the loopback address only and Valkey has no network listener. - For production, a DNS name you control (for example
secrets.example.com) and, ideally, a certificate from a certificate authority. Until then the instance uses its public IP address with its own self-signed certificate.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick OTS 1.23.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair, and a security group as above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --image-id <ami-id-from-the-listing> --instance-type m5.large \
--key-name <your-key-pair> --security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=ots}]'
First boot takes a few seconds after the instance is running. If you want the instance to use your own name from the start, put it in /etc/cloudimg/ots-domain.preset with user data (for example a cloud-init write_files entry); otherwise first boot uses the instance's public IP address (or its private address if it has no public one), and also serves its EC2 public DNS name.
Step 2: Connect to your instance
| Operating system | SSH user |
|---|---|
| Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
The login message shows the address of the web app.
Step 3: Check the services
systemctl is-active valkey-server ots nginx
active
active
active
(ots-firstboot runs once, on the first boot, and shows inactive on every later boot.)
Only SSH, HTTP and HTTPS are public. OTS listens on the loopback address, and Valkey only on its local socket:
sudo ss -tln | grep -E ':(22|80|443|3000|6379) '
sudo ss -xl | grep valkey
LISTEN 0 128 127.0.0.1:3000 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
LISTEN 0 4096 [::]:22 [::]:*
LISTEN 0 511 [::]:80 [::]:*
LISTEN 0 511 [::]:443 [::]:*
u_str LISTEN 0 511 /run/valkey/valkey.sock 8480 * 0
A built-in self-test creates an encrypted secret through nginx, reads it once, checks that a second read is refused and that Valkey refuses an unauthenticated client:
sudo ots-selftest
ots-selftest: created (201, expires 2026-11-15T03:47:45.013953816Z), first read 200 + identical ciphertext + decrypts, second read 404, Valkey NOAUTH
OTS_SELFTEST_OK
Step 4: Read the address and the certificate fingerprint
sudo cat /root/ots-info.txt
The file (root only, mode 0600) gives this instance's address, the SHA-256 fingerprint of its certificate and the limits. There is no administrator password to collect: OTS has no accounts and no admin console.
# OTS 1.23.1 (One-Time Secrets) on this instance. Open the URL in a browser; there is no account or password.
# The certificate is self-signed for this instance: compare its SHA-256 fingerprint with OTS_TLS_SHA256 below.
OTS_URL=https://198.51.100.24/
OTS_HOST=198.51.100.24
OTS_TLS_SHA256=D9:0A:71:3B:...:(64 hex digits in pairs)
OTS_ADDRESS_SOURCE=imdsv2-public-ipv4
OTS_MAX_ATTACHMENT_BYTES=16777216
OTS_MAX_EXPIRY_SECONDS=604800
GUIDE=https://www.cloudimg.co.uk/guides/ots-1-23-1-on-ubuntu-24-04-aws/
The certificate is self-signed and unique to your instance, so your browser warns about it. Open the certificate details in the browser, check that its SHA-256 fingerprint matches OTS_TLS_SHA256, and only then continue. Do not add the certificate to your operating system's trust store; for production, replace it with a certificate from a certificate authority (Step 10). The certificate is an end-entity certificate for this instance's names only. Copy it to your home directory so that the commands in this guide can use it as their trust anchor:
sudo cat /etc/ssl/ots/ots.crt > ~/ots-instance.crt
openssl x509 -in ~/ots-instance.crt -noout -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltName
subject=CN = 198.51.100.24, O = cloudimg
X509v3 Subject Alternative Name:
IP Address:198.51.100.24, DNS:ec2-198-51-100-24.compute-1.amazonaws.com, IP Address:172.31.93.28, IP Address:127.0.0.1
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
Step 5: Share a secret
Browse to the OTS_URL address. Type or paste the secret and, if you like, attach files:

Choose Create the secret!. Your browser encrypts the secret and the attachments with a new password and uploads only the ciphertext; the page then shows the link, with buttons to copy it, show it as a QR code or burn (delete) the secret straight away, and when an unread secret will be deleted. Send the link to the recipient over any channel, and do not open it yourself:

The recipient opens the link. Nothing has been fetched yet; the page asks them to confirm, because reading destroys the secret:

They choose Show me the secret!. Their browser downloads the ciphertext, decrypts it with the password from the link, and shows the secret and the attached files, which they can download:

The secret is now gone from the server. Opening the same link again shows that it is no longer there; if the recipient sees this the first time they open a link, someone else has read it:

Step 6: Attachments and expiry
Attachments are limited to 16 MiB in total (the page shows Maximum size: 16.0 MiB). OTS checks the size in the browser before anything is encrypted or sent: a larger selection is refused with OTS's own message, and the create button stays disabled:

An unread secret is deleted after 7 days by default. The sender can choose a shorter time, from 5 minutes up:

The settings the page reads are served by OTS itself. On the instance, connect to nginx on the loopback address (--connect-to; the security group normally does not let an instance reach its own public address) with the instance certificate as the trust anchor:
U=$(sudo sed -n 's/^OTS_URL=//p' /root/ots-info.txt)
curl -s --cacert ~/ots-instance.crt --connect-to ::127.0.0.1:443 "${U}api/settings"; echo
{"appTitle":"OTS - One Time Secrets","acceptedFileTypes":"","disableFileAttachment":false,"maxAttachmentSizeTotal":16777216}
Step 7: Use the API from scripts
The web app talks to a small JSON API. The server never sees a password: whatever you send to POST /api/create is stored exactly as you send it, so encrypt it first. OTS uses the OpenSSL format, so openssl on any machine can create a secret that the web page opens:
U=$(sudo sed -n 's/^OTS_URL=//p' /root/ots-info.txt)
PASS=$(openssl rand -hex 16)
DATA=$(printf 'api-test secret' | openssl enc -aes-256-cbc -pbkdf2 -iter 300000 -md sha512 -salt -base64 -A -pass "pass:${PASS}")
ID=$(curl -s --cacert ~/ots-instance.crt --connect-to ::127.0.0.1:443 -H 'Content-Type: application/json' \
--data "{\"secret\":\"${DATA}\"}" "${U}api/create?expire=3600" | jq -r .secret_id)
echo "share link: ${U}#${ID}%7C<password>"
curl -s --cacert ~/ots-instance.crt --connect-to ::127.0.0.1:443 "${U}api/get/${ID}" | jq -r .secret \
| openssl enc -d -aes-256-cbc -pbkdf2 -iter 300000 -md sha512 -base64 -A -pass "pass:${PASS}"; echo
curl -s --cacert ~/ots-instance.crt --connect-to ::127.0.0.1:443 -o /dev/null -w 'second read: %{http_code}\n' "${U}api/get/${ID}"
share link: https://198.51.100.24/#523d1258-3db6-4b64-a2e1-1c06c59c1186%7C<password>
api-test secret
second read: 404
The share link is the address, #, the secret id, %7C (an encoded |) and the password; give it to the recipient instead of reading the secret yourself. ?expire= sets the lifetime in seconds; values above 7 days are lowered to 7 days.
Invalid requests are refused, and so is a request a page on another website makes (its Origin is not this instance). OTS answers errors with an error reference only, never with the reason:
U=$(sudo sed -n 's/^OTS_URL=//p' /root/ots-info.txt)
curl -s --cacert ~/ots-instance.crt --connect-to ::127.0.0.1:443 -w ' %{http_code}\n' -H 'Content-Type: application/json' --data '{"secret":""}' "${U}api/create"
curl -s --cacert ~/ots-instance.crt --connect-to ::127.0.0.1:443 -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/json' -H 'Origin: https://evil.example' --data '{"secret":"x"}' "${U}api/create"
{"success":false,"error":"824fdeee-32f5-497a-a4ed-217f937a3896"} 400
403
Step 8: Limits and who can create secrets
OTS runs with a 7-day maximum lifetime, its request log off and the size limits in /etc/ots/customize.yaml:
systemctl show -p ExecStart --value ots | grep -o 'argv\[\]=[^;]*'
sudo grep -E '^(maxAttachmentSizeTotal|maxSecretSize|metricsAllowedSubnets):' /etc/ots/customize.yaml
argv[]=/usr/local/bin/ots --listen 127.0.0.1:3000 --storage-type redis --secret-expiry 604800 --customize /etc/ots/customize.yaml --log-requests=false --log-level info
maxAttachmentSizeTotal: 16777216
maxSecretSize: 33554432
metricsAllowedSubnets: []
maxSecretSize (32 MiB) is the largest encrypted secret OTS accepts, which leaves room for 16 MiB of attachments once they are encrypted and encoded; nginx accepts at most 48 MiB on the create request and 1 MiB on every other request. OTS's /metrics page is not served to anyone. If you change the limits, keep maxSecretSize below nginx's client_max_body_size for /api/create in /usr/local/lib/cloudimg/ots/nginx-site.tmpl and run sudo systemctl restart ots.
nginx limits how fast one client address can create secrets and call the API. The address is the real TCP peer; a forged X-Forwarded-For header neither changes it nor appears in the log:
sudo grep -E 'limit_req_zone' /etc/nginx/conf.d/cloudimg-ots.conf
limit_req_zone $binary_remote_addr zone=ots_create:10m rate=12r/m;
limit_req_zone $binary_remote_addr zone=ots_api:10m rate=20r/s;
A client over the limit gets HTTP 429 until its allowance refills; other clients are not affected. The access log records secret reads without their ids:
sudo tail -3 /var/log/nginx/access.log | sed -E 's/^[0-9a-f.:]+ /<client-ip> /'
<client-ip> - [08/Nov/2026:03:56:19 +0000] "GET /api/get/<id-redacted> HTTP/2.0" 200 73 "curl/8.5.0"
<client-ip> - [08/Nov/2026:03:56:19 +0000] "GET /api/get/<id-redacted> HTTP/2.0" 404 65 "curl/8.5.0"
<client-ip> - [08/Nov/2026:03:56:19 +0000] "POST /api/create HTTP/2.0" 400 65 "curl/8.5.0"
Who can create secrets is decided by your security group. Keep the 443 and 80 rules limited to the networks that should use the service. To put the service on a private address only, launch it in a private subnet (or remove the public IP) and allow 443 from your VPC CIDR or VPN range.
To let everyone who can reach the instance read secrets but only some people create them, put nginx rules in a file in /etc/nginx/cloudimg/ots-create-access.d/; they apply to the create request only. For example, only your office network (203.0.113.0/24 here) may create:
echo 'allow 203.0.113.0/24; deny all;' | sudo tee /etc/nginx/cloudimg/ots-create-access.d/office.conf
sudo nginx -t && sudo systemctl reload nginx
Or require a user name and password for creating secrets (the browser asks for them when a secret is created; opening a link needs none):
printf 'alice:%s\n' "$(openssl passwd -apr1)" | sudo tee /etc/nginx/ots.htpasswd >/dev/null
sudo chown root:www-data /etc/nginx/ots.htpasswd && sudo chmod 0640 /etc/nginx/ots.htpasswd
echo 'auth_basic "OTS"; auth_basic_user_file /etc/nginx/ots.htpasswd;' | sudo tee /etc/nginx/cloudimg/ots-create-access.d/password.conf
sudo nginx -t && sudo systemctl reload nginx
Step 9: The secret store
Valkey keeps every secret in RAM, with a 512 MB cap and no snapshot or append-only file. It has no TCP listener (port 0); its socket is open to the valkey and ots groups only, and its default account is off:
sudo grep -E '^(bind|port|tls-port|unixsocket|unixsocketperm|save|appendonly|maxmemory|maxmemory-policy|aclfile) ' /etc/valkey/cloudimg-ots.conf
bind 127.0.0.1
port 0
tls-port 0
unixsocket /run/valkey/valkey.sock
unixsocketperm 660
save ""
appendonly no
maxmemory 512mb
maxmemory-policy noeviction
aclfile /etc/valkey/cloudimg-ots-users.acl
noeviction means Valkey never silently drops a stored secret to make room: when the store is full, a new secret is refused (the page reports an error) until secrets are read or expire. The ubuntu user cannot open the socket, and root without a password is refused:
valkey-cli -s /run/valkey/valkey.sock ping
sudo valkey-cli -s /run/valkey/valkey.sock ping
Could not connect to Valkey at /run/valkey/valkey.sock: Permission denied
NOAUTH Authentication required.
ots-valkey-admin runs valkey-cli as the read-only administrator account, with its password from /etc/ots/valkey-admin-password (root only). It can count and inspect secrets but not change or delete them. Every stored value is ciphertext in the OpenSSL format (U2FsdGVkX1 is the encoding of its Salted__ header):
sudo ots-valkey-admin dbsize
sudo ots-valkey-admin info memory | grep -E '^(used_memory_human|maxmemory_human|maxmemory_policy):'
for k in $(sudo ots-valkey-admin --scan --pattern 'io.luzifer.ots:*' | head -3); do sudo ots-valkey-admin get "$k" | cut -c1-10; done
1
used_memory_human:1.11M
maxmemory_human:512.00M
maxmemory_policy:noeviction
U2FsdGVkX1
A reboot, or a restart of Valkey, deletes every unread secret. This is by design: secrets are never written to disk. The service itself comes back on its own after a reboot.
Step 10: Use your own domain and certificate
Point a DNS record (for example secrets.example.com) at the instance, then give the new name first and any other names or addresses that should keep working after it:
sudo ots-set-domain secrets.example.com "$(sudo sed -n 's/^OTS_HOST=//p' /root/ots-info.txt)"
nginx then serves only those names and a new self-signed certificate is issued for them; /root/ots-info.txt shows the new address and fingerprint. Share links use the name the sender browsed to, so open OTS at https://secrets.example.com/. Run ots-set-domain again if a stop and start changes the instance's public IP address and you use the address rather than a DNS name.
To use a certificate from a certificate authority, run ots-set-domain with your name first, then replace /etc/ssl/ots/ots.crt (full chain) and /etc/ssl/ots/ots.key (mode 0600, root) and run sudo systemctl reload nginx. Running ots-set-domain again issues a new self-signed certificate, so put your certificate back after any later run.
Step 11: Security notes
- Metadata service. The AMI requires IMDSv2. OTS may talk to the loopback address only, and nginx, Valkey and the first-boot service are denied the instance metadata addresses by systemd. OTS makes no outbound requests at all: it never fetches a URL a user supplies.
systemctl show -p IPAddressAllow -p IPAddressDeny ots
systemctl show -p IPAddressDeny nginx valkey-server ots-firstboot
IPAddressAllow=127.0.0.0/8 ::1/128
IPAddressDeny=0.0.0.0/0 ::/0
IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 ::ffff:0.0.0.0/96 2001::/32 169.254.169.254/32 64:ff9b:1::/48 2002::/16 64:ff9b::/96
IPAddressDeny=64:ff9b::/96 169.254.169.254/32 2001::/32 ::ffff:0.0.0.0/96 fd00:ec2::254/128 64:ff9b:1::/48 2002::/16 168.63.129.16/32
IPAddressDeny=::ffff:0.0.0.0/96 fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 64:ff9b::/96 2001::/32 2002::/16 64:ff9b:1::/48
- Where the passwords live. OTS takes its Valkey password from
/etc/ots/ots.env(mode 0600, root), which systemd reads when it starts OTS; OTS has no other way to receive it. The administrator password is in/etc/ots/valkey-admin-password(mode 0600, root). Neither is on any command line, in the journal or in a log. Core dumps are off for OTS, nginx and Valkey. - Response headers. Every page is served with OTS's own Content-Security-Policy, nginx's
frame-ancestors 'none'policy,X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrerandStrict-Transport-Security. - Valkey advisories. Valkey 7.2.13 comes from the Ubuntu 24.04
universearchive. Ubuntu publishes the fixes in USN-8771-1 (CVE-2026-56684 and CVE-2026-63639, both rated medium) only through Ubuntu Pro (ESM Apps). Neither is reachable on this image: CVE-2026-56684 needs Valkey's own TLS listener, which is never enabled here (tls-port 0, and Valkey has no network listener at all), and CVE-2026-63639 needs a client that may runRESTORE, which neither of the image's Valkey accounts may. If you attach the instance to an Ubuntu Pro subscription, unattended upgrades install the fixed package automatically; otherwise it arrives with Ubuntu's next public Valkey update or the next cloudimg version of this AMI. - Upgrades. Ubuntu security updates (including nginx and Valkey) install automatically. OTS itself is a single binary; cloudimg publishes new versions of this AMI for new OTS releases.
Step 12: Licences and source
OTS is licensed under the Apache License 2.0. The image ships its licence and the exact release it was installed from:
cat /usr/share/doc/cloudimg-ots/SOURCE.txt
OTS 1.23.1 (Apache-2.0)
release asset: https://github.com/Luzifer/ots/releases/download/v1.23.1/ots_linux_amd64.tgz
sha256: d94792b7374f10236efe159365050d0d5fee3e9083620393fdcca2cef5f1acac
binary sha256: 0a836d89c5c5cd511b2a71f546a5adb8b4d6e40c9530ec0564d637d199807277
tag v1.23.1 = commit 200a813a5f179f12463de89765269b7f6007aa34 (GPG-signed tag)
source: https://github.com/Luzifer/ots/tree/v1.23.1
cloudimg installs the official upstream release binary, unmodified. Its SHA-256 is fixed in the cloudimg build and checked before installation; a different file stops the build. The licence text is in /usr/share/doc/cloudimg-ots/LICENSE-ots-Apache-2.0.txt, and Valkey (BSD-3-Clause) is the Ubuntu package (/usr/share/doc/valkey-server/copyright). OTS is a trademark of its respective owner. All product and company names are trademarks or registered trademarks of their respective holders; cloudimg is not affiliated with or endorsed by them.
Support
cloudimg supports this image: email support@cloudimg.co.uk. See the identity, secrets and PKI listing guide for the other versions in this listing.