Memos 如何用 /etc/secrets 部署配置保护 SMTP、S3 与 AI 密钥不落库?
【免费下载链接】memosOpen-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.项目地址: https://gitcode.com/GitHub_Trending/me/memos
当 Memos 的 SMTP 密码、S3 凭据和 AI API Key 通过 UI/API 配置时,它们会写进数据库的system_setting表。如果你的部署不想让这些密钥出现在数据库里,Memos 支持把这些设置改由挂载文件提供:Memos 在启动时扫描/etc/secrets目录,把匹配文件名模式的 JSON 文件校验后加载为进程内的不可变配置快照,密钥只存在于该目录的明文文件和进程内存中,不写入idp、system_setting或任何状态文件。这篇文章给出 SMTP(NOTIFICATION)、S3 附件存储(STORAGE)和 AI 提供商(AI)三类密钥文件的完整写法、启动校验规则,以及如何验证加载成功。
工作机制:文件如何变成生效配置
依据设计文档 docs/configuration-provisioning.md,Memos 的文件加载遵循以下规则:
- 只扫描
/etc/secrets的直接子文件,不递归子目录,也不创建、修改或删除该目录中的任何内容。目录中可以混放平台的其他密钥文件,无关文件名会被静默忽略。 - 文件名必须匹配小写 kebab-case 模式(
[a-z0-9]+(?:-[a-z0-9]+)*),扩展名为小写.json。两个受支持的模式:
| 文件名模式 | 消息类型 | 稳定键 |
|---|---|---|
memos-idp-<label>.json | memos.store.IdentityProvider | uid |
memos-instance-setting-<label>.json | memos.store.InstanceSetting | key |
- 每个匹配文件必须是一个合法的 protobuf JSON 消息、不含未知字段、不超过 1 MiB、只包含一个资源。这些约束在 store/deployment_config.go 中实现。
- 文件内容可以含明文密钥——该目录被整体视为敏感目录。
- 快照在启动时一次性构建:数据库迁移和 demo seed 之后、HTTP 与后台服务启动之前加载。任何一个匹配文件无效,快照不发布,启动直接失败。启动日志按资源类型输出匹配数量,但不打印文件内容。
- 文件是进程内配置的权威来源:文件声明的稳定键(
key或uid)遮蔽数据库中同键的行,UI 和 API 对文件背书的资源发起的创建、更新、删除会返回codes.FailedPrecondition。修改文件后必须重启进程才生效,第一版不支持文件监听。 - 删除文件并重启只是移除运行时覆盖:数据库中被遮蔽的行原样保留并重新生效,不会被删除或修改。
文件名的<label>只是描述性标签,资源身份由消息内部的key决定,重命名文件不影响生效配置。
准备 /etc/secrets 目录
把三类设置文件按建议的规范文件名放入/etc/secrets:
/etc/secrets/ ├── memos-instance-setting-notification.json ├── memos-instance-setting-storage.json └── memos-instance-setting-ai.json建议文件名与资源键一一对应(如NOTIFICATION对应memos-instance-setting-notification.json),便于排查。目录本身可以不存在——缺失的目录或没有匹配文件的目录是正常 no-op。注意:目录和文件必须对 Memos 进程可读;文档建议对该目录使用 owner-only 或应用组可读的文件系统权限,因为文件是敏感明文。
一个关键约束:同一设置键只允许一个文件声明(例如不能有两个key都是STORAGE的文件),重复会直接导致启动失败。
编写 NOTIFICATION 文件:SMTP 密钥不落库
memos-instance-setting-notification.json包含一个key为NOTIFICATION的memos.store.InstanceSetting,其中的smtpPassword只存在于文件中,不会写入system_setting。下面的值全部来自 docs/configuration-provisioning.md 的文档示例,实际部署时替换为你自己的 SMTP 信息:
{ "key": "NOTIFICATION", "notificationSetting": { "email": { "enabled": true, "smtpHost": "smtp.example.com", "smtpPort": 587, "smtpUsername": "memos", "smtpPassword": "smtp-secret", "fromEmail": "memos@example.com", "fromName": "Memos", "replyTo": "support@example.com", "useTls": true, "useSsl": false } } }字段含义与 proto/store/instance_setting.proto 中的EmailSetting一一对应:smtpHost/smtpPort指定 SMTP 服务器,smtpUsername/smtpPassword是凭据,fromEmail/fromName/replyTo控制发件人,useTls/useSsl选择加密方式。
启动校验规则(由 store/deployment_config.go 的validateAndNormalizeDeploymentInstanceSetting执行):
key为NOTIFICATION时notificationSetting必须已填充,否则报notificationSetting must be populated for key NOTIFICATION。email.enabled为 true 时,smtpHost、正的smtpPort和fromEmail缺一不可,否则报enabled notification email requires smtpHost, a positive smtpPort, and fromEmail。useTls与useSsl不能同时为 true,否则报notification email cannot enable both useTls and useSsl。
编写 STORAGE 文件:S3 凭据不落库
memos-instance-setting-storage.json承载附件存储配置。Memos 的存储模型(见 proto/store/instance_setting.proto)中,storages列表存放存储实例,defaultStorageId指定新附件写入哪一个;STORAGE组的默认读取行为是:存储类型、上传上限(30 MiB)、路径模板在对应字段未指定时回落到本地存储默认值。
S3 的最小生效写法(值以文档已有示例为模板,access-key-id等为占位说明,替换为你自己的凭据):
{ "key": "STORAGE", "storageSetting": { "storages": [ { "id": "s3-primary", "name": "Primary S3", "type": "STORAGE_TYPE_S3", "s3Config": { "accessKeyId": "access-key-id", "accessKeySecret": "access-key-secret", "endpoint": "s3.example.com", "region": "us-east-1", "bucket": "memos" } } ], "defaultStorageId": "s3-primary" } }StorageS3Config的可用字段为accessKeyId、accessKeySecret、endpoint、region、bucket、usePathStyle、insecureSkipTlsVerify。其中insecureSkipTlsVerify会禁用对 S3 端点的 TLS 证书校验,仅在内网使用自签名证书的可信端点才应开启——proto 注释明确说明它会移除对中间人攻击的保护。
启动校验会拒绝以下配置:
- 默认存储是 S3 但缺
accessKeyId、accessKeySecret、endpoint、region、bucket中任何一项,报错形如storageSetting default S3 config.<field> is required。 - 声明了 S3 但
s3Config与storages均为空,报错storageSetting.s3Config is required for S3(注释说明这是为了大声失败而不是静默自愈成 LOCAL)。 uploadSizeLimitMb为负数。
编写 AI 文件:API Key 不落库
memos-instance-setting-ai.json承载key为AI的设置。AI 部署配置走的是确定性的自包含归一化,而不是 UI 更新路径,规则是:
- 每个 provider 必须显式给出稳定的
id(loader 从不自动生成)、title、受支持的type(OPENAI或GEMINI)和非空apiKey,缺一即报错。 - 空端点会被归一化:OpenAI 为
https://api.openai.com/v1,Gemini 为https://generativelanguage.googleapis.com/v1beta。 - provider
id重复会被拒绝;transcription.providerId必须引用同一配置中存在的 provider。 - 任何 provider、API Key 或转写值都不会从被遮蔽的数据库设置中复制——文件是完整替换。
示例(模型名取自 proto 注释中的官方示例,api-key为占位说明,替换为你自己的密钥):
{ "key": "AI", "aiSetting": { "providers": [ { "id": "openai-main", "title": "OpenAI", "type": "OPENAI", "apiKey": "api-key" }, { "id": "gemini-main", "title": "Gemini", "type": "GEMINI", "apiKey": "api-key" } ], "transcription": { "providerId": "openai-main", "model": "gpt-4o-transcribe", "language": "en" } } }transcription.model可留空(回落到引擎默认);OpenAI 侧文档给出的示例有whisper-1、gpt-4o-transcribe、gpt-4o-mini-transcribe,Gemini 侧有gemini-2.5-flash(默认)、gemini-2.5-pro。transcription整个省略或providerId为空时,语音转写功能保持禁用。
启动并验证加载结果
三类文件都放好(或只放你需要的部分)后,正常启动 Memos。配置在数据库迁移和 demo seed 之后、服务对外提供之前加载,验证分三步:
启动日志确认加载数量。加载成功后会输出:
INFO loaded deployment configuration identityProviders=<n> instanceSettings=<n>日志带的是匹配数量而不是文件内容,用来确认文件名拼写正确——一个拼错的文件名不会静默生效,只会使数量对不上。
失败时看启动报错。无效文件会让启动直接失败,错误信息定位到具体文件名和字段,例如
invalid instance setting deployment file "memos-instance-setting-storage.json",或字段级错误如config.oauth2Config.scopes[0] must not be empty。未知字段(拼错的字段名)会报unknown field "..."——这是刻意保留的行为,用于拦截为更新版本写的配置。运行期确认 UI/API 改不动。在管理界面尝试修改被文件覆盖的
STORAGE/NOTIFICATION/AI分组,或经 API 更新同一资源,都会收到FailedPrecondition。这说明生效值确实来自文件而不是数据库;同时 API 响应继续对 client secret、SMTP 密码、S3 secret 和 AI API Key 做脱敏,密钥不会从任何接口回显。
生效规则、边界与清理
几个直接影响运维行为的规定:
- 整组替换,不做字段合并。一个设置文件替换整个生效分组,protobuf JSON 中省略的标量按 protobuf 默认值解码——省略不等于"保留数据库里的旧值"。同理,文件中的空字符串密钥就是空值,UI 更新那种"空串保留原凭据"的语义不适用于部署配置。
- 修改文件必须重启。快照只加载一次,运行中改动文件无效果;Kubernetes Secret 卷这类平台管理的符号链接是受支持的文件形态。
- 多副本要求挂载同一份文件。每个副本独立在启动时加载,同一部署的所有副本必须挂载相同的文件;改动认证或存储配置时使用不会把流量路由到不同文件代际副本的发布策略。
BASIC和TAGS键被拒绝。BASIC包含实例密钥与数据库 schema 版本,文件覆盖会破坏会话与迁移状态;TAGS保留作兼容,活动标签元数据按用户存储。- SSO-only 部署需要两个文件。同时挂载身份提供方文件和
memos-instance-setting-general.json(disallowPasswordAuth为 true)才能启用纯 SSO 行为——文件侧的GENERAL声明若禁用普通用户密码登录而没有任何生效的 IdP,启动会直接报错。 - 老版本数据库写入 bootstrap 的遗留清理。早期版本曾把
memos-idp-*.json中的 IdP(含 client secret)拷进idp表,新 loader 不会自动清理这些行。当文件遮蔽了同 UID 的存储 provider 时,启动会打出不含密钥的告警deployment identity provider shadows a stored provider; the stored configuration remains in the database。文档给出的清理顺序是:备份数据库并保留管理员密码 → 临时移除 IdP 文件并重启使存储 provider 不再被遮蔽 → 经管理员 UI/API 删除或更新该存储 provider(或做等价的离线数据库维护)→ 恢复文件并重启。在完成清理前,旧的存储 provider 和密钥仍在数据库里,文件被移除时会重新出现。 - 不落库保证的适用范围。该保证针对新 loader:文件内容不写入数据库。它不声称抹除早期版本已写入数据库的密钥。
如果三类文件加载数量与预期一致、UI/API 对覆盖分组返回FailedPrecondition,SMTP、S3 与 AI 密钥就只存在于/etc/secrets的挂载文件中——此后密钥的变更路径是改文件加重启,而不是数据库更新。
【免费下载链接】memosOpen-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.项目地址: https://gitcode.com/GitHub_Trending/me/memos
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考