Capítulo 13b Clientes, groupware & CLI Admin: #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.

Dica operacional & nota de arquitetura

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.

Referência / configuração
# 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:

Referência / configuração
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.

Referência / configuração
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.

Referência / configuração
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.

Referência / configuração
# .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.

Referência / configuraçã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.