#pConfigFile
配置即代码:导出、计划、应用与 Git 流水线
整个服务器状态以 YAML 文件形式保存在 Git 中:租户、域(DKIM 选择器、MTA-STS、路由)、含别名、转发和自动回复的邮箱、通讯组列表、共享邮箱、会议室、全局地址列表和资源规则、共享和委派、SMTP、智能主机、路由、传输规则、垃圾邮件和病毒防护、防火墙和自动封禁、TLS、身份源、API 密钥(仅元数据)、Webhook、通知、运维告警、POP3、文件、预约、归档以及 DNS 故障转移。nextxch-cli config plan 会像 terraform plan 一样显示将要创建、更改和删除的内容;apply 会先检查全部内容,再按固定顺序应用。管理:系统 > 以文件形式配置。
机密信息绝不写入文件,也绝不导出:密码、令牌和密钥写作 secretRef: {env: NAME} 或 secretRef: {file: path},并在应用时解析(在 CI 运行器上由 nextxch-cli 解析;在服务器上仅限 NEXTXCH_SECRET_* 变量以及 <data>/secrets/ 下的文件)。DKIM 密钥、证书和密码哈希保留在服务器上。与 API v1 一样属于 Business 功能;JSON Schema 免费。
1. 文件格式:apiVersion、kind、metadata.name、spec
一个文件包含多个文档(以 --- 分隔),或一个文件夹中每个领域一个文件(config export --split)。文件中缺少的字段在服务器上保持不变;列表(别名、成员)作为整体计算。
# yaml-language-server: $schema=https://nextxch.com/schemas/nextxch-config-v1.schema.json
apiVersion: nextxch/v1
kind: Manifest # optional: areas in which --prune may delete
metadata: {name: nextxch}
spec: {areas: [domains, accounts, sharing, transport]}
---
apiVersion: nextxch/v1
kind: Domain
metadata: {name: company.com}
spec:
tenant: default
catchAll: info@company.com
dkim: {mode: dual, rsaSelector: s2026, ed25519Selector: ed2026} # keys stay on the server
mtaSts: {mode: enforce, maxAgeSeconds: 604800, mxHosts: [mail.company.com]}
---
apiVersion: nextxch/v1
kind: Account
metadata: {name: anna.smith@company.com}
spec:
displayName: Anna Smith
role: User # User, TenantAdmin, ServerAdmin, ComplianceAuditor
quotaMb: 10240
aliases: [anna@company.com]
forwarding: {address: anna@private.example, keepCopy: true}
outOfOffice: {enabled: false}
password: {secretRef: {env: ANNA_PASSWORD}} # never the password itself
---
apiVersion: nextxch/v1
kind: Account
metadata: {name: info@company.com}
spec: {type: shared, displayName: Info mailbox} # user, shared, room, equipment
---
apiVersion: nextxch/v1
kind: Share
metadata: {name: "info@company.com/mailbox -> anna.smith@company.com"}
spec: {permissions: [read, write, sendAs]}
---
apiVersion: nextxch/v1
kind: SmtpSettings
metadata: {name: server} # server-wide settings are always named "server"
spec:
outboundSmartHost: smtp.provider.example
outboundSmartPort: 587
smartHostPassword: {secretRef: {file: secrets/smarthost.txt}}
2. 使用 nextxch-cli 的工作流
先导出并提交,然后通过拉取请求进行更改。计划输出示例:
export NEXTXCH_URL=https://mail.company.com
export NEXTXCH_API_KEY=smk_... # key with config.read or config.write
nextxch-cli config export --split nextxch/
nextxch-cli config validate -f nextxch/ # offline: kinds and fields
nextxch-cli config plan -f nextxch/
~ Domain company.com
mtaSts.mode: "testing" -> "enforce"
~ Account anna.smith@company.com
quotaMb: 10240 -> 20480
aliases: ["anna@company.com"] -> ["a.smith@company.com","anna@company.com"]
~ password: (secret changed)
+ Account info@company.com
type: "shared"
displayName: "Info mailbox"
Not in the file (1), deleted only with --prune:
Account old@company.com
Plan: 1 to create, 2 to update, 0 to delete, 41 unchanged.
# exit code 2 = changes
nextxch-cli config apply -f nextxch/ --prune --yes
nextxch-cli config history
3. GitHub Actions:拉取请求时执行 plan,合并时执行 apply
包含注释步骤的完整文件:仓库中的 deploy/gitops/github-actions.yml。API 密钥来自仓库机密,绝不来自该文件。
name: nextxch config
on:
pull_request: {paths: ["nextxch/**"]}
push: {branches: [main], paths: ["nextxch/**"]}
permissions: {contents: read, pull-requests: write}
concurrency: {group: nextxch-config, cancel-in-progress: false}
env: {NEXTXCH_IMAGE: "nextxch/server:latest"}
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Plan
env:
NEXTXCH_URL: ${{ vars.NEXTXCH_URL }}
NEXTXCH_API_KEY: ${{ secrets.NEXTXCH_CONFIG_READ_KEY }}
run: |
set +e
docker run --rm -e NEXTXCH_URL -e NEXTXCH_API_KEY -e NO_COLOR=1 -v "$PWD:/work" -w /work \
--entrypoint nextxch-cli "$NEXTXCH_IMAGE" config plan -f nextxch/ --prune > plan.txt 2>&1
code=$?; cat plan.txt; [ "$code" -ne 1 ]
# danach: actions/github-script postet plan.txt als Kommentar (siehe deploy/gitops)
apply:
if: github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- env:
NEXTXCH_URL: ${{ vars.NEXTXCH_URL }}
NEXTXCH_API_KEY: ${{ secrets.NEXTXCH_CONFIG_WRITE_KEY }}
SMARTHOST_PASSWORD: ${{ secrets.SMARTHOST_PASSWORD }}
run: |
docker run --rm -e NEXTXCH_URL -e NEXTXCH_API_KEY -e SMARTHOST_PASSWORD -v "$PWD:/work" -w /work \
--entrypoint nextxch-cli "$NEXTXCH_IMAGE" config apply -f nextxch/ --prune --yes
4. GitLab CI:以合并请求备注形式输出 plan,在默认分支上执行 apply
完整文件:deploy/gitops/gitlab-ci.yml。密钥为已屏蔽、受保护的 CI/CD 变量;resource_group 可防止两次 apply 同时运行。
stages: [validate, plan, apply]
variables: {CONFIG_DIR: nextxch, NO_COLOR: "1"}
.nextxch-cli:
image: {name: "nextxch/server:latest", entrypoint: [""]}
validate:
extends: .nextxch-cli
stage: validate
script: [nextxch-cli config validate -f "$CONFIG_DIR"]
plan:
extends: .nextxch-cli
stage: plan
rules: [{if: '$CI_PIPELINE_SOURCE == "merge_request_event"'}]
variables: {NEXTXCH_API_KEY: $NEXTXCH_CONFIG_READ_KEY}
script:
- set +e
- nextxch-cli config plan -f "$CONFIG_DIR" --prune > plan.txt 2>&1; code=$?
- cat plan.txt; test "$code" -ne 1
artifacts: {when: always, paths: [plan.txt]}
# plan-comment: curl auf /merge_requests/:iid/notes mit plan.txt (siehe deploy/gitops)
apply:
extends: .nextxch-cli
stage: apply
resource_group: nextxch-config
environment: production
rules: [{if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'}]
variables: {NEXTXCH_API_KEY: $NEXTXCH_CONFIG_WRITE_KEY}
script: [nextxch-cli config apply -f "$CONFIG_DIR" --prune --yes]
5. 编辑器中的 JSON Schema 与 REST 调用
VS Code(Red Hat "YAML" 扩展)和 JetBrains IDE 会在输入时检查文件。每台服务器还在 /api/v1/config/schema 提供该架构。
# .vscode/settings.json
{
"yaml.schemas": {
"https://nextxch.com/schemas/nextxch-config-v1.schema.json": ["nextxch/*.yaml"]
}
}
# Plan ohne CLI, direkt per REST (YAML als Body):
curl -s -X POST "$NEXTXCH_URL/api/v1/config/plan?prune=true" \
-H "Authorization: Bearer $NEXTXCH_API_KEY" \
-H "Content-Type: application/yaml" --data-binary @nextxch.yaml
6. 列表中的密钥与无需重启的 ACME
带认证头和来自流水线的 HMAC 密钥的 MTA 钩子、带 Webhook 密钥的告警目标,以及切换到使用 DNS-01 的 Let's Encrypt:一次应用,无需重启。
apiVersion: nextxch/v1
kind: MtaFilters
metadata: {name: server}
spec:
hooks:
- name: spamcheck
url: https://hooks.company.com/check
stages: [data]
authHeaderName: Authorization
authHeaderValue: {secretRef: {env: HOOK_TOKEN}} # in the plan: hooks[spamcheck].authHeaderValue
hmacSecret: {secretRef: {file: secrets/hook-hmac.txt}}
---
apiVersion: nextxch/v1
kind: OpsAlerts
metadata: {name: server}
spec:
targets:
- name: pager
type: webhook
address: https://pager.company.com/hook
secret: {secretRef: {env: PAGER_SECRET}}
---
apiVersion: nextxch/v1
kind: TlsSettings
metadata: {name: server}
spec:
acme:
enabled: true
directoryUrl: https://acme-v02.api.letsencrypt.org/directory
contactEmail: postmaster@company.com
challenge: Dns01
renewBeforeExpiryDays: 30
设置、默认值与最佳实践
| 设置 / 位置 | 说明 | 默认值 | 生产环境最佳实践 |
|---|---|---|---|
config export [--area a,b] [--split ordner/] [-o datei] |
以确定性方式导出服务器状态:固定的文档顺序、排序后的列表、无时间戳。两次导出得到完全相同的字节,Git diff 只显示真实的更改。 | alle Bereiche |
对代码仓库使用 --split(每个领域一个文件:domains.yaml、accounts.yaml ...);小型拉取请求依然易于阅读。 |
config plan -f datei|ordner [--prune] |
按对象显示 + 创建、~ 更新(字段:旧值 -> 新值)和 - 删除。退出代码 0 = 无变更,2 = 有变更,1 = 错误。机密信息仅显示为 "(secret changed)"。 | ohne Löschen |
在拉取请求上运行,并将结果作为评论发布(示例见下文)。 |
config apply -f ... [--prune] [--yes] [--force] [--dry-run] |
先检查所有内容(字段、引用、机密、安全规则),然后按固定顺序创建和更新(租户、域、账户、列表、规则、共享、传输……),删除则按相反顺序进行。如果某一步失败,已完成的步骤会被撤销。每项更改都会写入审计日志。 | fragt nach |
在流水线中请始终传入 --yes;否则在没有终端时 apply 会拒绝执行。--dry-run 等同于 plan。 |
--prune und Manifest |
删除仅在使用 --prune 时发生,且仅限 Manifest 文档所列的范围(没有 Manifest 时:仅限文件中出现的类型)。服务器设置永远不会被删除。 | aus |
导出会写入 Manifest;如果您只管理部分内容,请缩小范围(areas)。 |
Schutz: letzter Admin, Domain mit Postfächern |
如果计划会导致没有任何活动的服务器管理员,或要删除仍有邮箱的域,则会被拒绝。--force 可强制执行(此时该域会被停用而不是删除)。API 密钥不能吊销自身。 | aktiv |
切勿在流水线中硬编码 --force。 |
secretRef: {env: NAME} | {file: pfad} |
引用机密而非其值。nextxch-cli 在运行器上解析它(任意变量,或相对于配置的文件)。服务器自身只读取 NEXTXCH_SECRET_* 以及 <data>/secrets/ 下的文件。再次应用相同的值不算作更改。 | - |
将机密从 GitHub/GitLab 机密存储以环境变量形式传递给作业。 |
Scopes config.read / config.write |
REST:GET /api/v1/config/export、POST /api/v1/config/plan、POST /api/v1/config/apply、GET /api/v1/config/history、GET /api/v1/config/schema(免费)。密钥:租户集成密钥;仅当其创建者仍是管理员时才有效。服务器管理员的密钥可查看整个服务器,租户管理员的密钥仅能查看该租户。config.read 足以用于导出和规划,应用则需要 config.write。 | - |
两个密钥:config.read 用于拉取请求中的 plan,config.write 仅用于 main 分支上的 apply 作业,并尽可能限制为 Runner 的 IP。 |
Mandanten-Admins |
租户管理员(会话或其某个密钥)只能导出、规划和应用其租户内的对象:Domain(仅修改,不可创建或删除)、Account、DistributionList、Share、GalVisibilityRule、ResourceAccessRule 和 Webhook。服务器设置、租户、身份源和 API 密钥仍由服务器管理员管理;角色为 ServerAdmin 或 ComplianceAuditor 的账户不可操作。--prune 仅在自己的租户内删除,history 仅显示自己的应用记录。 | - |
适用于经销商和部门:每个租户使用自己的仓库和自己的密钥。"tenant" 字段可以省略,系统会自动填入自己的租户。 |
secretRef in Listen |
列表条目同样通过 secretRef 获取密钥:MtaFilters hooks[].authHeaderValue 和 hooks[].hmacSecret、OpsAlerts targets[].secret(Webhook 目标的 HMAC)。计划通过条目的 id 或名称来指代它,例如 hooks[spamcheck].authHeaderValue;列表中的明文值会被拒绝。 | - |
没有 id 的条目会在首次应用时获得一个 id;导出后请在文件中保留这些 id,这样在列表重新排序时,密钥仍会保留在正确的条目上。 |
ACME zur Laufzeit |
TlsSettings.acme(enabled、directoryUrl、contactEmail、质询 Http01/TlsAlpn01/Dns01、renewBeforeExpiryDays、reusePrivateKeyForDane)无需重启即可生效:appsettings.json 仍为基础配置,差异保存在 <data>/acme-settings.json 中,并应用于整个集群。更改目录 URL 会在下一次订单时创建新的 ACME 账户。 | appsettings.json |
请先使用 Let's Encrypt 测试(staging)目录进行测试,然后再切换到生产环境。 |
Arten (kinds) |
Tenant、Domain、Account、DistributionList、GalVisibilityRule、ResourceAccessRule、Share、SmtpSettings、Route、TransportRules、SpamFilter、MtaFilters、FirewallRule、TlsSettings、IdentitySource、ApiKey、Webhook、Notifications、OpsAlerts、DnsFailover、Pop3、FileStorage、Booking、ArchivePolicy。所有字段:JSON Schema。 | - |
API 密钥不会从文件创建(其密钥只存在一次);文件可更改标签和权限范围,并通过 --prune 吊销。会议室和预订设置在每个集群节点上保持一致。 |