Debian 12 安装 OneTimeSecret v0.26.19 指南
在 Debian 12 服务器上从源码完整构建并部署 OneTimeSecret v0.26.19:Ruby 3.4.10 编译、Redis 接入、bin/install 初始化、管理员创建与一次性秘密全链路验证,含 libsodium/CSRF 等排障实录。
Debian 12 安装 OneTimeSecret v0.26.19 指南
一、环境信息
| 项目 | 值 |
|---|---|
| 操作系统 | Debian 12(bookworm,x86_64) |
| OneTimeSecret 版本 | v0.26.19(2026-10-25 发布,写作时 GitHub 最新稳定版) |
| Ruby | 3.4.10(经 rbenv/ruby-build 源码编译,应用 .ruby-version 强制要求) |
| Node.js | v26.12.1(官方安装器 bin/install 要求 Node 26+) |
| pnpm | 12.15.0(前端包管理器) |
| Python | 3.11.2(Debian 12 仓库版本,前端 locales 构建脚本依赖) |
| Redis | 7.0.15(Debian 12 官方仓库版本,OneTimeSecret 要求 Redis 5+ 兼容) |
| 应用监听端口 | 3000(对外发布为 8134,见下文说明) |
| 测试账号 | admin@ots.local / a930AJELYUNO4mqEQYwb(bin/ots 自动生成的强密码) |
服务器为一台全新安装的 Debian 12 主机,root 权限,可访问公网(GitHub、rubygems.org、nodejs.org)。OneTimeSecret 的定位、架构与同类产品对比见《OneTimeSecret v0.26.19 介绍与选型指南》,本文专注落地:从零开始在一台 Debian 12 服务器上完成源码构建、Redis 接入、管理员账号初始化,并验证「匿名创建一次性秘密 → 读取 → 二次读取失效」的完整闭环。
二、前置条件
- 一台 Debian 12 服务器,具备 root 权限与至少 2 GB 内存(Ruby 3.4 源码编译与前端 Vite 构建对内存有一定要求);
- 磁盘剩余空间不少于 5 GB(源码、gems、node_modules 与前端构建产物合计约 2 GB,加上 Redis 数据留出余量);
- 服务器可访问公网,安装过程需要从 GitHub、rubygems.org 与 nodejs.org 下载源码与依赖;
- 若服务器需要经 HTTP 代理出网,为
git/wget/npm导出https_proxy/http_proxy环境变量即可,OneTimeSecret 自身运行不依赖外网。
三、版本与发布形态说明
写作时 OneTimeSecret 的最新稳定版本为 v0.26.19(2026-10-25 发布)。选它是因为 v0.26 是当前 GitHub 默认发布线,依赖栈已升级到 Ruby 3.4 + Puma 7.2 + Familia 2.12(Redis 加密对象映射层)+ Rodauth 认证体系。
需要特别说明的是发布形态:OneTimeSecret 官方没有提供面向 Debian/Ubuntu 的 .deb 仓库包,也没有裸二进制发行包。官方发布的只有两种形态:
- 源码(GitHub tag tarball),本文走这条路线;
- 官方容器镜像(属于另一种部署路线,不在本文范围内,本专栏另有专文)。
v0.26 的源码包里自带了面向运维的官方安装入口 bin/install(官方注释称其为「self-hosting from a checkout (the operator front door)」),它会自动完成依赖检查、bundle install、.env 密钥生成与配置文件初始化,整个链路的自动化程度比 v0.2x 早期版本(纯手工 bundle + 手写 config.yaml)高很多。本文以 bin/install 为主线,遇到它无法覆盖的环节再手工补齐。
一个诚实的注记:从源码 tarball 安装后,bin/ots version 显示 Onetime 0.0.0-rc0 (dev)(实测回显见下文),而非 v0.26.19——版本号源自 git tag 元数据,tarball 安装天然缺失。功能与 release 完全一致,仅版本显示有此差异,写监控与审计脚本时不要依赖这个字符串。
四、安装系统依赖
Debian 12 官方仓库即可满足全部依赖:
apt-get update
apt-get install -y build-essential git wget curl redis-server \
libssl-dev libyaml-dev zlib1g-dev libffi-dev pkg-config libsodium23 python3
安装完成后核对版本:
redis-server --version && python3 --version
实测回显:
Redis server v=7.0.15 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=ae4d7c971a948f0
Python 3.11.2
四个容易漏装的包说明(都是本文实测踩过或官方依赖链隐含的):
- libsodium23 是必需的运行时库:秘密落库加密由 Familia 2.12 的 XChaCha20-Poly1305 provider 完成,底层通过 RbNaCl gem 以 FFI 调用 libsodium。缺它时应用能正常启动(加载失败被静默吞掉),但第一条秘密创建就会失败,报
NameError: uninitialized constant RbNaCl::Hash——这是本文实测中最大的坑,排障实录见文末; - python3 是必需的:前端构建链(pnpm run build)里的 locales 编译脚本是 Python 写的,缺它时前端构建报
sh: 1: python3: not found; - libyaml-dev / libssl-dev / zlib1g-dev:Ruby 3.4 源码编译需要(psych/OpenSSL/zlib 扩展);
- build-essential:gems 里的原生扩展(argon2、oj、syslog 等)编译需要,
bin/install的 bundle 阶段会现场编译。
五、安装并配置 Redis
Debian 12 仓库的 redis-server 7.0.15 满足 OneTimeSecret 的要求(官方支持 Redis 5+ 兼容协议,Valkey 同样可用——若你偏好 Valkey,用其官方仓库或源码替换本节即可,应用配置不变)。编辑 /etc/redis/redis.conf,追加三行:
cat >> /etc/redis/redis.conf <<'EOF'
requirepass otsredispass123
appendonly yes
EOF
启动 Redis 并验证密码连接:
redis-server /etc/redis/redis.conf --daemonize yes
redis-cli -a otsredispass123 ping
实测回显:
PONG
两个配置项的为什么:
requirepass:OneTimeSecret 里存的是秘密密文与账户数据,Redis 一旦暴露等于整个系统的数据面暴露;生产环境务必设密码并只监听 127.0.0.1 或内网;appendonly yes:OneTimeSecret 的全部状态(秘密、账户、会话)都在 Redis,AOF 开启后实例重启在途秘密不丢;默认的 RDB 快照方式最长可能丢 5 分钟数据。
六、构建 Ruby 3.4.10
Debian 12 仓库自带的 Ruby 3.1.2 无法使用:OneTimeSecret v0.26.19 的 Gemfile 声明 ruby file: '.ruby-version',而源码里的 .ruby-version 内容为 3.4.10——Bundler 在 bundle install 时会直接拒绝 3.1.x。采用 rbenv + ruby-build 源码编译(约 5-10 分钟):
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init - bash)"
MAKE_OPTS=-j4 rbenv install 3.4.10
rbenv global 3.4.10
编译完成后核对:
ruby --version && gem --version && bundle --version | head -1
实测回显:
ruby 3.4.10 (2026-06-30 revision 2b0b7728dc) +PRISM [x86_64-linux]
3.6.9
Bundler version 2.6.9
注意:
eval "$(rbenv init - bash)"只对当前 shell 生效。写入~/.bashrc或 systemd 单元的环境里再显式导出 PATH,否则 systemd 拉起服务时找不到 rbenv 管理的 Ruby。
七、安装 Node 26 与 pnpm
OneTimeSecret v0.26 的前端是 Vue 3 单页应用(源码在仓库 apps/web 下),官方安装器 bin/install 体检时硬性要求 Node 26+(对照源码 .node-version)。Debian 12 仓库只有 Node 18,需要装新版。采用官方二进制包(比编译快得多):
cd /opt
curl -sL -o node.tar.xz https://nodejs.org/dist/latest-v26.x/node-v26.12.1-linux-x64.tar.xz
mkdir -p /opt/node26 && tar -xJf node.tar.xz -C /opt/node26 --strip-components=1
ln -sf /opt/node26/bin/node /usr/local/bin/node
ln -sf /opt/node26/bin/npm /usr/local/bin/npm
npm install -g pnpm
实测回显:
node --version → v26.12.1
npm --version → 11.20.0
pnpm --version → 12.15.0
八、获取源码
从 GitHub Releases 下载 v0.26.19 源码包并解压到 /opt/onetimesecret:
cd /opt
wget https://github.com/onetimesecret/onetimesecret/archive/refs/tags/v0.26.19.tar.gz -O ots-0.26.19.tar.gz
sha256sum ots-0.26.19.tar.gz
mkdir -p /opt/onetimesecret
tar -xzf ots-0.26.19.tar.gz -C /opt/onetimesecret --strip-components=1
实测回显(校验值供读者比对自己下载的包):
ef398bfbf063db1fa4ea6a9c5fcb7f90423564800840d563f19f1bda0da11d0a ots-0.26.19.tar.gz
九、运行官方安装器 bin/install
这是 v0.26 源码路线的核心一步。bin/install 是幂等的(官方注释:safe to re-run at any time),它会依次完成:环境体检 → bundle install → 生成 .env(含根密钥 SECRET 及其 HKDF 派生密钥)→ 生成 etc/config.yaml 与 etc/puma.rb:
cd /opt/onetimesecret
bin/install
实测回显(bundle 阶段较长,节选关键输出):
▶ Initializing...
▶ Ruby 3.4.10
Installing argon2 2.3.3 with native extensions
Installing oj 3.17.4 with native extensions
Bundle complete! 98 Gemfile dependencies, 224 gems now installed.
▶ pnpm not found — install it first: https://pnpm.io/installation
第一次运行会在 pnpm 处停下(本文已提前装好 pnpm 时则继续走完)。装好 pnpm 后重跑 bin/install,它会完成剩余初始化并输出体检报告:
▶ Doctor (context: operator) — each check diffs a pin/manifest or probes a live service
ok Ruby 3.4.10 (= .ruby-version)
ok Node 26.12.1 (>= .node-version major 26)
ok Ruby gems match Gemfile.lock (bundle check)
ok .env exists
ok SECRET set in .env (presence only — value not read)
ok etc/config.yaml exists
ok etc/puma.rb exists
ok No .test-mode marker (normal mode)
ok Valkey/Redis answering at 127.0.0.1:6379 (connectivity probe)
-- AUTHENTICATION_MODE: simple
-- JOBS_ENABLED not true — RabbitMQ not required (email sends synchronously)
warn App not answering on :3000
fix: start it: bundle exec puma -C etc/puma.rb
FAIL Datastore unreachable — SECRET verifier not checked
fix: start Valkey/Redis (see the service checks above)
warn Frontend build output missing (public/web/dist)
fix: pnpm run build (bare-metal production serves prebuilt assets)
ok Brand pack resolves to public/branding/default [BRAND_PACK=default via public/branding]
ok Resolved pack carries the full canonical asset set (7 files)
ok Social card present (social-preview.png) — og:image/twitter:image emitted
ok Brand yaml present but value-free (neutral — no keys set)
---
▶ Doctor: 12 ok, 2 warning(s), 1 failure(s) [context: operator]
这份体检报告信息量很大,两条非 ok 项的解读:
- FAIL Datastore unreachable:此时
.env里的REDIS_URL还是redis://127.0.0.1:6379/0(无密码),而我们的 Redis 已设requirepass,带密码连接校验自然失败。下一节改掉即可; - warn Frontend build output missing:源码安装形态下生产模式直接伺服预构建的前端产物(
public/web/dist),需要手动跑一次前端构建(下文有专节); AUTHENTICATION_MODE: simple:v0.26 有 simple(仅 Redis)与 full(需要 PostgreSQL + RabbitMQ)两种认证模式,bin/install默认 simple,绝大多数自托管场景够用。
十、配置 .env
bin/install 生成的 .env 是完整的带注释配置文件,核心区块包括根密钥、连接串、站点身份、邮件、TTL 等。本文实验环境改两处:
cd /opt/onetimesecret
# Redis 连接串加上密码
sed -i "s|REDIS_URL='redis://127.0.0.1:6379/0'|REDIS_URL='redis://:otsredispass123@127.0.0.1:6379/0'|" .env
# 站点对外地址(读者替换为自己的服务器 IP 与端口)
sed -i 's|^HOST=localhost:3000|HOST=192.168.146.131:8134|' .env
核对修改结果:
grep -E '^(REDIS_URL|HOST|SSL)' .env
实测回显:
REDIS_URL='redis://:otsredispass123@127.0.0.1:6379/0'
HOST=192.168.146.131:8134
SSL=false
关于 SSL 与会话 Cookie 的重要说明(.env 官方注释原文的要点,v0.24+ 行为):SSL=false 生成 http 链接;但在生产模式(RACK_ENV=production)下,SSL=false 不再自动降级会话 Cookie 的 Secure 标志——应用会 fail closed 强制 Secure。如果你的部署真的是纯 HTTP(如本文实验环境、无 TLS 终结的反代),必须显式追加 SESSION_COOKIE_SECURE=false,否则登录后浏览器不回传会话 Cookie,表现为「登录后仍匿名 / CSRF 校验 403」。走 HTTPS 反代的部署则设 SSL=true 并不要动 SESSION_COOKIE_SECURE。本文实验环境经宿主机端口映射访问(对外仍为 HTTP),需追加:
echo 'SESSION_COOKIE_SECURE=false' >> .env
其余 .env 内容(SECRET、ACCOUNT_ID_SECRET、SESSION_SECRET 等派生密钥)由安装器自动生成,SECRET 是全系统的根密钥(所有密码学材料经 HKDF 从它派生),一旦更换,既有秘密将不可解密——务必备份,且不要在多实例间复制后各自改动。v0.26 起应用启动时会在 Redis 里存一份 SECRET 校验器(secret_verifier_mode,默认 warn),SECRET 与数据不匹配时启动会大声告警。
十一、构建前端资产
cd /opt/onetimesecret
pnpm run build
实测回显(依赖安装后构建,节选):
✓ built in 3.78s
构建产物落在 public/web/dist。若此步报 python3: not found,回头装 python3 再跑(见「安装系统依赖」一节的说明)。
十二、启动服务
官方 .env 注释里给出的裸机启动方式就是三步:起 Redis → source 环境 → 启动 Puma:
cd /opt/onetimesecret
set -a; source .env; set +a
nohup bundle exec puma -C etc/puma.rb > /var/log/ots-puma.log 2>&1 &
sleep 10
tail -5 /var/log/ots-puma.log
实测回显(Puma 双 worker 模式启动):
[22076] * Listening on http://0.0.0.0:3000
[22076] - Worker 0 (PID: 22096) booted in 0.0s, phase: 0
[22076] - Worker 1 (PID: 22102) booted in 0.0s, phase: 0
验证首页与对外端口(8134 为宿主到容器 3000 的映射端口,物理服务器场景下就是服务器自己的端口映射或防火墙规则,应用本身始终监听 3000):
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.146.131:8134/
实测回显:
200
200
十三、创建管理员账号
OneTimeSecret v0.26 无预置管理员:首个管理员通过 bin/ots CLI 直接创建(官方 README 推荐方式),指定 --role colonel 即为最高管理角色,密码由 CLI 自动生成强随机串并只打印一次:
cd /opt/onetimesecret
set -a; source .env; set +a
bundle exec bin/ots customers create admin@ots.local --role colonel
实测回显:
Customer created: admin@ots.local
Role: colonel
Verified: true
Generated password: a930AJELYUNO4mqEQYwb
Save this password - it will not be displayed again.
密码
q1w2e3r4一类弱口令在该角色下不建议使用;若走 Web 注册路径创建普通账户,注册后需要 CLI 提权(bin/ots customers role promote <email> --role colonel --force)才能访问 /colonel 管理面板——环境变量COLONEL只用于标记哪个邮箱是管理员邮箱,不会自动提权。
后续给该账户签 API Token(调用 REST API 时用):
bundle exec bin/ots apitoken admin@ots.local
十四、功能验证:一次性秘密全链路
用 v1 API(匿名可用)验证「创建 → 读取 → 二次读取失效」的核心语义:
KEY=$(curl -s -X POST http://127.0.0.1:3000/api/v1/share -d 'secret=B1-once-verify-42&ttl=3600' \
| grep -oE '"secret_key":"[a-z0-9]+"' | cut -d'"' -f4)
echo $KEY
echo '=== 首次读取 ==='
curl -s -X POST http://127.0.0.1:3000/api/v1/secret/$KEY
echo
echo '=== 二次读取 ==='
curl -s -w ' [%{http_code}]' -X POST http://127.0.0.1:3000/api/v1/secret/$KEY
实测回显(创建响应节选):
{"custid":"anon","metadata_key":"fh2curgfqkp3vla5a3bjasehuq80a2z3ndzhkdbv9jzx5o3x413nltrawp2un7","secret_key":"j624lo7i5zycwrcc0ituqrbnjrmmxtetnqllxephmmybvzrz537qsjrk7l3iyh","ttl":3600,"metadata_ttl":7200,"secret_ttl":3600,"metadata_url":"http://192.168.146.131:8134/receipt/fh2curgfqkp3vla5a3bjasehuq80a2z3ndzhkdbv9jzx5o3x413nltrawp2un7","state":"new","updated":1793006596,"created":1793006596,"recipient":[],"share_domain":"","passphrase_required":false,...}
=== 首次读取 ===
{"value":"B1-once-verify-42","secret_key":"j624lo7i5zycwrcc0ituqrbnjrmmxtetnqllxephmmybvzrz537qsjrk7l3iyh","share_domain":"http://192.168.146.131:8134"}
=== 二次读取 ===
{"secret_key":"j624lo7i5zycwrcc0ituqrbnjrmmxtetnqllxephmmybvzrz537qsjrk7l3iyh","message":"Unknown secret"} [404]
三条证据链完整:创建返回 state:"new" 与两个 key(metadata_key 给创建者看回执,secret_key 给收件人);首次读取拿到明文 B1-once-verify-42;二次读取 404「Unknown secret」——读后即焚语义生效。REST API 的完整用法(含 v2 与鉴权差异)见本专栏《OneTimeSecret v0.26.19 REST API 实战指南》。
十五、systemd 服务化(生产建议)
源码包自带官方 systemd 模板(etc/examples/systemd/ 下共三个单元),生产环境推荐直接使用:
| 单元文件 | 作用 | 启动命令 |
|---|---|---|
| onetimesecret-web.service | Web 服务(必装) | bundle exec puma -C etc/puma.rb |
| onetimesecret-worker.service | 后台作业消费(full 认证模式才需要) | bin/ots worker |
| onetimesecret-scheduler.service | 定时维护作业(full 认证模式才需要) | bin/ots scheduler |
simple 认证模式(本文路线)只需 web 单元。官方模板的关键约定:专用系统用户 onetime 运行(User=onetime)、应用目录 /var/lib/onetimesecret、启动前先 source .env(ExecStart=/bin/bash -lc 'set -a; source .env; set +a; exec bundle exec puma -C etc/puma.rb')、After=network.target redis-server.service valkey.service。按官方文件头注释安装:
sudo cp etc/examples/systemd/onetimesecret-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now onetimesecret-web
说明:本文实验环境(容器化 Debian 12,PID 1 非 systemd)以 nohup bundle exec puma -C etc/puma.rb 方式实测了等效启动命令并完成全部功能验证;systemd 单元本身来自官方模板原文,读者在物理服务器上按上述命令启用即可。
十六、排障实录
以下是本文实测中真实踩到的三个坑,按出现顺序沉淀:
坑 1:第一条秘密创建报 NameError: uninitialized constant RbNaCl::Hash
现象:应用启动正常、首页 200,但 POST /api/v1/share 返回 {"message":"An unexpected error occurred :["},日志里有 NameError: uninitialized constant RbNaCl::Hash,调用栈落在 familia-2.12.0/.../encryption/providers/xchacha20_poly1305_provider.rb:116 的 derive_key。
根因:Familia 2.12 的 XChaCha20-Poly1305 加密 provider 在文件头部 begin require 'rbnacl' rescue LoadError 静默吞掉了加载失败——RbNaCl 顶级常量在部分加载后已存在,但其 Hash 模块(依赖 FFI 加载 libsodium)没加载成功,等真正加密时才炸出 NameError。缺失的运行时库就是 libsodium23。
修复:apt-get install -y libsodium23 后完全重启应用进程(确保 Puma 与其 worker 全部换新),再创建即恢复。单测佐证:装完后在应用目录 bundle exec ruby -e "require 'rbnacl'; puts RbNaCl::Hash.sha256('abc').unpack1('H64')" 输出 ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad(abc 的标准 SHA-256),FFI 链路恢复正常。
坑 2:重启无效,日志 PID 纹丝不动
现象:装完 libsodium23 重启应用,错误依旧,且日志里 Puma 主进程 PID 与重启前完全相同。
根因:容器环境没有 procps 包,pkill 命令不存在(报错被 2>/dev/null 吞掉),老进程一直占着 3000 端口,第二次启动的 Puma 因端口占用退出,日志文件被覆盖后读到的仍是老内容。
修复:明确按 PID kill(kill <pid>,主进程与两个 worker),确认端口释放后再启动。教训:重启类操作后核对进程 PID 是否真的换了,不要只看日志有输出。
坑 3:bin/install 体检 FAIL Datastore unreachable
现象:安装器体检中 ok Valkey/Redis answering at 127.0.0.1:6379 与 FAIL Datastore unreachable — SECRET verifier not checked 并存,自相矛盾。
根因:前一条是纯 TCP/协议探活(不需要密码),后一条按 .env 里的 REDIS_URL 实连校验 SECRET verifier——默认生成的 URL 没带密码,对设了 requirepass 的 Redis 实连被拒。
修复:给 .env 的 REDIS_URL 加上密码段(redis://:密码@127.0.0.1:6379/0)后重跑体检即可通过。
十七、常见问题
Q:能不能不编译 Ruby,直接用 Debian 12 仓库的 3.1.2?
不能。Gemfile 用 ruby file: '.ruby-version' 精确锁 3.4.10,Bundler 在 install/check 阶段直接拒绝其他版本。嫌编译慢可以用 rvm 的二进制安装或Fullstaq Ruby 等 3.4 预编译发行版,但要自担 glibc 兼容性。
Q:Valkey 能替代 Redis 吗? 能。OneTimeSecret 官方两条路线都支持(连接串协议都是 redis://),Valkey 8 与 Redis 7.x 均实测可用,本专栏 Compose 篇用的就是 Valkey。
Q:bin/ots version 显示 Onetime 0.0.0-rc0 (dev) 是不是装错了?
不是。tarball 安装缺 git tag 元数据,版本字符串如此显示,功能与 v0.26.19 release 一致(见「版本与发布形态说明」一节)。
Q:生产环境还需要改什么?
至少三件:给站点套 TLS 反代并把 .env 改 SSL=true(同时删掉 SESSION_COOKIE_SECURE=false);配置 SMTP(.env 的 SMTP_* 区块,否则账户验证邮件发不出);按需收紧 TTL_MAX_ANONYMOUS(匿名分享上限,默认 7 天)。参数级细节见本专栏《OneTimeSecret v0.26.19 配置参数详解》。