#pConfigFile
Konfiguration als Datei: Export, Plan, Apply und Git-Pipeline
Der gesamte Server-Zustand als YAML-Dateien in Git: Mandanten, Domains (DKIM-Selektoren, MTA-STS, Routing), Postfächer mit Aliasen, Weiterleitung und Abwesenheit, Verteiler, Shared Mailboxes, Räume, GAL- und Ressourcen-Regeln, Freigaben und Stellvertretungen, SMTP, Smart Host, Routen, Transportregeln, Spam- und Virenschutz, Firewall und Auto-Ban, TLS, Identitätsquellen, API-Keys (nur Metadaten), Webhooks, Benachrichtigungen, Betriebsalarme, POP3, Dateien, Terminbuchung, Archiv und DNS-Failover. nextxch-cli config plan zeigt wie terraform plan, was angelegt, geändert und gelöscht würde; apply prüft alles vorher und wendet es in fester Reihenfolge an. Admin: System > Konfiguration als Datei.
Secrets kommen nie in die Datei und werden nie exportiert: Passwörter, Tokens und Schlüssel stehen als secretRef: {env: NAME} oder secretRef: {file: pfad} drin und werden beim Apply aufgelöst (von nextxch-cli auf dem CI-Runner, auf dem Server nur Variablen NEXTXCH_SECRET_* und Dateien unter <data>/secrets/). DKIM-Schlüssel, Zertifikate und Passwort-Hashes bleiben auf dem Server. Business-Funktion wie API v1; das JSON-Schema ist frei.
1. Dateiformat: apiVersion, kind, metadata.name, spec
Eine Datei mit mehreren Dokumenten (getrennt durch ---) oder ein Ordner mit einer Datei pro Bereich (config export --split). Felder, die in der Datei fehlen, bleiben auf dem Server unverändert; Listen (Aliase, Mitglieder) gelten als Ganzes.
# yaml-language-server: $schema=https://nextxch.com/schemas/nextxch-config-v1.schema.json
apiVersion: nextxch/v1
kind: Manifest # optional: Bereiche, in denen --prune löschen darf
metadata: {name: nextxch}
spec: {areas: [domains, accounts, sharing, transport]}
---
apiVersion: nextxch/v1
kind: Domain
metadata: {name: firma.de}
spec:
tenant: default
catchAll: info@firma.de
dkim: {mode: dual, rsaSelector: s2026, ed25519Selector: ed2026} # Schlüssel bleiben auf dem Server
mtaSts: {mode: enforce, maxAgeSeconds: 604800, mxHosts: [mail.firma.de]}
---
apiVersion: nextxch/v1
kind: Account
metadata: {name: anna.muster@firma.de}
spec:
displayName: Anna Muster
role: User # User, TenantAdmin, ServerAdmin, ComplianceAuditor
quotaMb: 10240
aliases: [anna@firma.de]
forwarding: {address: anna@privat.example, keepCopy: true}
outOfOffice: {enabled: false}
password: {secretRef: {env: ANNA_PASSWORD}} # nie das Passwort selbst
---
apiVersion: nextxch/v1
kind: Account
metadata: {name: info@firma.de}
spec: {type: shared, displayName: Info-Postfach} # user, shared, room, equipment
---
apiVersion: nextxch/v1
kind: Share
metadata: {name: "info@firma.de/mailbox -> anna.muster@firma.de"}
spec: {permissions: [read, write, sendAs]}
---
apiVersion: nextxch/v1
kind: SmtpSettings
metadata: {name: server} # Einstellungen des ganzen Servers heißen immer "server"
spec:
outboundSmartHost: smtp.provider.example
outboundSmartPort: 587
smartHostPassword: {secretRef: {file: secrets/smarthost.txt}}
2. Ablauf mit nextxch-cli
Erst exportieren und einchecken, dann Änderungen über Pull Requests. Beispielausgabe eines Plans:
export NEXTXCH_URL=https://mail.firma.de
export NEXTXCH_API_KEY=smk_... # Schlüssel mit config.read oder config.write
nextxch-cli config export --split nextxch/
nextxch-cli config validate -f nextxch/ # offline: Arten und Felder
nextxch-cli config plan -f nextxch/
~ Domain firma.de
mtaSts.mode: "testing" -> "enforce"
~ Account anna.muster@firma.de
quotaMb: 10240 -> 20480
aliases: ["anna@firma.de"] -> ["a.muster@firma.de","anna@firma.de"]
~ password: (secret changed)
+ Account info@firma.de
type: "shared"
displayName: "Info-Postfach"
Not in the file (1), deleted only with --prune:
Account alt@firma.de
Plan: 1 to create, 2 to update, 0 to delete, 41 unchanged.
# Exit-Code 2 = Änderungen
nextxch-cli config apply -f nextxch/ --prune --yes
nextxch-cli config history
3. GitHub Actions: Plan im Pull Request, Apply nach Merge
Vollständige Datei mit Kommentar-Schritt: deploy/gitops/github-actions.yml im Repository. Der API-Key kommt aus den Repository-Secrets, nie aus der Datei.
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 als Merge-Request-Notiz, Apply auf dem Default-Branch
Vollständige Datei: deploy/gitops/gitlab-ci.yml. Die Schlüssel liegen als maskierte, geschützte CI/CD-Variablen vor; resource_group verhindert zwei gleichzeitige Applies.
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 im Editor und REST-Aufruf
VS Code (Erweiterung "YAML" von Red Hat) und JetBrains-IDEs prüfen die Dateien beim Tippen. Das Schema liefert auch jeder Server unter /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 in Listen und ACME ohne Neustart
MTA-Hooks mit Auth-Header und HMAC-Secret aus der Pipeline, ein Alarmziel mit Webhook-Secret und der Wechsel auf Let's Encrypt mit DNS-01: ein Apply, kein Neustart.
apiVersion: nextxch/v1
kind: MtaFilters
metadata: {name: server}
spec:
hooks:
- name: spamcheck
url: https://hooks.firma.de/check
stages: [data]
authHeaderName: Authorization
authHeaderValue: {secretRef: {env: HOOK_TOKEN}} # im 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.firma.de/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@firma.de
challenge: Dns01
renewBeforeExpiryDays: 30
Einstellungen, Standardwerte & Best Practices
| Einstellung / Ort | Funktion | Standardwert | Best Practice im Betrieb |
|---|---|---|---|
config export [--area a,b] [--split ordner/] [-o datei] |
Exportiert den Server-Zustand deterministisch: feste Reihenfolge der Dokumente, sortierte Listen, keine Zeitstempel. Zweimal exportieren ergibt dieselben Bytes, Git-Diffs zeigen nur echte Änderungen. | alle Bereiche |
Für Repositories --split nutzen (eine Datei pro Bereich: domains.yaml, accounts.yaml ...); kleine Pull Requests bleiben so übersichtlich. |
config plan -f datei|ordner [--prune] |
Zeigt pro Objekt + anlegen, ~ ändern (Feld: alt -> neu) und - löschen. Exit-Code 0 = keine Änderungen, 2 = Änderungen, 1 = Fehler. Secrets erscheinen nur als "(secret changed)". | ohne Löschen |
Im Pull Request laufen lassen und als Kommentar posten (Beispiele unten). |
config apply -f ... [--prune] [--yes] [--force] [--dry-run] |
Prüft erst alles (Felder, Verweise, Secrets, Schutzregeln), dann Anlage und Änderung in fester Reihenfolge (Mandanten, Domains, Konten, Verteiler, Regeln, Freigaben, Transport ...), Löschungen in umgekehrter Reihenfolge. Scheitert ein Schritt, werden die bereits ausgeführten zurückgenommen. Jede Änderung landet im Audit-Log. | fragt nach |
In der Pipeline immer --yes; ohne Terminal verweigert apply sonst. --dry-run ist dasselbe wie plan. |
--prune und Manifest |
Gelöscht wird nur mit --prune und nur in den Bereichen, die das Manifest-Dokument nennt (ohne Manifest: nur die Arten, die in der Datei vorkommen). Server-Einstellungen werden nie gelöscht. | aus |
Export schreibt das Manifest mit; wer nur Teile verwaltet, schränkt areas ein. |
Schutz: letzter Admin, Domain mit Postfächern |
Ein Plan, der keinen aktiven Server-Admin übrig ließe, und das Löschen einer Domain mit Postfächern werden abgelehnt. Mit --force geht es trotzdem (die Domain wird dann abgeschaltet statt gelöscht). Der eigene API-Key kann sich nicht selbst widerrufen. | aktiv |
--force nie fest in die Pipeline schreiben. |
secretRef: {env: NAME} | {file: pfad} |
Verweis auf ein Secret statt des Werts. nextxch-cli löst ihn auf dem Runner auf (beliebige Variable, Datei relativ zur Konfiguration). Der Server liest selbst nur NEXTXCH_SECRET_* und Dateien unter <data>/secrets/. Ein erneutes Apply mit demselben Wert ist keine Änderung. | - |
Secrets aus dem Secret-Speicher von GitHub/GitLab als Umgebungsvariable an den Job geben. |
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 (frei). Schlüssel: Mandanten-Integrationsschlüssel; sie gelten nur, solange ihr Ersteller Administrator ist. Schlüssel eines Server-Admins sehen den ganzen Server, Schlüssel eines Mandanten-Admins nur dessen Mandanten. config.read reicht für export und plan, apply braucht config.write. | - |
Zwei Schlüssel: config.read für Pläne in Pull Requests, config.write nur für den Apply-Job auf main, wenn möglich auf die Runner-IPs beschränkt. |
Mandanten-Admins |
Ein Mandanten-Admin (Sitzung oder einer seiner Schlüssel) exportiert, plant und wendet nur Objekte seines Mandanten an: Domain (nur ändern, nicht anlegen oder löschen), Account, DistributionList, Share, GalVisibilityRule, ResourceAccessRule und Webhook. Server-Einstellungen, Mandanten, Identitätsquellen und API-Keys bleiben beim Server-Admin; Konten mit Rolle ServerAdmin oder ComplianceAuditor sind tabu. --prune löscht nur im eigenen Mandanten, die Historie zeigt nur eigene Applies. | - |
Für Reseller und Abteilungen: jeder Mandant pflegt sein eigenes Repository mit eigenem Schlüssel. Ein "tenant"-Feld darf fehlen; es wird mit dem eigenen Mandanten gefüllt. |
secretRef in Listen |
Auch Einträge von Listen nehmen Secrets per secretRef: MtaFilters hooks[].authHeaderValue und hooks[].hmacSecret, OpsAlerts targets[].secret (HMAC des Webhook-Ziels). Der Plan nennt den Eintrag über seine id oder seinen Namen, etwa hooks[spamcheck].authHeaderValue; ein Klartextwert in der Liste wird abgelehnt. | - |
Einträge ohne id bekommen sie beim ersten Apply; nach einem Export die ids in der Datei lassen, dann bleiben Secrets beim Umsortieren am richtigen Eintrag. |
ACME zur Laufzeit |
TlsSettings.acme (enabled, directoryUrl, contactEmail, challenge Http01/TlsAlpn01/Dns01, renewBeforeExpiryDays, reusePrivateKeyForDane) wird ohne Neustart übernommen: appsettings.json bleibt die Grundlage, Abweichungen liegen in <data>/acme-settings.json und gelten im ganzen Cluster. Ein Wechsel der Directory-URL legt beim nächsten Auftrag ein neues ACME-Konto an. | appsettings.json |
Erst mit der Staging-Directory von Let's Encrypt testen, dann auf Produktion umstellen. |
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. Alle Felder: JSON-Schema. | - |
API-Keys werden nicht aus Dateien angelegt (ihr Secret gibt es nur einmal); die Datei ändert Bezeichnung und Scopes und widerruft mit --prune. Raum- und Buchungseinstellungen sind im Cluster auf allen Knoten gleich. |