← WAF: proteção e operação de aplicações
11 / 12 · 60 MIN

Origem, proxies e confiança

Executa pedidos HTTPS locais para distinguir o peer de ligação de uma declaração X-Forwarded-For e desenha a aceitação de uma cadeia de proxies.

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")
}
NA PRÁTICA

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

Leva esta ideia contigo

Decide que origem usar apenas depois de demonstrar quem a fornece e como cada percurso é tratado. Conserva o peer observado como evidência distinta.

Criar conta

Referência: Coraza HTTP middleware source · DR WAF 2026-09; selected AWS WAF and OWASP CRS operational concepts