Skip to content

Menutup Pintu Tidak Menyembunyikan Laci

Adityo Guni Waluyo

Port deploy ditutup, /.git/ tetap kebaca lewat hostname staging. Dua lapis penutup berikutnya: guard regex nginx edge dan network split api/db.

Ringkasan

Tutup port inbound doang ternyata belum cukup aman, soalnya path sensitif kayak .git dan .env masih kebuka lewat hostname staging. Solusinya tiga lapis: blokir dotfiles di nginx edge, pisah network Docker per kelompok service biar nggak ada jalur lateral, plus server_tokens off buat sembunyiin versi. Kuncinya, satu lapis nggak cukup, harus saling nutup lubang.

Pagi itu, setelah port inbound pada deployment ditutup, saya menjalankan pengujian sederhana ke hostname staging dengan curl. Saya berharap mendapatkan halaman error standar atau timeout koneksi, tanda jaringan sudah tertutup rapat. Sebaliknya, request ke path /.git/ dan /.env justru mengembalikan isinya dengan status 200 OK. Menutup pintu akses utama dari internet ternyata tidak otomatis menyembunyikan laci yang terbuka di dalam sistem.

Dugaan Awal yang Keliru

Saya berasumsi konfigurasi tunnel outbound-only sudah cukup. Logikanya sederhana: tidak ada permukaan yang terpapar langsung ke internet, jadi lapisan edge dan jaringan internal tidak perlu disentuh lagi. Permukaan dianggap aman dan sesi pengamanan dianggap selesai.

Kenyataannya, tunnel tersebut berhenti di nginx edge yang berjalan di mesin yang sama dengan aplikasi. Path pengembangan tetap terbaca melalui hostname staging karena tidak ada aturan pemblokiran. Lebih jauh lagi, pola bawaan Docker Compose menempatkan semua layanan dalam satu network untuk seluruh proyek [3], sehingga semua service bisa saling berbicara hanya karena ikut deklarasi proyek yang sama [2]. Menutup port deploy ternyata baru lapis pertama.

Guard di Lapisan Edge

Solusi pertama dimulai di edge. Modul ngx_http_access_module mendukung aturan deny dan allow dalam konteks server atau location [1], dan aturannya dicek berurutan sampai cocok pertama sebelum evaluasi berhenti [1]. Sebuah location bisa berupa prefix atau regex, dengan modifier ~ untuk case-sensitive dan ~* untuk case-insensitive; polanya dicocokkan terhadap URI ternormalisasi, di mana karakter %XX sudah di-decode dan referensi . atau .. sudah diselesaikan sebelum matching berjalan [4]. Upaya traversal yang di-encode ikut terblokir otomatis karena polanya sudah kena di lapis normalisasi:

location ~ /\.(?!well-known) {
    deny all;
}

Aturan deny yang menumpuk punya catatan resmi: kalau daftarnya panjang, nginx menyarankan memakai variabel dari modul geo agar tetap terbaca [1]. Exception /.well-known/ saya sisakan sengaja supaya challenge ACME untuk sertifikat Let's Encrypt tetap terjawab — guard yang memblokir terlalu lebar bisa memutus paspor domain sendiri.

Penutup versi ikut dibersihkan dengan server_tokens off: nginx berhenti menyebut versinya di header Server dan halaman error, yang secara default selalu ditampilkan [4].

Isolasi Jaringan Docker

Detail kecil yang mudah terlewat: pada bridge bawaan, container tidak bisa memanggil satu sama lain lewat nama [2], yang sejarahnya memaksa orang menyetel IP statis antar container — kebiasaan yang justru membuat audit isolasi makin sulit. User-defined network menyelesaikan dua masalah sekaligus: nama service hidup kembali lewat DNS bawaan, dan jangkauannya bisa dibatasi per kelompok [2].

Lapisan kedua membatasi siapa yang boleh bicara ke siapa. Bridge default Docker memberi container akses tak terbatas satu sama lain [2]. Pola yang lebih aman: user-defined network per kelompok layanan — API dan database hanya berbagi jaringan internal, terpisah dari jaringan web [2]. Compose memang membuat satu network default per proyek dengan DNS nama service [3], jadi network terpisah harus dideklarasikan eksplisit supaya komunikasi lateral mati sejak awal. Dua driver ekstrem diingat saja sebagai pagar: host menghapus isolasi antara container dan host, none mengisolasi container sepenuhnya dari host dan container lain [2]. Verifikasinya ringkas dan bisa diulang siapa pun: kirim request ke path titik-titik sensitif, pastikan ditolak di edge dengan 403 sebelum menyentuh aplikasi, lalu masuk ke container database dan pastikan tidak ada jalur apa pun menuju jaringan web dari sana.

Sesi pengamanan tidak dianggap selesai dengan satu lapisan yang rapi. Tunnel yang benar berarti tidak ada pintu masuk tak terduga. Guard edge berarti tidak ada laci konfigurasi yang terbuka. Network split berarti tidak ada jalur lateral untuk container yang dikompromikan. Ketiganya berdiri sendiri-sendiri dan saling menutupi lubang yang ditinggalkan yang lain; dua dari tiga tetap menyisakan cerita yang sama seperti pagi pembuka tadi.

Sumber

[1] nginx Docs, Module ngx_http_access_module
[2] Docker Docs, Networking overview
[3] Docker Docs, Networking in Compose
[4] nginx Docs, Module ngx_http_core_module

Artikel terkait