#pConfigFile
Configuración como código: exportar, planificar, aplicar y pipeline de Git
Todo el estado del servidor como archivos YAML en Git: inquilinos, dominios (selectores DKIM, MTA-STS, enrutamiento), buzones con alias, reenvío y fuera de la oficina, listas de distribución, buzones compartidos, salas, GAL y reglas de recursos, usos compartidos y delegación, SMTP, smart host, rutas, reglas de transporte, protección antispam y antivirus, firewall y bloqueo automático, TLS, fuentes de identidad, claves API (solo metadatos), webhooks, notificaciones, alertas de operaciones, POP3, archivos, reservas, archivo y failover de DNS. nextxch-cli config plan muestra, como terraform plan, qué se crearía, modificaría y eliminaría; apply lo comprueba todo primero y lo aplica en un orden fijo. Administración: Sistema > Configuración como archivo.
Los secretos nunca se incluyen en el archivo ni se exportan: las contraseñas, los tokens y las claves se escriben como secretRef: {env: NAME} o secretRef: {file: path} y se resuelven en el momento de aplicar (por nextxch-cli en el runner de CI; en el servidor, solo variables NEXTXCH_SECRET_* y archivos bajo <data>/secrets/). Las claves DKIM, los certificados y los hashes de contraseñas permanecen en el servidor. Función de Business, como la API v1; el JSON Schema es gratuito.
1. Formato de archivo: apiVersion, kind, metadata.name, spec
Un archivo con varios documentos (separados por ---) o una carpeta con un archivo por área (config export --split). Los campos que faltan en el archivo se mantienen tal como están en el servidor; las listas (alias, miembros) cuentan como un todo.
# 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. Flujo de trabajo con nextxch-cli
Primero exporte y confirme en el repositorio; después, realice los cambios mediante pull requests. Ejemplo de salida de plan:
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 en el pull request, apply en el merge
Archivo completo con el paso de comentario: deploy/gitops/github-actions.yml en el repositorio. La clave de API procede de los secretos del repositorio, nunca del archivo.
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 como nota de la merge request, apply en la rama predeterminada
Archivo completo: deploy/gitops/gitlab-ci.yml. Las claves son variables de CI/CD enmascaradas y protegidas; resource_group impide dos apply simultáneos.
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 en el editor y llamada REST
VS Code (extensión "YAML" de Red Hat) y los IDE de JetBrains comprueban los archivos mientras se escribe. Cada servidor también sirve el esquema en /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. Secretos en listas y ACME sin reinicio
Hooks de MTA con encabezado de autenticación y secreto HMAC desde la canalización, un destino de alertas con secreto de webhook y el cambio a Let's Encrypt con DNS-01: una sola aplicación, sin reinicio.
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
Configuración, valores predeterminados y buenas prácticas
| Ajuste / ubicación | Descripción | Valor predeterminado | Buena práctica de producción |
|---|---|---|---|
config export [--area a,b] [--split ordner/] [-o datei] |
Exporta el estado del servidor de forma determinista: orden fijo de documentos, listas ordenadas, sin marcas de tiempo. Dos exportaciones dan exactamente los mismos bytes y los diffs de Git muestran solo cambios reales. | alle Bereiche |
Utilice --split para los repositorios (un archivo por área: domains.yaml, accounts.yaml ...); los pull requests pequeños siguen siendo fáciles de leer. |
config plan -f datei|ordner [--prune] |
Muestra por objeto + crear, ~ actualizar (campo: antiguo -> nuevo) y - eliminar. Código de salida 0 = sin cambios, 2 = cambios, 1 = error. Los secretos solo aparecen como "(secreto modificado)". | ohne Löschen |
Ejecútelo en las pull requests y publíquelo como comentario (ejemplos a continuación). |
config apply -f ... [--prune] [--yes] [--force] [--dry-run] |
Primero lo comprueba todo (campos, referencias, secretos, reglas de seguridad), después crea y actualiza en un orden fijo (inquilinos, dominios, cuentas, listas, reglas, usos compartidos, transporte ...) y elimina en orden inverso. Si un paso falla, se deshacen los pasos ya realizados. Cada cambio se registra en el registro de auditoría. | fragt nach |
Pase siempre --yes en los pipelines; sin un terminal, apply se niega a ejecutarse en caso contrario. --dry-run equivale a plan. |
--prune und Manifest |
Las eliminaciones solo se producen con --prune y únicamente en las áreas que nombra el documento Manifest (sin Manifest: solo los tipos presentes en el archivo). La configuración del servidor nunca se elimina. | aus |
La exportación escribe el Manifest; si solo gestiona partes, limite las áreas. |
Schutz: letzter Admin, Domain mit Postfächern |
Se rechazan un plan que dejaría sin ningún administrador de servidor activo y la eliminación de un dominio que todavía tiene buzones. --force lo anula (el dominio se desactiva en lugar de eliminarse). La propia clave de API no puede revocarse a sí misma. | aktiv |
No fije nunca --force en una pipeline. |
secretRef: {env: NAME} | {file: pfad} |
Referencia a un secreto en lugar de su valor. nextxch-cli lo resuelve en el runner (cualquier variable, archivo relativo a la configuración). El propio servidor solo lee NEXTXCH_SECRET_* y archivos bajo <data>/secrets/. Volver a aplicar el mismo valor no supone ningún cambio. | - |
Pase los secretos del almacén de secretos de GitHub/GitLab al job como variables de entorno. |
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 (gratuito). Claves: claves de integración de inquilino; solo funcionan mientras su creador siga siendo administrador. Las claves de un administrador del servidor ven todo el servidor; las de un administrador de inquilino, solo ese inquilino. config.read basta para exportar y planificar; aplicar requiere config.write. | - |
Dos claves: config.read para los planes en pull requests, config.write solo para el job de apply en main, restringidas a las IP de los runners si es posible. |
Mandanten-Admins |
Un administrador de inquilino (sesión o una de sus claves) exporta, planifica y aplica solo objetos de su inquilino: Domain (solo cambios, sin crear ni eliminar), Account, DistributionList, Share, GalVisibilityRule, ResourceAccessRule y Webhook. La configuración del servidor, los inquilinos, las fuentes de identidad y las claves de API siguen en manos del administrador del servidor; las cuentas con el rol ServerAdmin o ComplianceAuditor quedan excluidas. --prune solo elimina dentro del propio inquilino, y history muestra solo las aplicaciones propias. | - |
Para revendedores y departamentos: cada inquilino mantiene su propio repositorio con su propia clave. El campo "tenant" puede omitirse; se rellena con el propio inquilino. |
secretRef in Listen |
Las entradas de listas también reciben secretos mediante secretRef: MtaFilters hooks[].authHeaderValue y hooks[].hmacSecret, OpsAlerts targets[].secret (HMAC del destino de webhook). El plan identifica la entrada por su id o su nombre, por ejemplo hooks[spamcheck].authHeaderValue; un valor en texto plano en la lista se rechaza. | - |
Las entradas sin id reciben uno en la primera aplicación; tras una exportación, conserve los id en el archivo, así los secretos permanecen con la entrada correcta cuando se reordena la lista. |
ACME zur Laufzeit |
TlsSettings.acme (enabled, directoryUrl, contactEmail, desafío Http01/TlsAlpn01/Dns01, renewBeforeExpiryDays, reusePrivateKeyForDane) se aplica sin reinicio: appsettings.json sigue siendo la base, las diferencias se guardan en <data>/acme-settings.json y se aplican a todo el clúster. Cambiar la URL del directorio crea una nueva cuenta ACME en el siguiente pedido. | appsettings.json |
Pruebe primero con el directorio de staging de Let's Encrypt y luego cambie a producción. |
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. Todos los campos: JSON Schema. | - |
Las claves de API no se crean desde archivos (su secreto existe solo una vez); un archivo cambia la etiqueta y los ámbitos, y revoca con --prune. Los ajustes de salas y reservas son iguales en todos los nodos del clúster. |