Onetime Secret 0.26.19 on AWS User Guide
Overview
This guide covers launching and using the Onetime Secret 0.26.19 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.
Onetime Secret is an open source service for sharing a secret through a link that works only once. Paste a password, an API key or a private note, optionally protect it with a passphrase, and send the link. The recipient opens it once and the secret is destroyed; a second visit finds nothing, and an unread secret is deleted when its lifetime runs out.
The cloudimg image is a hardened, ready-to-use Onetime Secret server:
- nginx in front, Onetime Secret behind it. nginx is the only public listener (ports 80 and 443). Onetime Secret runs under Puma on
127.0.0.1:3000, the loopback address only, as its own unprivileged user, and cannot modify its own code. - 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 EC2 public DNS name at first boot; you can change it to your own domain.
- Secrets encrypted with a per-instance key. Onetime Secret encrypts every secret before it is stored, with a key derived from an application secret that first boot generates on your instance. Neither the store's append-only file nor its snapshot holds a secret in plain text.
- Per-instance secrets, never in a process environment. The application secret, the session secret and the store password are generated on each instance's first boot and kept only in
/etc/onetime/config.yaml(readable by the service, not by nginx or Valkey). They are never on a command line, in a systemd unit or in a process environment. - A per-instance administrator, no default accounts. First boot creates the colonel (Onetime Secret's administrator) with a random password that Onetime Secret itself generates. There are no demo or default accounts, and self-signup is off: anonymous one-time secret sharing works without an account, and only the accounts you create can sign in.
- Valkey on localhost. Secrets and accounts are stored by Valkey, the Redis-compatible store from Ubuntu 24.04, on
127.0.0.1and::1only, with a per-instance password (Valkey keeps only its SHA-256). Its one account may not run administrative commands. Persistence is on, so accounts and unread secrets survive a reboot. - Real client addresses, fixed host name. nginx passes the real TCP peer address to Onetime Secret and never a client's
Host,X-Forwarded-HostorForwardedheader, so links and e-mails always use the configured name and rate limits cannot be dodged with a forged header. Requests for any other host name are dropped. - Abuse limits. Anyone who can reach the address can create secrets (that is how Onetime Secret works without accounts). nginx limits each client address to 30 secret creations, reveals and sign-ins per minute, and 6 sign-in attempts per minute; Onetime Secret adds its own per-address limits. A page on another website cannot create, reveal or burn secrets here.
- No outgoing mail by default. No mail server credentials are baked in; mail is off until you configure it (Step 9).
- Locked down. TLS 1.2 and 1.3 only; Onetime Secret, nginx, Valkey, the command-line tool and the first-boot service are blocked from the instance metadata service, and the AMI requires IMDSv2.
- Built from source, verified, and fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on. See Step 13 for exactly how it is built.
What is on the image: Onetime Secret 0.26.19 (a cloudimg build of the upstream v0.26.19 tag in /opt/onetimesecret/app, with Ruby 3.4.10 in /opt/onetimesecret/ruby), Valkey 7.2 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: Onetime Secret and Valkey listen on the loopback address only. - 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 EC2 public DNS name 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 Onetime Secret 0.26.19 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=onetimesecret}]'
First boot takes about half a minute after the instance is running. If you want the instance to use your own name from the start, put it in /etc/cloudimg/onetimesecret-domain with user data (for example a cloud-init write_files entry); otherwise first boot uses the instance's public DNS name, or its public IP address if it has no DNS name.
Step 2: Connect to your instance
| Operating system | SSH user |
|---|---|
| Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
systemctl is-active onetimesecret-firstboot valkey-server onetimesecret nginx
active
active
active
active
Only SSH, HTTP and HTTPS are public. Onetime Secret (Puma) and Valkey listen on the loopback address:
sudo ss -tln | grep -E ':(22|80|443|3000|6379) '
LISTEN 0 1024 127.0.0.1:3000 0.0.0.0:*
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 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 [::]:443 [::]:*
LISTEN 0 4096 [::]:22 [::]:*
LISTEN 0 511 [::]:80 [::]:*
LISTEN 0 511 [::1]:6379 [::]:*
Step 4: Read the address and the administrator password
sudo cat /root/onetimesecret-credentials.txt
The file (root only, mode 0600) gives this instance's address, the SHA-256 fingerprint of its certificate and the colonel (administrator) sign-in:
# Onetime Secret 0.26.19 - written on first boot by onetimesecret-firstboot.service. Unique to this instance.
ONETIMESECRET_URL=https://ec2-52-55-145-0.compute-1.amazonaws.com/
ONETIMESECRET_TLS_SHA256=DC:64:7E:6B:...:(32 bytes in hex pairs)
COLONEL_EMAIL=colonel@onetimesecret.invalid
COLONEL_PASSWORD=<20 random characters, unique to this instance>
# Anyone who can reach ONETIMESECRET_URL can create one-time secrets (no account needed, by design). Restrict the
# security group's 80/443 rules to the networks that should use it (your office, VPN or VPC CIDR).
# The colonel is the administrator: sign in at https://ec2-52-55-145-0.compute-1.amazonaws.com/signin and open https://ec2-52-55-145-0.compute-1.amazonaws.com/colonel .
# Self-signup is OFF (site.authentication.signup in /etc/onetime/config.yaml); outgoing mail is OFF until you set it.
# The certificate is self-signed and unique to this instance: compare its SHA-256 fingerprint (above) with the one
# your browser shows, or install a CA-issued certificate. Do not add it to a trust store.
# This instance's app secret, session secret and store password are in /etc/onetime/config.yaml (0640 root:ots).
# store password check: sudo onetimesecret-valkey-password check new one: sudo onetimesecret-valkey-password rotate
# Your own name (point DNS at this instance first): sudo onetimesecret-set-domain secrets.example.com
# User guide: https://www.cloudimg.co.uk/guides/onetime-secret-0-26-19-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 ONETIMESECRET_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:
openssl x509 -in /etc/ssl/onetimesecret/onetimesecret.crt -noout -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltName
subject=CN = ec2-52-55-145-0.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-52-55-145-0.compute-1.amazonaws.com, IP Address:52.55.145.0, IP Address:172.31.93.253
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 ONETIMESECRET_URL address. Paste the secret, optionally add a passphrase and choose how long the link may live:

