Capitolo 13b Client, groupware & CLI Admin: #pConfigFile

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

L'intero stato del server come file YAML in Git: tenant, domini (selettori DKIM, MTA-STS, routing), caselle di posta con alias, inoltro e risposta automatica, liste di distribuzione, caselle condivise, sale, GAL e regole delle risorse, condivisioni e delega, SMTP, smart host, route, regole di trasporto, protezione antispam e antivirus, firewall e auto-ban, TLS, origini di identità, chiavi API (solo metadati), webhook, notifiche, avvisi operativi, POP3, file, prenotazioni, archivio e failover DNS. nextxch-cli config plan mostra, come terraform plan, cosa verrebbe creato, modificato ed eliminato; apply verifica prima tutto e lo applica in un ordine fisso. Admin: Sistema > Configurazione come file.

Suggerimento operativo & nota sull'architettura

I segreti non finiscono mai nel file e non vengono mai esportati: password, token e chiavi si scrivono come secretRef: {env: NAME} o secretRef: {file: path} e vengono risolti al momento dell'apply (da nextxch-cli sul runner CI; sul server solo variabili NEXTXCH_SECRET_* e file sotto <data>/secrets/). Chiavi DKIM, certificati e hash delle password restano sul server. Funzionalità Business come l'API v1; lo JSON Schema è gratuito.

1. Formato del file: apiVersion, kind, metadata.name, spec

Un file con più documenti (separati da ---) o una cartella con un file per area (config export --split). I campi assenti dal file restano invariati sul server; le liste (alias, membri) contano nel loro insieme.

Riferimento / configurazione
# 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. Flusso di lavoro con nextxch-cli

Prima esportare e fare il commit, poi apportare modifiche tramite pull request. Esempio di output del piano:

Riferimento / configurazione
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 sulla pull request, apply al merge

File completo con il passaggio di commento: deploy/gitops/github-actions.yml nel repository. La chiave API proviene dai secret del repository, mai dal file.

Riferimento / configurazione
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 come nota della merge request, apply sul branch predefinito

File completo: deploy/gitops/gitlab-ci.yml. Le chiavi sono variabili CI/CD mascherate e protette; resource_group impedisce due apply contemporanei.

Riferimento / configurazione
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 nell'editor e chiamata REST

VS Code (estensione Red Hat "YAML") e gli IDE JetBrains verificano i file durante la digitazione. Ogni server fornisce inoltre lo schema su /api/v1/config/schema.

Riferimento / configurazione
# .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. Segreti negli elenchi e ACME senza riavvio

Hook MTA con header di autenticazione e segreto HMAC dalla pipeline, una destinazione di avviso con segreto webhook e il passaggio a Let's Encrypt con DNS-01: un solo apply, nessun riavvio.

Riferimento / configurazione
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

Impostazioni, valori predefiniti e best practice

