Executar HTTPS com dados fictícios
Guarda o código completo abaixo como main.go numa pasta nova. Executa go mod init example.test/waf-origin, go get github.com/corazawaf/coraza/v3@v3.8.0 e go run . evidence.json. O ensaio registado usa Go 1.27.1. Conserva go.mod e go.sum; depois de obter as dependências, podes repetir com GOPROXY=off. Cada caso cria um servidor HTTPS em loopback, usa o cliente que confia no certificado de teste e fecha os recursos. Não precisas de acquisition.json para executar. Os valores enviados são inventados e o programa não contacta aplicações externas.
Comparar ligação e declaração
O cliente envia X-Forwarded-For com 203.0.113.77, mas a regra de REMOTE_ADDR corresponde ao endereço de loopback da ligação. A regra sobre o valor literal do cabeçalho também consegue corresponder num caso separado. Estas observações são compatíveis: o wrapper ensaiado recebe o peer e os cabeçalhos como entradas distintas. O código versionado confirma essa integração. Não existe neste laboratório um proxy que reescreva o endereço. Num sistema real, regista o significado dos campos em cada camada antes de concluir que dois endereços diferentes representam um erro.
Presença não valida conteúdo
Sem o cabeçalho, a comparação com um valor literal não corresponde. Uma regra separada sobre a contagem recusa a ausência, mas um campo presente e vazio passa nesse controlo. O exercício separa três perguntas: existe uma entrada, o conteúdo é válido e a origem da declaração é confiável? Não confundas as respostas. Um IP sintaticamente válido pode continuar a ter sido fornecido pelo cliente. A política do serviço deve definir como tratar ausência, vazio, formato inválido e listas de valores, com exemplos positivos e negativos autorizados.
Desenhar a fronteira dos proxies
Numa API fictícia, desenha os caminhos pelo gateway e qualquer acesso direto permitido. Identifica quem aceita, substitui ou acrescenta valores e que componente escolhe a origem usada pela política. Uma lista de endereços não traz por si só a prova de confiança. A documentação AWS alerta para modificação e ausência de cabeçalhos; isso orienta a análise, mas não torna este fixture num ensaio AWS WAF. Para outro conector, valida a sua configuração e comportamento. O laboratório direto apenas demonstra a entrada fornecida por WrapHandler nesta versão.
Investigar uma migração com recusas
Num cenário fictício de consulta de posições, a entrada passa para um gateway comum. Vários consumidores ficam associados ao mesmo peer e uma política por IP começa a recusar tráfego legítimo. Compara a chave efetiva antes e depois, os volumes e a topologia. Não aumentes o limiar sem perceber a agregação, nem aceites imediatamente qualquer cabeçalho como identidade. Propõe uma mitigação delimitada, reproduz o efeito com clientes sintéticos autorizados e define rollback. O gestor coordena rede, APS e segurança para fechar a decisão com evidência.
Preparar a aceitação de origem
A matriz de aceitação proposta inclui passagem pelo gateway, acesso direto autorizado, campo ausente, vazio e cadeia com vários hops. Para cada linha, regista peer observado, declaração, origem escolhida, resultado esperado e resultado obtido. Preserva a correlação sem recolher credenciais reais. A verificação TLS local demonstra confiança no certificado de teste, não autenticação do cabeçalho nem aceitação da PKI de produção. Entrega a RUN os responsáveis, a configuração e os passos de diagnóstico. Esta ficha é uma proposta didática, sem workshop humano executado ou aprovação de produção.
// Original DR loopback TLS fixture. Harmless values; no external targets or real secrets.
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"time"
"github.com/corazawaf/coraza/v3"
corazahttp "github.com/corazawaf/coraza/v3/http"
"github.com/corazawaf/coraza/v3/types"
)
type Result struct {
Name string `json:"name"`
Configuration string `json:"configuration"`
Status int `json:"status"`
AppCalls int `json:"applicationCalls"`
TLSVerified bool `json:"tlsVerified"`
RuleIDs []int `json:"matchedRuleIds"`
MatchedValues []string `json:"matchedValues"`
AuditRecords int `json:"auditRecords"`
AuditContainsBodyMarker bool `json:"auditContainsBodyMarker"`
AuditContainsFakeToken bool `json:"auditContainsFakeToken"`
AuditHasRequestBody bool `json:"auditHasRequestBody"`
AuditHasRequestHeaders bool `json:"auditHasRequestHeaders"`
AuditHasMessages bool `json:"auditHasMessages"`
RawAudit string `json:"rawAudit"`
ServerClosed bool `json:"serverClosed"`
WAFClosed bool `json:"wafClosed"`
}
func check(ok bool, message string) {
if !ok {
panic(message)
}
}
func hashFile(name string) string {
data, err := os.ReadFile(name)
if err != nil {
panic(err)
}
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
const bodyMarker = "DR-NOTE-ALPHA"
const fakeToken = "DR-FAKE-TOKEN-NOT-A-CREDENTIAL"
func evaluate(root, name, auditMode, parts, rules string, forwarded *string, body string, appStatus int) Result {
r := Result{Name: name, RuleIDs: []int{}, MatchedValues: []string{}}
logPath := filepath.Join(root, name+".json")
r.Configuration = "SecRuleEngine On\nSecRequestBodyAccess On\nSecResponseBodyAccess Off\nSecAuditEngine " + auditMode + "\nSecAuditLogType Serial\nSecAuditLogFormat JSON\nSecAuditLogParts " + parts + "\nSecAuditLogRelevantStatus ^5\nSecAuditLog " + logPath + "\n" + rules
waf, err := coraza.NewWAF(coraza.NewWAFConfig().WithDirectives(r.Configuration).WithErrorCallback(func(m types.MatchedRule) {
r.RuleIDs = append(r.RuleIDs, m.Rule().ID())
for _, d := range m.MatchedDatas() {
r.MatchedValues = append(r.MatchedValues, d.Value())
}
}))
if err != nil {
panic(err)
}
closer, ok := waf.(io.Closer)
check(ok, "WAF closer available")
defer func() {
if !r.WAFClosed {
_ = closer.Close()
}
}()
app := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r.AppCalls++
_, err := io.ReadAll(req.Body)
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(appStatus)
_, err = io.WriteString(w, "synthetic-result")
if err != nil {
panic(err)
}
})
server := httptest.NewTLSServer(corazahttp.WrapHandler(waf, app))
defer server.Close()
client := server.Client()
client.Timeout = 5 * time.Second
transport := client.Transport.(*http.Transport)
check(!transport.TLSClientConfig.InsecureSkipVerify, "TLS verification enabled")
req, err := http.NewRequest("POST", server.URL+"/submit", strings.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", fakeToken)
if forwarded != nil {
req.Header.Set("X-Forwarded-For", *forwarded)
}
response, err := client.Do(req)
if err != nil {
panic(err)
}
_, err = io.ReadAll(response.Body)
if err != nil {
panic(err)
}
response.Body.Close()
r.Status = response.StatusCode
r.TLSVerified = response.TLS != nil && len(response.TLS.VerifiedChains) > 0
transport.CloseIdleConnections()
server.Close()
_, err = server.Listener.Accept()
r.ServerClosed = errors.Is(err, net.ErrClosed)
err = closer.Close()
if err != nil {
panic(err)
}
r.WAFClosed = true
data, err := os.ReadFile(logPath)
if err != nil && !os.IsNotExist(err) {
panic(err)
}
r.RawAudit = string(data)
r.AuditContainsBodyMarker = strings.Contains(r.RawAudit, bodyMarker)
r.AuditContainsFakeToken = strings.Contains(r.RawAudit, fakeToken)
for _, line := range strings.Split(strings.TrimSpace(r.RawAudit), "\n") {
if line == "" {
continue
}
var record map[string]any
if err = json.Unmarshal([]byte(line), &record); err != nil {
panic(err)
}
r.AuditRecords++
tx, _ := record["transaction"].(map[string]any)
request, _ := tx["request"].(map[string]any)
bodyValue, _ := request["body"].(string)
r.AuditHasRequestBody = bodyValue != ""
headers, _ := request["headers"].(map[string]any)
r.AuditHasRequestHeaders = len(headers) > 0
messages, _ := record["messages"].([]any)
r.AuditHasMessages = len(messages) > 0
}
return r
}
func has(r Result, id int) bool {
for _, n := range r.RuleIDs {
if n == id {
return true
}
}
return false
}
func main() {
check(len(os.Args) == 2, "output path required")
root, err := os.MkdirTemp("", "dr-waf-origin-")
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
forwarded := "203.0.113.77"
empty := ""
peerRule := `SecRule REMOTE_ADDR "@ipMatch 127.0.0.1,::1" "id:6101,phase:1,t:none,pass,log,noauditlog,msg:'observed loopback peer'"
SecRule REMOTE_ADDR "@ipMatch 203.0.113.77" "id:6102,phase:1,t:none,deny,status:403,log,msg:'configured remote address'"`
headerRule := `SecRule REQUEST_HEADERS:X-Forwarded-For "@streq 203.0.113.77" "id:6103,phase:1,t:none,deny,status:403,log,msg:'literal client-supplied header'"`
presenceRule := `SecRule &REQUEST_HEADERS:X-Forwarded-For "@eq 0" "id:6104,phase:1,t:none,deny,status:400,log,msg:'required fixture header absent'"`
auditRule := `SecRule ARGS:comment "@streq DR-NOTE-ALPHA" "id:6201,phase:2,t:none,pass,log,auditlog,msg:'original audit marker',logdata:'%{MATCHED_VAR}'"`
noAuditRule := strings.Replace(auditRule, ",auditlog,", ",noauditlog,", 1)
cases := map[string]Result{}
add := func(name, mode, parts, rules string, h *string, body string, status int) Result {
r := evaluate(root, name, mode, parts, rules, h, body, status)
cases[name] = r
check(r.TLSVerified && r.ServerClosed && r.WAFClosed, name+" TLS and cleanup")
return r
}
checks := map[string]any{}
r := add("tls-clean", "Off", "AZ", "", nil, "comment=ordinary", 200)
check(r.Status == 200 && r.AppCalls == 1, "TLS clean")
checks["tlsControl"] = map[string]any{"status": r.Status, "verified": r.TLSVerified, "applicationCalls": r.AppCalls}
r = add("peer-vs-header", "Off", "AZ", peerRule, &forwarded, "comment=ordinary", 200)
check(r.Status == 200 && has(r, 6101) && !has(r, 6102), "peer vs forwarded header")
checks["peerIdentity"] = map[string]any{"loopbackRuleMatched": has(r, 6101), "forwardedValueUsedAsRemoteAddress": has(r, 6102)}
r = add("literal-header", "Off", "AZ", headerRule, &forwarded, "comment=ordinary", 200)
check(r.Status == 403 && r.AppCalls == 0 && has(r, 6103), "literal header")
checks["literalHeaderRule"] = map[string]any{"status": r.Status, "applicationCalls": r.AppCalls, "clientSupplied": true}
r = add("header-absent", "Off", "AZ", headerRule, nil, "comment=ordinary", 200)
check(r.Status == 200 && !has(r, 6103), "header absent")
checks["absentHeader"] = map[string]any{"status": r.Status, "literalRuleMatched": has(r, 6103)}
r = add("presence-required", "Off", "AZ", presenceRule, nil, "comment=ordinary", 200)
check(r.Status == 400 && has(r, 6104) && r.AppCalls == 0, "explicit presence rule")
checks["explicitPresencePolicy"] = map[string]any{"status": r.Status, "applicationCalls": r.AppCalls}
r = add("header-empty", "Off", "AZ", presenceRule+"\n"+headerRule, &empty, "comment=ordinary", 200)
check(r.Status == 200 && !has(r, 6104) && !has(r, 6103), "empty differs from absent")
checks["emptyHeader"] = map[string]any{"status": r.Status, "missingRuleMatched": has(r, 6104), "literalRuleMatched": has(r, 6103), "validIPAddressProven": false}
r = add("audit-on", "On", "ABCFHZ", "", nil, "comment="+bodyMarker, 200)
check(r.AuditRecords == 1 && r.AuditHasRequestBody && r.AuditHasRequestHeaders && r.AuditContainsFakeToken, "audit all parts")
checks["auditAllTransactions"] = map[string]any{"records": r.AuditRecords, "bodyPresent": r.AuditHasRequestBody, "fakeTokenPresent": r.AuditContainsFakeToken}
clean := add("relevant-clean", "RelevantOnly", "AHZ", "", nil, "comment=ordinary", 200)
failure := add("relevant-503", "RelevantOnly", "AHZ", "", nil, "comment=ordinary", 503)
check(clean.AuditRecords == 0 && failure.AuditRecords == 1, "relevant status")
checks["statusSelection"] = map[string]any{"clean200Records": clean.AuditRecords, "application503Records": failure.AuditRecords}
r = add("match-audit-filtered", "RelevantOnly", "AHZ", auditRule, nil, "comment="+bodyMarker, 200)
check(r.Status == 200 && r.AuditRecords == 0 && has(r, 6201), "v3.8.0 status filter still applies")
checks["ruleAuditFiltered"] = map[string]any{"status": r.Status, "records": r.AuditRecords, "matched": has(r, 6201)}
r = add("match-noaudit-503", "RelevantOnly", "AHZ", noAuditRule, nil, "comment="+bodyMarker, 503)
check(r.Status == 503 && r.AuditRecords == 1 && has(r, 6201) && !r.AuditHasMessages, "noaudit does not veto status-selected transaction")
checks["ruleNoAuditStatus"] = map[string]any{"status": r.Status, "records": r.AuditRecords, "matched": has(r, 6201), "messagesPresent": r.AuditHasMessages}
r = add("message-data", "On", "AHZ", auditRule, nil, "comment="+bodyMarker, 200)
check(!r.AuditHasRequestBody && !r.AuditHasRequestHeaders && r.AuditHasMessages && r.AuditContainsBodyMarker && !r.AuditContainsFakeToken, "H still contains matched data")
checks["messageDataRemains"] = map[string]any{"bodyPartPresent": r.AuditHasRequestBody, "headersPartPresent": r.AuditHasRequestHeaders, "messagesPresent": r.AuditHasMessages, "markerPresent": r.AuditContainsBodyMarker}
r = add("minimal-az", "On", "AZ", auditRule, nil, "comment="+bodyMarker, 200)
check(r.AuditRecords == 1 && !r.AuditHasMessages && !r.AuditContainsBodyMarker && !r.AuditContainsFakeToken, "AZ reduced evidence")
checks["minimalRecord"] = map[string]any{"records": r.AuditRecords, "messagesPresent": r.AuditHasMessages, "markerPresent": r.AuditContainsBodyMarker, "fakeTokenPresent": r.AuditContainsFakeToken}
info, ok := debug.ReadBuildInfo()
check(ok, "build info")
version := ""
for _, d := range info.Deps {
if d.Path == "github.com/corazawaf/coraza/v3" {
version = d.Version
}
}
check(version == "v3.8.0", "pinned Coraza")
artifacts := map[string]string{}
for _, n := range []string{"main.go", "go.mod", "go.sum"} {
artifacts[n] = hashFile(n)
}
if _, err = os.Stat("acquisition.json"); err == nil {
artifacts["acquisition.json"] = hashFile("acquisition.json")
}
if err = os.RemoveAll(root); err != nil {
panic(err)
}
_, err = os.Stat(root)
removed := os.IsNotExist(err)
result := map[string]any{"checkedAt": time.Now().UTC().Format(time.RFC3339), "engine": "Coraza " + version, "goVersion": runtime.Version(), "passed": len(checks), "failed": 0, "checks": checks, "cases": cases, "artifacts": artifacts, "temporaryDirectoryRemoved": removed, "scope": "Actual HTTPS over loopback with Coraza WrapHandler and httptest TLS servers; verified test-server certificate, synthetic headers and JSON audit files. No external targets, trusted-proxy rewriting, identity authentication, full CRS, AWS WAF, attacks, real secrets or production acceptance.", "independentSpecialistReview": false}
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
panic(err)
}
if err = os.WriteFile(os.Args[1], append(data, '\n'), 0600); err != nil {
panic(err)
}
fmt.Println("Coraza origin and audit: twelve original HTTPS observation groups passed")
}
Depois de uma migração fictícia, consumidores partilham o peer do gateway. O ensaio deve explicar a agregação antes de alterar a chave ou o limiar.
Armadilhas comuns
Confundir peer com origem declarada, presença com validade e TLS do servidor com autenticação do cabeçalho; generalizar o wrapper direto para outro conector.
Tópicos relacionados: Arquitetura e cobertura do WAF · Taxa, origem e clientes · Logs, mudanças e passagem para RUN
Decide que origem usar apenas depois de demonstrar quem a fornece e como cada percurso é tratado. Conserva o peer observado como evidência distinta.
Referência: Coraza HTTP middleware source · DR WAF 2026-09; selected AWS WAF and OWASP CRS operational concepts