OTS 1.23.1 常用 CLI 命令与 API 详解

OTS v1.23.1 自动化使用详解:ots-cli create/fetch 全参数实战、REST API 六端点 curl 实测、openssl 零知识加密流程与错误排障,全部真实回显。

OTS自托管APIGolang安全2026/10/30

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/healthzGET存活探测(无副作用)200
/api/isWritableGET写入能力探测(LB 健康检查用)204
/api/createPOST存入秘密(JSON 或表单),支持 ?expire= 覆盖201
/api/get/{id}GET读取并销毁秘密200
/api/settingsGET前端定制参数投影200
/metricsGETPrometheus 指标(网段 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,只有两类有对应记录(源码中这两类错误携带描述才记录,其余路径刻意静默):

现象状态码响应示例服务端日志(实测)
读已销毁/不存在的 id404{"success":false,"error":"66e1fa8a-…"}msg="reading & destroying secret" err_id=66e1fa8a-… error="secret not found"
非法 JSON400{"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=abc400{"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 与明文从未离开本地。

六、注意事项

  1. 一次性语义优先于一切重试:fetch/API GET 成功一次后 id 立即作废;自动化脚本读失败重试时若怀疑「其实第一次已成功」,应重新创建秘密而非反复 GET;
  2. URL 锚点必须完整传递:# 后的 %7C(|)分隔 id 与密码,聊天工具自动断链/终端换行都可能截断;ots-cli fetch 需要完整 URL,手拼时记得对 | 做 URL 编码;
  3. curl 的引号与 $ 转义:密文含 + / =,JSON 体务必用单引号包裹(见第五章实测命令),双引号会让 shell 展开产生坏 JSON(触发 invalid_json 400);
  4. --expire 的单位:ots-cli 的 --expire 是 Go duration(30m/1h/24h),API 的 ?expire= 是秒数——两套单位不同,勿混写;
  5. 日志只有元数据:访问日志与错误日志不含秘密内容与解密口令(零知识架构使服务端无从记录),可放心排查;但别把完整分享 URL(含口令)贴进任何日志或工单;
  6. /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 节排障套路)。

八、参考链接