Bali, Indonesia
Insights

Membedah Kegagalan Latensi WAF pada Concurrency Tinggi: Analisis Profiling ModSecurity v3 vs Coraza Engine

October 13, 2026
0:00
Ketika lonjakan trafik mencapai 75.000 request per detik pada klaster edge reverse proxy kami, anomali kritis muncul seketika: p99 latency melonjak dari 14ms menjadi 890ms, sementara utilisasi memori pada node Nginx melonjak hingga memicu kernel Out-Of-Memory (OOM) killer. Di balik kegagalan ini bukan terletak pada kapasitas throughput jaringan, melainkan pada overhead pemrosesan Web Application Firewall (WAF) yang menggunakan ModSecurity v3 (libmodsecurity) terikat OWASP Core Rule Set (CRS) v4. Evaluasi ulang terhadap subsistem inspeksi payload menjadi prioritas mutlak, mendorong kami melakukan profiling mendalam dan pengujian komparatif head-to-head terhadap implementasi modern berbasis Go: Coraza WAF.
ModSecurity v3 dirancang ulang dari arsitektur monolitik v2 menjadi model modular berbasis pustaka (libmodsecurity) yang dipisahkan dari Nginx connector. Secara konseptual, pemisahan ini menjanjikan isolasi proses yang bersih, namun pada beban konkurensi masif, pola alokasi memori internalnya menjadi liabilitas. Dalam ModSecurity v3, setiap request HTTP menginstansiasi objek transaksi terisolasi (msc_new_transaction). Alokasi memori untuk evaluasi variabel (seperti ARGS_NAMES, REQUEST_HEADERS, REQUEST_BODY) mengandalkan std::string dan container standar C++ yang memicu alokasi heap mikro ribuan kali per transaksi. Pada sistem multithreaded atau multiproses Nginx pekerja tinggi, alokator glibc standar menciptakan ratusan memori arena (malloc arenas) untuk meminimalkan lock contention global.
TEXT
+-------------------------------------------------------------+
|                        NGINX WORKER                         |
|                                                             |
|  +------------------+         +--------------------------+  |
|  | HTTP Request Buf | ------> | Nginx ModSec Connector   |  |
|  +------------------+         +--------------------------+  |
|                                            |                |
|                                            v                |
|                               +--------------------------+  |
|                               | libmodsecurity C++ API   |  |
|                               | - msc_process_headers()  |  |
|                               | - msc_process_body()     |  |
|                               +--------------------------+  |
|                                            |                |
|            +-------------------------------+----------------+  |
|            |                               |                |
|            v                               v                |
|   +------------------+            +------------------+      |
|   | PCRE2 Regex JIT  |            | glibc Heap Arena |      |
|   | Backtracking     |            | Fragmentation    |      |
|   +------------------+            +------------------+      |
+-------------------------------------------------------------+
Fragmentasi ini diperparah oleh mesin regex PCRE/PCRE2. Ketika aturan regex kompleks CRS dievaluasi pada payload body yang terfragmentasi, alokasi memori transient melonjak drastis. Jika libpcre2 tidak dikompilasi dengan dukungan JIT stack yang cukup, evaluasi regex terpaksa fallback ke heap allocation untuk menyimpan state backtracking, menghasilkan saturasi instruksi CPU per cycle dan lonjakan alokasi dirty pages di tingkat kernel.
Coraza mengadopsi pendekatan arsitektur yang berakar pada karakteristik runtime Go moderen. Alih-alih melakukan alokasi heap untuk setiap token string yang diparsing, Coraza memaksimalkan penggunaan slice referencing langsung terhadap buffer transaksi yang mendasarinya (zero-copy parsing jika payload sudah berada di memory). Keuntungan deterministik Coraza terletak pada integrasi sync.Pool untuk pooling struktur internal transaksi dan buffer evaluasi regex. Alih-alih mendelegasikan alokasi jutaan pointer kecil ke glibc, Coraza mendaur ulang objek transaksi yang sudah selesai. Hal ini menekan aktivitas Go Garbage Collector (GC) secara radikal, membatasi siklus mark-and-sweep bahkan ketika konkurensi melonjak tinggi.
GO
// cuplikan abstraksi pooling transaksi Coraza internal
type TransactionPool struct {
    pool sync.Pool
}

func NewTransactionPool(waf WAF) *TransactionPool {
    return &TransactionPool{
        pool: sync.Pool{
            New: func() any {
                return waf.NewTransaction()
            },
        },
    }
}

func (p *TransactionPool) Acquire() *Transaction {
    tx := p.pool.Get().(*Transaction)
    tx.Reset()
    return tx
}

func (p *TransactionPool) Release(tx *Transaction) {
    p.pool.Put(tx)
}
Implementasi interface regex pada Coraza mengintegrasikan engine berbasis Go RE2 murni atau wrapper binding CGO ke PCRE2 yang dioptimalkan secara ketat tanpa lock traversal berulang pada shared state.
Pengujian dilakukan di lingkungan bare-metal terisolasi dengan spesifikasi:
  • CPU: AMD EPYC 9654 (128 Cores, 256 Threads)
  • RAM: 256 GB DDR5 ECC
  • OS: Ubuntu 24.04 LTS (Kernel 6.8.0-38-generic)
  • Trafik Generator: k6 cluster running on 4 dedicated bare-metal nodes via 100GbE Mellanox ConnectX-6
