Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 24 additions & 66 deletions content/docs/configure/encryption.mdx
Original file line number Diff line number Diff line change
@@ -1,92 +1,50 @@
---
title: 数据加密
description: 加密你的 Mix Space
description: v17 凭据加密配置、原密钥保留与 v18 准备
---

Mix Space 提供了敏感配置的加密存储能力。默认为关。
本文固定已发布的 Core v17.0.4。未发布的 v18 开发分支已删除关闭加密和旧格式写入,不能沿用下文的 v17 逃生口,详见 [v18 准备说明](/docs/migrate/v18-preparation)。

为什么需要 Key 加密。
## 三种秘密分别保管

假设数据库被脱库。如果开启了 Key 加密,即便攻击者拿到了全部数据,也读不出被标记为加密的字段。同时,也需要谨慎开启此功能——开启后你需要记住加密密钥,否则你将永远丢失这些数据。
| 秘密 | 用途 | 恢复时的要求 |
| --- | --- | --- |
| `MX_ENCRYPT_KEY` / `ENCRYPT_KEY` | 配置、Snippet、Webhook 的字段加密 | 保留产生这些密文的原密钥 |
| `JWT_SECRET` | 登录与 Better Auth OAuth token 加密 | 保留原值;轮换需要维护窗口 |
| 备份恢复私钥 | 打开 `.mxbackup` 归档 | 与备份分开保存;不能替代上面两种秘密 |

<Callout type="warn">
开启前请务必备份数据库。忘记密钥 = 这些数据永久无法恢复。
已有实例不要为解决解密错误而生成新密钥。新密钥无法解开旧数据。先从部署配置、秘密管理器或历史快照找回原值;丢失原秘密时,需要重新授权或重新配置相应凭据。
</Callout>

## 哪些字段会被加密
## v17 如何开启

加密范围不是全部配置项,而是由服务端 schema 上的 `encrypt` 元数据决定,**随版本变化**。

判定规则:服务端遍历配置 schema 时,会读每个字段的 `encrypt` 元数据;**父级被标记时,整棵子树都继承加密**。绝大多数密码类字段是通过 `field.password()` / `field.passwordHalfGrid()` 定义的,这两个 helper 会在构造 schema 时自动注入 `encrypt: true`,所以你在配置里看到的「密码」输入框,基本都属于加密范围。

当前版本(v14.15.2)覆盖的主要字段:

| 区域 | 字段 |
| --- | --- |
| OAuth | 各服务商的 client secret、Apple 私钥 |
| 图床 / 对象存储 | S3、R2 的 `secretKey` |
| 邮件 | Resend API Key、SMTP 密码 |
| AI | 各 Provider 的 `apiKey` |
| Webhook | 签名密钥 |
| 搜索推送 | 百度推送 Token、Bing API key |
| 会员 | API Key |
| 其他 | 高德地图 Key、设备 Key、Personal Access Token 等 |

<Callout type="info">
想确认当前版本的准确清单,可以看服务端 `apps/core/src/modules/configs/` 下的 `configs.schema.ts`:凡是写成 `field.password(...)` 或 `field.passwordHalfGrid(...)` 的字段都会被加密,另加显式标注 `{ encrypt: true }` 的字段。文档这里的说明可能落后于代码,以源码为准。
</Callout>

## 如何开启

你可以附加 `--encrypt_enable` 来启动服务。如:
为从未加密过的新实例生成随机字段密钥,并保存到秘密管理器:

```bash
node out/main.mjs --encrypt_enable
openssl rand -hex 32
```

<Callout type="warn">
**只要设置了加密密钥而没有显式关闭,加密就会被自动开启。**

