Segurança do provedor

Para: Dev · Adm — Conferido em 2026-10-31.

Um provedor de liquidação é o único processo do Dracma que fala com um terceiro que move dinheiro e que recebe, por chamada, a credencial do tenant em claro. Por isso a fronteira dele é desenhada como a de um componente comprometível: se ele cair nas mãos de um atacante, o estrago precisa parar nele. Esta página reúne o que o desenvolvedor do provedor e o administrador do host precisam cumprir.

O que é

O conjunto de regras de segurança do SPP v1 (decisões S2, S3, S9, S12 e S13 do contrato) aplicado ao dia a dia:

  • isolamento do processo (usuário Linux próprio, unit systemd endurecida);
  • rede mínima (escuta só em loopback, saída só para os hosts do descriptor);
  • autenticação do único chamador permitido (o executor), com HMAC e anti-replay;
  • credencial por chamada, sem guarda de segredo de longa duração;
  • PII mínima e logs limpos;
  • guarda de ambiente e nenhum efeito externo real fora de prd;
  • cadeia de suprimentos e auditoria.

Quando usar

  • Antes de escrever a primeira rota de um provedor novo (desenho).
  • No code review de qualquer provedor, nosso ou de terceiro.
  • Ao preparar o host (ops) e antes do enable em hml ou prd.
  • Na revisão periódica de provedores plugados: use o checklist no final.

Como funciona

flowchart LR EX[executor<br/>unico chamador] -->|HTTP loopback<br/>HMAC v2 + credencial no corpo| PV[provedor<br/>usuario Linux proprio] PV -->|HTTPS<br/>somente egress_hosts| PT[parceiro] PT -->|callback| IN[ingress] --> EX PV -.nao acessa.-> DB[(banco)] PV -.nao acessa.-> CF[(cofre e KMS)] PV -.nao acessa.-> OS[outros servicos]

Fronteiras de confiança:

Fronteira Quem confia em quem
executor para provedor O provedor só aceita o executor (HMAC v2, Dracma-Caller: executor). O executor trata resposta do provedor como dado não confiável de negócio (leitor tolerante, unrecognized).
provedor para parceiro O parceiro é um terceiro. O provedor só sai para egress_hosts, sem repassar traceparent, e trata toda resposta dele como entrada não confiável.
parceiro para ingress Callback é afirmação, nunca prova. Só o fetch-back confirma.
provedor para o resto do host Nenhum acesso: sem banco, sem cofre, sem KMS, sem outro serviço.

Isolamento

Usuário Linux e unit systemd

Cada provedor roda com usuário Linux próprio, sem shell, sem home e sem grupo compartilhado, em uma unit systemd endurecida. Quem cria usuário, unit e porta é o ops; nunca à mão no servidor. Exemplo completo de unit para o provedor maximaxpay (caminhos e nomes são ilustrativos; o ops gera o equivalente):

[Unit]
Description=Dracma PSP maximaxpay (SPP v1)
Documentation=https://developers.dracma.asia/provedores
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=dracma_psp_maximaxpay
Group=dracma_psp_maximaxpay
ExecStart=/dracma/dracma_psp_maximaxpay/bin/dracma_psp_maximaxpay
WorkingDirectory=/dracma/dracma_psp_maximaxpay
EnvironmentFile=/etc/dracma/psp_maximaxpay.env
Restart=on-failure
RestartSec=2s
TimeoutStopSec=20s

# Estado minimo (repositorio de idempotencia sem segredo), unico caminho gravavel
StateDirectory=dracma_psp_maximaxpay
StateDirectoryMode=0700
UMask=0077

# Privilegios
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
MemoryDenyWriteExecute=yes
RemoveIPC=yes

# Sistema de arquivos
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
ProcSubset=pid

# Rede e syscalls
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @mount @debug

# Limites
MemoryMax=512M
TasksMax=256
LimitNOFILE=4096
LimitCORE=0

[Install]
WantedBy=multi-user.target

