Hardening API Auth di edge berarti memindahkan sebagian kontrol keamanan paling kritis ke titik masuk trafik, sebelum request mencapai service internal. Tujuannya bukan sekadar menolak request tanpa token, tetapi membangun pipeline autentikasi yang cepat untuk kasus normal dan ketat untuk kasus berisiko: header diparse secara defensif, token diverifikasi dengan benar, hasil verifikasi dapat di-cache secara aman, rate limit diterapkan per IP dan per identitas, serta sistem tetap fail-closed ketika dependency auth bermasalah.

Pendekatan ini mirip prinsip pemisahan jalur cepat dan jalur aman pada sistem akselerator modern: request yang valid dan umum diproses seefisien mungkin, sementara request ambigu, mencurigakan, atau butuh verifikasi lebih dalam dialihkan ke jalur yang lebih ketat. Pada API gateway modern, desain seperti ini membantu menekan biaya validasi, mengurangi risiko brute force, dan membatasi dampak gangguan pada identity provider, key store, atau service revocation.

Mengapa autentikasi di edge perlu di-hardening

Banyak sistem berhenti di langkah “cek token ada atau tidak”. Itu tidak cukup. Pada praktiknya, kegagalan auth di edge sering terjadi karena kombinasi hal berikut:

  • Header Authorization diparse terlalu longgar, sehingga format tidak valid tetap lolos ke service.
  • JWT hanya di-decode, bukan diverifikasi tanda tangannya.
  • Cache hasil verifikasi dipakai tanpa memperhatikan masa berlaku token atau status revocation.
  • Rate limit hanya per IP, padahal serangan sering menyebar ke banyak IP dengan identitas yang sama, atau sebaliknya.
  • Saat dependency auth gagal, gateway tetap membiarkan request lewat demi “availability”, padahal ini membuka celah bypass.

Hardening yang baik harus mengurangi ruang kesalahan tersebut tanpa membuat latensi melonjak tajam. Kuncinya adalah menyusun pipeline yang berlapis dan deterministik.

Desain pipeline auth berlapis di API gateway

Berikut alur yang praktis untuk gateway atau edge proxy. Ide utamanya: lakukan pemeriksaan murah dan deterministik lebih dulu, lalu masuk ke verifikasi yang lebih mahal hanya jika request lolos tahap awal.

Client
  |
  v
[Edge / API Gateway]
  |
  +-- 1. Normalisasi request dan parsing header ketat
  |
  +-- 2. Klasifikasi credential
  |       - Bearer JWT
  |       - API Key
  |       - Tidak ada / invalid format
  |
  +-- 3. Rate limit awal per IP / subnet / route
  |
  +-- 4. Validasi credential
  |       - JWT: signature, iss, aud, exp, nbf, alg, kid
  |       - API key: lookup hash, status, scope, expiry
  |
  +-- 5. Cache hasil verifikasi yang aman
  |
  +-- 6. Cek revocation / status session
  |
  +-- 7. Rate limit lanjutan per identitas / tenant / token fingerprint
  |
  +-- 8. Forward ke upstream dengan identity context minimal
  |
  v
[Backend Service]

Pipeline ini memisahkan pekerjaan menjadi dua jenis:

  • Jalur cepat: validasi murah, deterministik, dan cacheable untuk request normal.
  • Jalur aman: pemeriksaan tambahan untuk token yang butuh verifikasi mendalam, request yang mendekati limit, dependency yang lambat, atau indikasi brute force.

1. Parsing header yang ketat

Header auth adalah input dari pihak luar. Jangan menganggap formatnya selalu rapi. Parsing yang terlalu permisif dapat memicu bypass, cache poisoning, atau perbedaan perilaku antara gateway dan backend.

Prinsip yang sebaiknya diterapkan:

  • Terima hanya satu header Authorization. Jika ada duplikasi, tolak.
  • Terima skema yang eksplisit, misalnya hanya Bearer atau header khusus X-API-Key bila memang dibutuhkan.
  • Tolak karakter kontrol, spasi berlebih yang tidak wajar, atau token kosong.
  • Jangan fallback ke query parameter untuk token kecuali benar-benar diwajibkan dan risikonya dipahami.
  • Pastikan normalisasi konsisten antara edge dan service internal.

