Establish secure account and managed AI backend
Provide the production foundation for Apple identity, immutable credits, referrals, integrity checks, managed providers, and hardened Docker deployment.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# OSG Account Server 部署指南
|
||||
|
||||
本方案使用一个非 root、只读文件系统兼容的 JDK 21 容器。OpenResty 终止 TLS 并反向代理,
|
||||
MySQL 复用已有实例。`compose.yaml` 不会创建数据库容器。
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
- `account.osglab.com` 与 `osglab.com` 已解析到 1Panel 主机。
|
||||
- Docker Engine、Docker Compose v2 和 1Panel OpenResty 已安装。
|
||||
- 已有 MySQL 8 实例可从应用容器通过 Docker 外部网络或私网访问。
|
||||
- 服务器时钟同步,Apple、DeepSeek、Volcengine 的出站 HTTPS/WSS 可用。
|
||||
- App 的 Associated Domains 包含 `applinks:osglab.com`。
|
||||
|
||||
邀请使用一方 Universal Link,不依赖 Firebase Dynamic Links 或 Branch。
|
||||
|
||||
## 2. 注册邀请网页路由
|
||||
|
||||
网页实现在 `features/inviteweb`,资源在 `src/main/resources/invite/index.html`。组合根应只注册
|
||||
一个 `/i/{code}` 路由;不要与旧 `features/invite` 路由同时挂载。`ReferralLookupPort` 是只读、
|
||||
大小写敏感的验证边界,适配器应在 referrals 模块中用有界查询检查邀请码及活动状态:
|
||||
|
||||
先在 Koin 注册一个 `ReferralLookupPort` 实现;公共接线会从现有 `AppConfig` 读取 App Store URL、
|
||||
邀请域名和 Apple Team ID/bundle ID:
|
||||
|
||||
```kotlin
|
||||
routing {
|
||||
configureInviteWebRoutes()
|
||||
}
|
||||
```
|
||||
|
||||
测试或不使用 Koin 的宿主可调用显式重载
|
||||
`configureInviteWebRoutes(referralLookup, InviteWebConfig(...))`。
|
||||
|
||||
邀请码规则与当前生成器一致:16 字节随机值编码为无填充 Base64URL,即固定 22 位、区分大小写,
|
||||
字符白名单为 `A-Z`、`a-z`、`0-9`、`_`、`-`。结构非法或查无记录均返回相同 404;查询超时或
|
||||
失败返回不泄露内部信息的 503。页面和两个 AASA endpoint 均发送 `no-store`。
|
||||
|
||||
## 3. 准备现有 MySQL
|
||||
|
||||
先创建数据库,字符集使用 `utf8mb4`。使用 `docs/mysql-minimum-privileges.sql` 创建相互独立的
|
||||
迁移账号和运行账号,并将示例来源网段替换为实际应用容器网段。不要使用公网来源或 `%`:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE osg_account
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_0900_ai_ci;
|
||||
```
|
||||
|
||||
应用启动时使用迁移账号执行 Flyway,随后 Hikari 连接池只使用运行账号。运行账号不能更新或删除
|
||||
不可变 ledger/usage 历史,也不能访问 Flyway 元数据或未来未审查的表。禁止授予全局权限、
|
||||
`FILE`、`PROCESS`、`SUPER`、`CREATE USER`、`GRANT OPTION`。不要复用 root 或其他应用用户。
|
||||
|
||||
若 MySQL 是容器,把它和应用接入同一个已存在的外部网络:
|
||||
|
||||
```bash
|
||||
docker network create account-backend
|
||||
docker network connect account-backend <existing-mysql-container>
|
||||
```
|
||||
|
||||
已存在网络时不要重复创建。`DATABASE_URL` 中使用该 MySQL 容器在网络内可解析的名称。若 MySQL
|
||||
位于私网主机,使用私有 DNS/IP,并在数据库防火墙中只允许应用主机或容器网段。
|
||||
|
||||
## 4. 配置环境与秘密
|
||||
|
||||
复制部署所需变量到项目根目录的未跟踪 `.env`,或使用 1Panel 的环境变量/秘密管理。不要把真实
|
||||
值写入 YAML、镜像层或 Git。
|
||||
|
||||
必须设置:
|
||||
|
||||
- `DATABASE_URL=jdbc:mysql://<existing-mysql>:3306/osg_account?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true`
|
||||
- `DATABASE_USER`、`DATABASE_PASSWORD`
|
||||
- `DATABASE_MIGRATION_USER`、`DATABASE_MIGRATION_PASSWORD`:必须与运行账号及其密码不同
|
||||
- `JWT_SECRET`:至少 32 个随机字节
|
||||
- `FIELD_ENCRYPTION_KEY`:恰好 32 个随机字节的 Base64
|
||||
- `IDENTITY_HMAC_KEY`:至少 32 个随机字节的独立 Base64,不得复用字段加密密钥
|
||||
- `APPLE_TEAM_ID`、`APPLE_KEY_ID`、`APPLE_PRIVATE_KEY_PEM`
|
||||
- `VOLCENGINE_API_KEY`、`DEEPSEEK_API_KEY`
|
||||
- `APP_STORE_URL`:正式 App Store HTTPS 地址;生产 Compose 不接受缺失值
|
||||
|
||||
生成新秘密的示例:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 48
|
||||
openssl rand -base64 32
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
三个结果分别用于 JWT、字段加密和身份 HMAC,不能相互复用。`APPLE_PRIVATE_KEY_PEM` 可使用包含
|
||||
字面量 `\n` 的单行值;应用配置会在内存中还原换行。限制 `.env` 权限:
|
||||
|
||||
```bash
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
生产 Compose 已固定 `APP_ENV=production`、Apple production 环境及两项完整性强制开关。
|
||||
Apple 配置使用所属开发者账号的 Team ID、Key ID、bundle ID 和 `.p8` 私钥;火山引擎使用 SAUC
|
||||
v3 WSS endpoint、资源 ID 和 API Key;DeepSeek 使用 HTTPS endpoint、已开通模型名和 API Key。
|
||||
三方凭据分别创建、分别轮换,不得复用。
|
||||
|
||||
## 5. 构建与启动
|
||||
|
||||
先检查变量插值。`docker compose config` 会展开秘密,不要把输出上传或粘贴到工单:
|
||||
|
||||
```bash
|
||||
./gradlew test
|
||||
docker compose config --quiet
|
||||
docker compose build --pull
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
curl --fail http://127.0.0.1:18080/health/ready
|
||||
```
|
||||
|
||||
容器以 UID/GID 10001 运行,根文件系统只读,仅 `/tmp` 是带大小限制的 tmpfs。应用端口只映射到
|
||||
宿主机回环地址,不能改成 `0.0.0.0`。
|
||||
|
||||
查看日志时不得记录或复制 token、Apple subject、音频、prompt、转录或模型响应:
|
||||
|
||||
```bash
|
||||
docker compose logs --since=10m account-server
|
||||
```
|
||||
|
||||
## 6. 配置 1Panel / OpenResty
|
||||
|
||||
在 1Panel 创建 `account.osglab.com`、`osglab.com` 两个 HTTPS 网站并签发证书。使用
|
||||
`deploy/openresty-account.conf`:
|
||||
|
||||
1. 将 `map`、`limit_req_zone`、`limit_req_status`、`upstream` 放入 OpenResty `http` 上下文。
|
||||
2. 将三个 `server` 块作为站点配置;按 1Panel 实际证书路径调整 `ssl_certificate`。
|
||||
3. 示例 upstream 指向宿主机 `127.0.0.1:18080`。若 OpenResty 自身在容器中,则将其加入
|
||||
`account-backend`,并改为 `account-server:8080`。
|
||||
4. 配置明确对 `/admin`、`/internal`、`/v1/admin` 返回 404;不要新增绕过该规则的泛域名代理。
|
||||
5. API 示例按 IP 限制 20 请求/秒,邀请页限制 5 请求/秒,可基于真实流量谨慎调整。
|
||||
6. 代理统一支持 HTTP/1.1 Upgrade/Connection,因此当前 HTTP API 与后续 WebSocket 入口都可用。
|
||||
|
||||
两个 AASA 地址由 Ktor 根据 `appleAppId` 模板输出,不需要复制静态文件。配置检查成功后再通过
|
||||
1Panel 重载 OpenResty:
|
||||
|
||||
```bash
|
||||
openresty -t
|
||||
```
|
||||
|
||||
## 7. 验证
|
||||
|
||||
检查健康状态、反向代理、隐藏路径、限流和 AASA:
|
||||
|
||||
```bash
|
||||
curl --fail https://account.osglab.com/health/ready
|
||||
curl -i https://account.osglab.com/internal/
|
||||
curl -i https://osglab.com/.well-known/apple-app-site-association
|
||||
curl -i https://osglab.com/i/AbCdEf0123456789_-AbCd
|
||||
```
|
||||
|
||||
预期结果:
|
||||
|
||||
- 健康检查返回 200。
|
||||
- 内部路径返回 404。
|
||||
- AASA 返回 200、`Content-Type: application/json`,且不发生重定向。
|
||||
- 合法 22 位邀请码返回双语 HTML;非法、未知或失效邀请码统一返回 404。
|
||||
- 邀请页包含 nonce CSP、`Cache-Control: no-store`、`X-Robots-Tag`,无第三方请求。
|
||||
|
||||
使用真机验证 Universal Link。安装带 Associated Domains entitlement 的 App 后,从信息或邮件点击
|
||||
`https://osglab.com/i/<code>` 应直接进入 App;未安装时应显示网页。AASA 更新受系统缓存影响,
|
||||
首次验证应预留传播时间。
|
||||
|
||||
## 8. 防火墙
|
||||
|
||||
- 公网入站仅允许 TCP 80/443。
|
||||
- SSH 仅允许管理员固定 IP 或 VPN;不公开 18080、8080、3306。
|
||||
- MySQL 3306 仅允许 Docker 私网或指定应用主机。
|
||||
- 出站允许 DNS、NTP、Apple HTTPS、DeepSeek HTTPS、Volcengine HTTPS/WSS。
|
||||
- 云安全组、主机防火墙和 1Panel 防火墙应采用相同边界,避免其中一层意外放行。
|
||||
|
||||
## 9. 更新与回滚
|
||||
|
||||
更新前备份 MySQL 并记录当前镜像标签。使用不可变标签构建:
|
||||
|
||||
```bash
|
||||
IMAGE_TAG=<release-tag> docker compose build
|
||||
IMAGE_TAG=<release-tag> docker compose up -d
|
||||
```
|
||||
|
||||
Flyway 迁移只向前执行。若新版本包含数据库迁移,应用镜像回滚不等于数据库回滚;应先按迁移影响
|
||||
制定恢复方案。无数据库变更时,可把 `IMAGE_TAG` 切回上一版本并重新执行 `docker compose up -d`。
|
||||
|
||||
备份、时间点恢复和每月恢复演练按 `docs/BACKUP.md` 执行。
|
||||
|
||||
## 10. 分阶段验收
|
||||
|
||||
1. **本地阶段**:`./gradlew test` 通过;合法、未知、格式错误和 lookup 异常路径符合预期;HTML
|
||||
不加载第三方资源。
|
||||
2. **容器阶段**:镜像以 UID/GID 10001 运行;根文件系统只读;`/health/live` 与
|
||||
`/health/ready` 通过;容器无法取得额外 Linux capability。
|
||||
3. **预发布阶段**:Flyway 使用迁移账号成功,应用使用运行账号成功;撤销运行账号 DDL 权限后服务
|
||||
仍正常;OpenResty HTTP、HTTPS 和 WSS 代理验证通过。
|
||||
4. **Apple 阶段**:两个 AASA 地址返回无重定向 JSON;真机从信息或邮件点击
|
||||
`https://osglab.com/i/<code>` 可打开 App;未安装 App 时显示双语落地页。
|
||||
5. **生产阶段**:公网仅开放 80/443,3306/8080/18080 不可达;邀请 URL 不出现在访问日志;
|
||||
供应商凭据可用;完成加密备份并记录一次隔离恢复结果。
|
||||
Reference in New Issue
Block a user