# TOTP Vault v1.2.0 完整部署、运维、恢复与品牌定制指南

> **适用环境**：Ubuntu 22.04 · 宝塔面板 · Nginx · PHP 8.2 · SQLite · Microsoft 365 企业版 OneDrive · AGE 加密备份
>
> **文档定位**：部署、排错、搬迁、灾难恢复、离线解密统一操作手册。所有需要输入的命令均使用独立代码块，复制后直接执行。

## 阅读图例

| 标识 | 在哪里操作 | 用途 |
|---|---|---|
| 🖥️ | 服务器 SSH / 终端 | Linux、PHP、Nginx、项目命令 |
| 🧰 | 宝塔面板 | 软件、网站、PHP、SSL、计划任务 |
| 🌐 | 浏览器 / Microsoft Entra | DNS、OAuth、网站后台 |
| 🪟 | Windows PowerShell | AGE 下载、私钥生成与离线解密 |
| ✅ | 核对结果 | 只看结果，不要复制为命令 |
| ⚠️ | 风险提示 | 操作前确认，尤其是密钥、恢复和权限 |

## 推荐阅读路线

| 你的目标 | 从这里开始 |
|---|---|
| 全新服务器第一次部署 | [第一章 开始之前](#第一章开始之前) → 第二章 → 第六章 |
| OneDrive / OAuth 配置 | [第五章 Microsoft OneDrive](#第五章microsoft-onedrive) |
| AGE 与自动备份 | [第六章 AGE 与自动备份](#第六章age-与自动备份) |
| 遇到 400 / 500 / 404 / 权限问题 | [第七章 故障排查](#第七章故障排查) |
| 搬服务器 | [第八章 网站搬迁](#第八章网站搬迁) |
| 网站没了，只想解出全部 Secret | [第九章 灾难恢复与离线解密](#第九章灾难恢复与离线解密) |

---

## 第一章　开始之前

> 先理解最终架构、版本特征与部署前准备。

### 1. 最终版架构

```text
公开访问
https://你的域名/
        ↓
浏览器本地生成 TOTP
Secret 不上传服务器

管理员后台
https://你的域名/admin
        ↓
SQLite
        ↓
TOTP Secret 使用 XChaCha20-Poly1305 加密
        ↓
vault.key 位于 Web Root 外

备份
SQLite + vault.key + config + manifest
        ↓
tar.gz
        ↓
AGE Public Key 加密
        ↓
.tar.gz.age
        ↓
Microsoft 365 OneDrive
TOTP-Vault-Backup/Backups
```

最终 OneDrive 身份验证方式：

```text
OAuth 2.0 Authorization Code Flow
Delegated Files.ReadWrite
offline_access
Refresh Token
```

最终版：

```text
不需要 Drive ID
不使用 client_credentials
不使用 Files.ReadWrite.AppFolder Application permission
不使用 /special/approot
```

---

### 2. 最终发布包已合并的修复

最终代码已经合并：

```text
TOTP-Vault-v1.0.0-Baota
+
TOTP-Vault-OneDrive-Delegated-Hotfix-20260824
+
TOTP-Vault-OneDrive-Fixed-Folder-Hotfix-20260824
+
实际部署过程中确认的最终修复
```

最终额外修复：

```text
1. Ubuntu apt 安装 AGE 后自动识别 /usr/bin/age
2. proc_open 在安装阶段直接检查
3. Composer 遇到 putenv 被禁用时自动跳过，不再中断安装
4. /www/server/totp-vault 父目录权限自动修复
5. 安装中断后可以安全重新运行 install.sh
6. 已存在管理员时不会再次创建
7. shell_exec 被禁用时 artisan 不再直接 Fatal Error
8. HEAD 请求按 GET 路由处理，curl -I 不再产生假 404
9. OAuth Session SameSite 改为 Lax
10. CSP 放行 https://login.microsoftonline.com OAuth 跳转
11. OneDrive 使用 Delegated Files.ReadWrite + Refresh Token
12. OneDrive 固定只操作 TOTP-Vault-Backup/Backups
13. Health Check 增加 proc_open 检查
14. 新增 tools/offline-export.php，可在网站完全不可用时离线导出全部 TOTP Secret
```

---

### 3. 新服务器部署前准备

准备：

```text
Ubuntu 22.04 服务器
服务器公网 IPv4
一个域名或子域名
SSH 登录权限
Microsoft 365 企业账号
Microsoft Entra App Registration 管理权限
Windows 电脑，用于离线保存 AGE Private Key
```

本文示例：

```text
域名：2fa.example.com
项目：/www/wwwroot/2fa.example.com
PHP：8.2
PHP CLI：/www/server/php/82/bin/php
PHP-FPM socket：/tmp/php-cgi-82.sock
PHP-FPM 用户：www
```

把 `2fa.example.com` 换成你自己的域名。

---

---

## 第二章　服务器初始化

> 从 SSH、系统更新、时间同步、Swap、防火墙到宝塔安装。

### 4. SSH 登录并切换 root

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

Windows PowerShell：

```powershell
ssh ubuntu@服务器IP
```

进入服务器后：

```bash
sudo -i
whoami
```

必须：

```text
root
```

---

### 5. 检查系统

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```bash
cat /etc/os-release
uname -m
nproc
free -h
df -h
lsblk
```

推荐：

```text
Ubuntu 22.04
x86_64 或 aarch64
2 GB 以上内存
20 GB 以上可用磁盘
```

---

### 6. 更新系统与安装基础工具

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```bash
apt update
DEBIAN_FRONTEND=noninteractive apt upgrade -y
apt install -y curl wget unzip zip tar ca-certificates gnupg lsb-release nano vim jq util-linux
```

检查：

```bash
command -v curl
command -v wget
command -v unzip
command -v tar
command -v flock
```

---

### 7. 设置时间与 NTP

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

TOTP 必须依赖准确时间。

```bash
timedatectl set-timezone Asia/Shanghai
timedatectl set-ntp true
timedatectl status
date
```

必须重点确认：

```text
System clock synchronized: yes
```

---

### 8. 2 GB 内存服务器创建 2 GB Swap

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

先检查：

```bash
swapon --show
free -h
```

如果没有 Swap：

```bash
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
printf 'vm.swappiness=10\n' >/etc/sysctl.d/99-totp-vault-swap.conf
sysctl --system
free -h
swapon --show
```

---

### 9. 云服务器安全组

> ☁️ **执行位置：云服务器控制台 + 服务器终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

至少开放入站：

```text
TCP 80    0.0.0.0/0
TCP 443   0.0.0.0/0
SSH 端口  建议只允许自己的公网 IP
宝塔端口  建议只允许自己的公网 IP
```

不需要开放：

```text
3306
6379
SQLite 端口
```

SQLite 没有网络端口。

---

### 10. Ubuntu UFW

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```bash
ufw status verbose
```

如果：

```text
Status: inactive
```

无需处理。

如果 active：

```bash
ufw allow 80/tcp
ufw allow 443/tcp
ufw reload
ufw status
```

不要误删 SSH 放行规则。

---

### 11. 安装宝塔面板

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

宝塔官方正式版安装命令：

```bash
if [ -f /usr/bin/curl ];then curl -sSO https://download.bt.cn/install/install_panel.sh;else wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh;fi;bash install_panel.sh docscenter
```

出现：

```text
Do you want to install Bt-Panel to the /www directory now?(y/n)
```

输入：

```text
y
```

安装完成后保存：

```text
宝塔完整登录地址
宝塔端口
安全入口
用户名
密码
```

查看宝塔信息：

```bash
bt 14
```

宝塔菜单：

```bash
bt
```

---

---

## 第三章　宝塔与运行环境

> 安装 Nginx、PHP 8.2、扩展、AGE，并完成域名与站点基础配置。

### 12. 宝塔只安装必要软件

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

进入：

```text
宝塔 → 软件商店
```

安装：

```text
Nginx
PHP 8.2
```

本项目不需要：

```text
MySQL
MariaDB
Redis
Docker
Node.js 常驻
Java
phpMyAdmin
FTP
```

---

### 13. PHP 8.2 扩展

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

进入：

```text
宝塔 → 软件商店 → PHP 8.2 → 安装扩展
```

确保：

```text
pdo_sqlite
sqlite3
sodium
curl
mbstring
fileinfo
openssl
json
```

检查：

```bash
/www/server/php/82/bin/php -v
```

```bash
/www/server/php/82/bin/php -r '
foreach (["pdo_sqlite","sqlite3","sodium","curl","json","mbstring","openssl","fileinfo"] as $extension) {
    echo $extension . ": " . (extension_loaded($extension) ? "OK" : "MISSING") . PHP_EOL;
}
'
```

必须至少：

```text
pdo_sqlite: OK
sodium: OK
curl: OK
json: OK
mbstring: OK
```

---

### 14. PHP 禁用函数

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

进入：

```text
宝塔 → PHP 8.2 → 禁用函数
```

最终必须长期可用：

```text
proc_open
```

AGE 加密备份和恢复都依赖它。

检查：

```bash
/www/server/php/82/bin/php -r '
echo "proc_open: " . (function_exists("proc_open") ? "AVAILABLE" : "DISABLED") . PHP_EOL;
'
```

必须：

```text
proc_open: AVAILABLE
```

`putenv`：

```text
可以禁用。
最终安装脚本检测到 putenv 不可用时会跳过可选 Composer 步骤。
```

`shell_exec`：

```text
可以禁用。
如果禁用，创建管理员时密码可能不能隐藏回显，但最终代码不会因此 Fatal Error。
```

---

### 15. 安装服务器 AGE

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

Ubuntu 22.04：

```bash
apt update
apt install -y age
command -v age
age --version
```

正常：

```text
/usr/bin/age
```

最终安装脚本会自动写入正确 `AGE_BINARY`。

---

### 16. 域名 DNS

> 🌐 **执行位置：浏览器 / Microsoft Entra / 网站后台**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

添加 A 记录：

```text
类型：A
主机：2fa
值：服务器公网 IPv4
```

检查：

```bash
getent ahostsv4 2fa.example.com
```

Windows：

```powershell
nslookup 2fa.example.com
```

必须解析到新服务器。

没有使用 IPv6 时，不要保留错误的 AAAA 记录。

---

### 17. 宝塔创建网站

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```text
宝塔 → 网站 → 添加站点
```

填写：

```text
域名：2fa.example.com
根目录：/www/wwwroot/2fa.example.com
PHP：8.2
数据库：不创建
FTP：不创建
```

---

### 18. 防跨站攻击 / open_basedir

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

部署阶段建议：

```text
宝塔 → 网站 → 站点 → 网站目录 → 防跨站攻击
关闭
```

原因：

程序必须访问：

```text
/www/server/totp-vault/data
/www/server/totp-vault/secrets
/www/server/totp-vault/tmp
```

如果以后一定要开启 open_basedir，至少允许：

```text
/www/wwwroot/2fa.example.com/
/www/server/totp-vault/
/tmp/
/proc/
```

开启后必须重新测试网页与：

```bash
sudo -u www /www/server/php/82/bin/php artisan totp:health --online
```

---

---

## 第四章　项目安装与上线

> 上传最终源码、执行安装脚本、修复权限、配置 Nginx/SSL 并完成后台首次登录。

### 19. 上传最终源码

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

最终 ZIP：

```text
TOTP-Vault-v1.2.0-Baota-Final-20260824.zip
```

上传到：

```text
/www/wwwroot
```

终端：

```bash
cd /www/wwwroot
unzip TOTP-Vault-v1.2.0-Baota-Final-20260824.zip
```

如果 ZIP 解压得到：

```text
/www/wwwroot/totp-vault
```

移动为你的站点目录：

```bash
rm -rf /www/wwwroot/2fa.example.com/*
cp -a /www/wwwroot/totp-vault/. /www/wwwroot/2fa.example.com/
rm -rf /www/wwwroot/totp-vault
```

注意：如果网站目录里已有重要文件，不要先执行 `rm -rf`。首次空站才使用上面的清理命令。

检查：

```bash
cd /www/wwwroot/2fa.example.com
ls -la
```

必须能看到：

```text
.env.example
artisan
bootstrap.php
app
public
routes
scripts
storage
vendor
tools
```

---

### 20. 执行最终安装脚本

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```bash
cd /www/wwwroot/2fa.example.com
export PATH=/www/server/php/82/bin:$PATH
php -v
bash scripts/install.sh
```

填写：

#### 20.1 正式网站地址

```text
https://2fa.example.com
```

#### 20.2 SQLite

```text
SQLite 路径 [/www/server/totp-vault/data/database.sqlite]:
```

直接 Enter。

#### 20.3 vault.key

```text
vault.key 路径 [/www/server/totp-vault/secrets/vault.key]:
```

直接 Enter。

#### 20.4 secure-store.enc

```text
加密配置路径 [/www/server/totp-vault/secrets/secure-store.enc]:
```

直接 Enter。

#### 20.5 PHP-FPM 用户

```text
PHP-FPM 用户 [www]:
```

直接 Enter。

#### 20.6 管理员

设置唯一管理员用户名与强密码。

如果安装中断后重新执行：

```bash
bash scripts/install.sh
```

最终脚本会检测已有管理员并自动跳过，不会重复创建。

---

### 21. 安装后权限检查

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```bash
namei -l /www/server/totp-vault/data/database.sqlite
```

```bash
ls -ld /www/server/totp-vault
ls -ld /www/server/totp-vault/data
ls -ld /www/server/totp-vault/secrets
ls -ld /www/server/totp-vault/tmp
ls -l /www/server/totp-vault/data/database.sqlite
ls -l /www/server/totp-vault/secrets/vault.key
```

推荐：

```text
/www/server/totp-vault       root:www 750
data                           www:www 700
secrets                        www:www 700
tmp                            www:www 700
database.sqlite                www:www 600
vault.key                      www:www 600
```

用网页运行用户检查：

```bash
sudo -u www test -r /www/server/totp-vault/data/database.sqlite && echo 'DB READ OK' || echo 'DB READ FAIL'
sudo -u www test -w /www/server/totp-vault/data/database.sqlite && echo 'DB WRITE OK' || echo 'DB WRITE FAIL'
sudo -u www test -w /www/server/totp-vault/data && echo 'DB DIR WRITE OK' || echo 'DB DIR WRITE FAIL'
```

必须：

```text
DB READ OK
DB WRITE OK
DB DIR WRITE OK
```

---

### 22. 宝塔运行目录

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```text
宝塔 → 网站 → 站点 → 网站目录
```

设置：

```text
网站目录：/www/wwwroot/2fa.example.com
运行目录：/public
```

最终 Nginx root 必须：

```text
/www/wwwroot/2fa.example.com/public
```

---

### 23. Nginx 最终配置

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

项目自带：

```text
docs/nginx-baota-location.conf
```

先生成适合当前域名的临时版本：

```bash
cd /www/wwwroot/2fa.example.com
DOMAIN='2fa.example.com'
PHP_VER='82'
sed "s#SITE_PATH#$DOMAIN#g; s#php-cgi-82#php-cgi-$PHP_VER#g" docs/nginx-baota-location.conf > /root/totp-nginx-location.conf
cat /root/totp-nginx-location.conf
```

把输出的 location、安全响应头等规则整合到宝塔当前 HTTPS `server {}` 中。

必须：

```nginx
root /www/wwwroot/2fa.example.com/public;
index index.php;

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

# Direct TOTP：Secret 位于 URL 中，因此该路径不写访问日志。
location ^~ /2fa/ {
    access_log off;
    log_not_found off;
    error_log /dev/null crit;

    try_files /index.php =404;
    include fastcgi_params;
    fastcgi_pass unix:/tmp/php-cgi-82.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php;
    fastcgi_param SCRIPT_NAME /index.php;
    fastcgi_param HTTP_PROXY "";
    fastcgi_read_timeout 60s;
    fastcgi_hide_header X-Powered-By;
}

location = /index.php {
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_pass unix:/tmp/php-cgi-82.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param HTTP_PROXY "";
    fastcgi_hide_header X-Powered-By;
}

location ~ \.php(?:/|$) {
    return 404;
}
```

CSP 必须包含：

```text
form-action 'self' https://login.microsoftonline.com
```

否则点击“连接 Microsoft OneDrive”后浏览器可能不跳 Microsoft 登录页。

不要同时保留宝塔默认：

```nginx
include enable-php-82.conf;
```

如果它会再创建任意 PHP 执行规则，就删除该 include，使用上面的精确 `/index.php` 规则。

检查：

```bash
/www/server/nginx/sbin/nginx -t
```

必须：

```text
syntax is ok
test is successful
```

然后：

```bash
/etc/init.d/nginx reload
```

---

### 24. SSL

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```text
宝塔 → 网站 → SSL → Let's Encrypt
```

申请后开启：

```text
强制 HTTPS
```

检查：

```bash
ss -lntp | grep -E ':80|:443'
```

---

### 25. 本机 HTTPS 测试

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

不要只用旧版程序的 HEAD 结果判断。
最终版已经兼容 HEAD，但建议测试真实 GET：

```bash
curl -k --resolve 2fa.example.com:443:127.0.0.1 \
  https://2fa.example.com/ \
  -o /dev/null \
  -w 'HTTP_CODE=%{http_code}\n'
```

正常：

```text
HTTP_CODE=200
```

后台：

```bash
curl -k --resolve 2fa.example.com:443:127.0.0.1 \
  https://2fa.example.com/admin \
  -o /dev/null \
  -w 'HTTP_CODE=%{http_code}\nREDIRECT=%{redirect_url}\n'
```

未登录通常：

```text
302
/admin/login
```

---

### 26. 基础健康检查

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```bash
cd /www/wwwroot/2fa.example.com
sudo -u www /www/server/php/82/bin/php artisan totp:migrate
sudo -u www /www/server/php/82/bin/php artisan totp:test
sudo -u www /www/server/php/82/bin/php artisan totp:health
```

重点必须 PASS：

```text
PHP version
pdo_sqlite
sodium
curl
json
mbstring
SQLite connection
Database outside web root
Vault key
Vault key outside web root
Vault key permissions
storage writable
Backup temp writable
HTTPS URL
APP_DEBUG
proc_open
age binary
tar binary
```

OneDrive/备份/计划任务尚未配置时 WARN 正常。

---

### 27. 管理员后台

> 🌐 **执行位置：浏览器 / Microsoft Entra / 网站后台**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

打开：

```text
https://2fa.example.com/admin
```

登录后立即：

```text
安全设置 → 开启管理员 MFA
```

管理员 MFA 不要只保存在这个 Vault 自己内部。

---

---

## 第五章　Microsoft OneDrive

> 配置 Entra OAuth、Delegated Files.ReadWrite、Refresh Token 与固定备份目录。

### 28. Microsoft Entra 最终配置

> 🌐 **执行位置：浏览器 / Microsoft Entra / 网站后台**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

#### 28.1 App Registration

建议：

```text
Name: TOTP Vault
Supported account types: Accounts in this organizational directory only
```

记录：

```text
Directory (tenant) ID
Application (client) ID
```

不要使用 Object ID。

#### 28.2 Redirect URI

```text
App registrations
→ TOTP Vault
→ Authentication
→ Add a platform
→ Web
```

填写：

```text
https://2fa.example.com/admin/backups/onedrive/callback
```

必须：

```text
Web
HTTPS
没有末尾多余 /
与网站 APP_URL 域名一致
```

#### 28.3 API Permission

```text
API permissions
→ Add a permission
→ Microsoft Graph
→ Delegated permissions
```

添加：

```text
Files.ReadWrite
```

最终版代码会请求：

```text
offline_access https://graph.microsoft.com/Files.ReadWrite
```

不要给：

```text
Files.ReadWrite.All Application
Sites.ReadWrite.All Application
```

#### 28.4 Client Secret

```text
Certificates & secrets
→ Client secrets
→ New client secret
```

网站填写的是：

```text
Value
```

不是：

```text
Secret ID
```

Client Secret Value 只显示一次，保存到密码管理器。

---

### 29. 网站连接 OneDrive

> 🌐 **执行位置：浏览器 / Microsoft Entra / 网站后台**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

后台：

```text
备份
```

填写：

```text
Tenant ID
Client ID
Client Secret Value
```

最终版没有 Drive ID 输入框。

点击：

```text
加密保存配置
```

然后：

```text
连接 Microsoft OneDrive
```

完成一次 Microsoft 登录授权。

成功后：

```text
OneDrive: Connected
自动刷新令牌已加密保存
```

Refresh Token 存储于：

```text
/www/server/totp-vault/secrets/secure-store.enc
```

`secure-store.enc` 使用 `vault.key` 加密。

---

### 30. OneDrive 固定目录

> 🌐 **执行位置：浏览器 / Microsoft Entra / 网站后台**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

点击：

```text
测试连接
```

程序会自动创建：

```text
OneDrive
└── TOTP-Vault-Backup
    └── Backups
```

网站文案应显示：

```text
OneDrive 连接成功：TOTP-Vault-Backup / Backups 可访问。
```

---

---

## 第六章　AGE 与自动备份

> 在 Windows 生成 AGE 密钥、完成第一次加密备份并配置宝塔计划任务。

### 31. Windows 下载 AGE 到 D:\SoftWare\Age

> 🪟 **执行位置：你自己的 Windows 电脑 PowerShell**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

打开 PowerShell：

```powershell
New-Item -ItemType Directory -Force -Path "D:\SoftWare\Age"
cd "D:\SoftWare\Age"
```

下载官方 Windows x64 AGE v1.3.1：

```powershell
Invoke-WebRequest `
  -Uri "https://github.com/FiloSottile/age/releases/download/v1.3.1/age-v1.3.1-windows-amd64.zip" `
  -OutFile "D:\SoftWare\Age\age-v1.3.1-windows-amd64.zip"
```

SHA256：

```powershell
Get-FileHash "D:\SoftWare\Age\age-v1.3.1-windows-amd64.zip" -Algorithm SHA256
```

Windows amd64 v1.3.1 参考 SHA256：

```text
c56e8ce22f7e80cb85ad946cc82d198767b056366201d3e1a2b93d865be38154
```

解压：

```powershell
Expand-Archive `
  -Path "D:\SoftWare\Age\age-v1.3.1-windows-amd64.zip" `
  -DestinationPath "D:\SoftWare\Age\Extracted" `
  -Force
```

查找：

```powershell
Get-ChildItem "D:\SoftWare\Age\Extracted" -Recurse -Filter "age-keygen.exe"
```

假设路径：

```text
D:\SoftWare\Age\Extracted\age\age-keygen.exe
```

进入：

```powershell
cd "D:\SoftWare\Age\Extracted\age"
```

检查：

```powershell
.\age.exe --version
.\age-keygen.exe --version
```

---

### 32. Windows 生成 AGE 密钥

> 🪟 **执行位置：你自己的 Windows 电脑 PowerShell**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

创建私钥目录：

```powershell
New-Item -ItemType Directory -Force -Path "D:\SoftWare\Age\PrivateKey"
```

生成：

```powershell
.\age-keygen.exe -o "D:\SoftWare\Age\PrivateKey\totp-vault-age-private-key.txt"
```

终端会显示：

```text
Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

网站只填写：

```text
age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

私钥文件：

```text
D:\SoftWare\Age\PrivateKey\totp-vault-age-private-key.txt
```

里面的：

```text
AGE-SECRET-KEY-1...
```

绝对不要：

```text
上传服务器
上传 OneDrive
发聊天软件
提交 Git
填入网站
```

至少保留两份离线副本。

---

### 33. 网站保存 AGE Public Key

> 🌐 **执行位置：浏览器 / Microsoft Entra / 网站后台**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

后台 → 备份 → AGE 加密与保留策略。

填写：

```text
AGE Public Key：age1...
每日备份保留：30
月度备份保留：12
```

保存后应：

```text
AGE ENCRYPTION: Public key ready
```

---

### 34. 第一次完整备份

> 🌐 **执行位置：浏览器 / Microsoft Entra / 网站后台**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

备份前最后检查：

```bash
/www/server/php/82/bin/php -r '
echo "proc_open: " . (function_exists("proc_open") ? "AVAILABLE" : "DISABLED") . PHP_EOL;
'
```

必须：

```text
proc_open: AVAILABLE
```

AGE：

```bash
sudo -u www /usr/bin/age --version
```

`.env`：

```bash
cd /www/wwwroot/2fa.example.com
grep '^AGE_BINARY=' .env
```

应：

```text
AGE_BINARY="/usr/bin/age"
```

或等效 `/usr/bin/age`。

后台点击：

```text
立即备份
```

成功后 OneDrive：

```text
TOTP-Vault-Backup
└── Backups
    └── totp-vault-YYYYMMDD-HHMMSS.tar.gz.age
```

OneDrive 不应出现：

```text
database.sqlite
vault.key
.tar.gz 明文
```

---

### 35. 宝塔计划任务

> 🧰 **执行位置：宝塔面板（部分步骤配合 SSH）**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

#### 35.1 每日完整备份

```text
任务类型：Shell 脚本
任务名称：TOTP Vault 每日完整备份
周期：每天
时间：03:30
```

脚本：

```bash
cd /www/wwwroot/2fa.example.com
PHP_BINARY=/www/server/php/82/bin/php bash scripts/backup.sh
```

#### 35.2 延迟备份检查

```text
任务类型：Shell 脚本
任务名称：TOTP Vault 延迟备份检查
周期：N 分钟
分钟：5
```

脚本：

```bash
cd /www/wwwroot/2fa.example.com
PHP_BINARY=/www/server/php/82/bin/php bash scripts/backup-due.sh
```

分别手动执行一次。

每日成功：

```text
Backup uploaded and verified.
File: totp-vault-...
SHA256: ...
```

延迟任务无待办时：

```text
No deferred backup is due.
```

这是正常结果。

---

### 36. 最终在线健康检查

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

```bash
cd /www/wwwroot/2fa.example.com
sudo -u www /www/server/php/82/bin/php artisan totp:health --online
```

最终目标：

```text
OneDrive configuration PASS
OneDrive connection PASS
Recent backup PASS
BT cron heartbeat PASS
proc_open PASS
age binary PASS
```

生产检查：

```bash
cd /www/wwwroot/2fa.example.com
PHP_BINARY=/www/server/php/82/bin/php \
  bash scripts/check-production.sh https://2fa.example.com
```

目标：

```text
Production Ready
```

---

### 37. 敏感地址测试

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

浏览器或 curl：

```bash
curl -I https://2fa.example.com/.env
curl -I https://2fa.example.com/composer.json
curl -I https://2fa.example.com/.git/config
curl -I https://2fa.example.com/storage/logs/php-error.log
curl -I https://2fa.example.com/database/database.sqlite
```

必须 403/404，不允许返回文件内容。

---

---

## 第七章　故障排查

> 按实际部署中出现过的错误逐项定位，不再靠猜。

### 38. 实际部署中遇到的问题与解决方法

> 🧯 **用途：故障排查；先看症状，再执行对应检查命令**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

#### 38.1 Composer 报 putenv undefined

报错：

```text
Call to undefined function Composer\XdebugHandler\putenv()
```

旧版原因：宝塔禁用了 `putenv`，但安装脚本强行运行 Composer。

最终版：

```text
如果 putenv 不可用，install.sh 自动跳过 Composer。
项目已经自带 vendor/autoload.php。
```

如果仍想运行 Composer：

```text
宝塔 → PHP 8.2 → 禁用函数 → 删除 putenv → 重启 PHP
```

---

#### 38.2 age binary FAIL

旧报错：

```text
[FAIL] age binary /usr/local/bin/age
```

Ubuntu apt 实际：

```text
/usr/bin/age
```

最终版安装脚本自动执行等效：

```bash
command -v age
```

并写入 `.env`。

手动修复：

```bash
cd /www/wwwroot/2fa.example.com
AGE_PATH="$(command -v age)"
sed -i "s#^AGE_BINARY=.*#AGE_BINARY=${AGE_PATH}#" .env
grep '^AGE_BINARY=' .env
```

---

#### 38.3 网站浏览器 ERR_CONNECTION_CLOSED

检查：

```bash
ufw status verbose
ss -lntp | grep -E ':80|:443'
/www/server/nginx/sbin/nginx -t
getent ahostsv4 2fa.example.com
```

云安全组必须开放 TCP 80/443。

---

#### 38.4 HTTPS 本机返回 404

检查：

```bash
grep -nE 'server_name|root|include enable-php|fastcgi_pass' /www/server/panel/vhost/nginx/2fa.example.com.conf
```

必须：

```text
root /www/wwwroot/2fa.example.com/public;
```

必须有：

```nginx
location / {
    try_files $uri $uri/ /index.php?$query_string;
}
```

不要同时保留一个冲突的默认 PHP location。

最终版 Router 也已经把 HEAD 当 GET，避免旧版 `curl -I` 假 404。

---

#### 38.5 CLI 说管理员存在，网页却说未创建管理员并 500

典型命令：

```bash
sudo -u www /www/server/php/82/bin/php artisan totp:health
```

旧报错：

```text
ERROR: Unable to create database directory.
```

原因：

```text
/www/server/totp-vault
```

父目录是 `root:root 700`，www 无法穿过。

手动修复：

```bash
chown root:www /www/server/totp-vault
chmod 750 /www/server/totp-vault
chown -R www:www /www/server/totp-vault/data
chown -R www:www /www/server/totp-vault/secrets
chown -R www:www /www/server/totp-vault/tmp
chmod 700 /www/server/totp-vault/data
chmod 700 /www/server/totp-vault/secrets
chmod 700 /www/server/totp-vault/tmp
chmod 600 /www/server/totp-vault/data/database.sqlite
chmod 600 /www/server/totp-vault/secrets/vault.key
```

最终版安装脚本已自动处理父目录。

---

#### 38.6 open_basedir 导致数据库或 vault.key 无法访问

检查：

```bash
find /www/wwwroot/2fa.example.com -maxdepth 2 -name '.user.ini' -print
```

部署阶段关闭宝塔“防跨站攻击”。

不要用 `chmod -R 777` 解决。

---

#### 38.7 Graph Explorer 查到的是 User ID，不是 Drive

旧版曾需要 Drive ID，容易误填用户 Object ID。

最终 v1.2.0：

```text
完全不需要 Drive ID。
```

不要再配置 Drive ID。

---

#### 38.8 Files.ReadWrite.AppFolder + client_credentials 返回 400

旧架构：

```text
Application Files.ReadWrite.AppFolder
/drives/{drive-id}/special/approot
```

实际企业 OneDrive 环境中返回：

```text
HTTP 400 invalidRequest
```

最终版已经取消该架构。

---

#### 38.9 点击“连接 Microsoft OneDrive”没反应

原因：CSP 仍是：

```text
form-action 'self'
```

OAuth POST 后跳 Microsoft 被浏览器拦截。

最终 PHP + Nginx CSP 必须：

```text
form-action 'self' https://login.microsoftonline.com
```

最终源码已修复。

---

#### 38.10 Microsoft 授权成功，但 callback 显示 Graph 400

旧 Delegated 热修复仍使用：

```text
/me/drive/special/approot
```

最终版取消 approot，改为：

```text
/me/drive/root:/TOTP-Vault-Backup
/me/drive/root:/TOTP-Vault-Backup/Backups
```

---

#### 38.11 备份报 proc_open undefined

报错：

```text
Call to undefined function TotpVault\Services\proc_open()
```

处理：

```text
宝塔 → PHP 8.2 → 禁用函数 → 删除 proc_open → 重启 PHP
```

检查：

```bash
/www/server/php/82/bin/php -r '
echo function_exists("proc_open") ? "AVAILABLE\n" : "DISABLED\n";
'
```

`proc_open` 必须长期可用。

最终健康检查会把它作为阻断项。

---

---

## 第八章　网站搬迁

> 旧服务器需要保存什么、如何打迁移包、新服务器怎样恢复。

### 39. 网站搬迁：必须保存什么

> ✅ **用途：核对 / 验收 / 发布前检查**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

搬迁前建议先成功执行一次最新 AGE 备份。

#### 39.1 必须保存

```text
A. 最终源码或你当前实际运行的项目目录
B. /www/wwwroot/你的域名/.env
C. /www/server/totp-vault/data/database.sqlite
D. /www/server/totp-vault/secrets/vault.key
E. /www/server/totp-vault/secrets/secure-store.enc
F. OneDrive 中的 .tar.gz.age 备份
G. AGE Private Key（本来就在你自己的离线电脑，不从服务器下载）
```

`database.sqlite` 已包含：

```text
全部 Vault 记录
网站设置
Logo/Favicon
标签
审计日志
管理员账号哈希
管理员 MFA 加密数据
备份记录
```

`secure-store.enc` 包含：

```text
Microsoft Tenant/Client 配置
Client Secret
Access Token
Refresh Token
```

`secure-store.enc` **不进入 AGE 数据备份**。

---

### 40. 搬迁前在旧服务器制作迁移包

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

停止管理员写入操作，先做最新备份。

```bash
cd /www/wwwroot/2fa.example.com
PHP_BINARY=/www/server/php/82/bin/php bash scripts/backup.sh
```

创建仅 root 可读迁移目录：

```bash
MIGRATE_DIR="/root/totp-vault-migrate-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$MIGRATE_DIR"
chmod 700 "$MIGRATE_DIR"
```

复制关键数据：

```bash
cp -a /www/wwwroot/2fa.example.com/.env "$MIGRATE_DIR/.env"
cp -a /www/server/totp-vault/data/database.sqlite "$MIGRATE_DIR/database.sqlite"
cp -a /www/server/totp-vault/secrets/vault.key "$MIGRATE_DIR/vault.key"
cp -a /www/server/totp-vault/secrets/secure-store.enc "$MIGRATE_DIR/secure-store.enc"
```

打包：

```bash
tar -czf "${MIGRATE_DIR}.tar.gz" -C "$(dirname "$MIGRATE_DIR")" "$(basename "$MIGRATE_DIR")"
chmod 600 "${MIGRATE_DIR}.tar.gz"
sha256sum "${MIGRATE_DIR}.tar.gz"
```

这个迁移包含极敏感数据，只能通过安全通道传输。

传输完成后旧服务器删除临时迁移包：

```bash
rm -rf "$MIGRATE_DIR"
shred -u "${MIGRATE_DIR}.tar.gz" 2>/dev/null || rm -f "${MIGRATE_DIR}.tar.gz"
```

---

### 41. 新服务器恢复搬迁数据

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

先安装最终源码和 PHP/Nginx/AGE，不要重新生成新数据覆盖旧数据。

把迁移包上传到新服务器 root 临时目录。

解压：

```bash
mkdir -p /root/totp-migrate-restore
chmod 700 /root/totp-migrate-restore
tar -xzf /root/你的迁移包.tar.gz -C /root/totp-migrate-restore --strip-components=1
```

停止 PHP 写入：

```bash
/etc/init.d/php-fpm-82 stop
```

复制：

```bash
cp -a /root/totp-migrate-restore/.env /www/wwwroot/2fa.example.com/.env
cp -a /root/totp-migrate-restore/database.sqlite /www/server/totp-vault/data/database.sqlite
cp -a /root/totp-migrate-restore/vault.key /www/server/totp-vault/secrets/vault.key
cp -a /root/totp-migrate-restore/secure-store.enc /www/server/totp-vault/secrets/secure-store.enc
```

权限：

```bash
chown root:www /www/server/totp-vault
chmod 750 /www/server/totp-vault
chown -R www:www /www/server/totp-vault/data /www/server/totp-vault/secrets /www/server/totp-vault/tmp
chmod 700 /www/server/totp-vault/data /www/server/totp-vault/secrets /www/server/totp-vault/tmp
chmod 600 /www/server/totp-vault/data/database.sqlite
chmod 600 /www/server/totp-vault/secrets/vault.key
chmod 600 /www/server/totp-vault/secrets/secure-store.enc
chown www:www /www/wwwroot/2fa.example.com/.env
chmod 600 /www/wwwroot/2fa.example.com/.env
```

清理 SQLite 临时文件：

```bash
rm -f /www/server/totp-vault/data/database.sqlite-wal
rm -f /www/server/totp-vault/data/database.sqlite-shm
```

迁移：

```bash
cd /www/wwwroot/2fa.example.com
sudo -u www /www/server/php/82/bin/php artisan totp:migrate
sudo -u www /www/server/php/82/bin/php artisan totp:test
sudo -u www /www/server/php/82/bin/php artisan totp:health
```

启动 PHP：

```bash
/etc/init.d/php-fpm-82 start
/etc/init.d/nginx reload
```

最后删除新服务器临时迁移包。

---

### 42. 搬迁时域名发生变化

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

编辑：

```text
/www/wwwroot/新域名/.env
```

修改：

```text
APP_URL=https://新域名
```

Entra → Authentication 中增加新：

```text
https://新域名/admin/backups/onedrive/callback
```

已有 Refresh Token 可能继续工作，但为了确认新 callback 与未来重新授权正常，建议迁移完成后重新连接一次 Microsoft OneDrive。

确认新连接成功后再删除旧 Redirect URI。

---

### 43. 如果不复制 secure-store.enc

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

可以不迁移它。

新服务器：

```text
后台 → 备份
重新填写 Tenant ID
Client ID
Client Secret Value
点击连接 Microsoft OneDrive
```

OneDrive 原来的：

```text
TOTP-Vault-Backup/Backups
```

不会丢失。

---

---

## 第九章　灾难恢复与离线解密

> 标准恢复、无网站离线解密、离线导出全部 Secret 与被入侵后的恢复策略。

### 44. 标准备份恢复

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

从 OneDrive 下载：

```text
totp-vault-YYYYMMDD-HHMMSS.tar.gz.age
```

准备：

```text
AGE Private Key
该文件 SHA256
```

上传到隔离恢复服务器：

```text
/tmp/backup.age
/tmp/totp-vault-age-private-key.txt
```

权限：

```bash
chmod 600 /tmp/totp-vault-age-private-key.txt
```

恢复：

```bash
cd /www/wwwroot/2fa.example.com
PHP_BINARY=/www/server/php/82/bin/php bash scripts/restore.sh \
  /tmp/backup.age \
  /tmp/totp-vault-age-private-key.txt \
  这里填写64位SHA256
```

输入：

```text
RESTORE
```

完成后：

```bash
/www/server/php/82/bin/php artisan totp:test
/www/server/php/82/bin/php artisan totp:health --online
```

删除服务器临时 AGE Private Key：

```bash
shred -u /tmp/totp-vault-age-private-key.txt 2>/dev/null || rm -f /tmp/totp-vault-age-private-key.txt
```

---

### 45. 不搭网站：直接解密 OneDrive 备份

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

适用：

```text
网站完全被黑
无法运行 Web 服务
只想救出全部账号与 TOTP Secret
```

最终发布包提供：

```text
tools/offline-export.php
```

整个过程不需要 Nginx、宝塔、域名、管理员登录或 Microsoft OAuth。

---

### 46. Windows 解开 AGE 外层

> 🪟 **执行位置：你自己的 Windows 电脑 PowerShell**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

从 OneDrive 下载：

```text
totp-vault-YYYYMMDD-HHMMSS.tar.gz.age
```

保存：

```text
D:\SoftWare\Age\Restore\backup.tar.gz.age
```

创建目录：

```powershell
New-Item -ItemType Directory -Force -Path "D:\SoftWare\Age\Restore"
```

验证 SHA256：

```powershell
Get-FileHash "D:\SoftWare\Age\Restore\backup.tar.gz.age" -Algorithm SHA256
```

和网站备份记录中的 SHA256 对比。

解密：

```powershell
& "D:\SoftWare\Age\Extracted\age\age.exe" `
  --decrypt `
  --identity "D:\SoftWare\Age\PrivateKey\totp-vault-age-private-key.txt" `
  --output "D:\SoftWare\Age\Restore\backup.tar.gz" `
  "D:\SoftWare\Age\Restore\backup.tar.gz.age"
```

查看归档：

```powershell
tar -tzf "D:\SoftWare\Age\Restore\backup.tar.gz"
```

必须只有：

```text
database.sqlite
vault.key
config.json
manifest.json
```

解压：

```powershell
New-Item -ItemType Directory -Force -Path "D:\SoftWare\Age\Restore\Extracted"
tar -xzf "D:\SoftWare\Age\Restore\backup.tar.gz" -C "D:\SoftWare\Age\Restore\Extracted"
```

得到：

```text
D:\SoftWare\Age\Restore\Extracted\database.sqlite
D:\SoftWare\Age\Restore\Extracted\vault.key
D:\SoftWare\Age\Restore\Extracted\config.json
D:\SoftWare\Age\Restore\Extracted\manifest.json
```

---

### 47. 为什么 database.sqlite 不能直接看到 Secret

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

数据库只保存：

```text
secret_ciphertext
secret_nonce
```

每条 TOTP Secret 使用：

```text
XChaCha20-Poly1305
vault.key
AAD = totp-entry:{entry-id}
```

因此必须同时有：

```text
database.sqlite
vault.key
```

才能解密。

---

### 48. 无网站离线导出全部 Secret：推荐使用干净 Ubuntu/WSL

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

在一台可信干净 Ubuntu/WSL 环境：

```bash
sudo apt update
sudo apt install -y php-cli php-sqlite3 php-common
```

检查：

```bash
php -v
php -m | grep -Ei 'pdo_sqlite|sodium'
```

必须看到：

```text
pdo_sqlite
sodium
```

把最终代码包中的：

```text
tools/offline-export.php
```

复制到安全目录。

把刚才解出的：

```text
database.sqlite
vault.key
```

一起放入安全目录。

执行：

```bash
php offline-export.php \
  --database=/safe/database.sqlite \
  --vault-key=/safe/vault.key \
  --output=/safe/totp-vault-decrypted.json \
  --csv=/safe/totp-vault-entries.csv \
  --uris=/safe/totp-vault-otpauth.txt
```

输出：

```text
totp-vault-decrypted.json
totp-vault-entries.csv
totp-vault-otpauth.txt
```

`totp-vault-otpauth.txt` 每行一个 `otpauth://` URI，可在新 TOTP Vault 的“批量导入”页面直接粘贴。

包含：

```text
平台
账号
Issuer
明文 TOTP Secret
Algorithm
Digits
Period
Notes
Tags
Favorite
otpauth URI
管理员 MFA Secret（如果启用）
网站 settings
backup_records
audit_logs
login_attempts
```

不会包含：

```text
Microsoft Client Secret
Access Token
Refresh Token
secure-store.enc
```

因为 `secure-store.enc` 本来就不在 AGE 数据备份中。

---

### 49. 离线导出文件安全处理

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

离线导出的：

```text
totp-vault-decrypted.json
totp-vault-entries.csv
```

已经包含所有明文 TOTP Secret。

只能保存到：

```text
BitLocker 加密磁盘
加密 U 盘
完全离线恢复环境
```

用完删除：

Linux：

```bash
shred -u /safe/totp-vault-decrypted.json 2>/dev/null || rm -f /safe/totp-vault-decrypted.json
shred -u /safe/totp-vault-entries.csv 2>/dev/null || rm -f /safe/totp-vault-entries.csv
shred -u /safe/totp-vault-otpauth.txt 2>/dev/null || rm -f /safe/totp-vault-otpauth.txt
```

SSD/现代文件系统的 `shred` 不保证物理块完全擦除，所以最可靠的是整个恢复目录放在加密卷里，使用后删除加密卷或销毁其密钥。

---

### 50. 网站被黑后的推荐恢复方式

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

如果确认服务器可能已经被入侵，不建议简单复制旧服务器所有运行文件继续使用。

推荐：

```text
1. 断开旧服务器公网
2. 保留取证快照
3. 从 OneDrive 下载最近一次确认正常的 .age 备份
4. 在可信离线环境解密/检查
5. 新服务器从最终源码重新部署
6. 生成新的 vault.key
7. 使用离线导出的 `totp-vault-otpauth.txt` 在新后台“批量导入”，让所有 Secret 用新 vault.key 重新加密
8. 重置管理员密码与管理员 MFA
9. 在 Entra 删除旧 Client Secret，创建新 Client Secret
10. 重新执行 Microsoft OAuth 授权
11. 视风险生成新的 AGE Key Pair
12. 新系统首次成功备份后再恢复对外服务
```

如果只做灾难恢复而没有证据表明密钥泄露，可以使用标准 restore 保留原 `vault.key`。

如果明确怀疑：

```text
vault.key
secure-store.enc
```

已经被攻击者拿走，应按“新 Vault + 重新导入 Secret”方式轮换，而不是继续使用旧 vault.key。

---

---

## 第十章　发布与命令速查

> 源码发布检查、版本更新、常用命令、上线清单与禁止事项。

### 51. 发布给其他人前检查源码包

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

最终发布 ZIP 内不得包含：

```text
.env
database.sqlite
vault.key
secure-store.enc
AGE Private Key
Microsoft Client Secret
Access Token
Refresh Token
真实 Tenant ID
真实 Client ID
真实域名配置
真实管理员账号数据
storage/logs 中真实日志
```

检查：

```bash
unzip -l TOTP-Vault-v1.2.0-Baota-Final-20260824.zip
```

源码包应该只有模板与空 `.gitkeep` 数据目录。

---

### 52. 更新最终源码到已有网站

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

先备份：

```bash
cd /www/wwwroot/2fa.example.com
PHP_BINARY=/www/server/php/82/bin/php bash scripts/backup.sh
```

备份关键运行配置：

```bash
UPGRADE_BAK="/root/totp-vault-upgrade-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$UPGRADE_BAK"
cp -a .env "$UPGRADE_BAK/.env"
cp -a /www/server/totp-vault/data/database.sqlite "$UPGRADE_BAK/database.sqlite"
cp -a /www/server/totp-vault/secrets/vault.key "$UPGRADE_BAK/vault.key"
cp -a /www/server/totp-vault/secrets/secure-store.enc "$UPGRADE_BAK/secure-store.enc" 2>/dev/null || true
chmod -R go-rwx "$UPGRADE_BAK"
```

替换源码时：

```text
保留 .env
不要覆盖 /www/server/totp-vault/data
不要覆盖 /www/server/totp-vault/secrets
```

替换后：

```bash
cd /www/wwwroot/2fa.example.com
/www/server/php/82/bin/php artisan totp:migrate
/www/server/php/82/bin/php artisan totp:test
sudo -u www /www/server/php/82/bin/php artisan totp:health --online
/etc/init.d/php-fpm-82 restart
/etc/init.d/nginx reload
```

---

### 53. 最常用命令

> 🖥️ **执行位置：服务器 SSH / 终端**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

#### PHP

```bash
/www/server/php/82/bin/php -v
```

#### 项目测试

```bash
cd /www/wwwroot/2fa.example.com
/www/server/php/82/bin/php artisan totp:test
```

#### 健康检查

```bash
sudo -u www /www/server/php/82/bin/php artisan totp:health
```

#### 在线健康检查

```bash
sudo -u www /www/server/php/82/bin/php artisan totp:health --online
```

#### 手动备份

```bash
PHP_BINARY=/www/server/php/82/bin/php bash scripts/backup.sh
```

#### 延迟备份

```bash
PHP_BINARY=/www/server/php/82/bin/php bash scripts/backup-due.sh
```

#### Nginx

```bash
/www/server/nginx/sbin/nginx -t
/etc/init.d/nginx reload
```

#### PHP 重启

```bash
/etc/init.d/php-fpm-82 restart
```

#### 日志

```bash
tail -n 100 /www/wwwroot/2fa.example.com/storage/logs/php-error.log
tail -n 100 /www/wwwlogs/2fa.example.com.error.log
```

#### AGE

```bash
command -v age
age --version
```

#### proc_open

```bash
/www/server/php/82/bin/php -r 'echo function_exists("proc_open") ? "AVAILABLE\n" : "DISABLED\n";'
```

---

### 54. 最终上线 Checklist

> ✅ **用途：核对 / 验收 / 发布前检查**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

#### 系统

- [ ] Ubuntu 时间同步正常
- [ ] 2 GB 服务器已有 Swap
- [ ] 云安全组 TCP 80/443 放行
- [ ] 宝塔端口已限制访问来源

#### PHP

- [ ] PHP 8.2+
- [ ] pdo_sqlite
- [ ] sodium
- [ ] curl
- [ ] json
- [ ] mbstring
- [ ] proc_open AVAILABLE

#### 数据安全

- [ ] database.sqlite 在 Web Root 外
- [ ] vault.key 在 Web Root 外
- [ ] secure-store.enc 在 Web Root 外
- [ ] vault.key 600
- [ ] 运行目录是 public
- [ ] 没有 chmod 777

#### Web

- [ ] HTTPS 正常
- [ ] HTTP 强制 HTTPS
- [ ] Nginx 只执行 public/index.php
- [ ] `.env` 403/404
- [ ] `/admin` 未登录不可进入
- [ ] 管理员 MFA 已启用

#### Microsoft

- [ ] Redirect URI 是 Web
- [ ] Delegated Files.ReadWrite
- [ ] Client Secret 填的是 Value
- [ ] OneDrive Connected
- [ ] TOTP-Vault-Backup/Backups 可访问
- [ ] Refresh Token 已加密保存

#### AGE

- [ ] AGE Private Key 只在自己的离线设备
- [ ] AGE Private Key 有至少两份离线副本
- [ ] 网站只填写 age1... Public Key
- [ ] AGE binary PASS

#### 备份

- [ ] 手动备份成功
- [ ] OneDrive 只有 `.tar.gz.age`
- [ ] SHA256 已记录
- [ ] 每日 03:30 任务成功
- [ ] 每 5 分钟任务成功
- [ ] health --online 无 FAIL
- [ ] 至少进行一次隔离恢复或离线导出演练

---

### 55. 绝对不要做

> ✅ **用途：核对 / 验收 / 发布前检查**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

不要：

```bash
chmod -R 777 /www
```

不要：

```bash
rm -rf /www/server/totp-vault/secrets/vault.key
```

不要把：

```text
AGE Private Key
vault.key
secure-store.enc
.env
Microsoft Client Secret
Refresh Token
```

上传公开 GitHub 或发给不可信第三方。

不要用新生成 `vault.key` 去尝试解密旧数据库。

不要在唯一生产实例上做第一次恢复演练。

---

### 56. 官方参考

> ✅ **用途：核对 / 验收 / 发布前检查**
> 
> 下方命令块可以直接复制；带“检查/预期”的内容用于核对结果，不要当作命令输入。

宝塔官方安装：

```text
https://docs.bt.cn/getting-started/quick-installation-of-bt-panel
```

Microsoft Authorization Code Flow：

```text
https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
```

Microsoft Graph Files.ReadWrite 权限：

```text
https://learn.microsoft.com/en-us/graph/permissions-reference
```

Microsoft Graph 创建 OneDrive 文件夹：

```text
https://learn.microsoft.com/en-us/graph/api/driveitem-post-children?view=graph-rest-1.0
```

AGE 官方：

```text
https://github.com/FiloSottile/age/releases
```

---

## 第十一章　Direct TOTP 直达验证码页

> v1.2.0 新增功能。现有首页、管理员后台、OneDrive、AGE 和数据库逻辑不变。

### 57. 功能地址

> 🌐 **执行位置：浏览器**

普通首页仍然是：

```text
https://2fa.example.com/
```

Direct TOTP 地址：

```text
https://2fa.example.com/2fa/你的Base32Secret
```

公开测试值：

```text
JBSWY3DPEHPK3PXP
```

测试：

```text
https://2fa.example.com/2fa/JBSWY3DPEHPK3PXP
```

页面应显示当前 6 位验证码、30 秒倒计时、进度条和复制按钮，并自动刷新。

> ⚠️ **安全提醒**
>
> `/2fa/{secret}` 的 Secret 位于浏览器 URL 中。它可能被浏览器历史记录保存，所以不要公开分享这类地址，也不要把真实 Secret 粘贴进工单、聊天、公开文档或截图。

### 58. Direct TOTP 的计算位置

> ✅ **核对说明**

Direct 页面没有调用第三方验证码接口，而是继续使用：

```text
public/assets/totp-core.js
```

浏览器端 Web Crypto 本地计算：

```text
RFC 6238
SHA1
6 digits
30 seconds
```

Direct 页面不应：

```text
写 database.sqlite
写 Session
写 Audit
写 localStorage
写 sessionStorage
调用第三方 TOTP API
使用 fetch/XHR/WebSocket/sendBeacon 发送 Secret
```

### 59. Nginx 必须关闭 `/2fa/` 日志

> 🧰 **执行位置：宝塔面板 → 网站 → 站点 → 配置文件**

在普通 `location /` 前加入：

```nginx
location ^~ /2fa/ {
    access_log off;
    log_not_found off;
    error_log /dev/null crit;

    try_files /index.php =404;
    include fastcgi_params;
    fastcgi_pass unix:/tmp/php-cgi-82.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php;
    fastcgi_param SCRIPT_NAME /index.php;
    fastcgi_param HTTP_PROXY "";
    fastcgi_read_timeout 60s;
    fastcgi_hide_header X-Powered-By;
}
```

> ⚠️ **不要照抄错误 PHP 版本**
>
> 如果你的站点实际使用 PHP 8.2，就必须是：
>
> ```text
> /tmp/php-cgi-82.sock
> ```
>
> 只有真实切换到 PHP 8.3 后才改为 `83`。

CSP 必须继续保留：

```text
form-action 'self' https://login.microsoftonline.com
```

否则 Microsoft OneDrive OAuth 会再次被浏览器安全策略阻止。

保存前检查：

```bash
/www/server/nginx/sbin/nginx -t
```

必须：

```text
syntax is ok
test is successful
```

然后：

```bash
/etc/init.d/nginx reload
```

### 60. Direct 页面 Header 验收

> 🖥️ **执行位置：服务器 SSH / 终端**

```bash
curl -kIsS 'https://2fa.example.com/2fa/JBSWY3DPEHPK3PXP'
```

至少核对：

```text
Cache-Control: no-store
Referrer-Policy: no-referrer
X-Robots-Tag: noindex, nofollow, noarchive, nosnippet
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Content-Security-Policy: ...
```

### 61. 确认 Secret 没进入 Nginx 日志

> 🖥️ **执行位置：服务器 SSH / 终端**

先访问测试地址，然后：

```bash
grep -F 'JBSWY3DPEHPK3PXP' /www/wwwlogs/2fa.example.com.log
```

> ✅ **正确结果：无输出**

如果存在 error log：

```bash
grep -F 'JBSWY3DPEHPK3PXP' /www/wwwlogs/2fa.example.com.error.log
```

也应无输出。

### 62. 与首页验证码对比

> 🌐 **执行位置：浏览器**

首页粘贴：

```text
JBSWY3DPEHPK3PXP
```

同时打开：

```text
https://2fa.example.com/2fa/JBSWY3DPEHPK3PXP
```

在同一个 30 秒时间窗内，两边 6 位验证码必须一致。

再测试小写：

```text
https://2fa.example.com/2fa/jbswy3dpehpk3pxp
```

再测试非法内容：

```text
https://2fa.example.com/2fa/INVALID0000
```

非法 Secret 应显示友好错误，不应 500。

### 63. 从 v1.1.0 升级到 v1.2.0

> 🖥️ **执行位置：服务器 SSH / 终端**

先完整备份当前程序文件：

```bash
cd /www/wwwroot
cp -a 2fa.example.com "2fa.example.com-before-v1.2.0-$(date +%Y%m%d-%H%M%S)"
```

不要覆盖以下运行数据：

```text
/www/server/totp-vault/data/database.sqlite
/www/server/totp-vault/secrets/vault.key
/www/server/totp-vault/secrets/secure-store.enc
```

上传新版源码后，只更新程序目录；不要重新运行会替换密钥的数据初始化逻辑。

更新完成后：

```bash
cd /www/wwwroot/2fa.example.com
/www/server/php/82/bin/php artisan totp:test
sudo -u www /www/server/php/82/bin/php artisan totp:health --online
```

重启 PHP：

```bash
/etc/init.d/php-fpm-82 restart
```

Nginx 修改后：

```bash
/www/server/nginx/sbin/nginx -t && /etc/init.d/nginx reload
```

### 64. Direct TOTP 回滚

> 🖥️ **执行位置：服务器 SSH / 终端**

如果升级后异常，恢复升级前整站程序目录即可。运行数据目录 `/www/server/totp-vault` 不要删除。

---

## 第十二章　TOTP Vault 自有品牌定制

### 65. 最推荐：后台直接修改品牌

> 🧰 **执行位置：管理员后台 → 系统设置**

当前程序已经支持不改源码直接配置：

```text
Website Name
Website Subtitle
Footer
SEO Title
SEO Description
Logo
Favicon
```

Logo 要求：

```text
PNG
最大 512 KB
推荐正方形透明背景
推荐至少 128×128
```

Favicon：

```text
PNG
最大 512 KB
推荐 64×64 或 128×128
```

> ✅ **说明**
>
> 后台上传的品牌图片会存入设置数据库，并随 AGE 数据备份一起保存。日常给自己的网站换品牌时，优先用这个方式，不需要 SSH、不需要改 PHP。

### 66. 网站名称与 `.env` 的关系

> 🖥️ **执行位置：服务器 SSH / 终端**

`.env` 中：

```text
APP_NAME="TOTP Vault"
```

是默认回退值。

已经安装并在后台设置过 Website Name 后，优先使用数据库中的 `website_name`。

如果你制作一个要重新发布给其他人的品牌发行版，可以修改：

```text
.env.example
```

例如：

```text
APP_NAME="Your Brand 2FA"
```

### 67. 修改发布包默认 Logo 与 Favicon

> 🖥️ **执行位置：源码目录 / Codex**

默认资源：

```text
public/assets/logo.svg
public/favicon.svg
```

如果后台从未上传品牌图片，程序会回退到这两个文件。

因此，制作自己的发行版时可以替换它们。

> ⚠️ **不要把自己的私钥、Token 或真实 Secret 放进 Logo 目录或源码包。**

### 68. 修改后台固定副标题

> 🖥️ **执行位置：源码目录 / Codex**

当前管理后台存在固定英文：

```text
Private Admin Vault
```

位置：

```text
resources/views/layouts/admin.php
resources/views/admin/login.php
```

可以改为例如：

```text
Your Brand Security Console
```

修改后语法检查：

```bash
/www/server/php/82/bin/php -l resources/views/layouts/admin.php
/www/server/php/82/bin/php -l resources/views/admin/login.php
```

### 69. 修改网站主题色

> 🖥️ **执行位置：源码目录 / Codex**

打开：

```text
public/assets/app.css
```

顶部有：

```css
:root {
    --brand: #4f46e5;
    --brand-dark: #4338ca;
    --brand-soft: #eef2ff;
}
```

只修改这三个品牌变量，就能统一改变：

```text
主按钮
链接
图标
高亮背景
进度条
侧栏选中状态
```

CSS 改完如果浏览器仍显示旧颜色，先强制刷新：

```text
Ctrl + F5
```

或者把模板引用中的：

```text
app.css?v=1.2.0
```

升级一个静态资源版本号。

### 70. 修改 SEO

> 🧰 **执行位置：管理员后台 → 系统设置**

直接填写：

```text
SEO Title
SEO Description
```

公开首页会使用这些内容。

Direct TOTP 页面始终：

```text
noindex
nofollow
noarchive
```

不要为了 SEO 去掉 Direct 页面的 `noindex`。

### 71. 品牌修改后检查

> 🌐 **执行位置：浏览器**

依次打开：

```text
/
/admin/login
/admin
/admin/settings
/2fa/JBSWY3DPEHPK3PXP
```

核对：

```text
Logo
Favicon
Website Name
后台标题
公开页标题
Direct TOTP 标题
手机端显示
```

---

## 第十三章　静态技术文档站品牌修改

> 这一章针对单独部署在 `docs.example.com` 的纯静态技术文档站，不是 TOTP Vault PHP 主站。

### 72. 推荐目录

> 🧰 **执行位置：宝塔面板**

推荐：

```text
TOTP 主站：
/www/wwwroot/2fa.example.com

文档站：
/www/wwwroot/docs.example.com
```

不要把文档站文件直接混入 TOTP 主站 `/public`。

### 73. 文档站需要准备的品牌文件

> 🖥️ **执行位置：本地电脑 / Codex / 宝塔文件管理器**

建议准备：

```text
assets/brand-logo.svg
assets/favicon.svg
```

推荐 Logo：

```text
SVG 或透明 PNG
1:1
简洁图形
深浅色背景都能识别
```

推荐 Favicon：

```text
SVG
或 64×64 PNG
```

### 74. 修改文档站网站名称

文档页面顶部原本类似：

```html
<a class="brand" href="/">
  <span class="brand-mark">⌁</span>
  <span>TOTP Vault Docs<small>Deployment & Recovery</small></span>
</a>
```

替换为：

```html
<a class="brand" href="/">
  <img class="brand-logo" src="/assets/brand-logo.svg" alt="Your Brand">
  <span>Your Brand Docs<small>Security & Operations</small></span>
</a>
```

CSS：

```css
.brand-logo {
    width: 34px;
    height: 34px;
    object-fit: contain;
    border-radius: 9px;
}
```

### 75. 添加文档站 Favicon

每个 HTML 的 `<head>` 内加入：

```html
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
```

同时修改：

```html
<title>Your Brand Docs · 部署与运维文档</title>
<meta name="description" content="Your Brand 2FA 部署、备份、搬迁与恢复文档">
<meta name="theme-color" content="#你的品牌色">
```

### 76. 修改文档站主题色

打开：

```text
assets/style.css
```

顶部：

```css
:root {
    --accent: #5b4cf0;
    --accent2: #785ff7;
    --accent-soft: #f0edff;
}
```

改成你的品牌颜色即可。

深色模式也有对应变量：

```css
[data-theme="dark"] {
    --accent: ...;
    --accent2: ...;
    --accent-soft: ...;
}
```

### 77. 文档站全局品牌替换检查

> 🖥️ **执行位置：服务器 SSH / 终端**

例如文档站：

```text
/www/wwwroot/docs.example.com
```

查找旧名称：

```bash
grep -RIn 'TOTP Vault Docs\|Deployment & Recovery\|TOTP Vault v1.1.0' \
/www/wwwroot/docs.example.com \
--exclude='*.md'
```

根据结果逐项修改。

修改完检查静态文件：

```bash
find /www/wwwroot/docs.example.com -type f | sort
```

文档站是静态 HTML/CSS/JS，所以正常情况下：

```text
不需要重启 PHP
不需要数据库
不需要 Composer
不需要 npm build
```

Nginx 配置没有改时，甚至不需要 reload；如果改了 Nginx，再执行：

```bash
/www/server/nginx/sbin/nginx -t && /etc/init.d/nginx reload
```

### 78. 给 Codex 的品牌修改提示词

> 💻 **执行位置：Codex**

直接把下面整段给 Codex，并把方括号内容替换成自己的品牌：

```text
我有一个纯静态技术文档网站，根目录中包含多个 index.html、assets/style.css、assets/app.js 和 assets/search-index.json。

不要重构，不要引入 Node.js，不要引入框架，不要使用外部 CDN。

请只做全站品牌替换和必要的视觉适配。

我的品牌资料：
网站名称：[YOUR BRAND DOCS]
副标题：[YOUR SUBTITLE]
主站名称：[YOUR 2FA PRODUCT NAME]
品牌主色：[HEX]
品牌深色：[HEX]
品牌浅色背景：[HEX]
Logo 文件：[assets/brand-logo.svg]
Favicon 文件：[assets/favicon.svg]

要求：
1. 所有 HTML 顶部品牌名称统一替换；
2. 原来的文字图标 brand-mark 改为真实 Logo 图片；
3. 所有 HTML head 增加 favicon；
4. 更新 title、meta description、theme-color；
5. 首页 Hero Badge、主标题、介绍语改为我的品牌；
6. 页脚改为我的品牌版权；
7. style.css 只修改品牌变量，不破坏布局；
8. 保留顶部栏、左侧目录、右侧 TOC、代码复制、搜索、深浅色模式、手机端菜单；
9. 不改变任何部署命令和技术内容；
10. 搜索全项目确保旧品牌文字没有遗漏；
11. 不修改代码块里的域名占位符 2fa.example.com / docs.example.com，除非我明确要求；
12. 输出修改文件清单和 grep 检查命令。

修改完成后请执行静态检查，确保所有 HTML/CSS/JS 文件路径存在、内部链接没有 404。
```

---

## 第十四章　v1.2.0 最终验收清单

### 79. 程序版本

> 🖥️ **执行位置：服务器 SSH / 终端**

```bash
cat /www/wwwroot/2fa.example.com/VERSION
```

应为：

```text
1.2.0
```

### 80. PHP 与完整测试

```bash
cd /www/wwwroot/2fa.example.com
/www/server/php/82/bin/php artisan totp:test
sudo -u www /www/server/php/82/bin/php artisan totp:health --online
```

### 81. OneDrive

> 🧰 **执行位置：管理员后台 → 备份**

确认：

```text
OneDrive: Connected
AGE Encryption: Public key ready
```

不要因为更新 Direct TOTP 再次创建 Azure App、重新生成 Client Secret 或重新生成 AGE 密钥。

### 82. Direct TOTP

> 🌐 **执行位置：浏览器 + SSH**

测试：

```text
https://2fa.example.com/2fa/JBSWY3DPEHPK3PXP
```

日志：

```bash
grep -F 'JBSWY3DPEHPK3PXP' /www/wwwlogs/2fa.example.com.log
```

必须无输出。

### 83. 品牌

确认：

```text
公开首页 Logo / Favicon
网站名称
页脚
SEO
后台标题
Direct TOTP 页面 Logo / 网站名
静态文档站 Logo / Favicon / 网站名 / 主题色
```

至此为 TOTP Vault v1.2.0 的最终部署、运维、Direct TOTP、迁移、恢复、离线解密和品牌定制基线。