Choose Create Link. The page shows the one-time link; send it to the recipient over any channel, and the passphrase over a different one:

The recipient opens the link, enters the passphrase and reveals the secret:

The secret is now gone. Opening the same link again finds nothing:

Step 6: Use the API
Onetime Secret has a JSON API you can call from scripts. On the instance itself, with the instance's own certificate as the trust anchor, create a secret protected by a passphrase, try a wrong passphrase, reveal it, and try again:
U=$(sudo sed -n 's/^ONETIMESECRET_URL=//p' /root/onetimesecret-credentials.txt)
C=(-s --cacert /etc/ssl/onetimesecret/onetimesecret.crt)
KEY=$(curl "${C[@]}" --data 'secret=api-test-value&passphrase=correct-horse&ttl=3600' "${U}api/v1/share" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["secret_key"])')
echo "created a secret with a key of ${#KEY} characters"
curl "${C[@]}" -o /dev/null -w 'wrong passphrase: %{http_code}\n' --data 'passphrase=wrong' "${U}api/v1/secret/${KEY}"
curl "${C[@]}" -w '\nfirst read: %{http_code}\n' --data 'passphrase=correct-horse' "${U}api/v1/secret/${KEY}" | sed -E 's/"secret_key":"[^"]*"/"secret_key":"..."/'
curl "${C[@]}" -o /dev/null -w 'second read: %{http_code}\n' --data 'passphrase=correct-horse' "${U}api/v1/secret/${KEY}"
created a secret with a key of 62 characters
wrong passphrase: 404
{"value":"api-test-value","secret_key":"...","share_domain":"https://ec2-52-55-145-0.compute-1.amazonaws.com"}
first read: 200
second read: 404
The shortest lifetime Onetime Secret accepts is 60 seconds (ttl, in seconds); a secret nobody reads is deleted when it runs out. A request a page on another website makes (its Origin is not this instance) is refused:
U=$(sudo sed -n 's/^ONETIMESECRET_URL=//p' /root/onetimesecret-credentials.txt)
curl -s --cacert /etc/ssl/onetimesecret/onetimesecret.crt -o /dev/null -w '%{http_code}\n' -H 'Origin: https://evil.example' --data 'secret=x&ttl=3600' "${U}api/v1/share"
403
Step 7: Sign in as the colonel (administrator)
Browse to https://<your-instance-name>/signin and sign in with COLONEL_EMAIL and COLONEL_PASSWORD from the credentials file, then open /colonel for the administration console (users, secrets statistics, system settings):

