OTS 1.23.1 常用 CLI 命令与 API 详解
OTS v1.23.1 自动化使用详解:ots-cli create/fetch 全参数实战、REST API 六端点 curl 实测、openssl 零知识加密流程与错误排障,全部真实回显。
OTS 1.23.1 常用 CLI 命令与 API 详解
目标:把一次性秘密分享平台 OTS v1.23.1 的两条自动化使用路径讲全——官方命令行客户端
ots-cli(create/fetch,本地加密零知识)与 REST API(/api/create、/api/get/{id}等六端点),每条命令与每个请求均配 Ubuntu 22.04 上对 OTS v1.23.1(监听 8146)实测的真实回显,可独立执行复现。
一、环境信息
- 操作系统:Ubuntu 22.04
- OTS 服务端:v1.23.1(官方二进制,systemd 托管,监听
0.0.0.0:8146,存储mem,--secret-expiry=604800) - CLI 客户端:ots-cli v1.23.1(与服务器同版本,官方 Releases 发行)
- 实验目录:
/opt/ots-cli-demo/(本文临时文件均在此,命令可独立执行) - API 风格:JSON over HTTP,创建
POST /api/create、读取GET /api/get/{id},错误统一返回{"success":false,"error":"<UUID>"} - 版本与端点取证日期:2026-10-30(GitHub Releases 与仓库源码 api.go)
- 相关篇目:《OTS 介绍与选型指南》、《Ubuntu 22.04 安装 OTS 指南》、《OTS 1.23.1 配置参数详解》
二、工具概览
与 OTS 打交道共有三套工具,定位不同:
| 工具 | 形态 | 适用 |
|---|---|---|
| Web 前端(内嵌于服务端) | 浏览器 | 人工一次性分享(本文不展开) |
| ots-cli(官方 CLI,独立发行) | 单二进制 | 脚本/终端里创建与读取秘密,本地加密、零知识 |
| REST API(服务端内建) | HTTP JSON | 程序集成、健康探测、Prometheus 抓取 |
三者操作同一个服务端。ots-cli 与浏览器走同一条加密纪律:先本地加密、再上送密文;直接裸调 API 虽然也能传明文(服务端不解析内容),但违背项目设计初衷,本文在第五章给出规范的 openssl 加密姿势。
2.1 安装 ots-cli
从官方 Releases 下载(与服务端同目录约定,v1.23.1 的 CLI 发行包为 ots-cli_linux_amd64.tgz,官方 SHA256 清单中该文件校验值 5e211e9e7770c83ff959e9e2036bf3d73c9444a52b67195aaddeb38e2d1241af,实测校验一致):
mkdir -p /opt/ots-cli-demo && cd /opt/ots-cli-demo
curl -fsSLo ots-cli_linux_amd64.tgz https://github.com/Luzifer/ots/releases/download/v1.23.1/ots-cli_linux_amd64.tgz
sha256sum ots-cli_linux_amd64.tgz
实测回显:
5e211e9e7770c83ff959e9e2036bf3d73c9444a52b67195aaddeb38e2d1241af ots-cli_linux_amd64.tgz
tar xzf ots-cli_linux_amd64.tgz
install -m 0755 ots-cli /usr/local/bin/ots-cli
ots-cli version
实测回显:
ots-cli v1.23.1
三、ots-cli 命令详解
3.1 命令总览
ots-cli --help
实测回显(全文照抄):
Utility to interact with encrypted secrets in an OTS instance
Usage:
[command]
Available Commands:
completion Generate the autocompletion script for the specified shell
create Create a new encrypted secret in the given OTS instance
fetch Retrieves a secret from the instance by its URL
help Help about any command
version Displays the tool version
Flags:
-h, --help help for this command
--log-level string Level to use for logging (trace, debug, info, warn, error, fatal) (default "info")
Use " [command] --help" for more information about a command.
核心只有 create(创建)与 fetch(读取)两个动作,外加 shell 补全生成。两条命令的日志一律走 stderr,结果走 stdout——管道友好,可直接用于脚本。
3.2 create:创建加密秘密
ots-cli create --help
实测回显(全文照抄):
Create a new encrypted secret in the given OTS instance
Usage:
create [-f file]... [--instance url] [--secret-from file] [flags]
Examples:
echo "I'm a very secret secret" | ots-cli create
Flags:
--expire duration When to expire the secret (0 to use server-default)
-f, --file strings File(s) to attach to the secret
-H, --header strings Headers to include in the request (i.e. 'Authorization: Token ...')
-h, --help help for create
--instance string Instance to create the secret with (default "https://ots.fyi/")
--no-text Disable secret read (create a secret with only files)
--secret-from string File to read the secret content from ("-" for STDIN) (default "-")
-u, --user string Username / Password for basic auth, specified as 'user:pass'
Global Flags:
--log-level string Level to use for logging (trace, debug, info, warn, error, fatal) (default "info")
实战 1:从管道创建,指定实例与有效期。 把目标实例写进环境变量 OTS_INSTANCE,避免每条命令重复 --instance:
export OTS_INSTANCE=http://127.0.0.1:8146/
echo "db-password-Pr3dict4bl3" | ots-cli create --expire 1h > /opt/ots-cli-demo/url1.txt 2> /opt/ots-cli-demo/create1.log
cat /opt/ots-cli-demo/create1.log
echo "URL=$(cat /opt/ots-cli-demo/url1.txt)"
实测回显:
time="2026-10-29T17:37:31Z" level=info msg="reading secret content..."
time="2026-10-29T17:37:31Z" level=info msg="creating the secret..."
time="2026-10-29T17:37:31Z" level=info msg="secret created, see URL below" expires-at="2026-10-29 18:37:31.242436633 +0000 UTC"
URL=http://127.0.0.1:8146/#7177829e-f847-4eb8-b8ee-8213ac8b4e7b%7CHNnXn7A25cUkEfu2ABky
逐项解读:三行 INFO 是 stderr 日志(读入 → 创建 → 完成);expires-at 与 --expire 1h 一致;输出 URL 的 # 后是 secret_id%7Cpassword(%7C 即 URL 编码的 |),竖线后面那段随机串就是解密密码,只在 URL 锚点里,服务器不知道。把整条 URL 交给接收方即可。
实战 2:从文件读入秘密(避免命令行历史落明文):
echo "another-secret" > /opt/ots-cli-demo/plain.txt
ots-cli create --secret-from /opt/ots-cli-demo/plain.txt --expire 30m > url2.txt 2>/dev/null
cat url2.txt
实测回显:
http://127.0.0.1:8146/#63cfa875-3aa5-4629-b874-4a6691948290%7Cxq83VDiGOzIlfCtpkNX7
(secret-from 默认值即 -(标准输入),与管道等价。)
实战 3:带附件创建(--file / --no-text)。 附件与文本一起加密成单个 blob;--no-text 表示不含正文、只有附件:
echo "attach-content-123" > /opt/ots-cli-demo/attach.txt
ots-cli create -f /opt/ots-cli-demo/attach.txt --no-text --expire 1h > url3.txt 2>create3.log
cat create3.log
echo "URL=$(cat url3.txt)"
实测回显:
time="2026-10-29T17:37:31Z" level=info msg="reading secret content..."
time="2026-10-29T17:37:31Z" level=info msg="creating the secret..."
time="2026-10-29T17:37:31Z" level=info msg="secret created, see URL below" expires-at="2026-10-29 18:37:31.885477607 +0000 UTC"
URL=http://127.0.0.1:8146/#d9cb2776-7495-436f-8728-efa0f8b85d4a%7CWiXgu4PyqD72uTNKRdWj
需要认证的实例(服务端被反代加了 Basic Auth 或 Token 时,README 官方用法):ots-cli create --instance https://ots.example.com/ -u myuser:mypass 或 -H 'Authorization: Token abcde'(-H 可重复以携带多个头)。本文实例无认证,未展开实测。
3.3 fetch:读取秘密
ots-cli fetch --help
实测回显(全文照抄):
Retrieves a secret from the instance by its URL
Usage:
fetch <url> [flags]
Flags:
--file-dir string Where to put files attached to the secret (default ".")
-h, --help help for fetch
Global Flags:
--log-level string Level to use for logging (trace, debug, info, warn, error, fatal) (default "info")
实战 1:一次性读取成功。 用 3.2 实战 1 生成的 URL:
ots-cli fetch "$(cat /opt/ots-cli-demo/url1.txt)"
实测回显:
time="2026-10-29T17:37:31Z" level=info msg="fetching secret..."
db-password-Pr3dict4bl3
stderr 两行之外,stdout 打出解密后的原文——ots-cli 从 URL 锚点取出 id 与密码,本地完成解密。
实战 2:第二次读取失败(阅后即焚实证)。 同一条 URL 再 fetch 一次:
ots-cli fetch "$(cat /opt/ots-cli-demo/url1.txt)"; echo "exit=$?"
实测回显:
time="2026-10-29T17:37:31Z" level=info msg="fetching secret..."
Error: fetching secret
exit=1
秘密已销毁,命令以退出码 1 结束——脚本里可在 fetch 失败(退出码 1)时重新生成分享链接兜底。
实战 3:读取附件。 用 3.2 实战 3 的纯附件 URL,指定落盘目录:
mkdir -p /opt/ots-cli-demo/fetched
ots-cli fetch --file-dir /opt/ots-cli-demo/fetched "$(cat /opt/ots-cli-demo/url3.txt)"
cat /opt/ots-cli-demo/fetched/attach.txt
实测回显:
time="2026-10-29T17:37:31Z" level=info msg="fetching secret..."
time="2026-10-29T17:37:31Z" level=info msg="storing file..." file=attach.txt
attach-content-123
3.4 服务端二进制的命令行面
服务端 ots 自身只有参数没有子命令,--help 全量回显与逐参数解析见 《OTS 1.23.1 配置参数详解》第三章,版本查询实测:
ots --version
实测回显:
time="2026-10-29T17:49:40Z" level=info msg=ots version=v1.23.1
四、REST API 详解
OTS 服务端内建一套极简 JSON API(官方 OpenAPI 规范 docs/openapi.yaml 随仓库发布,并以内嵌页面 /api.html 提供可视化文档,实测可访问)。所有端点路径前缀 /api(/metrics 除外)。
4.1 端点总览
| 端点 | 方法 | 作用 | 成功状态码 |
|---|---|---|---|
/api/healthz | GET | 存活探测(无副作用) | 200 |
/api/isWritable | GET | 写入能力探测(LB 健康检查用) | 204 |
/api/create | POST | 存入秘密(JSON 或表单),支持 ?expire= 覆盖 | 201 |
/api/get/{id} | GET | 读取并销毁秘密 | 200 |
/api/settings | GET | 前端定制参数投影 | 200 |
/metrics | GET | Prometheus 指标(网段 ACL 控制) | 200 |
认证模型:API 无内置认证,信任边界在部署层(内网/防火墙/反代 Basic Auth——ots-cli 的 -u/-H 即为此准备)。
4.2 POST /api/create:存入秘密
请求体两种形态:Content-Type: application/json 的 {"secret":"..."},或表单字段 secret=...;可选查询参数 expire=<秒> 覆盖有效期(受服务端 --secret-expiry 封顶,规则详见配置篇 5.3 节)。
JSON 形态实测:
curl -s -X POST -H 'content-type: application/json' \
-d '{"secret":"U2FsdGVkX1+test-encrypted-blob="}' \
'http://127.0.0.1:8146/api/create?expire=3600'
实测回显:
{"success":true,"expires_at":"2026-10-29T18:36:20.086722912Z","secret_id":"12f3ed7d-9b8f-498e-bd2c-782a9a3cd1be"}
表单形态实测(不带 expire,走服务端默认 604800 秒):
curl -s -X POST -d 'secret=myformsecret' http://127.0.0.1:8146/api/create
实测回显:
{"success":true,"expires_at":"2026-11-05T17:36:20.096973401Z","secret_id":"86a3cf3f-9cd7-44ef-a3a9-09b3aaab4182"}
字段解读:secret_id 是后续取用的 UUID;expires_at 为 UTC 时刻,expire 参数为 0 且服务端也未设上限时该字段缺省。上例表单创建的 expires_at 落在 7 天后,与服务端 --secret-expiry=604800 吻合。
4.3 GET /api/get/{id}:一次性读取
第一次读取(取上面 JSON 形态创建的 id):
curl -s http://127.0.0.1:8146/api/get/12f3ed7d-9b8f-498e-bd2c-782a9a3cd1be
实测回显:
{"success":true,"secret":"U2FsdGVkX1+test-encrypted-blob="}
第二次读取(同 id):
curl -s -w '\nHTTP_CODE=%{http_code}\n' http://127.0.0.1:8146/api/get/12f3ed7d-9b8f-498e-bd2c-782a9a3cd1be
实测回显:
{"success":false,"error":"66e1fa8a-7794-4868-9eb5-daf907c9f180"}
HTTP_CODE=404
读取即删除:第一次 200 返回内容,第二次 404。注意 API 返回的 secret 就是调用方上送的原样内容——若按第五章姿势上送的是密文,API 消费方拿到的也是密文,需自行解密;浏览器前端才负责解密展示。
4.4 辅助端点实测
curl -s -o /dev/null -w 'healthz=%{http_code}\n' http://127.0.0.1:8146/api/healthz
curl -s -o /dev/null -w 'isWritable=%{http_code}\n' http://127.0.0.1:8146/api/isWritable
curl -s http://127.0.0.1:8146/api/settings
实测回显:
healthz=200
isWritable=204
{"appTitle":"Corp Secrets - One Time Secrets","expiryChoices":[300,3600,86400,604800],"acceptedFileTypes":"","disableFileAttachment":false,"maxAttachmentSizeTotal":10485760,"footerLinks":[{"name":"Internal Security Policy","url":"https://intranet.example.com/security"}]}
healthz/isWritable 供负载均衡与监控探活;settings 返回 customize.yaml 中非零值字段的 JSON 投影(前端据此渲染有效期下拉、附件开关等)。
4.5 GET /metrics:Prometheus 指标
默认拒绝一切来源(404),需在 customize.yaml 的 metricsAllowedSubnets 放行抓取方网段(配置方法与双向实测见配置篇 5.6 节)。放行后:
curl -s http://127.0.0.1:8146/metrics | grep '^ots'
实测回显(节选):
ots_secrets_create_errors{reason="invalid_expiry"} 1
ots_secrets_create_errors{reason="invalid_json"} 1
ots_secrets_create_errors{reason="secret_missing"} 1
ots_secrets_created 3
ots_secrets_read 1
ots_secrets_read_errors{reason="secret_not_found"} 3
ots_secrets_stored 2
指标族解读:ots_secrets_created / ots_secrets_read 为成功计数;ots_secrets_create_errors / ots_secrets_read_errors 按 reason 标签细分(invalid_expiry/invalid_json/secret_missing/secret_size/storage_error/secret_not_found);ots_secrets_stored 为当前存量(每分钟刷新,官方为多实例部署设计的同步机制)。
4.6 错误模型与排障定位
所有业务错误统一为 {"success":false,"error":"<UUID>"} + 对应 HTTP 状态码(400 参数错误 / 404 未找到或不曾存在 / 500 存储故障)。UUID 是日志关联键——服务端日志同刻会落一行带相同 err_id 的记录,给出真实原因:
time="2026-10-29T17:41:00Z" level=error msg="reading & destroying secret" err_id=ddf5013f-5b41-46c0-b218-67fc2e25223e error="secret not found"
排障套路(实测验证):拿到响应里的 UUID → journalctl -u ots | grep <UUID前8位> → 看 error= 字段定位原因。注意服务端不是每类错误都落日志——实测触发四类错误后检索 journal,只有两类有对应记录(源码中这两类错误携带描述才记录,其余路径刻意静默):
| 现象 | 状态码 | 响应示例 | 服务端日志(实测) |
|---|---|---|---|
| 读已销毁/不存在的 id | 404 | {"success":false,"error":"66e1fa8a-…"} | msg="reading & destroying secret" err_id=66e1fa8a-… error="secret not found" |
| 非法 JSON | 400 | {"success":false,"error":"cffca2a7-…"} | msg="decoding request body" err_id=cffca2a7-… error="invalid character 'b' looking for beginning of object key string" |
空 secret({"secret":""}) | 400 | {"success":false,"error":"25ca1048-…"} | 无日志(该路径不落盘,UUID 仅作请求关联号) |
?expire=abc | 400 | {"success":false,"error":"2d4dbf17-…"} | 无日志(同上) |
五、规范姿势:本地加密后调 API(openssl)
直接给 /api/create 传明文意味着「服务器内存里出现过明文」,违背 OTS 零知识设计。官方 README 强烈推荐的脚本姿势:openssl 本地加密 → 上送密文 → 分享链接里带口令 → 接收方本地解密。全流程实测(每步真实回显):
第 1 步:本地加密。
echo "topsecret-api-test" | openssl aes-256-cbc -base64 -pass pass:mypass123 -pbkdf2 -iter 300000 -md sha512
实测回显:
U2FsdGVkX19gDXKuaqELA/d//ECZ1xAUvBPFf+MAfQhO6WSn+7OfADb5Iwzt5iD7
💡 技术拓展:openssl enc 支持
-aes-256-cbc/-aes-256-ctr等多种对称算法与 KDF 参数——-pbkdf2 -iter 300000是 OpenSSL v1.1.1+ 的推荐口令派生(防彩虹表暴力),-md sha512指定散列;旧版默认的 EVP_BytesToKey 派生已视为弱实践。ots-cli/Web 前端内部同样采用「随机强口令 + AES-256」的思路,本节只是把同一纪律落到 shell 层。本文实测采用-aes-256-cbc -pbkdf2(与官方 README 示例一致)。
第 2 步:上送密文。
curl -s -X POST -H 'content-type: application/json' \
-d '{"secret": "U2FsdGVkX19gDXKuaqELA/d//ECZ1xAUvBPFf+MAfQhO6WSn+7OfADb5Iwzt5iD7"}' \
http://127.0.0.1:8146/api/create
实测回显:
{"success":true,"expires_at":"2026-11-05T17:41:00.318504711Z","secret_id":"8866920b-d07b-4f42-9b2d-6acb8394feea"}
第 3 步:取回密文并本地解密(分享链接形态为 http://<域名>/#<secret_id>|mypass123,接收方在浏览器打开即自动完成本步骤;此处演示脚本化取用):
curl -s http://127.0.0.1:8146/api/get/8866920b-d07b-4f42-9b2d-6acb8394feea \
| sed -E 's/.*"secret":"([^"]+)".*/\1/' > /opt/ots-cli-demo/blob.txt
openssl aes-256-cbc -d -base64 -pass pass:mypass123 -pbkdf2 -iter 300000 -md sha512 -in /opt/ots-cli-demo/blob.txt
实测回显:
topsecret-api-test
第 4 步:二次读取确认已焚。
curl -s -w '\ncode=%{http_code}\n' http://127.0.0.1:8146/api/get/8866920b-d07b-4f42-9b2d-6acb8394feea
实测回显:
{"success":false,"error":"bfbb3b4a-ab98-47f0-a894-a26c8f33e880"}
code=404
整条链路中服务器只见密文,口令 mypass123 与明文从未离开本地。
六、注意事项
- 一次性语义优先于一切重试:fetch/API GET 成功一次后 id 立即作废;自动化脚本读失败重试时若怀疑「其实第一次已成功」,应重新创建秘密而非反复 GET;
- URL 锚点必须完整传递:
#后的%7C(|)分隔 id 与密码,聊天工具自动断链/终端换行都可能截断;ots-cli fetch 需要完整 URL,手拼时记得对|做 URL 编码; - curl 的引号与
$转义:密文含+/=,JSON 体务必用单引号包裹(见第五章实测命令),双引号会让 shell 展开产生坏 JSON(触发 invalid_json 400); --expire的单位:ots-cli 的--expire是 Go duration(30m/1h/24h),API 的?expire=是秒数——两套单位不同,勿混写;- 日志只有元数据:访问日志与错误日志不含秘密内容与解密口令(零知识架构使服务端无从记录),可放心排查;但别把完整分享 URL(含口令)贴进任何日志或工单;
- /metrics 与 /api.html 默认状态不同:metrics 默认 404(网段 ACL),
/api.html默认可访问——公网部署时在反代上按需收敛这两个路径。
七、常见问题(FAQ)
Q1:ots-cli create 报 Error: creating the secret / 超时?
按序排查:--instance 是否可达(curl -s -o /dev/null -w '%{http_code}' $OTS_INSTANCE/api/healthz 应 200);实例是否开了认证(加 -u user:pass 或 -H);--expire 是否超过服务端 --secret-expiry(会被静默封顶但不报错,见配置篇 5.3 节);URL 末尾的 / 不能丢(OTS_INSTANCE 默认值自带)。
Q2:API 创建成功但前端打开链接显示已失效?
三种可能:① 秘密已被读走(一次性);② 已过 expires_at(mem 后端按周期清理、Redis 按 TTL 删除);③ 链接锚点不完整。用 /api/get/<id> 区分:返回 404 且服务端日志 secret not found 即已焚毁,只能重新创建。
Q3:ots-cli 与 Web 前端创建的秘密互通吗?
互通。两者都是「本地加密 → 同一个 /api/create」,ots-cli 生成的 URL 浏览器可直接打开,反之 Web 创建的 URL 也可用 ots-cli fetch 读取(同一加密格式,版本配对即可——本文两端均为 v1.23.1 实测)。
Q4:能把 API 直接暴露给外部系统调用吗?
技术上可以(无认证、无速率限制),但强烈建议前面加反代做来源限制或 Basic Auth(ots-cli 已原生支持 -u/-H 透传);同时注意服务端 maxSecretSize 是唯一服务端大小限制,外放前在反代配置 client_max_body_size 对齐。
Q5:curl 响应里的 error UUID 能换成人话吗?
不能,服务端刻意只回 UUID(不向调用方泄露内部细节),真实原因在服务端日志按 err_id 关联(4.6 节排障套路)。
八、参考链接
- 官方 README(CLI 与 openssl 用法出处):https://github.com/Luzifer/ots
- 官方 Releases(ots-cli 与服务端发行包):https://github.com/Luzifer/ots/releases/tag/v1.23.1
- 仓库内 OpenAPI 规范:https://github.com/Luzifer/ots/blob/master/docs/openapi.yaml
- 本系列文章:《OTS 介绍与选型指南》、《Ubuntu 22.04 安装 OTS 指南》、《OTS 1.23.1 配置参数详解》
- 同类 API 对照:《OneTimeSecret v0.26.19 REST API 实战指南》(Ruby 系同类平台的 API 面,可对照端点语义差异)