Chapitre 13b Clients, groupware & CLI Admin : #pConfigFile

Configuration as code : export, plan, apply et pipeline Git

L'état complet du serveur sous forme de fichiers YAML dans Git : locataires, domaines (sélecteurs DKIM, MTA-STS, routage), boîtes aux lettres avec alias, transfert et absence, listes de distribution, boîtes aux lettres partagées, salles, règles GAL et de ressources, partages et délégation, SMTP, smart host, routes, règles de transport, protection antispam et antivirus, pare-feu et auto-ban, TLS, sources d'identité, clés API (métadonnées uniquement), webhooks, notifications, alertes d'exploitation, POP3, fichiers, réservation, archive et basculement DNS. nextxch-cli config plan affiche, comme terraform plan, ce qui serait créé, modifié et supprimé ; apply vérifie d'abord l'ensemble puis l'applique dans un ordre fixe. Admin : Système > Configuration sous forme de fichier.

Conseil d'exploitation & note d'architecture

Les secrets n'entrent jamais dans le fichier et ne sont jamais exportés : mots de passe, jetons et clés s'écrivent secretRef: {env: NAME} ou secretRef: {file: path} et sont résolus au moment de l'application (par nextxch-cli sur le runner CI ; sur le serveur, uniquement les variables NEXTXCH_SECRET_* et les fichiers sous <data>/secrets/). Les clés DKIM, certificats et hachages de mots de passe restent sur le serveur. Fonctionnalité Business comme l'API v1 ; le JSON Schema est gratuit.

1. Format de fichier : apiVersion, kind, metadata.name, spec

Un fichier contenant plusieurs documents (séparés par ---) ou un dossier avec un fichier par domaine (config export --split). Les champs absents du fichier restent inchangés sur le serveur ; les listes (alias, membres) sont prises en compte dans leur ensemble.

Référence / configuration
# 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. Workflow avec nextxch-cli

Exportez et commitez d'abord, puis modifiez via des pull requests. Exemple de sortie de plan :

Référence / configuration
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 sur pull request, apply au merge

Fichier complet avec l'étape de commentaire : deploy/gitops/github-actions.yml dans le dépôt. La clé API provient des secrets du dépôt, jamais du fichier.

Référence / configuration
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 en note de merge request, apply sur la branche par défaut

Fichier complet : deploy/gitops/gitlab-ci.yml. Les clés sont des variables CI/CD masquées et protégées ; resource_group empêche deux applications simultanées.

Référence / configuration
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 dans l'éditeur et appel REST

VS Code (extension Red Hat "YAML") et les IDE JetBrains vérifient les fichiers pendant la saisie. Chaque serveur fournit également le schéma sous /api/v1/config/schema.

Référence / configuration
# .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. Secrets dans les listes et ACME sans redémarrage

Hooks MTA avec en-tête d'authentification et secret HMAC issus du pipeline, une cible d'alerte avec secret de webhook et le passage à Let's Encrypt avec DNS-01 : une seule application, sans redémarrage.

Référence / configuration
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

Paramètres, valeurs par défaut & bonnes pratiques