判定逻辑是`ENCRYPT_ENABLE` 未设置时,回落到「是否设置了密钥」这一项。也就是说,你只填了密钥、没管开关,加密就已经生效了。如果暂时只想把密钥填进配置、暂不加密,必须显式写 `ENCRYPT_ENABLE=false`。
</Callout>
将结果配置为 `MX_ENCRYPT_KEY`(优先)或 `ENCRYPT_KEY`,同时在 app 和 mx-migrate 的共享环境中启用 `MX_ENCRYPT_ENABLE=true`。开启时必须显式提供 **64 位十六进制**密钥;长度不足、非 hex、仅提供开启开关都会在启动时失败。不要依赖 machine-id 回退,也不要使用示例公共密钥。

## 指定密钥
设置密钥且未指定开关时会自动开启加密。命令行 `--encrypt_key` / `--encrypt_enable` 仍可使用;升级工具使用环境变量,不读取业务进程的 CLI/YAML 参数。

可以通过 `--encrypt_key <key>` 指定,也可以用环境变量。环境变量有两名,`MX_ENCRYPT_KEY` 优先于 `ENCRYPT_KEY`:
## V2 写入与历史数据

```bash
# 推荐
export MX_ENCRYPT_KEY=<64 位密钥>
# 等价写法
export ENCRYPT_KEY=<64 位密钥>
```
`MX_SECRET_STORAGE_V2=true` 要求已开启字段加密,且 `JWT_SECRET` 至少 32 字符。它为配置、Snippet 和 Webhook 使用认证加密,为 API Key 使用哈希,为 OAuth access/refresh token 使用 Better Auth 加密。配置的密码字段由 schema 的 `encrypt` 元数据决定,数据库和 Redis 中均保留密文;空秘密 PATCH 保留旧值。

密钥的校验分两层,容易搞混:
V2 字段密文绑定字段路径;不能把密文直接搬到另一个字段。旧明文和 ECB 保留读取与迁移能力。`ENCRYPT_ALGORITHM` 默认 `aes-256-ecb`,供旧格式使用;V2 固定使用 AES-256-GCM。

| 情况 | 行为 |
|------|------|
| 提供的密钥**不是 64 位** | 启动即报错 |
| 提供的密钥**短于 64 位** | 不报错,服务端会用 `sha256(key + 'mx-encrypt-salt')` 派生并补齐到 64 位 |
| 提供的密钥**长于 64 位** | 启动即报错 |
| 提供的密钥**不是合法 hex 字符** | **启动时不校验**,直到真正加解密时才抛出 `Invalid key length` |
未发布的 v18 开发分支已删除 `ENCRYPT_ALGORITHM`、`--encrypt_algorithm` 和 YAML `encrypt_algorithm`,新写入固定 AES-256-GCM。旧项即使填默认值也会报错,应删除;历史读取固定为 AES-256-ECB。使用其他历史算法的实例须先在旧版本用原密钥和原算法转换为 V2 并验证,再升级;详见 [v18 准备说明](/docs/migrate/v18-preparation)。

推荐用 `openssl rand -hex 32` 生成,产出的正是 64 位小写 hex,一次通过全部校验:

```bash
openssl rand -hex 32
```
滚动部署时,先让全部副本具备新格式读取能力,再在全部副本启用 V2,最后回填旧数据。旧 `migrate:secrets` 完成记录仅代表该次凭据回填,不代表已完成未来 v18 的配置与指标准备。新准备工具尚未发布,见 [准备流程](/docs/migrate/v18-preparation)。

加密算法可用 `ENCRYPT_ALGORITHM` 调整,默认 `aes-256-ecb`。
## v17.0.4 临时明文逃生口