Impostazione / posizione Descrizione Valore predefinito Best practice per la produzione
config export [--area a,b] [--split ordner/] [-o datei] Esporta lo stato del server in modo deterministico: ordine fisso dei documenti, elenchi ordinati, nessun timestamp. Due esportazioni producono gli stessi byte, i diff Git mostrano solo le modifiche reali. alle Bereiche Usare --split per i repository (un file per area: domains.yaml, accounts.yaml ...); le pull request piccole restano facili da leggere.
config plan -f datei|ordner [--prune] Mostra per ogni oggetto + create, ~ update (campo: vecchio -> nuovo) e - delete. Codice di uscita 0 = nessuna modifica, 2 = modifiche, 1 = errore. I segreti compaiono solo come "(secret changed)". ohne Löschen Eseguirlo sulle pull request e pubblicarlo come commento (esempi sotto).
config apply -f ... [--prune] [--yes] [--force] [--dry-run] Verifica prima tutto (campi, riferimenti, segreti, regole di sicurezza), poi crea e aggiorna in un ordine fisso (tenant, domini, account, liste, regole, condivisioni, trasporto ...) ed elimina in ordine inverso. Se un passaggio non riesce, i passaggi già eseguiti vengono annullati. Ogni modifica finisce nel registro di controllo. fragt nach Nelle pipeline passare sempre --yes; senza un terminale, apply altrimenti si rifiuta di procedere. --dry-run equivale a plan.
--prune und Manifest Le eliminazioni avvengono solo con --prune e solo nelle aree indicate dal documento Manifest (senza Manifest: solo i tipi presenti nel file). Le impostazioni del server non vengono mai eliminate. aus L'esportazione scrive il Manifest; se si gestiscono solo alcune parti, restringere le aree.
Schutz: letzter Admin, Domain mit Postfächern Un piano che lascerebbe il sistema senza un amministratore server attivo e l'eliminazione di un dominio che ha ancora caselle di posta vengono rifiutati. --force scavalca il blocco (il dominio viene allora disattivato invece che eliminato). La propria chiave API non può revocare sé stessa. aktiv Non inserire mai --force in modo fisso in una pipeline.
secretRef: {env: NAME} | {file: pfad} Riferimento a un secret anziché al suo valore. nextxch-cli lo risolve sul runner (qualsiasi variabile, file relativo alla configurazione). Il server stesso legge solo NEXTXCH_SECRET_* e i file sotto <data>/secrets/. Riapplicare lo stesso valore non costituisce una modifica. - Passare i segreti dall'archivio dei segreti di GitHub/GitLab al job come variabili d'ambiente.
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 (libero). Chiavi: chiavi di integrazione del tenant; funzionano solo finché il loro creatore è ancora amministratore. Le chiavi di un amministratore del server vedono l'intero server, quelle di un amministratore del tenant solo quel tenant. config.read basta per export e plan, apply richiede config.write. - Due chiavi: config.read per i piani nelle pull request, config.write solo per il job di apply su main, limitata se possibile agli IP dei runner.
Mandanten-Admins Un amministratore del tenant (sessione o una delle sue chiavi) esporta, pianifica e applica solo oggetti del proprio tenant: Domain (solo modifica, nessuna creazione o eliminazione), Account, DistributionList, Share, GalVisibilityRule, ResourceAccessRule e Webhook. Impostazioni del server, tenant, origini di identità e chiavi API restano all'amministratore del server; gli account con ruolo ServerAdmin o ComplianceAuditor sono esclusi. --prune elimina solo all'interno del proprio tenant, la cronologia mostra solo le proprie applicazioni. - Per rivenditori e reparti: ogni tenant mantiene il proprio repository con la propria chiave. Un campo "tenant" può essere omesso; viene compilato con il proprio tenant.
secretRef in Listen Anche le voci degli elenchi ricevono segreti tramite secretRef: MtaFilters hooks[].authHeaderValue e hooks[].hmacSecret, OpsAlerts targets[].secret (HMAC della destinazione webhook). Il piano identifica la voce tramite id o nome, ad esempio hooks[spamcheck].authHeaderValue; un valore in chiaro nell'elenco viene rifiutato. - Le voci senza id ne ricevono uno al primo apply; dopo un'esportazione mantenere gli id nel file, così i segreti restano con la voce corretta quando l'elenco viene riordinato.
ACME zur Laufzeit TlsSettings.acme (enabled, directoryUrl, contactEmail, challenge Http01/TlsAlpn01/Dns01, renewBeforeExpiryDays, reusePrivateKeyForDane) si applica senza riavvio: appsettings.json resta la base, le differenze risiedono in <data>/acme-settings.json e valgono per l'intero cluster. Modificare l'URL della directory crea un nuovo account ACME al successivo ordine. appsettings.json Testare prima con la directory di staging di Let's Encrypt, poi passare alla produzione.
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. Tutti i campi: JSON Schema. - Le chiavi API non vengono create da file (il loro segreto esiste una sola volta); un file modifica etichetta e ambiti e revoca con --prune. Le impostazioni di sale e prenotazioni sono identiche su ogni nodo del cluster.