Capítulo 13b Clientes, groupware & CLI Admin: #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.

Consejo operativo y nota de arquitectura

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.

Referencia / configuración
# 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:

Referencia / configuración
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.

Referencia / configuración
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.

Referencia / configuración
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.

Referencia / configuración
# .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.

Referencia / configuración
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.