Endpoint autentikasi publik seperti /api/auth/login merupakan target utama serangan credential stuffing dan brute force. Tanpa mekanisme pembatasan frekuensi permintaan yang ketat, penyerang dapat mengeksekusi ribuan kombinasi kredensial per detik hingga membebani basis data atau menemukan kombinasi kata sandi yang valid. Di ekosistem Rust, rate limiting Actix Web dapat diimplementasikan secara efisien menggunakan crate actix-governor.

Artikel ini membahas konfigurasi produksi untuk actix-governor, strategi mitigasi bypass IP di balik reverse proxy, isolasi kuota per endpoint, custom response HTTP 429 dengan header Retry-After, serta verifikasi perilakunya via integration testing.

1. Memahami Algoritma GCRA pada Governor

Mayoritas library rate limiter tradisional menggunakan algoritma Token Bucket atau Leaky Bucket dengan sinkronisasi berbasis timer. actix-governor dibangun di atas crate governor, yang mengimplementasikan Generic Cell Rate Algorithm (GCRA).

GCRA memperlakukan lalu lintas jaringan seperti sel individual dalam saluran telekomunikasi. Alih-alih menghitung ulang jumlah token setiap detik, GCRA menghitung satu nilai waktu teoritis kedatangan sel berikutnya (Theoretical Arrival Time / TAT). Pendekatan ini menghasilkan keunggulan penting untuk aplikasi web berkinerja tinggi:

  • Efisiensi Memori: Setiap entri IP/kunci hanya menyimpan satu representasi waktu (misal 64-bit integer), bukan struktur data kompleks dengan counter dan timer.
  • Bebas Background Thread: Tidak membutuhkan worker thread terpisah untuk proses reset token secara periodik.
  • Lock Contention Rendah: Struktur internal berbasis atomic/non-blocking data structures meminimalkan bottleneck pada aplikasi multi-threaded Actix Web.

2. Dependensi dan Setup Proyek

Tambahkan dependensi berikut ke dalam file Cargo.toml Anda:

[dependencies]
actix-web = "4.9"
actix-governor = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

3. Strategi KeyExtractor di Balik Reverse Proxy

Secara default, actix-governor menggunakan PeerIpKeyExtractor yang membaca IP peer socket koneksi TCP. Jika aplikasi dijalankan di belakang reverse proxy seperti NGINX, Cloudflare, atau AWS ALB, seluruh permintaan masuk akan memiliki IP peer yang sama, yaitu IP internal proxy tersebut.

Peringatan Keamanan: Jangan mengambil header mentah seperti X-Forwarded-For tanpa validasi ketat. Klien luar dapat memalsukan (spoof) header tersebut untuk melewati batasan rate limit jika reverse proxy Anda tidak membersihkan atau menimpa header tersebut.

Gunakan helper connection_info().realip_remote_addr() dari Actix Web yang telah dikonfigurasi dengan trust proxy, atau buat custom KeyExtractor yang mengambil IP secara deterministik:

use actix_governor::{KeyExtractor, SimpleKeyExtractionError};
use actix_web::dev::ServiceRequest;
use std::net::IpAddr;

#[derive(Clone)]
pub struct SecureClientIpExtractor;

impl KeyExtractor for SecureClientIpExtractor {
    type Key = IpAddr;
    type KeyExtractionError = SimpleKeyExtractionError;

    fn extract(&self, req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
        // realip_remote_addr() membaca header yang dikonfigurasi dari reverse proxy terpercaya
        req.connection_info()
            .realip_remote_addr()
            .and_then(|addr| addr.parse::<IpAddr>().ok())
            .ok_or_else(|| SimpleKeyExtractionError::new("Gagal mengekstrak IP klien yang valid"))
    }
}

4. Konfigurasi Governor dan Custom Error Handler (HTTP 429)

Klien API membutuhkan respons terstruktur ketika terkena rate limit, termasuk header standar Retry-After untuk memberitahukan kapan permintaan dapat dicoba kembali. Konfigurasikan GovernorConfigBuilder dengan batas burst yang ketat untuk login (misal: maksimum 5 percobaan instan, isi ulang kuota 1 token per 2 detik):

use actix_governor::{GovernorConfig, GovernorConfigBuilder};
use actix_web::{http::header::RETRY_AFTER, HttpResponse};
use serde_json::json;