<Callout type="info">
不指定密钥时,服务端默认取机器的
[machine-id](https://www.npmjs.com/package/node-machine-id) 并派生。
</Callout>
只有关闭字段加密且 `MX_SECRET_STORAGE_V2=false` 时,普通配置写入可继续保留明文。它不适用于已有密文,也不适用于明确要求加密的授权功能。关闭开关不会解密旧数据;遇到已有密文仍须开启加密并提供原密钥。

<Callout type="warn">
关闭加密开关**不会**把已经写入的密文还原成明文。此前加密过的字段会继续以密文形式存储和返回,密文带有 `$${mx}$$` 标识前缀。如果你打算关闭加密,需要先准备好原密钥完成解密。
</Callout>
此逃生口仅适用于已发布的 v17.0.4。未发布的 v18 开发分支已经移除它,生产要求显式字段密钥、至少 32 字符的 JWT,并固定 V2 写入;旧关闭配置须删除。源码 dev 未配置字段密钥时可自动生成并保存在 `tmp/dev-encryption.key`,重启复用;开发库和该文件须一起保留,详见 [v18 开发环境说明](/docs/migrate/v18-preparation#源码开发环境)。

## 疑难解答
## 解密失败时

如果出现 `Invalid key length`,说明你的密钥**不是合法的 hex 字符串**(长度问题在启动阶段就会被拦下,不会走到这里)。请用 `openssl rand -hex 32` 重新生成一个 64 位小写 hex 字符串。
检查密钥来源、别名优先级、原 JWT 和密文所属路径。错误密钥、损坏密文或未知格式会失败,不会回落为明文成功。备份解封装成功也不表示归档内凭据能解密,详见 [备份与恢复](/docs/use/backup-restore)。
20 changes: 13 additions & 7 deletions content/docs/configure/environment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ description: 所有环境变量的完整说明
如果你不确定某个变量的作用,保持默认值即可。
</Callout>

以下默认行为固定已发布的 Core v17.0.4。未发布的 v18 开发分支已强制字段加密和 V2 写入,生产环境须提供显式字段密钥,源码 dev 可自动生成并持久保存本地密钥;JWT 至少 32 字符,旧关闭配置须删除,见 [v18 准备说明](/docs/migrate/v18-preparation)。

以下环境变量适用于 Mix Space Core 后端服务。Docker 用户在 `docker-compose.yml` 顶部的 `x-mx-env` 锚点中设置;源码用户在 `.env` 或 `ecosystem.config.cjs` 中设置。

<Callout type="warn">
Expand All @@ -17,8 +19,8 @@ description: 所有环境变量的完整说明

| 变量名 | 说明 | 默认值 | 示例 |
| --- | --- | --- | --- |
| `JWT_SECRET` | JWT 签名密钥。**运行时不做长度校验**,建议 16-32 个字符。未设置时 token 会在重启后全部失效 | - | `my-secret-key` |
| `JWTSECRET` | `JWT_SECRET` 的别名,两者都设置时以 `JWT_SECRET` 为准 | - | `my-secret-key` |
| `JWT_SECRET` | 登录与 OAuth token 加密秘密。启用 V2 时至少 32 字符;已有实例保留原值,短 JWT 用维护轮换处理 | - | `原实例秘密或新生成的随机值` |
| `JWTSECRET` | `JWT_SECRET` 的别名,两者都设置时以 `JWT_SECRET` 为准 | - | `原实例秘密或新生成的随机值` |
| `JWT_EXPIRE` | 登录令牌有效期(天) | `14` | `30` |
| `ALLOWED_ORIGINS` | 允许的跨域域名。**自建站必须显式设置**,否则你的域名不在白名单内 | 内置默认白名单(`innei.ren`、`localhost:*`、`*.dev`、`*.vercel.app` 等) | `example.com,www.example.com` |
| `SNOWFLAKE_WORKER_ID` | 工作节点 ID,取值 `0-1023` 的整数。**生产环境不设置会直接启动失败**,单实例填 `1` | - | `1` |
Expand All @@ -45,9 +47,9 @@ cluster 模式下可安全使用多实例:服务端会基于 `NODE_APP_INSTANC

| 变量名 | 说明 | 默认值 | 示例 |
| --- | --- | --- | --- |
| `REDIS_URL` | 完整连接串,**优先级最高**,会覆盖下面三项。支持 `redis://` / `rediss://`(`rediss` 自动启用 TLS),path 里的 `/0` 表示 db 序号 | - | `redis://:pass@host:6379/0` |
| `REDIS_CONNECTION` | `REDIS_URL` 的别名 | - | - |
| `REDIS_CONNECTION_STRING` | `REDIS_URL` 的别名,优先级最高 | - | - |
| `REDIS_URL` | 完整连接串,优先级低于 `REDIS_CONNECTION_STRING`、`REDIS_CONNECTION`。支持 `redis://` / `rediss://`(`rediss` 自动启用 TLS),path 里的 `/0` 表示 db 序号 | - | `redis://:pass@host:6379/0` |
| `REDIS_CONNECTION` | 完整连接串,优先于 `REDIS_URL` | - | - |
| `REDIS_CONNECTION_STRING` | 完整连接串,优先级最高 | - | - |
| `REDIS_HOST` | 地址 | `localhost` | `redis` |
| `REDIS_PORT` | 端口 | `6379` | `6379` |
| `REDIS_PASSWORD` | 密码 | - | `secret` |
Expand All @@ -57,16 +59,20 @@ cluster 模式下可安全使用多实例:服务端会基于 `NODE_APP_INSTANC
| 变量名 | 说明 | 默认值 | 示例 |
| --- | --- | --- | --- |
| `ENCRYPT_ENABLE` | 启用加密 | `false`(⚠️ 只要设置了密钥就等价于 `true`)| `true` |
| `ENCRYPT_KEY` | 加密密钥,必须正好 64 位 | 自动取 machine-id 派生 | `openssl rand -hex 32` |
| `ENCRYPT_KEY` | 开启时必须显式提供 64 位十六进制字段密钥 | 无可用默认密钥 | `openssl rand -hex 32` |
| `MX_ENCRYPT_KEY` | `ENCRYPT_KEY` 的别名,**优先于** `ENCRYPT_KEY` | 同上 | - |
| `MX_ENCRYPT_ENABLE` | `ENCRYPT_ENABLE` 的别名,**优先于** `ENCRYPT_ENABLE` | 同上 | - |
| `ENCRYPT_ALGORITHM` | 加密算法 | `aes-256-ecb` | `aes-256-cbc` |
| `ENCRYPT_ALGORITHM` | v17 旧格式算法;V2 固定 AES-256-GCM。v18 已删除此项,保留会报错 | `aes-256-ecb`(仅 v17) | `aes-256-ecb`(仅 v17) |
| `MX_SECRET_STORAGE_V2` | V2 凭据写入;要求启用加密、JWT 至少 32 字符,全部副本兼容后才启用 | `false` | `true` |
| `MX_MIGRATE_SECRETS` | 旧凭据回填的一次性迁移开关,不放入常驻 app 环境;不等于 v18 准备完成 | 未开启 | `1` |
| `ADMIN_UPDATE_S3_BASE_URL` | 后台更新包(`latest.json` + `admin-<version>.zip`)的下载源 | `https://admin-r2.innei.dev` | - |

<Callout type="warn">
`ENCRYPT_ENABLE` 的判定逻辑是「未设置时,回落到是否设置了密钥」。**只填密钥、不管开关,加密就已经开启。** 想暂不加密必须显式写 `ENCRYPT_ENABLE=false`。详见 [数据加密](/docs/configure/encryption)。
</Callout>

当前 v17.0.4 保留关闭加密的临时明文逃生口,不能读取已有密文或绕过明确要求加密的功能。未发布的 v18 开发分支已实现强制加密,新准备工具也尚未发布,见 [v18 准备说明](/docs/migrate/v18-preparation)。

## 缓存与限流

| 变量名 | 说明 | 默认值 | 示例 |
Expand Down
6 changes: 3 additions & 3 deletions content/docs/deploy/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ Agent 可以帮你执行命令行操作和调用 API,但涉及敏感信息(
你是 Mix Space 部署专家。请 SSH 到我的服务器(IP: ___,用户: root),
帮我完成以下操作:
1. 安装 Docker 和 Docker Compose
2. 下载 Mix Space 的 docker-compose.yml
3. 设置 JWT_SECRET 为随机字符串,ALLOWED_ORIGINS 为我的域名
4. 启动所有服务
2. 固定同一个已发布版本的 docker-compose.yml 和 app/mx-migrate 镜像,先阅读大版本迁移说明
3. 新装生成并保存至少 32 字符的 JWT;启用加密时保存 hex64 字段密钥,ALLOWED_ORIGINS 为我的域名;已有实例保留原秘密,短 JWT 需要维护轮换
4. 为 app/mx-migrate 配置相同环境,发布前迁移成功后再启动业务副本;v18 准备工具尚未发布,不能在 v17.0.4 上调用
5. 配置 Nginx 反向代理,将 ___ 域名指向 localhost:2333
6. 用 Certbot 申请 SSL 证书
7. 验证 https://___ 可以正常访问
Expand Down
24 changes: 14 additions & 10 deletions content/docs/deploy/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,19 @@ docker-compose -v
## 拉取编排文件 [step]

<Callout title="对于旧版本升级请注意!" type="warn">
自 v12 起 Core 已从 MongoDB 迁移到 PostgreSQL,编排文件中不再有 mongo 服务。当前版本(v14)的 `docker-compose.yml` 由 `app`、`mx-migrate`、`postgres`、`redis` 四个服务组成。
自 v12 起 Core 已从 MongoDB 迁移到 PostgreSQL,编排文件中不再有 mongo 服务。当前 v17 的 `docker-compose.yml` 由 `app`、`mx-migrate`、`postgres`、`redis` 四个服务组成。
</Callout>

```bash
cd && mkdir -p mx-space/core && cd $_

# 拉取 docker-compose.yml 文件(全球)
wget https://fastly.jsdelivr.net/gh/mx-space/core@master/docker-compose.yml
wget https://fastly.jsdelivr.net/gh/mx-space/core@v17.0.4/docker-compose.yml
# 拉取 docker-compose.yml 文件(Gh-Proxy 全球)
wget "https://gh-proxy.org/https://github.com/mx-space/core/blob/master/docker-compose.yml"
wget "https://gh-proxy.org/https://github.com/mx-space/core/blob/v17.0.4/docker-compose.yml"
```
本文固定已发布的 v17.0.4 编排。下载后把 app 和 mx-migrate 两处镜像都固定为 `innei/mx-server:17.0.4`,不要让一方使用 latest。未来准备工具尚未包含在该镜像,见 [v18 准备说明](/docs/migrate/v18-preparation)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the upgrade procedure to account for pinned image tags

For installations following this new version-pinning instruction, the Docker update procedure in content/docs/use/update.mdx:16–22 no longer upgrades Core: it only runs docker compose pull && docker compose up -d, leaving both services pinned to 17.0.4. Docker documents that pull fetches the image “associated with a service defined in a compose.yaml file,” not a newer release tag (command reference). Keep the explicit pins, but update the upgrade guide to require selecting a target release and changing both app and mx-migrate tags before pulling; otherwise users following the documented update flow remain on the old release.

Useful? React with 👍 / 👎.


## 配置环境变量 [step]

我们需要正确配置环境变量,打开获取到的`docker-compose.yml`文件。
Expand All @@ -71,15 +73,15 @@ x-mx-env: &mx-env
```

<Callout type="warn">
`SNOWFLAKE_WORKER_ID` 是必填项(`0-1023` 的整数),生产环境缺失会导致 mx-core 直接启动失败。官方文件里的默认值是 `'1'`。另外 `mx-migrate` 也会引用同一个 `*mx-env` 块并跑完整的 Nest 上下文,所以这里改了迁移步骤也会同步生效——**不要只给 `app` 服务单独加变量**。
`SNOWFLAKE_WORKER_ID` 是必填项(`0-1023` 的整数),生产环境缺失会导致 mx-core 直接启动失败。官方文件里的默认值是 `'1'`。另外 `mx-migrate` 也会引用同一个 `*mx-env` 块执行独立发布前迁移,所以这里改了迁移步骤也会同步生效——**不要只给 `app` 服务单独加变量**。
</Callout>

### 配置生成

在下方的组件中填入配置,点击复制后粘贴到 `docker-compose.yml` 的 `x-mx-env` 块中,覆盖 `JWT_SECRET` 和 `ALLOWED_ORIGINS` 的值,如果需要开启加密功能,请添加并将 `ENCRYPT_ENABLE` 的值改为 `true`,并填入 `ENCRYPT_KEY`。
在下方的组件中填入配置,点击复制后粘贴到 `docker-compose.yml` 的 `x-mx-env` 块中,覆盖 `JWT_SECRET` 和 `ALLOWED_ORIGINS` 的值,如果需要开启加密功能,请添加并将 `ENCRYPT_ENABLE` 的值改为 `true`,并填入原 `ENCRYPT_KEY`;从未加密的新实例才用 `openssl rand -hex 32` 生成并持久保存。复制内容是 YAML 映射,所有值按字符串引用,美元符号按 Compose 规则转义。

<Callout type="warn">
注意:只要设置了 `ENCRYPT_KEY`(或 `MX_ENCRYPT_KEY`)而没有显式设 `ENCRYPT_ENABLE=false`,加密就会被**隐式开启**。如果暂时只想把密钥填进去而暂不加密,必须显式写 `ENCRYPT_ENABLE: false`。
注意:只要设置了 `ENCRYPT_KEY`(或 `MX_ENCRYPT_KEY`)而没有显式设 `ENCRYPT_ENABLE=false`,加密就会被**隐式开启**。如果暂时只想把密钥填进去而暂不加密,必须显式写 `ENCRYPT_ENABLE: 'false'`。
</Callout>

<EnvVariableConfig
Expand All @@ -88,7 +90,7 @@ x-mx-env: &mx-env
variableNames={[
{
key: 'JWT_SECRET',
name: '[JWT 密钥] 长度 16-32 个字符',
name: '[JWT 密钥] 至少 32 字符,已有实例保留原值',
},
{
key: 'ALLOWED_ORIGINS',
Expand All @@ -100,7 +102,7 @@ x-mx-env: &mx-env
},
{
key: 'ENCRYPT_KEY',
name: '[加密密钥] 开启加密时必填',
name: '[加密密钥] 开启时必填:64 位十六进制',
},
]}
/>
Expand All @@ -112,7 +114,7 @@ x-mx-env: &mx-env
type={{
'JWT 密钥': {
description:
'用于签发用户 JWT,务必保存好不要泄露。运行时不做长度校验,但建议使用 16-32 个字符。未设置时 token 会在服务重启后全部失效。',
'用于签发用户 JWT,务必保存好不要泄露。V2 要求至少 32 字符;已有 JWT 不要直接更换,短值需要维护轮换。',
type: 'string',
},
'被允许的域名': {
Expand All @@ -127,12 +129,14 @@ x-mx-env: &mx-env
},
'加密密钥': {
description:
'开启加密后必填,必须正好 64 位,否则启动即报错。推荐用 openssl rand -hex 32 生成(产出 64 位小写 hex)。未提供时服务端会用 machine-id 派生。此操作不可逆,请谨慎。',
'开启加密后必填,必须正好 64 位,否则启动即报错。推荐用 openssl rand -hex 32 生成(产出 64 位小写 hex)。必须显式配置,不依赖 machine-id。已有实例保留原密钥,新实例随机生成并单独保存。',
type: 'string',
},
}}
/>

开启 V2 前确认全部副本支持新格式,JWT 至少 32 字符,再在共享块设置 `MX_SECRET_STORAGE_V2: 'true'`。历史实例还需要凭据回填,不能只切开关。

### 启动服务

```bash
Expand Down
Loading
Loading