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