pub fn build_auth_governor_config() -> GovernorConfig<SecureClientIpExtractor> {
    GovernorConfigBuilder::default()
        .per_second(2)         // 1 token setiap 2 detik
        .burst_size(5)         // Kapasitas burst maksimum 5 request
        .key_extractor(SecureClientIpExtractor)
        .error_handler(|err| {
            // Custom JSON response ketika kuota habis
            HttpResponse::TooManyRequests()
                .insert_header((RETRY_AFTER, "2"))
                .json(json!({
                    "status": "error",
                    "code": 429,
                    "message": "Terlalu banyak percobaan autentikasi. Akses ditangguhkan sementara.",
                    "error": err.to_string()
                }))
        })
        .finish()
        .expect("Konfigurasi Governor tidak valid")
}

5. Isolasi Rate Limiter pada Scope Endpoint Sensitif

Menerapkan rate limiting ketat secara global pada seluruh aplikasi web adalah anti-pattern yang dapat merusak user experience untuk aset statis atau query baca normal. Terapkan middleware Governor hanya pada scope /auth atau langsung pada route spesifik:

use actix_governor::Governor;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};

async fn login_handler() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({"message": "Login diproses"}))
}

async fn public_handler() -> impl Responder {
    HttpResponse::Ok().body("Endpoint publik tanpa rate limit ketat")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let auth_governor_conf = build_auth_governor_config();

    HttpServer::new(move || {
        App::new()
            // Route umum tanpa limit agresif
            .route("/api/ping", web::get().to(public_handler))
            // Scope khusus autentikasi dengan isolasi rate limit
            .service(
                web::scope("/api/auth")
                    .wrap(Governor::new(&auth_governor_conf))
                    .route("/login", web::post().to(login_handler)),
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

6. Verifikasi Perilaku Rate Limiter via Integration Test

Pastikan perilaku rate limiting teruji secara otomatis menggunakan modul actix_web::test. Test berikut memverifikasi bahwa setelah 5 request beruntun berhasil, request ke-6 langsung ditolak dengan status HTTP 429 Too Many Requests dan membawa header Retry-After.

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::http::{header, StatusCode};
    use actix_web::{test, web, App};

    #[actix_web::test]
    async fn test_auth_rate_limiting_enforcement() {
        let governor_conf = build_auth_governor_config();

        let app = test::init_service(
            App::new().service(
                web::scope("/api/auth")
                    .wrap(Governor::new(&governor_conf))
                    .route("/login", web::post().to(login_handler)),
            ),
        )
        .await;

        // 5 request pertama diizinkan (sesuai burst_size = 5)
        for _ in 0..5 {
            let req = test::TestRequest::post()
                .uri("/api/auth/login")
                .peer_addr("192.0.2.1:12345".parse().unwrap()) // Mocked client IP
                .to_request();

            let resp = test::call_service(&app, req).await;
            assert_eq!(resp.status(), StatusCode::OK);
        }

        // Request ke-6 harus di-reject dengan HTTP 429
        let blocked_req = test::TestRequest::post()
            .uri("/api/auth/login")
            .peer_addr("192.0.2.1:12345".parse().unwrap())
            .to_request();

        let blocked_resp = test::call_service(&app, blocked_req).await;
        assert_eq!(blocked_resp.status(), StatusCode::TOO_MANY_REQUESTS);
        assert!(blocked_resp.headers().contains_key(header::RETRY_AFTER));
    }
}

7. Trade-off dan Pertimbangan Produksi

Sebelum menerapkan konfigurasi ini di lingkungan produksi, pertimbangkan batasan operasional berikut:

  • In-Memory Storage: actix-governor menyimpan state rate limit di memori lokal proses Actix Web. Jika aplikasi di-scale secara horizontal ke beberapa kontainer (misal: Kubernetes pods), kuota rate limit tidak terbagi antar kontainer. Solusinya: gunakan rate limiter berbasis Redis terpusat atau terapkan rate limiting di lapisan ingress/API Gateway.
  • Kelemahan CGRA pada NAT Bersama: Banyak klien sah yang berada di belakang satu NAT perusahaan atau ISP seluler akan berbagi kuota IP yang sama. Untuk skenario tingkat lanjut, kombinasikan ekstraksi IP dengan identifier lain, seperti hash dari username yang dicoba pada payload login.