#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.
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.
# 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 :
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.
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.
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.
# .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.
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. |