Pontos que não podem faltar:

  • NoNewPrivileges, CapabilityBoundingSet= vazio, ProtectSystem=strict (sistema de arquivos somente leitura exceto StateDirectory), ProtectHome, PrivateTmp, RestrictAddressFamilies.
  • MemoryDenyWriteExecute=yes quando a linguagem permitir (Rust, Go e Zig permitem; runtimes com JIT não). Se não puder ligar, registre o motivo no ADR do provedor.
  • LimitCORE=0: dump de memória é um vazamento de credencial em claro.
  • O arquivo de ambiente contém só o par HMAC com o executor (INTERNAL_HMAC_<PROVEDOR>_EXECUTOR), a porta e a configuração de telemetria. Nunca a credencial de nenhum tenant e nunca o par HMAC de outro provedor. Modo 0400, dono root (o systemd lê como root).
  • Em container (quando houver): usuário não-root, sistema de arquivos de raiz somente leitura, imagem por digest.

Verificação do isolamento

O ops confere e a certificação exige. Teste manual no host:

systemd-analyze security dracma_psp_maximaxpay.service
ps -o user,pid,cmd -C dracma_psp_maximaxpay
ss -ltnp | grep 7101

O esperado: nota de exposição baixa, processo com o usuário do provedor e escuta somente em 127.0.0.1:7101 (nunca 0.0.0.0 nem ::).

Rede: loopback e egress por UID

Loopback

  • O provedor escuta só em 127.0.0.1:<porta>. Uma porta por provedor, atribuída pelo ops.
  • Não há TLS em loopback; o isolamento é o do host (usuário próprio por serviço). Por isso o corpo carrega o hash no HMAC: a credencial no corpo é protegida contra adulteração local.
  • Provedor remoto (fora do host) não existe na v1.

Egress por UID derivado de egress_hosts

O ops lê egress_hosts do descriptor e gera uma regra de saída por UID do provedor: apenas esses host:porta, mais DNS e telemetria locais (loopback). Todo o resto é descartado e alerta. O ops gera e confere; ninguém edita regra à mão.

Exemplo do que o ops gera (nftables; o endereço é de documentação):

table inet dracma_psp_egress {
  set maximaxpay_hosts {
    type ipv4_addr
    elements = { 203.0.113.10 }
  }

  chain output {
    type filter hook output priority 0; policy accept;

    meta skuid "dracma_psp_maximaxpay" oifname "lo" accept
    meta skuid "dracma_psp_maximaxpay" ip daddr @maximaxpay_hosts tcp dport 443 accept
    meta skuid "dracma_psp_maximaxpay" log prefix "psp-egress-drop " counter drop
  }
}

Regras:

Regra Motivo
A lista vem só de egress_hosts (todos os environments, não apenas o ativo) Fonte única; trocar de parceiro muda o descriptor, não regra solta.
Porta explícita em cada item Evita liberar o host inteiro.
Resolução de nomes feita pelo ops e reaplicada em intervalo Parceiro troca de IP; a regra precisa acompanhar sem abrir um domínio inteiro.
Pacote descartado gera log e alerta Saída bloqueada é sinal de bug, de configuração errada ou de comprometimento.
Sem proxy e sem redirecionamento HTTP para outro host Um 302 do parceiro (ou de um atacante que o represente) não pode levar a credencial para outro lugar.

Mudou egress_hosts ou environments? É mudança incompatível de descriptor: o provedor volta a registered até o ops reaplicar o egress. Veja Descriptor.

HMAC e replay

Toda rota do provedor (inclusive GET /descriptor e GET /health) exige o HMAC interno v2 do ADR-0015. Cabeçalhos:

Cabeçalho Conteúdo
Dracma-Caller Deve ser executor. Qualquer outro chamador autenticado recebe 403 caller_not_allowed (e alerta).
Dracma-Internal-Timestamp Segundos Unix, forma canônica (sem sinal, sem zero à esquerda).
Dracma-Internal-Nonce 32 hexadecimais minúsculos, CSPRNG, único por chamada.
Dracma-Internal-Signature v2=<hex> onde hex = HMAC-SHA256(segredo, canônico).

Texto canônico (separado por \n, método em maiúsculas, path completo com query):