Catatan: Banyak bug auth bukan berasal dari kriptografi, tetapi dari parsing yang tidak konsisten. Jika gateway menerima format yang backend tafsirkan berbeda, Anda membuka ruang perilaku tak terduga.

2. Validasi JWT dengan benar

Untuk JWT, hardening tidak berhenti di signature check. Minimal, gateway perlu memeriksa:

  • Algoritma: jangan menerima algoritma yang tidak diizinkan. Hindari pemilihan algoritma hanya dari isi token tanpa whitelist.
  • Signature: verifikasi menggunakan kunci yang sesuai.
  • Issuer (iss): pastikan token diterbitkan oleh issuer yang dipercaya.
  • Audience (aud): cocokkan dengan API atau gateway ini, bukan sekadar “token valid”.
  • Expiry (exp) dan Not Before (nbf): terapkan toleransi skew waktu secara kecil dan terukur.
  • Key ID (kid): bila menggunakan JWKS, pastikan resolusi kunci aman dan tidak memicu lookup liar.
  • Subject / client / scope: pastikan klaim yang dipakai untuk otorisasi memang ada dan valid.

Kesalahan umum adalah menganggap JWT yang dapat di-decode berarti valid. Token harus diverifikasi tanda tangannya dan klaimnya, lalu dipetakan ke kebijakan akses yang tepat.

3. Validasi API key tanpa menyimpan plaintext

Jika API Anda juga menerima API key, perlakukan API key seperti kredensial sensitif:

  • Simpan bentuk hash-nya, bukan plaintext.
  • Gunakan prefix publik untuk lookup awal agar pencarian efisien, lalu verifikasi hash penuh.
  • Simpan metadata: status aktif, tenant, scope, masa berlaku, dan waktu rotasi.
  • Dukung revocation cepat tanpa menunggu TTL cache terlalu lama.

API key cocok untuk integrasi server-to-server yang sederhana, tetapi biasanya lebih lemah dari token yang punya masa berlaku pendek. Karena itu, pembatasan scope, rate limit, dan observability menjadi lebih penting.

4. Cache hasil verifikasi, bukan keputusan permanen

Cache di edge penting untuk menekan latensi dan beban ke identity provider atau key store. Namun cache auth harus dirancang hati-hati.

Yang aman untuk di-cache biasanya adalah hasil verifikasi token atau metadata API key dengan TTL pendek, bukan keputusan akses yang terlalu luas dan tahan lama.

  • Untuk JWT, TTL cache tidak boleh melebihi sisa umur token.
  • Untuk API key, TTL harus seimbang dengan kebutuhan revocation. TTL terlalu lama membuat pencabutan lambat terasa.
  • Sertakan faktor penting dalam cache key, misalnya fingerprint token, issuer, audience, dan versi kebijakan bila relevan.
  • Jangan cache hasil gagal terlalu lama jika penyebabnya bisa bersifat sementara, misalnya JWKS belum tersinkron.

Strategi yang umum: positive cache pendek untuk token valid, negative cache sangat singkat untuk kegagalan tertentu, dan pemisahan yang jelas antara error operasional dan token yang memang tidak sah.

Rotasi secret, revocation, dan session invalidation

Rotasi kunci tanpa downtime

Hardening auth tidak lengkap tanpa rotasi kunci. Untuk JWT dengan penandatanganan asimetris, rotasi biasanya lebih aman karena verifikasi dapat dilakukan dengan public key yang dipublikasikan melalui JWKS. Untuk secret simetris atau API key, proses rotasi perlu lebih hati-hati karena distribusi secret lebih sensitif.

Praktik yang disarankan:

  • Dukung beberapa kunci aktif dalam masa transisi.
  • Gunakan kid atau identifier kunci yang jelas.
  • Pastikan cache kunci di edge punya TTL yang masuk akal dan bisa di-refresh.
  • Siapkan prosedur rotasi darurat saat kompromi terindikasi.

Kesalahan umum adalah mengganti kunci sekaligus tanpa masa overlap, yang memicu lonjakan error validasi di edge.

Revocation token dan session

JWT sering dipilih karena bisa diverifikasi lokal tanpa panggilan ke auth server. Trade-off-nya: revocation menjadi lebih sulit, terutama untuk token dengan masa berlaku panjang. Jika Anda butuh kemampuan logout cepat, pemutusan sesi, atau pemblokiran token terkompromi, edge perlu memeriksa sumber revocation.

