#pConfigFile
Configuração como código: exportar, planejar, aplicar e pipeline Git
Todo o estado do servidor como arquivos YAML no Git: locatários, domínios (seletores DKIM, MTA-STS, roteamento), caixas de correio com aliases, encaminhamento e resposta automática (ausência), listas de distribuição, caixas de correio compartilhadas, salas, GAL e regras de recursos, compartilhamentos e delegação, SMTP, smart host, rotas, regras de transporte, proteção contra spam e vírus, firewall e auto-ban, TLS, fontes de identidade, chaves de API (somente metadados), webhooks, notificações, alertas de operação, POP3, arquivos, agendamento, arquivo e failover de DNS. O nextxch-cli config plan mostra, como o terraform plan, o que seria criado, alterado e excluído; o apply verifica tudo primeiro e aplica em uma ordem fixa. Admin: Sistema > Configuração como arquivo.
Segredos nunca entram no arquivo e nunca são exportados: senhas, tokens e chaves são escritos como secretRef: {env: NAME} ou secretRef: {file: path} e resolvidos no momento do apply (pelo nextxch-cli no runner de CI; no servidor, somente variáveis NEXTXCH_SECRET_* e arquivos abaixo de <data>/secrets/). Chaves DKIM, certificados e hashes de senha permanecem no servidor. Recurso do Business, como a API v1; o JSON Schema é gratuito.
1. Formato de arquivo: apiVersion, kind, metadata.name, spec
Um arquivo com vários documentos (separados por ---) ou uma pasta com um arquivo por área (config export --split). Campos ausentes no arquivo permanecem como estão no servidor; listas (aliases, membros) contam como um todo.
# 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. Fluxo de trabalho com nextxch-cli
Primeiro exporte e faça commit, depois altere por meio de pull requests. Exemplo de saída do 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 no pull request, apply no merge
Arquivo completo com a etapa de comentário: deploy/gitops/github-actions.yml no repositório. A chave de API vem dos secrets do repositório, nunca do arquivo.
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: plano como nota no merge request, apply na branch padrão
Arquivo completo: deploy/gitops/gitlab-ci.yml. As chaves são variáveis de CI/CD mascaradas e protegidas; resource_group impede dois applies simultâneos.
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 no editor e chamada REST
O VS Code (extensão "YAML" da Red Hat) e as IDEs JetBrains verificam os arquivos durante a digitação. Todo servidor também disponibiliza o esquema em /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. Segredos em listas e ACME sem reinicialização
Hooks MTA com cabeçalho de autenticação e segredo HMAC vindos do pipeline, um destino de alerta com segredo de webhook e a mudança para Let's Encrypt com DNS-01: um único apply, sem reinicialização.
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
Configurações, valores padrão e boas práticas
| Configuração / local | Descrição | Valor padrão | Boa prática para produção |
|---|---|---|---|
config export [--area a,b] [--split ordner/] [-o datei] |
Exporta o estado do servidor de forma determinística: ordem fixa dos documentos, listas ordenadas, sem carimbos de data/hora. Exportar duas vezes gera os mesmos bytes, e os diffs do Git mostram apenas mudanças reais. | alle Bereiche |
Use --split para repositórios (um arquivo por área: domains.yaml, accounts.yaml ...); pull requests pequenos continuam fáceis de ler. |
config plan -f datei|ordner [--prune] |
Mostra por objeto + criar, ~ atualizar (campo: antigo -> novo) e - excluir. Código de saída 0 = sem alterações, 2 = alterações, 1 = erro. Segredos aparecem apenas como "(secret changed)". | ohne Löschen |
Execute-o em pull requests e publique-o como comentário (exemplos abaixo). |
config apply -f ... [--prune] [--yes] [--force] [--dry-run] |
Primeiro verifica tudo (campos, referências, segredos, regras de segurança), depois cria e atualiza em uma ordem fixa (locatários, domínios, contas, listas, regras, compartilhamentos, transporte ...) e exclui na ordem inversa. Se uma etapa falhar, as etapas já concluídas são desfeitas. Cada alteração vai para o log de auditoria. | fragt nach |
Sempre passe --yes em pipelines; sem um terminal, o apply se recusa a executar caso contrário. --dry-run equivale a plan. |
--prune und Manifest |
Exclusões acontecem somente com --prune e somente nas áreas que o documento Manifest nomeia (sem Manifest: somente os tipos presentes no arquivo). As configurações do servidor nunca são excluídas. | aus |
A exportação grava o Manifest; se você gerencia apenas partes, restrinja as áreas. |
Schutz: letzter Admin, Domain mit Postfächern |
Um plano que deixaria nenhum administrador de servidor ativo e a exclusão de um domínio que ainda tem caixas de correio são recusados. --force ignora isso (o domínio é então desativado em vez de excluído). A própria chave de API não pode revogar a si mesma. | aktiv |
Nunca fixe --force no código de um pipeline. |
secretRef: {env: NAME} | {file: pfad} |
Referência a um segredo em vez de seu valor. O nextxch-cli o resolve no runner (qualquer variável, arquivo relativo à configuração). O próprio servidor lê apenas NEXTXCH_SECRET_* e arquivos abaixo de <data>/secrets/. Reaplicar o mesmo valor não é uma alteração. | - |
Passe os segredos do repositório de segredos do GitHub/GitLab para o job como variáveis de 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 (livre). Chaves: chaves de integração de locatário; funcionam apenas enquanto o criador ainda for administrador. Chaves de um administrador do servidor veem o servidor inteiro, chaves de um administrador de locatário apenas esse locatário. config.read basta para export e plan, apply requer config.write. | - |
Duas chaves: config.read para planos em pull requests, config.write apenas para o job de apply na main, restrita aos IPs dos runners, se possível. |
Mandanten-Admins |
Um administrador de locatário (sessão ou uma de suas chaves) exporta, planeja e aplica apenas objetos do próprio locatário: Domain (somente alteração, sem criar ou excluir), Account, DistributionList, Share, GalVisibilityRule, ResourceAccessRule e Webhook. Configurações do servidor, locatários, fontes de identidade e chaves de API permanecem com o administrador do servidor; contas com a função ServerAdmin ou ComplianceAuditor ficam fora de alcance. --prune exclui apenas dentro do próprio locatário, o histórico mostra apenas as próprias aplicações. | - |
Para revendedores e departamentos: cada locatário mantém seu próprio repositório com sua própria chave. Um campo "tenant" pode ser omitido; ele é preenchido com o próprio locatário. |
secretRef in Listen |
Entradas de lista também recebem segredos via secretRef: MtaFilters hooks[].authHeaderValue e hooks[].hmacSecret, OpsAlerts targets[].secret (HMAC do destino de webhook). O plano identifica a entrada pelo id ou pelo nome, por exemplo hooks[spamcheck].authHeaderValue; um valor em texto simples na lista é recusado. | - |
Entradas sem id recebem um no primeiro apply; após uma exportação, mantenha os ids no arquivo, assim os segredos permanecem com a entrada correta quando a lista é reordenada. |
ACME zur Laufzeit |
TlsSettings.acme (enabled, directoryUrl, contactEmail, challenge Http01/TlsAlpn01/Dns01, renewBeforeExpiryDays, reusePrivateKeyForDane) é aplicado sem reinicialização: o appsettings.json continua sendo a base, as diferenças ficam em <data>/acme-settings.json e valem para todo o cluster. Alterar a URL do diretório cria uma nova conta ACME no próximo pedido. | appsettings.json |
Teste primeiro com o diretório de staging do Let's Encrypt e depois mude para produção. |
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. Todos os campos: JSON Schema. | - |
Chaves de API não são criadas a partir de arquivos (o segredo existe apenas uma vez); um arquivo altera rótulo e escopos e revoga com --prune. As configurações de salas e reservas são as mesmas em todos os nós do cluster. |