v2
<timestamp>
<nonce>
<METODO>
<path?query>
<sha256 hexadecimal do corpo>

Exemplo de assinatura para teste (segredo obviamente falso):

SECRET='whsec_test_exemplo-nao-usar'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY='{"deposit_id":"00000000-0000-0000-0000-000000000000"}'
HASH=$(printf '%s' "$BODY" | sha256sum | cut -d' ' -f1)
CANON=$(printf 'v2\n%s\n%s\n%s\n%s\n%s' "$TS" "$NONCE" POST /spp/v1/charges/get "$HASH")
SIG=$(printf '%s' "$CANON" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
echo "Dracma-Internal-Signature: v2=$SIG"

Regras de implementação:

  • Comparação em tempo constante da assinatura (verify_slice em Rust, hmac.Equal em Go, CryptographicOperations.FixedTimeEquals em C#). Nunca == em string.
  • Janela de ±300 s no timestamp; fora dela, timestamp_out_of_window.
  • Nonce visto por 600 s: o provedor guarda os nonces recebidos por 600 s e recusa repetição com replayed_nonce. Memória basta; reinício apaga o cache e a janela do timestamp limita o dano. Registre o nonce depois que a assinatura confere (quem não tem o segredo não queima nonce alheio).
  • Ordem de recusas (adendo P051 do ADR-0015): missing_signature, unknown_caller, unsupported_signature_version, timestamp_out_of_window, invalid_nonce, invalid_signature, replayed_nonce, caller_not_allowed. Respostas 401 (autenticação) e 403 (chamador), com effect: none.
  • Segredo por provedor: o par INTERNAL_HMAC_<PROVEDOR>_EXECUTOR é único por provedor. Comprometer o de um não assina como outro. Rotacionar não toca credencial de tenant.
  • Deny-default por chamador: cada rota do OpenAPI tem x-callers: [executor]. Rota nova sem declaração de chamador não existe.
  • Teste de matriz de acesso no CI do provedor: anônimo, chamador errado, assinatura adulterada, nonce repetido, timestamp fora da janela, para cada rota (número de testes igual ao número de rotas do OpenAPI).

Replay do callback do parceiro

O HMAC acima protege o canal executor-provedor. O callback do parceiro tem a sua própria defesa, escolhida pelo esquema do parceiro:

Parceiro Assinatura Anti-replay
MaximaxPay x-signature: sha256=<hex> sobre o corpo cru Sem timestamp assinado. O callback só dispara fetch-back; repetir o mesmo callback não duplica efeito (idempotência por chave).
Mercado Pago x-signature: ts=<ts>,v1=<hex> sobre o manifesto ts assinado, janela de 600 s no adaptador (aceita segundos ou milissegundos). data.id é obrigatório e só aceita [A-Za-z0-9_-] até 64 caracteres.

Regras comuns: verificar sobre os bytes crus de body_base64 (nunca reserializar o JSON), em tempo constante, e não devolver o motivo fino da falha (422 callback_unauthenticated apenas). O parceiro que não assina tem callbacks.signature: null e callbacks: false.

Credencial por chamada

O executor guarda a credencial cifrada, decifra na hora do uso e a envia no corpo da chamada, em loopback. O provedor a usa e a descarta. Não existe segredo de tenant de longa duração no provedor. A alternativa de o provedor buscar a credencial no cofre foi rejeitada: ela daria ao provedor acesso ao KMS e a todos os tenants.

O que o provedor PODE fazer

Pode Condição
Ler credential do corpo e usá-la para chamar o parceiro declarado Somente os hosts de egress_hosts.
Derivar um token de curta duração (por exemplo OAuth) e guardá-lo em memória Chave (connection_id, sha256(credential)); TTL menor ou igual ao menor valor entre a expiração do token e 10 minutos; nunca em disco.
Validar credential contra o credential_schema Campo desconhecido gera 422 misconfigured com field.
Guardar o par HMAC com o executor É o único segredo permanente.
Guardar connection_id como escopo de cache e circuito Não é segredo.

O que o provedor NÃO PODE fazer

Não pode Por quê
Persistir credential (disco, banco, arquivo temporário) Comprometer o provedor expõe só as chamadas em voo, nunca um estoque.
Escrever credential em log, trace, métrica, mensagem de erro ou detail do problema Vazamento por telemetria é o caminho mais comum.
Devolvê-la em resposta A resposta é registrada pelo executor.
Enviá-la a qualquer terceiro que não seja o parceiro declarado Exfiltração.
Colocá-la em URL, query ou cabeçalho do canal SPP O corpo entra no hash do HMAC; URL e cabeçalho vazam em log de proxy. (Para o parceiro, siga o que o parceiro exige, como x-client-secret ou Authorization: Bearer.)
Reutilizar um token derivado depois que o hash da credencial mudou Rotação vale na próxima chamada; hash diferente significa cache diferente.
Cachear em disco, banco ou compartilhado entre processos O cache é local, em memória, por processo.
Imprimir a credencial em Debug ou toString Redija.

Zeroize e redação

  • Zerar buffers da credencial assim que a chamada ao parceiro termina (zeroize em Rust, Array.Clear ou CryptographicOperations.ZeroMemory em C#, equivalente na sua linguagem).
  • Redigir em Debug e toString: a representação de texto de qualquer tipo que contém segredo imprime ***.
  • Tipos de segredo não implementam serialização para saída; só para entrada.

O adaptador da MaximaxPay faz assim (Rust), e é o modelo a seguir:

#[derive(Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct MaximaxCredentials {
    pub client_id: String,
    pub client_secret: String,
    pub webhook_secret: String,
}

impl std::fmt::Debug for MaximaxCredentials {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("MaximaxCredentials(***)")
    }
}

Conexão desconhecida e credencial isca

O executor chama callbacks/verify com uma credencial aleatória do formato do schema quando a conexão não existe. O provedor não pode tentar distinguir isca de credencial real (nem por tempo de resposta, nem por log diferente). Se a isca "autenticar", o executor registra erro grave e recusa mesmo assim.

PII mínima e logs

O provedor recebe só o que o parceiro exige.

Dado Recebe? Observação
Nome e documento do pagador (cobrança) ou titular (saque) Sim, quando o parceiro exige Só dígitos no documento; descarte após a chamada.
IPv4 do jogador (owner.ip) Sim, no saque Obrigatório (C198).
Chave PIX do destino Sim, no saque Nunca em log.
tenant_id, e-mail de operador, saldo do ledger Não O provedor não conhece tenant nem ledger.
connection_id, operation_id Sim Correlação e escopo de cache; não são segredo.
Documento completo em keys/lookup Devolve, não registra O chamador compara e descarta; devolva também a versão mascarada.

Logs, traces e métricas:

  • Formato JSON, com trace_id, span_id, connection_id, provider, op, outcome.
  • Nunca: credencial, corpo devolvido pelo parceiro, documento, chave PIX, nome, IP do jogador.
  • Rótulos de métrica sem tenant nem connection_id (cardinalidade e privacidade): use op e outcome.
  • O traceparent do executor continua o trace no provedor, mas não é repassado ao parceiro.
  • Mensagem de erro (detail do problema) sem corpo do parceiro, sem credencial, sem PII.
  • Não use status do parceiro livre como motivo: reason de saque falho é curto, [a-z0-9_]{1,40}.

Exemplo de linha de log aceitável:

{
  "ts": "2026-10-31T12:00:00Z",
  "level": "warn",
  "service": "dracma_psp_maximaxpay",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "connection_id": "11111111-1111-4111-8111-111111111111",
  "provider": "maximaxpay",
  "op": "payouts.create",
  "outcome": "upstream_timeout",
  "upstream_status": null
}

Anti-SSRF de base_url

config.base_url é entrada do tenant (ou do operador). Sem controle, vira um pedido do host para onde o atacante quiser, com credencial anexada. Regras:

  1. O host de base_url deve estar em egress_hosts do descriptor (comparação exata do host e da porta; sem curinga). Fora disso: 422 misconfigured com field: base_url.
  2. Esquema https. Sem http, sem file, sem credenciais embutidas (usuario@host) na URL.
  3. Rejeite IP literal e nomes que resolvam para loopback, link-local, rede privada ou metadados de nuvem. Resolva o nome e confira o IP na hora da conexão (a conexão usa o IP validado, para não haver troca entre verificação e uso).
  4. Sem redirecionamento HTTP: o cliente não segue 3xx. Uma resposta de redirecionamento é erro upstream_unavailable ou unrecognized_response.
  5. O egress por UID é a segunda camada: mesmo com bug na validação, o pacote para host fora da lista é descartado.
  6. Fora de prd, a URL usada é a de environments.sandbox; a live só na exceção auditada. Veja a tabela da guarda.

Dependências e SBOM

  • Toda dependência nova tem nome, licença e versão verificados (typosquatting e licença incompatível são bloqueios).
  • Versões fixadas (lockfile commitado) e instalação reproduzível.
  • SBOM gerada no CI a cada build e guardada com o artefato.
  • Scan de vulnerabilidades no CI; achado crítico bloqueia a promoção.
  • Imagem (quando houver) por digest, assinada, não-root, somente leitura.
  • Sem dependência de domínio compartilhada entre provedores (nada de biblioteca commons): cada provedor é um serviço independente.
  • Binário promovido a partir do git (commit SHA); nunca compilado ou editado no servidor.

Provedor de terceiro: modelo de ameaça

Provedor de terceiro roda na mesma fronteira que o nosso, sem privilégio extra. A certificação é requisito, não confiança. Ele nunca recebe credencial de outro provedor, acesso ao cofre ou rede interna.

Resumo STRIDE:

Categoria Ameaça Vetor típico Mitigação
Spoofing Alguém se passa pelo executor Chamada local forjada, par HMAC vazado HMAC v2 com nonce e janela; x-callers deny-default; segredo por provedor; usuário próprio e loopback
Spoofing Alguém se passa pelo parceiro no callback POST forjado no ingress Assinatura do parceiro sobre corpo cru, tempo constante, IPs de origem, fetch-back antes de qualquer efeito
Tampering Adulteração do pedido em trânsito local Processo local lê e altera Hash do corpo no HMAC; usuário e namespace separados
Tampering Adulteração do resultado do saque Provedor mente settled Executor confirma por consulta; leitor tolerante (unrecognized nunca move dinheiro); certificação
Repudiation Negar uma operação Falta de rastro trace_id fim a fim; auditoria de register, enable, drain, disable, unplug, certify com descriptor_sha256
Information disclosure Vazamento de credencial ou PII Log, métrica, erro, dump de memória, rede Credencial só em memória e zerada; LimitCORE=0; logs sem PII; egress por UID; sem traceparent ao parceiro
Information disclosure Oráculo de existência de conexão Diferença de resposta no callback Credencial isca; 401 uniforme; sem motivo fino
Denial of service Provedor lento ou fora derruba o núcleo Parceiro caindo, flood Processo independente (piso 10); breaker no executor; 429 provider_busy; limites de corpo (1 MiB); Spp-Timeout-Ms
Denial of service Estouro de memória por corpo gigante Callback ou body_base64 enorme Teto de 1 MiB, decodificação com limite, schema estrito
Elevation of privilege Provedor comprometido alcança outros serviços Escalada local Usuário sem shell, NoNewPrivileges, capacidades vazias, ProtectSystem=strict, sem rede interna, sem banco e sem KMS
Elevation of privilege SSRF via base_url Config maliciosa Host em egress_hosts, sem redirecionamento, egress por UID
Supply chain Dependência ou binário malicioso Pacote envenenado, imagem alterada Versões fixadas, SBOM, scan, digest, certificação e promoção só por SHA

Específico de terceiro:

  • O código do terceiro passa por revisão e pela suíte de conformidade antes de enable em hml/prd.
  • Credencial de tenant só chega ao provedor do seu tipo. Um provedor de terceiro nunca vê credencial de MaximaxPay ou Mercado Pago.
  • Terceiro não tem rota própria de entrada para o mundo: callbacks passam pelo ingress e pelo executor.
  • Desplugar um terceiro não deixa segredo para trás (a credencial fica no cofre do executor, cifrada); o ciclo draining, disabled, unplugged preserva o dado financeiro.

Guarda de ambiente e efeito externo

Regra de ouro (piso 18): fora de prd, nada real chega a ninguém. A guarda é dupla: o executor aplica pelo descriptor e o provedor reaplica como defesa em profundidade.

Situação Resposta do provedor
mode: live com runtime_env diferente de prd, sem environment_policy permitir e sem context.live_exception: true 403 environment_forbidden, effect: none
mode: sandbox com runtime_env == prd 403 environment_forbidden, effect: none
Credencial de teste em live, ou credencial de produção em sandbox (via credential_mode) 403 environment_forbidden
synthetic: true em prd O executor recusa; o próprio provedor sintético recusa também
runtime_env ausente Tratado como não-prd (fail-closed)

Regras para o desenvolvedor:

  • Fora de prd, o provedor opera só contra sandbox ou fake local. Nunca contra a URL live.
  • A exceção "real em hml" (environment_policy.live_outside_prd = flag, PSP_REAL_OUTSIDE_PRD com o name, ambiente hml, operação production) é do dono, gravada só pelo ops, com auditoria. Código de provedor não tem atalho.
  • E-mail, SMS e webhook do parceiro nunca saem com destinatário real fora de prd. Fixtures usam apenas domínios de teste (.invalid, .example, .test) e endereços sintéticos.
  • Chave sandbox em não-prd; a chave de prd nunca existe em dev ou hml. O egress SMTP é bloqueado nesses ambientes.
  • Cobrança, saque e transferência real em teste automatizado são vetados. Teste do provedor roda contra servidor HTTP fake local.
  • A certificação prova a guarda (casos de ambiente e modo cruzados).

Auditoria

Evento Gravado
register, enable, drain, disable, unplug Quem, quando, motivo, descriptor_sha256, trace_id
certify Resultado da certificação (descriptor_sha256, versão da suíte, aprovados e total, hash do relatório)
unplug com force Motivo obrigatório (mínimo de 10 caracteres), break-glass auditado
Escrita de PSP_REAL_OUTSIDE_PRD Mesma auditoria da ADR-0026, só pelo ops
Chamada com HMAC inválido, chamador errado, replay Log de segurança e alerta
Decisão de guarda de ambiente (recusa) Log com trace_id, sem credencial

No provedor: registre outcome, op, provider, connection_id e trace_id por chamada, e as métricas spp_upstream_requests_total{op,outcome}, spp_upstream_duration_seconds{op}, spp_idempotent_replays_total{op} e spp_callbacks_verified_total{outcome}. Dashboards e alertas (provedor down, breaker aberto, unrecognized_response, dreno parado) são versionados como código.

Checklist de segurança do provedor

Marque cada item antes de pedir enable. Item não marcado bloqueia a promoção.

Isolamento e host

  • [ ] Roda com usuário Linux próprio, sem shell e sem home.
  • [ ] Unit systemd com NoNewPrivileges, CapabilityBoundingSet= vazio, ProtectSystem=strict, ProtectHome, PrivateTmp, RestrictAddressFamilies, LimitCORE=0.
  • [ ] MemoryDenyWriteExecute ligado (ou motivo registrado no ADR).
  • [ ] Único caminho gravável é o StateDirectory, sem segredo dentro.
  • [ ] Arquivo de ambiente 0400, só com o par HMAC do próprio provedor.
  • [ ] Container (se houver): não-root, raiz somente leitura, imagem por digest.

Rede

  • [ ] Escuta só em 127.0.0.1:<porta> (confirmado com ss).
  • [ ] Egress por UID gerado pelo ops a partir de egress_hosts; pacote fora da lista é descartado e alerta.
  • [ ] egress_hosts cobre todos os environments e tem host:porta.
  • [ ] Cliente HTTP sem proxy e sem seguir redirecionamento.
  • [ ] base_url fora de egress_hosts é recusada (422 misconfigured).

Autenticação

  • [ ] HMAC v2 em todas as rotas, com a ordem de recusas P051.
  • [ ] Comparação de assinatura em tempo constante.
  • [ ] Janela de ±300 s e nonce guardado por 600 s.
  • [ ] Só Dracma-Caller: executor; outro chamador recebe 403 caller_not_allowed.
  • [ ] Teste de matriz de acesso no CI cobre todas as rotas do OpenAPI.
  • [ ] Segredo HMAC exclusivo deste provedor.

Credencial e dados

  • [ ] Nenhuma credencial de tenant em disco, log, trace, métrica, erro ou resposta.
  • [ ] Buffers da credencial zerados e tipos de segredo redigidos em Debug e toString.
  • [ ] Cache só de token derivado, em memória, chave (connection_id, sha256(credential)), TTL no máximo 10 minutos.
  • [ ] Corpo, documento, chave PIX, nome e IP do jogador fora dos logs.
  • [ ] Rótulos de métrica sem tenant e sem connection_id.
  • [ ] traceparent não é repassado ao parceiro.

Protocolo

  • [ ] Schemas estritos nos pedidos (additionalProperties: false) e teto de corpo de 1 MiB.
  • [ ] Falha ambígua nunca vira definitiva (unknown) e recusa definitiva nunca vira ambígua.
  • [ ] Idempotência efetiva em charges e payouts (replay com Spp-Replayed: true, 409 em corpo diferente).
  • [ ] Saque nunca é reenviado; payouts/get responde para toda chave aceita.
  • [ ] Status desconhecido do parceiro vira unrecognized, nunca settled; saldo desconhecido nunca vira zero.
  • [ ] Callback verificado sobre bytes crus, em tempo constante, sem motivo fino, com isca indistinguível.
  • [ ] Capacidade true funciona e false responde 501 unsupported.

Ambiente e efeito externo

  • [ ] 403 environment_forbidden para modo e ambiente cruzados.
  • [ ] Fora de prd, só sandbox ou fake; nenhuma chamada à URL live.
  • [ ] Nenhum destinatário real em fixture, seed ou teste (somente .invalid, .example, .test).
  • [ ] Testes contra servidor fake local, nunca contra o parceiro real.

Cadeia de suprimentos e operação

  • [ ] Dependências com nome, licença e versão verificados; lockfile commitado.
  • [ ] SBOM gerada no CI e guardada com o artefato; scan sem achado crítico.
  • [ ] Artefato promovido do git por commit SHA; nada editado no servidor.
  • [ ] Suíte de conformidade aprovada para o descriptor_sha256 atual.
  • [ ] Auditoria de register, enable, drain, disable, unplug e certify funcionando.
  • [ ] Dashboards e alertas versionados no Helm chart.
  • [ ] Observabilidade: service.name igual a dracma_psp_<nome> e métricas spp_* publicadas.

Erros comuns

Erro Consequência Correção
Logar o corpo do parceiro "para depurar" Documento, nome e chave PIX em log Logue só outcome, upstream_status e trace_id
Cachear o token OAuth pela connection_id apenas Credencial trocada continua usando o token antigo Inclua sha256(credential) na chave
Comparar assinatura com == Ataque de tempo Função de tempo constante
Reserializar o JSON antes de verificar o callback Assinatura nunca confere (ou confere em corpo adulterado) Verifique os bytes de body_base64
Escutar em 0.0.0.0 "para testar" Provedor exposto à rede Só 127.0.0.1
Seguir redirecionamento do parceiro Credencial enviada a outro host Desligue o seguimento de redirecionamento
Liberar o domínio inteiro no egress Exfiltração fácil host:porta exatos, por UID
Tratar timeout do saque como falha definitiva Saque duplicado depois upstream_timeout com effect: unknown e consulta
Reutilizar o segredo HMAC entre provedores Um comprometido assina como todos Um par por provedor
Usar chave real em hml sem a exceção auditada Efeito real fora de prd Sandbox ou fake; exceção só pelo ops

Próximos passos

  • Referência do documento que declara tudo isso: Descriptor do provedor.
  • Conclua o checklist, rode a suíte de conformidade e só então peça o enable ao ops.