【实用的开源工具】OTS:发完就销毁的密码分享链接
「实用的开源工具」系列:我只收集值得用的开源项目,写清楚它能做什么、怎么部署、有哪些坑,并在文末标明出处。内容整理自官方文档,核对日期 2026-10-31,具体版本以官方为准。命令和配置都摘自官方 README、Wiki 和仓库里的 Compose 文件,本文没有做实机测试,请先在测试环境里走一遍。
项目信息卡
| 项目 | 说明 |
|---|---|
| 名称 | OTS(仓库名 Luzifer/ots),One-Time-Secret 一次性密文分享平台 |
| 做什么 | 密文在浏览器里用 256 位 AES 对称加密后再发给服务器,生成含“密文 ID + 密码”的链接;第一次读取后密文立即删除 |
| 许可证 | Apache-2.0 |
| 仓库 | github.com/Luzifer/ots |
| 最近版本 | v1.23.1(2026-10-23 发布,查询于 2026-10-31) |
| 维护状态 | 未归档,最近一次推送 2026-10-30;SECURITY.md 写明只支持最新发布版,master 分支被标为不稳定 |
| 运行方式 | 单个二进制或容器,默认监听 3000 端口;存储后端可选 mem(内存)或 redis |
| 镜像 | ghcr.io/luzifer/ots,仓库自带的 Compose 文件用的是 ghcr.io/luzifer/ots:v1.23.1 |
| 官方公共实例 | ots.fyi(作者自己托管)。官方建议用公共实例时不要信任作者,要先自己加密 |
它能做什么
- 浏览器端加密:密文在你的浏览器里加密后才发给服务器,密码不会发给服务器,所以服务器没有办法在合理代价内解密它保存的内容。
- 读一次就删除:密文在第一次被读取时立即删除。
- 过期时间:用
SECRET_EXPIRY控制密文的存活时间(秒)。 - 命令行工具 OTS-CLI:可以在脚本里创建和读取密文。
- 可定制:通过
--customize加载一个 YAML 文件,改标题、图标、过期选项等。
典型用法:要把服务器密码、API 密钥、临时账号发给同事或客户时,不直接贴在聊天记录里,而是发一个只能打开一次的链接。
注意它只是“少留痕”,不是万能的:链接发出去之后,谁先打开谁就拿到了内容,所以链接本身仍然要发给对的人。
部署
官方仓库里自带一个 docker-compose.yml,用 Valkey(Redis 兼容)存放密文。先建目录:
mkdir -p /opt/ots && cd /opt/otsdocker-compose.yml(内容取自官方仓库,我只改了端口绑定):
services:
app:
image: ghcr.io/luzifer/ots:v1.23.1
restart: always
environment:
# Optional, see "Customization" in README
#CUSTOMIZE: '/etc/ots/customize.yaml'
# See README for details
REDIS_URL: redis://valkey:6379/0
# 168h = 1w
SECRET_EXPIRY: "604800"
# "mem" or "redis" (See README)
STORAGE_TYPE: redis
depends_on:
- valkey
ports:
- 127.0.0.1:3000:3000
valkey:
image: docker.io/valkey/valkey:9.1.3-alpine
restart: always
volumes:
- ./data:/data要点:
- 版本:这是官方文件里写的标签(查询时与最新发布版一致)。升级时记得先确认新标签存在。
STORAGE_TYPE:官方 README 说明有两种后端,mem是内存存储,服务重启就清空;redis把密文存在一个 hash 里,用REDIS_URL连接,REDIS_KEY控制键名前缀(默认io.luzifer.ots)。SECRET_EXPIRY:单位是秒,默认0,表示不过期。上面的604800就是一周。- 端口:官方文件是
3000:3000,对所有网卡开放。这里绑定127.0.0.1,交给反向代理。 valkey没有发布端口,只能被同一个 Compose 网络里的app访问,这样比较稳妥。
启动:
docker compose up -d
docker compose logs -f app不想用 Docker,官方 Wiki 的说法是下载 Releases 里的二进制,配合 systemd 单元和环境文件运行,Wiki 里有完整示例,用到的环境变量有 LISTEN、STORAGE_TYPE、REDIS_URL、SECRET_EXPIRY、CUSTOMIZE。
第一次使用
- 浏览器打开
http://127.0.0.1:3000/(远程服务器可以先用 SSH 端口转发)。 - 输入要分享的内容,创建后会得到一个链接。链接里包含密文 ID 和密码,形如
https://ots.example.com/#密文ID|密码。官方解释:#后面的部分浏览器不会发给服务器,服务器只会看到https://ots.example.com/,然后页面里的脚本用 ID 取回密文并在本地解密。 - 把完整链接发给对方。对方打开读取之后,密文就会被删除,再打开就没有了。
所以:自己别用这个链接去“测试能不能打开”,打开一次,密文就没了。测试请用一条无关紧要的内容。
用命令行创建
官方提供 OTS-CLI,可以在 Releases 页面下载,也可以用 Go 工具链从仓库的 ./cmd/ots-cli 目录构建。官方 README 里的用法:
echo "my password" | ots-cli create --instance https://ots.example.com
ots-cli fetch 'https://ots.example.com/#...|...'create从标准输入或指定文件读取内容,把链接输出到标准输出;fetch把密文打印到标准输出,附件存到指定目录。- 日志走标准错误输出,可用
--log-level=fatal关闭。 - 常用的实例地址可以放到环境变量
OTS_INSTANCE,就不用每次写--instance。 - 如果你的实例给
/api/create加了认证,官方的做法是像 curl 一样传:-u myuser:mypass做 Basic 认证,或-H 'Authorization: Token abcde'。 - 其他参数看
ots-cli create --help和ots-cli fetch --help。
用 curl 自己加密(官方强烈推荐用于公共实例)
官方 README 还介绍了一种做法:先在本地加密,再把密文交给接口。这样即使对方的服务器是别人搭的,也读不到内容。官方示例:
echo "my password" | openssl aes-256-cbc -base64 -pass pass:mypass -pbkdf2 -iter 300000 -md sha512
curl -X POST -H 'content-type: application/json' -i -s \
-d '{"secret": "上一步输出的密文"}' https://ots.example.com/api/create接口返回的 JSON 里有 secret_id,最终要发给对方的链接是 https://ots.example.com/#<secret_id>|mypass。注意 -pass pass:mypass 会把密码留在 shell 历史里,这是通用的注意点,不是官方提示。
反向代理(Nginx)
官方 Wiki 的 HTTPS 部分给的是下面这份配置(Wiki 里 listen 80,让你再用 Let's Encrypt 加证书)。下面是在其基础上补了 443 的写法,代理部分保持原样:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name ots.example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
location / {
add_header X-Robots-Tag noindex;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:3000/;
}
}如果开放了附件功能,上传大小要在这里限制。官方 Wiki 写明:附件大小的上限(maxAttachmentSizeTotal)只在前端检查,想真正限制别人能发来的数据量,要在代理里设置请求体大小上限,也就是 Nginx 的 client_max_body_size(具体数值自己定)。
定制与限制
通过 --customize=路径 或环境变量 CUSTOMIZE 加载一个 YAML。下面这些键来自官方 Wiki 的 Customization 页:
appTitle: '' # 页面标题,留空用默认的 "OTS - One Time Secrets"
disableExpiryOverride: false # 关掉用户自选过期时间的下拉框和对应的接口能力
expiryChoices: [300, ...] # 下拉框里的过期时间选项,单位秒
disableFileAttachment: false # 关闭附件功能(仅前端层面)
maxAttachmentSizeTotal: 0 # 附件总大小(字节);0 表示内部上限 64 MiB,仅前端检查
maxSecretSize: 121168782 # 服务器端拒绝超过此大小的密文(这是 Wiki 里写的内部默认值)
metricsAllowedSubnets: [] # 允许访问 /metrics 的网段,默认全部拒绝
overlayFSPath: /path/to/ots-customization # 覆盖前端资源的目录完整键列表请看 Wiki 原文。
更新与卸载
更新:改 docker-compose.yml 里 app 的版本标签(先去 Releases 和镜像库确认新标签存在),然后:
cd /opt/ots
cp -a data data.bak-$(date +%F) # 通用做法:升级前先备份
docker compose pull
docker compose up -d官方 SECURITY.md 的说法是旧版本不再维护,建议尽快升级到最新发布版,还可以用 GitHub 的 Watch 功能关注新发布。
卸载:
cd /opt/ots
docker compose down这只会删除容器。./data 里是 Valkey 的数据(尚未被读取的密文),确认不需要后再手动删除:
rm -rf /opt/ots/data # 不可恢复,确认后再执行踩坑清单
- 链接必须完整。 密码是链接里
|后面的那段,不会发给服务器,也就没法找回。丢了、被聊天软件截断了,这条密文就读不出来了。 - 打开即销毁。 第一次读取后密文立即删除,包括你自己点开测试那一次。
- 默认存储是内存。 官方 README 说
mem在服务重启后清空。要保留重启前的密文,用redis(上面的 Compose 就是这种)。 - 默认不过期。
SECRET_EXPIRY默认0,表示不过期。没被读取的密文会一直躺在存储里,建议设一个期限。用户可选的过期时间由expiryChoices控制,想统一规定就开disableExpiryOverride。 - 创建接口默认谁都能用。 谁能访问你的实例,谁就能创建密文、占用你的存储。官方 Wiki 给的做法:在代理上给
/api/create和/api/isWritable加访问控制(比如只让登录了公司 SSO 的人访问);没有权限访问/api/isWritable的人,创建界面也会被关掉。 - 文件类型限制不是安全措施。
acceptedFileTypes、disableFileAttachment、maxAttachmentSizeTotal都只作用于前端。服务器看不到内容,无法判断是大密文还是小密文加附件。要限制数据量,请在代理里限制请求体大小。 - 改了
overlayFSPath里的文件要重启。 官方说明:否则文件的完整性哈希对不上,浏览器会拦截这些资源。 /metrics在代理后面要小心。 它按客户端地址判断是否放行,而经过反向代理时看到的是代理的地址,所以官方提醒:除非代理那一层也限制了/metrics,否则不要把代理的网段加进metricsAllowedSubnets。- 只用最新发布版。
SECURITY.md写明master不稳定、旧版本不再维护,所以固定到最新发布版本,而不是master或随手拉旧镜像。 - 务必走 HTTPS。 官方 Wiki 明确建议在前面加 Nginx 和 Let's Encrypt 这样的传输层加密。另外这类工具的安全性建立在“网页脚本没被篡改”之上,通用做法是别把它随便放在不受信任的环境里。
- Docker 发布的端口会绕过 ufw 之类的防火墙。 所以示例绑定到
127.0.0.1。这是 Docker 的通用行为,不是 OTS 特有的。 - 公共实例要先自己加密。 官方对 ots.fyi 的态度是:别把明文交给作者的服务器,而是用上面的 openssl + curl 方式先加密再提交。你自己搭的实例同理,对方并不知道你的服务器是否可信,涉及特别敏感的内容时可以参照这种做法。
出处
有想法或问题?去留言板留言 →