Pendekatan yang umum:

  • Token berumur pendek: mengurangi kebutuhan revocation aktif, tetapi meningkatkan frekuensi refresh.
  • Denylist / revocation store: edge memeriksa ID token, session ID, atau subject tertentu terhadap store berlatensi rendah.
  • Versioned session: setiap user atau client punya versi sesi; jika versi berubah, token lama ditolak.

Pilihan terbaik bergantung pada kebutuhan logout instan, beban trafik, dan toleransi latensi. Untuk API sensitif, menggabungkan token berumur pendek dengan revocation store biasanya lebih aman daripada hanya mengandalkan exp.

Rate limit berlapis: per IP dan per identitas

Rate limit bukan fitur tambahan; ini bagian dari hardening auth. Tujuannya bukan hanya mencegah overload, tetapi juga membatasi percobaan brute force, credential stuffing, dan abuse terhadap endpoint autentikasi.

Mengapa per IP saja tidak cukup

Rate limit per IP mudah diterapkan dan murah dihitung, tetapi punya keterbatasan:

  • Pengguna sah di belakang NAT dapat saling mempengaruhi.
  • Penyerang dapat menyebar serangan ke banyak IP.
  • Untuk API partner, satu IP tidak selalu merepresentasikan satu identitas.

Karena itu, rate limit yang lebih kuat biasanya menggabungkan beberapa dimensi:

  • Per IP atau subnet untuk penyaringan awal.
  • Per route, terutama endpoint login, token exchange, atau API sensitif.
  • Per identitas: subject, client ID, API key ID, tenant, atau session.
  • Per kombinasi IP + identitas untuk mendeteksi pola anomali.

Strategi yang praktis

Gunakan dua tahap pembatasan:

  1. Pre-auth rate limit sebelum verifikasi penuh: berbasis IP, subnet, user-agent fingerprint terbatas, dan route. Ini murah dan berguna untuk menyaring noise.
  2. Post-auth rate limit setelah identitas diketahui: berbasis user, client, tenant, API key, atau scope. Ini lebih presisi dan lebih adil.

Dengan strategi ini, request anonim yang berisik dapat diblok lebih cepat, sementara request sah dibatasi berdasarkan identitas sebenarnya.

Proteksi brute force dan abuse

Untuk endpoint sensitif seperti login, token issuance, atau API key introspection, pertimbangkan:

  • Backoff bertahap setelah beberapa kegagalan.
  • Bucket terpisah untuk sukses dan gagal.
  • Pembatasan terhadap kombinasi username/email + IP.
  • Pemisahan limit untuk endpoint murah dan endpoint mahal.
  • Alarm ketika ada lonjakan invalid token, invalid signature, atau API key miss.

Jangan mengembalikan pesan error yang terlalu detail pada endpoint publik. Untuk penyerang, perbedaan antara “user tidak ada”, “password salah”, atau “key nonaktif” adalah informasi berharga.

Fail-closed saat dependency auth bermasalah

Salah satu keputusan arsitektur terpenting adalah apa yang terjadi ketika dependency auth gagal: JWKS tidak dapat diambil, store revocation timeout, cache cluster bermasalah, atau database API key tidak responsif.

Untuk endpoint yang membutuhkan autentikasi, prinsip paling aman adalah fail-closed: jika gateway tidak bisa membuktikan request valid, request ditolak. Ini lebih aman daripada fail-open yang mengorbankan keamanan demi availability.

Namun implementasinya perlu nuansa:

  • Gunakan cache lokal atau distributed cache agar gangguan sesaat tidak langsung memutus semua request valid.
  • Bedakan dependency kritis dan non-kritis. Misalnya, logging tambahan boleh gagal tanpa menolak request; verifikasi signature tidak boleh.
  • Untuk route publik, fail-closed tidak relevan; pastikan kebijakan per route jelas.
  • Siapkan mode degradasi terkontrol, misalnya hanya menerima token yang sudah ada di cache valid lokal untuk waktu sangat terbatas saat auth server terganggu. Ini tetap harus dirancang hati-hati dan diaudit.

Prinsip praktis: jangan mengizinkan request terautentikasi hanya karena sistem verifikasi sedang tidak sehat. Lebih baik mengembalikan 503 atau 401/403 yang tepat daripada membuka bypass.

