OTS 1.23.1
Overview
This guide shows how to launch and use the OTS 1.23.1 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. The image is one version of the SecureAnyCloud identity, secrets and PKI listing on AWS Marketplace. You select it as the software version when you subscribe and launch.
OTS (One-Time Secrets) is an open-source service for sharing a secret exactly once. You 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. This SecureAnyCloud image is a hardened, ready-to-use OTS server.
What you get:
- 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. - HTTPS from the first boot. Browsers allow the Web Crypto functions OTS needs only on a secure (HTTPS) page. The image therefore 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, and 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. 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 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. Step 12 lists exactly what is installed.
- 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, x86_64 architecture and a recommended instance type of m5.large - 24/7 SecureAnyCloud support
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. Allow TCP 443 (the web app) and TCP 80 (redirects to HTTPS) only from the networks whose people should use this service, such as 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, because 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.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Select OTS 1.23.1 on Ubuntu 24.04 as the software version, select your Region, and choose Launch through EC2. Choose m5.large, your key pair and a security group as described in the prerequisites.
To launch with the AWS CLI instead, run the following command and 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 completes a few seconds after the instance is running. To use your own name from the start, put it in /etc/sac/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.
Connect to your instance
Connect over SSH as the ubuntu user, which is the default user on Ubuntu 24.04:
ssh -i <your-key.pem> ubuntu@<public-ip>The login message shows the address of the web app.
Confirm the services are running
Check that Valkey, OTS and nginx are all active:
systemctl is-active valkey-server ots nginxactive active activeThe ots-firstboot service 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 listens only on its local socket:
sudo ss -tln | grep -E ':(22|80|443|3000|6379) '
sudo ss -xl | grep valkeyLISTEN 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 * 0Run the built-in self-test. It creates an encrypted secret through nginx and reads it once. It then checks that a second read is refused and that Valkey refuses an unauthenticated client.
sudo ots-selftestots-selftest: created (201, expires 2026-11-15T03:47:45.013953816Z), first read 200 + identical ciphertext + decrypts, second read 404, Valkey NOAUTH OTS_SELFTEST_OKRead the address and the certificate fingerprint
Read the instance information file:
sudo cat /root/ots-info.txtThis root-only file (mode 0600) gives the instance's address, the SHA-256 fingerprint of its certificate and the limits. There is no administrator password to collect, because 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://secureanycloud.com/stacks/ots-1-23-1/The certificate is self-signed and unique to your instance, so your browser shows a warning. Open the certificate details in the browser and check that its SHA-256 fingerprint matches OTS_TLS_SHA256. Continue only if it matches.
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 the certificate 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,subjectAltNamesubject=CN = 198.51.100.24, O = sac 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 AuthenticationShare a secret
Browse to the OTS_URL address. Type or paste the secret and, optionally, 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. It also shows when an unread secret will be deleted. Send the link to the recipient over any channel, and do not open it yourself.
When the recipient opens the link, nothing has been fetched yet. The page asks them to confirm, because reading destroys the secret.
The recipient chooses Show me the secret!. Their browser downloads the ciphertext, decrypts it with the password from the link, and shows the secret and any attached files, which they can download.
The secret is now gone from the server. Opening the same link again shows that the secret is no longer there. If a recipient sees this the first time they open a link, someone else has already read the secret.
Attachments and expiry
Attachments are limited to 16 MiB in total, and 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.
OTS itself serves the settings that the page reads. On the instance, connect to nginx on the loopback address with --connect-to, and use the instance certificate as the trust anchor. The security group normally does not let an instance reach its own public address.
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}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: 404The share link is the address, #, the secret ID, %7C (an encoded |) and the password. Give the link to the recipient instead of reading the secret yourself. The ?expire= parameter sets the lifetime in seconds, and values above 7 days are lowered to 7 days.
OTS refuses invalid requests. It also refuses a request made by a page on another website, whose Origin is not this instance. Errors return an error reference only, never 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 403Limits and who can create secrets
OTS runs with a 7-day maximum lifetime, its request log off, and the size limits set in /etc/ots/customize.yaml:
systemctl show -p ExecStart --value ots | grep -o 'argv\[\]=[^;]*'
sudo grep -E '^(maxAttachmentSizeTotal|maxSecretSize|metricsAllowedSubnets):' /etc/ots/customize.yamlargv[]=/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: []The maxSecretSize value (32 MiB) is the largest encrypted secret OTS accepts. It 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/sac/ots/nginx-site.tmpl. Then 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/sac-ots.conflimit_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"Your security group decides who can create secrets. Keep the 443 and 80 rules limited to the networks that should use the service. To serve OTS 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/sac/ots-create-access.d/. These rules apply to the create request only. The following example allows only your office network (203.0.113.0/24 here) to create secrets:
echo 'allow 203.0.113.0/24; deny all;' | sudo tee /etc/nginx/sac/ots-create-access.d/office.conf
sudo nginx -t && sudo systemctl reload nginxAlternatively, require a user name and password for creating secrets. The browser asks for them when a secret is created, and 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/sac/ots-create-access.d/password.conf
sudo nginx -t && sudo systemctl reload nginxThe 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/sac-ots.confbind 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/sac-ots-users.aclThe noeviction policy means Valkey never silently drops a stored secret to make room. When the store is full, a new secret is refused and 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 pingCould not connect to Valkey at /run/valkey/valkey.sock: Permission denied NOAUTH Authentication required.The ots-valkey-admin helper 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 cannot change or delete them. Every stored value is ciphertext in the OpenSSL format, and 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; done1 used_memory_human:1.11M maxmemory_human:512.00M maxmemory_policy:noeviction U2FsdGVkX1A reboot, or a restart of Valkey, deletes every unread secret. This is by design, because secrets are never written to disk. The service itself comes back on its own after a reboot.
Use your own domain and certificate
Point a DNS record (for example secrets.example.com) at the instance. Then run the command below with the new name first, followed by any other names or addresses that should keep working:
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. The file /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/.
If you use the instance's IP address rather than a DNS name, run ots-set-domain again whenever a stop and start changes the public IP address.
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.
Security notes
- Metadata service. The AMI requires IMDSv2. OTS may talk to the loopback address only, and systemd denies nginx, Valkey and the first-boot service access to the instance metadata addresses. OTS makes no outbound requests, and it never fetches a URL that a user supplies.
systemctl show -p IPAddressAllow -p IPAddressDeny ots
systemctl show -p IPAddressDeny nginx valkey-server ots-firstbootIPAddressAllow=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 password appears 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 universe archive. 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 issue 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). CVE-2026-63639 needs a client that may run RESTORE, 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, the fix arrives with Ubuntu's next public Valkey update or the next SecureAnyCloud version of this AMI.
- Upgrades. Ubuntu security updates (including nginx and Valkey) install automatically. OTS itself is a single binary, and SecureAnyCloud publishes new versions of this AMI for new OTS releases.
Licences and source
OTS is licensed under the Apache License 2.0. The image ships its license and a record of the exact release it was installed from:
cat /usr/share/doc/sac-ots/SOURCE.txtOTS 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.1SecureAnyCloud installs the official upstream release binary, unmodified. Its SHA-256 is fixed in the SecureAnyCloud build and checked before installation, and a different file stops the build.
The license text is in /usr/share/doc/sac-ots/LICENSE-ots-Apache-2.0.txt. Valkey (BSD-3-Clause) is the Ubuntu package, and its license is in /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. SecureAnyCloud is not affiliated with or endorsed by them.
Support
SecureAnyCloud supports this image. For questions or help with your deployment, email sales@secureanycloud.com. See the identity, secrets and PKI listing guide for the other versions in this listing.
Deploy on AWS
Launch OTS 1.23.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com