Kapitel 13b Clients, Groupware & CLI Admin: #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.

Praxis-Tipp & Architektur-Hinweis

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.

Referenz / Konfiguration
# 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:

Referenz / Konfiguration
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.

Referenz / Konfiguration
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.

Referenz / Konfiguration
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.

Referenz / Konfiguration
# .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.

Referenz / Konfiguration
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.