Paramètre / emplacement Description Valeur par défaut Bonne pratique en production
config export [--area a,b] [--split ordner/] [-o datei] Exporte l'état du serveur de manière déterministe : ordre des documents fixe, listes triées, aucun horodatage. Deux exports donnent les mêmes octets, les diffs Git ne montrent que les vrais changements. alle Bereiche Utilisez --split pour les dépôts (un fichier par domaine : domains.yaml, accounts.yaml ...) ; les petites pull requests restent faciles à lire.
config plan -f datei|ordner [--prune] Affiche par objet + création, ~ mise à jour (champ : ancien -> nouveau) et - suppression. Code de sortie 0 = aucun changement, 2 = changements, 1 = erreur. Les secrets n'apparaissent que sous la forme "(secret changed)". ohne Löschen Exécutez-le sur les pull requests et publiez le résultat en commentaire (exemples ci-dessous).
config apply -f ... [--prune] [--yes] [--force] [--dry-run] Vérifie d'abord tout (champs, références, secrets, règles de sécurité), puis crée et met à jour dans un ordre fixe (locataires, domaines, comptes, listes, règles, partages, transport...), et supprime dans l'ordre inverse. Si une étape échoue, les étapes déjà effectuées sont annulées. Chaque modification est consignée dans le journal d'audit. fragt nach Passez toujours --yes dans les pipelines ; sans terminal, apply refuse sinon de s'exécuter. --dry-run équivaut à plan.
--prune und Manifest Les suppressions n'ont lieu qu'avec --prune et uniquement dans les zones que nomme le document Manifest (sans Manifest : uniquement les types présents dans le fichier). Les paramètres du serveur ne sont jamais supprimés. aus L'export écrit le manifeste ; si vous ne gérez que certaines parties, restreignez les domaines concernés.
Schutz: letzter Admin, Domain mit Postfächern Un plan qui ne laisserait aucun administrateur serveur actif, ainsi que la suppression d'un domaine qui contient encore des boîtes aux lettres, sont refusés. --force passe outre (le domaine est alors désactivé au lieu d'être supprimé). La clé API utilisée ne peut pas se révoquer elle-même. aktiv N'inscrivez jamais --force en dur dans un pipeline.
secretRef: {env: NAME} | {file: pfad} Référence à un secret au lieu de sa valeur. nextxch-cli la résout sur le runner (n'importe quelle variable, fichier relatif à la configuration). Le serveur lui-même ne lit que NEXTXCH_SECRET_* et les fichiers sous <data>/secrets/. Réappliquer la même valeur ne constitue pas une modification. - Transmettez les secrets du coffre de secrets GitHub/GitLab au job sous forme de variables d'environnement.
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 (gratuit). Clés : clés d'intégration de locataire ; elles ne fonctionnent que tant que leur créateur est encore administrateur. Les clés d'un administrateur du serveur voient tout le serveur, celles d'un administrateur de locataire uniquement ce locataire. config.read suffit pour l'export et le plan, l'application nécessite config.write. - Deux clés : config.read pour les plans dans les pull requests, config.write uniquement pour le job apply sur main, restreinte si possible aux IP des runners.
Mandanten-Admins Un administrateur de locataire (session ou l'une de ses clés) exporte, planifie et applique uniquement les objets de son locataire : Domain (modification uniquement, ni création ni suppression), Account, DistributionList, Share, GalVisibilityRule, ResourceAccessRule et Webhook. Les paramètres du serveur, les locataires, les sources d'identité et les clés d'API restent du ressort de l'administrateur du serveur ; les comptes ayant le rôle ServerAdmin ou ComplianceAuditor sont exclus. --prune ne supprime qu'à l'intérieur du propre locataire, history n'affiche que ses propres applications. - Pour les revendeurs et les services : chaque locataire conserve son propre dépôt avec sa propre clé. Le champ "tenant" peut être omis ; il est renseigné avec le propre locataire.
secretRef in Listen Les entrées de liste reçoivent elles aussi leurs secrets via secretRef : MtaFilters hooks[].authHeaderValue et hooks[].hmacSecret, OpsAlerts targets[].secret (HMAC de la cible webhook). Le plan désigne l'entrée par son id ou son nom, par exemple hooks[spamcheck].authHeaderValue ; une valeur en clair dans la liste est refusée. - Les entrées sans id en reçoivent un lors de la première application ; après un export, conservez les id dans le fichier : les secrets restent ainsi associés à la bonne entrée lorsque la liste est réordonnée.
ACME zur Laufzeit TlsSettings.acme (enabled, directoryUrl, contactEmail, défi Http01/TlsAlpn01/Dns01, renewBeforeExpiryDays, reusePrivateKeyForDane) s'applique sans redémarrage : appsettings.json reste la base, les différences sont stockées dans <data>/acme-settings.json et s'appliquent à l'ensemble du cluster. Changer l'URL du répertoire crée un nouveau compte ACME lors de la commande suivante. appsettings.json Testez d'abord avec le répertoire de staging de Let's Encrypt, puis passez en production.
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. Tous les champs : JSON Schema. - Les clés d'API ne sont pas créées à partir de fichiers (leur secret n'existe qu'une seule fois) ; un fichier modifie le libellé et les portées, et révoque avec --prune. Les paramètres de salles et de réservations sont identiques sur chaque nœud du cluster.