Only an account with the colonel role can open it; anonymous visitors and ordinary accounts are refused. Change the colonel's password from Account in the web app. The colonel e-mail address is a placeholder (colonel@onetimesecret.invalid); to use your own address, create a new colonel and keep or remove the first one (Step 8). Destructive colonel actions ask for the password again (Onetime Secret's step-up window).
Step 8: Accounts and sign-up
Accounts are optional: anyone who can reach the address can share secrets without one. An account keeps a list of the secrets you sent. Self-signup is off, so accounts are created by an administrator with Onetime Secret's own command-line tool, which prints a generated password once (never type a password on the command line):
sudo onetimesecret-ots customers create alice@example.com
Use --role colonel to create another administrator. sudo onetimesecret-ots customers list shows the accounts, and sudo onetimesecret-ots --help lists every command; the tool runs as the ots service user with metadata access blocked.
To let people create their own accounts, configure outgoing mail first (Step 9) because new accounts must confirm their e-mail address, then set signup: true under site.authentication in /etc/onetime/config.yaml and restart:
sudo grep -n -A5 '^ authentication:' /etc/onetime/config.yaml
14: authentication:
15- enabled: true
16- # Self-signup is OFF: only accounts an administrator creates can sign in. To open sign-up set signup: true
17- # (accounts must verify their e-mail address, so configure outgoing mail below first).
18- signup: false
19- signin: true
sudo systemctl restart onetimesecret
Step 9: Outgoing mail
Onetime Secret sends e-mail for account verification, password resets and the optional "send the link by e-mail" feature. The image ships with mail off (emailer: mode: disabled) and no credentials. To turn it on, replace the emailer: block at the end of /etc/onetime/config.yaml with your provider's settings, for example SMTP with STARTTLS on port 587:
emailer:
mode: smtp
from: secrets@example.com
from_name: Secrets
host: smtp.example.com
port: 587
user: "<your-smtp-user>"
pass: "<your-smtp-password>"
auth: login
tls: true
Keep the file's owner and mode (root:ots, 0640), then run sudo systemctl restart onetimesecret. Onetime Secret also supports Amazon SES, SendGrid and other providers (see the upstream configuration reference in /opt/onetimesecret/app/etc/defaults/config.defaults.yaml). Allow the outgoing port in your security group or network ACLs.
Step 10: Use your own domain and certificate
Point a DNS record (for example secrets.example.com) at the instance, then:
sudo onetimesecret-set-domain secrets.example.com
nginx then serves that name (the instance's EC2 name and addresses keep working), Onetime Secret uses it for the links and e-mails it generates, and, if the certificate is still the cloudimg self-signed one, a new self-signed certificate is issued for the new name. To use a certificate from a certificate authority, replace /etc/ssl/onetimesecret/onetimesecret.crt (full chain) and /etc/ssl/onetimesecret/onetimesecret.key (mode 0600, root) and run sudo systemctl reload nginx; onetimesecret-set-domain never overwrites a certificate that is not cloudimg's own. Check the current name and fingerprint with:
sudo onetimesecret-set-domain --show
name: ec2-52-55-145-0.compute-1.amazonaws.com
URL: https://ec2-52-55-145-0.compute-1.amazonaws.com/
also served: ec2-52-55-145-0.compute-1.amazonaws.com 52.55.145.0 172.31.93.253
certificate: SHA-256 DC:64:7E:6B:...
Step 11: The store, its password and encryption at rest
Valkey keeps Onetime Secret's data in /var/lib/valkey with persistence on (an append-only file written every second plus snapshots), so accounts and unread secrets survive a reboot. Secret contents are encrypted by Onetime Secret before they reach Valkey. The store is capped at 1 GB and refuses new secrets when full (it never silently drops a secret nobody has read yet):
sudo onetimesecret-valkey-password check
sudo grep -vE '^(#|$|user )' /etc/valkey/cloudimg-onetimesecret.conf
VALKEY_PASSWORD_OK auth=PONG anonymous=NOAUTH wrong=WRONGPASS config-set=NOPERM appendonly=yes
bind 127.0.0.1 -::1
protected-mode yes
port 6379
appendonly yes
appendfsync everysec
rdbcompression no
maxmemory 1gb
maxmemory-policy noeviction
tls-port 0
cluster-enabled no
The store password is generated on each instance's first boot and kept in /etc/onetime/config.yaml; Valkey's file holds only its SHA-256. To change it, run sudo onetimesecret-valkey-password rotate (it restarts Valkey and Onetime Secret; secrets and accounts are kept).
Never change site.secret in /etc/onetime/config.yaml on a running service: Onetime Secret derives the encryption key of every stored secret from it, so changing it makes every unread secret unreadable. Back up /etc/onetime/config.yaml together with /var/lib/valkey if you back up the store.
Step 12: Security notes
- Rate limits. nginx keys its limits on the real TCP peer address; a forged
X-Forwarded-Forheader neither changes it nor appears in the log. The access log never records secret or receipt keys:
sudo grep -E 'limit_req_zone' /etc/nginx/conf.d/cloudimg-onetimesecret-maps.conf
limit_req_zone $binary_remote_addr zone=ots_all:10m rate=20r/s;
limit_req_zone $ots_write_key zone=ots_write:10m rate=30r/m;
limit_req_zone $ots_write_key zone=ots_login:10m rate=6r/m;
- Metadata service. The AMI requires IMDSv2, and Onetime Secret, nginx, Valkey, the command-line tool and the first-boot service are denied the instance metadata addresses by systemd:
systemctl show -p IPAddressDeny onetimesecret nginx valkey-server onetimesecret-firstboot
IPAddressDeny=::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
- Logs. Onetime Secret's own log (the systemd journal,
journalctl -u onetimesecret) records requests with secret identifiers redacted, and the image sets its framework loggers to warnings only so identifiers of live secrets are not kept. - Valkey advisories. Valkey 7.2.13 comes from the Ubuntu 24.04
universearchive (Ubuntu 24.04 has no Redis-compatible server inmain). 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; nginx terminates TLS for the web app), and CVE-2026-63639 needs a client that may runRESTORE, which the image's only Valkey account may not. Valkey is also reachable only from the instance itself, with the per-instance password. If you attach the instance to an Ubuntu Pro subscription, unattended upgrades install the fixed package automatically. - Ruby. Ruby 3.4.10 is the exact version Onetime Secret 0.26.19 requires. Ruby 3.4.12 (2026-10-23) fixes memory-safety bugs in compiling attacker-supplied regular expressions (behind GitLab's CVE-2026-89078, -93577, -97229 and -110264); Onetime Secret never compiles a regular expression from request data. Ruby 3.4.10's bundled
resolvlibrary (CVE-2026-80212, CVE-2026-80213) is not used by Onetime Secret, whose e-mail validation is a pattern check. cloudimg publishes a new version of this AMI when Onetime Secret moves to a newer Ruby. - Upgrades. Ubuntu security updates (including nginx and Valkey) install automatically. cloudimg publishes new versions of this AMI for new Onetime Secret releases.
Step 13: Licences and source
Onetime Secret is MIT licensed. The image ships its licence, the exact source reference and the build record:
cat /usr/share/doc/cloudimg-onetimesecret/SOURCE.txt
sed -n '1,4p' /usr/share/doc/cloudimg-onetimesecret/BUILD-INFO
cloudimg Onetime Secret 0.26.19 appliance - source
Onetime Secret (MIT licence, LICENSE-onetimesecret-MIT.txt, Copyright (c) 2012-ongoing delano, contributors):
https://github.com/onetimesecret/onetimesecret/tree/v0.26.19 (commit e98a88c9b5832bbae249f38885aa28bd78a5db4d)
Built unmodified from that tag, with upstream's own build steps (its Dockerfile): the gems from Gemfile.lock in this
directory (upstream's lock with two in-range security raises: rack 3.2.7 -> 3.2.8 and sanitize 7.0.0 -> 7.0.1), the frontend from
pnpm-lock.yaml + pnpm-workspace.yaml in this directory (upstream's lock with same-major security overrides:
dompurify 3.4.17, vue-i18n / @intlify 11.4.14, follow-redirects 1.16.2). Test suites (spec/, tests/) and developer
tooling are not shipped. Build details: BUILD-INFO; the gem list: gems.txt.
Ruby 3.4.10 (Ruby licence or BSD-2-Clause: LICENSE-ruby-COPYING.txt, LICENSE-ruby-BSDL.txt) is built from
https://cache.ruby-lang.org/pub/ruby/3.4/ruby-3.4.10.tar.gz into /opt/onetimesecret/ruby.
Each gem's licence is in its own directory under /opt/onetimesecret/app/vendor/bundle/ruby/3.4.0/gems/.
Valkey (BSD-3-Clause) and nginx (BSD-2-Clause) are the Ubuntu 24.04 packages: /usr/share/doc/<package>/copyright.
Onetime Secret is the name of its open-source project; cloudimg is not affiliated with or endorsed by it or by
onetimesecret.com.
cloudimg build of Onetime Secret 0.26.19 (AWS bundle member onetimesecret-aws)
source: github.com/onetimesecret/onetimesecret tag v0.26.19 commit e98a88c9b5832bbae249f38885aa28bd78a5db4d (tag tarball sha256 ef398bfbf063db1fa4ea6a9c5fcb7f90423564800840d563f19f1bda0da11d0a)
licence: MIT (LICENSE.txt sha256 73fc32bac6ec19ddfc5986b9b740f05d988b15f81ccef65f91b4fc7349dd904d)
ruby: 3.4.10 from https://cache.ruby-lang.org/pub/ruby/3.4/ruby-3.4.10.tar.gz (sha256 ecee2d072a14f2d14347dd56dfd8fe5c3130abf5117bfaacbda0f4ef9cc429ec), static libruby, prefix /opt/onetimesecret/ruby
cloudimg builds Onetime Secret from the upstream tag with upstream's own build steps, on a separate build host (the image never contains a compiler, Node.js or a package cache). The only changes are in the dependency lock files: two in-range Ruby gem updates (rack 3.2.8 and sanitize 7.0.1) and same-major frontend updates (DOMPurify 3.4.17, vue-i18n 11.4.14, follow-redirects 1.16.2) that fix every advisory reported against the shipped dependencies. Test suites and developer tools are not shipped. All product and company names are trademarks or registered trademarks of their respective holders; cloudimg is not affiliated with or endorsed by them, or by onetimesecret.com.
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.