Contoh pseudo-code middleware di edge

Berikut pseudo-code yang menggambarkan pipeline auth berlapis. Sintaks dibuat generik agar bisa diadaptasi ke gateway, reverse proxy, atau middleware aplikasi.

function handleRequest(req) {
  routePolicy = lookupRoutePolicy(req.method, req.path)

  if (routePolicy.public) {
    return forward(req)
  }

  authHeader = getSingleHeader(req, "authorization")
  apiKeyHeader = getSingleHeader(req, "x-api-key")

  if (!preAuthRateLimitAllow(req.ip, req.path)) {
    return reject(429, "rate_limit_exceeded")
  }

  credential = parseCredentialStrict(authHeader, apiKeyHeader)
  if (!credential.ok) {
    recordAuthEvent("invalid_credential_format", req)
    return reject(401, "invalid_authorization")
  }

  principal = null

  if (credential.type == "jwt") {
    tokenFp = fingerprint(credential.token)
    principal = authCache.get(tokenFp)

    if (!principal) {
      keySet = loadTrustedKeysFailClosed()
      verified = verifyJwt({
        token: credential.token,
        allowedAlgs: routePolicy.allowedAlgs,
        issuer: routePolicy.issuer,
        audience: routePolicy.audience,
        keys: keySet,
        clockSkewSec: 30
      })

      if (!verified.ok) {
        recordAuthEvent("jwt_verification_failed", req, verified.reason)
        return reject(401, "invalid_token")
      }

      if (isRevoked(verified.claims.jti, verified.claims.sid, verified.claims.sub)) {
        recordAuthEvent("token_revoked", req)
        return reject(401, "revoked_token")
      }

      principal = buildPrincipalFromClaims(verified.claims)
      authCache.put(tokenFp, principal, ttl = min(verified.ttl, 60))
    }
  }

  if (credential.type == "api_key") {
    keyFp = fingerprint(credential.key)
    principal = authCache.get(keyFp)

    if (!principal) {
      keyRecord = lookupApiKeyByPrefix(credential.key.prefix)
      if (!keyRecord || !verifyApiKeyHash(credential.key, keyRecord.hash)) {
        recordAuthEvent("api_key_invalid", req)
        return reject(401, "invalid_api_key")
      }
      if (!keyRecord.active || keyRecord.expired) {
        return reject(401, "inactive_api_key")
      }
      principal = buildPrincipalFromKeyRecord(keyRecord)
      authCache.put(keyFp, principal, ttl = 30)
    }
  }

  if (!postAuthRateLimitAllow(principal.id, req.path, req.ip)) {
    recordAuthEvent("principal_rate_limited", req, principal.id)
    return reject(429, "rate_limit_exceeded")
  }

  req.headers["x-auth-sub"] = principal.sub
  req.headers["x-auth-tenant"] = principal.tenant
  req.headers["x-auth-scope"] = join(principal.scopes, " ")

  return forward(req)
}

Hal penting dari pseudo-code di atas:

  • Parsing credential dilakukan sebelum verifikasi berat.
  • JWT diverifikasi dengan whitelist algoritma dan klaim penting.
  • Cache dipakai hanya untuk hasil verifikasi yang sudah tervalidasi.
  • Revocation diperiksa sebelum request diteruskan.
  • Rate limit dilakukan dua kali: sebelum dan sesudah identitas diketahui.
  • Jika dependency kunci atau revocation gagal secara kritis, request tidak dibiarkan lolos diam-diam.

Trade-off performa vs keamanan

Tidak ada desain auth yang gratis. Semakin ketat pemeriksaan, semakin besar biaya CPU, memori, atau latensi. Yang perlu dicari adalah komposisi yang tepat.

Apa yang biasanya menambah latensi

  • Verifikasi kriptografi pada setiap request tanpa cache.
  • Lookup ke revocation store untuk semua request.
  • Rate limit dengan cardinality tinggi tanpa struktur data yang efisien.
  • Fetch kunci dinamis yang terlalu sering.

Apa yang biasanya mengurangi risiko

  • TTL token lebih pendek.
  • Revocation aktif untuk sesi berisiko tinggi.
  • Fail-closed untuk dependency auth kritis.
  • Rate limit per identitas selain per IP.
  • Parsing header yang sangat ketat.