Skenario uji menyuntikkan trafik HTTP/2 sintetis sebesar 60.000 RPS konstan selama 10 menit dengan variasi payload:
  1. 70% GET benign traffic dengan variasi query parameters.
  2. 20% POST payload JSON (5 KB - 25 KB) dengan sanitasi field dasar.
  3. 10% Malicious payload yang memicu signature SQL Injection dan Cross-Site Scripting (XSS).
Ketika profil CPU diambil menggunakan perf pada proses Nginx yang menjalankan ModSecurity v3:
BASH
perf record -F 99 -p $(pgrep -d',' -f 'nginx: worker') -g -- sleep 30
perf report --stdio --no-children
Output stack trace dominan menunjukkan bottlenecks berikut:
TEXT
# Overhead  Symbol
# ........  .......................................................
    24.12%  [libpcre2-8.so.0.11.2]        pcre2_match_8
    18.45%  [libc.so.6]                   _int_malloc
    14.20%  [libmodsecurity.so.3.0.10]    modsecurity::actions::transformations::...
    11.83%  [libc.so.6]                   _int_free
     8.15%  [libmodsecurity.so.3.0.10]    modsecurity::Transaction::processBody
     5.41%  [libpthread.so.0]             pthread_mutex_lock
Lebih dari 32% siklus CPU dihabiskan semata-mata untuk siklus alokasi dan dealokasi memori (_int_malloc dan _int_free) di dalam glibc, memicu lock contention pada arena memori saat multi-thread/multi-core mencoba mengklaim bucket memori yang sama.
Pada pengujian Coraza (dijalankan sebagai Envoy WASM filter / native reverse proxy daemon), profiling memori dan CPU diekstraksi melalui net/http/pprof:
BASH
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
Grafik eksekusi CPU Coraza menunjukkan distribusi yang jauh lebih sehat:
  • 48.2% runtime dialokasikan langsung untuk pencocokan regex dan transformasi rule.
  • 8.1% untuk decoding JSON dan multipart parser.
  • Alokasi runtime GC (runtime.gcWork, runtime.mallocgc) terkendali secara konsisten di bawah 7.4% dari total CPU footprint.
Berikut adalah data telemetri agregat yang dikumpulkan selama fase beban puncak (60.000 RPS, CRS v4 enabled):
Metrik EvaluasiNginx + ModSecurity v3Nginx / Caddy + Coraza EngineDelta Relatif
Throughput Stabil38.400 RPS (Bottlenecked)59.850 RPS+55.8%
P50 Latency4.8 ms1.2 ms-75.0%
P95 Latency38.1 ms4.9 ms-87.1%
P99 Latency412.0 ms12.8 ms-96.8%
Resident Set Size (RSS)38.4 GB (Terfragmentasi)4.2 GB-89.0%
CPU Utilization (Avg)98.2%46.5%-52.6%
Context Switches/sec~840.000~112.000-86.6%
Bagi infrastruktur yang belum dapat langsung beralih dari ModSecurity v3, mitigasi alokator memori wajib dilakukan dengan mengganti alokator default glibc menggunakan jemalloc guna mengurangi arena lock contention.
Tambahkan pre-loading pustaka jemalloc ke dalam systemd unit file Nginx:
INI
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=1048576
LimitMEMLOCK=infinity
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
Environment="MALLOC_CONF=background_thread:true,metadata_thp:auto,dirty_decay_ms:5000,muzzy_decay_ms:5000"
Optimasi konfigurasi modsecurity.conf untuk membatasi konsumsi memori parsing body:
NGINX
# Batasi alokasi memory buffer untuk request body
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyInMemoryLimit 131072

# Matikan inspeksi response body jika tidak krusial untuk mitigasi kebocoran data
SecResponseBodyAccess Off

# Tuning engine PCRE2 match limits untuk mencegah catastrophic backtracking
SecPcreMatchLimit 150000
SecPcreMatchLimitRecursion 150000
Dalam deployment berbasis Coraza (misalnya menggunakan Caddy Coraza extension atau Envoy Coraza WASM/Native), arsitektur inspeksi didefinisikan secara deklaratif dengan isolasi parsing terstruktur:
CADDYFILE
:443 {
    bind 10.0.4.15
    
    # TLS Offloading
    tls /etc/ssl/certs/edge.crt /etc/ssl/certs/edge.key {
        protocols tls1.3
    }

    coraza_waf {
        load_owasp_crs
        directives `
            Include /etc/coraza/coraza.conf
            SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=1"
            SecRuleEngine On
            SecRequestBodyAccess On
            SecRequestBodyLimit 10485760
            SecRequestBodyInMemoryLimit 262144
            
            # Konfigurasi tuning performa internal Coraza
            SecArgumentsLimit 250
        `
    }

    reverse_proxy 10.0.5.0/24 {
        transport http {
            keepalive 300s
            keepalive_idle_conns 1024
        }
    }
}
Pengaturan kernel host pendukung untuk mencegah soket backlog saturation pada layer ingress:
BASH
cat << 'EOF' > /etc/sysctl.d/99-waf-performance.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 100000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65