Kompromi yang sering masuk akal adalah:

  • Gunakan JWT berumur pendek dan cache verifikasi singkat di edge.
  • Terapkan revocation hanya pada kategori token atau route yang benar-benar sensitif.
  • Pisahkan kebijakan per route: endpoint baca publik tidak perlu perlakuan yang sama dengan endpoint admin atau mutasi finansial.
  • Gunakan jalur cepat untuk request valid yang umum, tetapi jangan menghapus jalur aman untuk kasus tepi dan anomali.

Checklist observability untuk auth di edge

Tanpa observability, hardening auth sulit diuji dan lebih sulit dioperasikan. Minimal, pantau metrik dan log berikut:

Metrik utama

  • Jumlah request per route, dipisah antara publik dan butuh auth.
  • Rasio 2xx, 401, 403, 429, dan 5xx.
  • Hit/miss cache verifikasi token dan metadata API key.
  • Latensi verifikasi JWT, lookup API key, dan cek revocation.
  • Jumlah kegagalan per reason: invalid format, invalid signature, expired, audience mismatch, revoked.
  • Jumlah trigger rate limit per IP, per principal, dan per route.
  • Kesehatan dependency: key store, JWKS fetch, revocation store, cache.

Logging yang perlu ada

  • Request ID atau correlation ID.
  • Hasil auth dan alasan penolakan yang dapat dianalisis.
  • Fingerprint token atau key, bukan nilai rahasianya.
  • Identitas principal setelah lolos verifikasi.
  • Keputusan rate limit dan bucket yang terpicu.

Pastikan log tidak menyimpan token atau API key mentah. Itu kesalahan yang masih sering terjadi dan dapat mengubah sistem logging menjadi sumber kebocoran rahasia.

Alert yang berguna

  • Lonjakan tajam 401 atau 429 pada route auth.
  • Penurunan mendadak cache hit rate.
  • Kegagalan fetch kunci atau lonjakan timeout pada store revocation.
  • Perubahan distribusi issuer, audience, atau algoritma token yang tidak biasa.

Anti-pattern umum di API gateway modern

  • Decode tanpa verify: isi JWT dibaca lalu langsung dipercaya.
  • Fail-open saat auth dependency gagal: request diteruskan karena auth server timeout.
  • Rate limit hanya per IP: mudah diakali dan sering merugikan pengguna sah di balik NAT.
  • Cache terlalu lama: token yang sudah dicabut tetap diterima.
  • Tidak ada rotasi kunci yang teruji: pergantian secret memicu outage atau jendela kompromi yang panjang.
  • Meneruskan seluruh token ke downstream tanpa perlu: memperluas permukaan paparan kredensial.
  • Error terlalu detail: membantu enumerasi akun atau jenis kegagalan token.
  • Auth policy tidak konsisten antar layer: gateway menolak satu format, backend menerima format lain, atau sebaliknya.

Rekomendasi implementasi yang realistis

Jika Anda sedang membangun atau merapikan gateway, urutan prioritas yang masuk akal adalah:

  1. Terapkan parsing header yang ketat dan konsisten.
  2. Pastikan JWT benar-benar diverifikasi, termasuk iss, aud, exp, dan whitelist algoritma.
  3. Tambahkan pre-auth rate limit per IP dan per route.
  4. Tambahkan post-auth rate limit per identitas atau tenant.
  5. Masukkan cache verifikasi dengan TTL pendek dan aman.
  6. Bangun mekanisme rotasi kunci dan prosedur darurat.
  7. Tambahkan revocation untuk token/sesi yang membutuhkan pemutusan cepat.
  8. Audit mode kegagalan dependency dan ubah ke fail-closed untuk route yang terlindungi.
  9. Lengkapi observability sebelum skala trafik meningkat.

Pada akhirnya, hardening API auth di edge bukan soal menumpuk fitur keamanan sebanyak mungkin. Yang lebih penting adalah menyusun jalur eksekusi yang jelas: pemeriksaan murah dilakukan lebih awal, validasi identitas dilakukan dengan benar, cache dipakai tanpa mengorbankan revocation, dan setiap kegagalan dependency ditangani dengan asumsi aman. Dengan desain seperti ini, Anda bisa menjaga latensi tetap terkendali sambil memperkecil peluang bypass, brute force, dan penyalahgunaan identitas di titik masuk sistem.