SSD Nodes Learn Hosting plans →
Panduan Matt ConnorOleh Matt Connor · Diperbarui 2026-08-29

Cara Memperbaiki Docker yang Melewati UFW

Docker memasang aturan iptables yang melewati UFW, sehingga port yang ditolak tetap menjawab dari Internet. Pahami penyebabnya dan dua perbaikan yang berfungsi.

Mengapa Docker melewati UFW

Docker melewati UFW karena port container yang dipublikasikan tidak pernah melewati aturan firewall yang dikelola UFW. Saat Anda menjalankan docker run -p 8080:80, Docker menulis aturan DNAT (destination network address translation) ke dalam chain PREROUTING pada tabel nat milik kernel. Aturan tersebut mengubah tujuan setiap paket menjadi alamat privat container sebelum kernel menentukan tujuan paket itu. Paket yang telah diubah kemudian diteruskan ke container melalui chain FORWARD, yang dikendalikan oleh Docker. Aturan UFW berada di chain INPUT, dan paket tersebut tidak pernah memasukinya. Karena itu, ufw status menampilkan default deny, sudo ufw deny 8080 melaporkan keberhasilan, tetapi port 8080 tetap merespons dari seluruh Internet.

Ini bukan bug Docker, dan UFW juga tidak rusak. Kedua alat tersebut memprogram firewall kernel yang sama. Aturan Docker hanya bekerja lebih awal pada jalur paket, sehingga UFW tidak pernah diminta untuk memprosesnya. Panduan ini mendemonstrasikan bypass tersebut, menjelaskan mekanismenya, lalu membahas dua perbaikan yang berfungsi: mempublikasikan port pada 127.0.0.1 dan melakukan pemfilteran pada chain DOCKER-USER. Jika UFW masih baru bagi Anda, siapkan terlebih dahulu dengan panduan dasar firewall UFW, karena firewall dengan kebijakan default-deny tetap merupakan dasar yang tepat untuk komponen lain di server.

Lihat bypass ini di server Anda sendiri

Mulai dari VPS dengan UFW yang aktif dan kebijakan default deny untuk trafik masuk. Jalankan container web dengan port yang dipublikasikan:

sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpine

ufw status verbose menampilkan Default: deny (incoming), allow (outgoing) dan tidak ada rule untuk port 8080. Berdasarkan laporan firewall sendiri, port tersebut tertutup. Sekarang lakukan pengujian dari mesin lain, bukan dari server itu sendiri:

curl -I http://your-vps-ip:8080/
HTTP/1.1 200 OK

Container merespons. Tambahkan rule deny secara eksplisit, lalu lakukan pengujian lagi:

sudo ufw deny 8080/tcp

Port tersebut masih merespons karena rule deny berada di chain yang tidak pernah dilewati paket. UFW tidak gagal. UFW memang tidak pernah dipanggil. Inilah alasan masalah ini sulit terdeteksi: tidak ada error yang ditampilkan di mana pun, deployment berhasil, dan output status firewall terlihat persis seperti server yang dikunci dengan baik.

Mekanismenya: PREROUTING berjalan sebelum INPUT

Kernel memproses paket masuk dalam urutan tetap, dan seluruh masalah ini disebabkan oleh urutan tersebut.

  1. PREROUTING berjalan lebih dahulu. Aturan di sini dapat menulis ulang tujuan paket, dan aturan Docker untuk port yang dipublikasikan memang melakukan hal itu.
  2. Keputusan routing berlangsung berikutnya. Paket yang ditujukan ke host itu sendiri masuk ke chain INPUT. Paket yang ditujukan ke mesin lain masuk ke chain FORWARD.
  3. Aturan UFW berada di INPUT. Aturan Docker berada di FORWARD.

Periksa aturan Docker untuk container yang baru saja Anda jalankan:

sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
target   prot opt source       destination
RETURN   0    --  0.0.0.0/0    0.0.0.0/0
DNAT     6    --  0.0.0.0/0    0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

Baris DNAT menjelaskan seluruh masalahnya. Setiap paket yang masuk untuk port 8080 akan ditulis ulang tujuannya menjadi 172.17.0.2:80, yaitu alamat container pada jaringan bridge privat Docker. Setelah penulisan ulang tersebut, paket tidak lagi ditujukan ke host. Karena itu, keputusan routing mengirimnya melalui jalur FORWARD, tempat Docker telah menambahkan aturan yang menerima trafik ke jaringannya sendiri. Aturan deny 8080/tcp Anda menunggu di INPUT untuk paket yang tidak pernah tiba.

Pada Ubuntu 24.04, perintah iptables merupakan front end untuk nftables, tetapi urutan chain dan hasilnya tetap sama. UFW dan Docker sama-sama menulis aturan ke pipeline paket kernel yang sama, dan titik masuk Docker berada lebih awal. Hal ini tidak khusus terjadi pada UFW: firewalld pada VPS Rocky atau AlmaLinux memfilter pada titik yang sama dalam pipeline tersebut dan dilewati oleh aturan DNAT yang sama, sehingga perbaikan di bawah ini juga berlaku di sana.

Perbaikan umum: publikasikan port pada 127.0.0.1

Sebagian besar container sejak awal tidak perlu dapat diakses publik. Database, server aplikasi di belakang reverse proxy, panel admin, atau endpoint metrik tidak boleh menjawab koneksi Internet secara langsung. Publikasikan layanan tersebut pada alamat loopback:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpine

Atau dalam file Compose:

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "127.0.0.1:8080:80"

Ini berfungsi karena aturan DNAT Docker sekarang hanya cocok dengan paket yang ditujukan ke 127.0.0.1. Paket dari Internet tidak mungkin secara sah membawa alamat tujuan tersebut, sehingga kernel menjatuhkannya sebelum aturan firewall mana pun dijalankan. Port tersebut dapat diakses dari host, tetapi tidak dari tempat lain. Verifikasi binding-nya:

sudo ss -tlnp | grep 8080

Output yang diharapkan adalah 127.0.0.1:8080, bukan 0.0.0.0:8080 atau [::]:8080. Selanjutnya, pastikan dari mesin lain bahwa curl http://your-vps-ip:8080/ ditolak.

Untuk layanan yang memang harus menghadap Internet, jalankan satu reverse proxy yang mengelola port 80 dan 443 serta melakukan routing berdasarkan hostname. Jangan publikasikan port lain. Itulah pola yang dibangun dalam panduan reverse proxy Traefik, dan cara aplikasi self-hosted seperti Nextcloud pada VPS tetap tidak dapat dijangkau selain melalui proxy-nya. Cara mendeklarasikan entri ports:, serta alur kerja Compose lainnya, dibahas dalam panduan dasar Docker Compose.

Dengan setiap container internal menggunakan loopback, UFW kembali menjalankan fungsi normalnya: melindungi port yang dilayani langsung oleh host. Susun set aturan tersebut di sini, lalu jalankan perintah-perintah berikut secara berurutan:

ToolUFW rule generator

Penyaringan nyata: chain DOCKER-USER

Terkadang port container harus tetap dipublikasikan ke jaringan, tetapi aksesnya harus dibatasi. Contohnya adalah port replika database yang hanya boleh diakses oleh satu alamat kantor. Untuk kebutuhan ini, Docker menyediakan chain DOCKER-USER. Setiap paket yang menuju container mana pun melewati DOCKER-USER sebelum mencapai aturan accept milik Docker, dan Docker tidak pernah menulis aturan ke dalamnya. Chain ini disediakan untuk aturan Anda, dan Docker tidak mengubah isinya saat daemon dimulai ulang.

Perhatikan satu hal sebelum menjalankan perintah: saat paket mencapai DOCKER-USER, penulisan ulang DNAT sudah terjadi. Port tujuan paket adalah port container, yaitu 80 dalam contoh ini, bukan port yang dipublikasikan, yaitu 8080. Karena itu, aturan yang mencocokkan --dport 8080 tidak akan cocok dengan paket mana pun. Cara yang andal adalah mencocokkan port yang pertama kali dituju client, yang disimpan oleh connection tracker kernel:

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP

Artinya: untuk paket yang masuk melalui eth0 dan termasuk dalam koneksi dengan port tujuan asli 8080, drop semua paket yang tidak dikirim dari 10.0.0.10. Pencocokan --ctdir ORIGINAL membatasi aturan ini pada arah client ke container, sehingga paket balasan tidak ikut tertangkap. Ganti eth0 dengan interface publik Anda; ip route | grep default menampilkan namanya. Uji dengan cara yang sama seperti sebelumnya: curl dari alamat yang diizinkan akan berhasil, sedangkan dari alamat lain koneksi akan timeout. Kondisi menunggu tersebut adalah tanda bahwa aturan DROP bekerja, bukan tanda bahwa tidak ada layanan di balik port tersebut. Perbedaan antara koneksi yang ditolak dan koneksi yang timeout merupakan cara tercepat untuk membedakan port yang difilter dari service yang memang tidak sedang listening.

Aturan yang ditambahkan dengan perintah iptables akan hilang saat reboot. Karena UFW sudah mengelola firewall ini, tempat yang tepat untuk menyimpannya secara permanen adalah /etc/ufw/after.rules. Tambahkan sebuah block di akhir file:

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMIT

Kemudian jalankan sudo ufw reload. UFW menerapkan kembali file tersebut setiap kali reload dan setiap boot. Dengan demikian, penyaringan container berada di tempat yang sama dengan aturan firewall lainnya dan tetap bertahan setelah reboot maupun upgrade Docker.

Mengapa integrasi iptables Docker tidak boleh dinonaktifkan

Jawaban lama untuk masalah ini menyarankan pengaturan { "iptables": false } di /etc/docker/daemon.json. Jangan lakukan itu. Aturan firewall Docker memiliki fungsi yang jauh lebih luas daripada hanya memublikasikan port. Aturan masquerade memungkinkan container mengakses Internet melalui alamat host. Jika integrasi dinonaktifkan, container tidak dapat mengambil image, mengakses mirror paket, atau memanggil API eksternal (application programming interface). Aturan DNAT membuat -p dapat berfungsi. Karena itu, port yang dipublikasikan akan sepenuhnya berhenti berfungsi. Aturan isolasi yang memisahkan jaringan Compose juga akan hilang. Anda memang memperbaiki bypass tersebut, tetapi dengan merusak jaringan container. Semua aturan itu kemudian harus Anda tulis dan pelihara sendiri secara manual. Dokumentasi Docker sendiri menjelaskan bahwa pengaturan ini ditujukan bagi pengguna yang memang bermaksud melakukan hal tersebut. Chain DOCKER-USER tersedia tepat agar tidak ada yang perlu mengaktifkan switch ini.

Sisi IPv6 dari masalah yang sama

Pertama, periksa tampilan port yang dipublikasikan melalui IPv6:

sudo ss -tlnp | grep 8080

Sejak Docker Engine 27, Docker mengelola ip6tables secara default. Pada jaringan Docker dengan IPv6 yang diaktifkan, port yang dipublikasikan mendapatkan perlakuan DNAT yang sama pada tabel IPv6. Karena itu, celah bypass yang sama juga ada di sana dan perbaikan yang sama berlaku: chain DOCKER-USER juga ada di ip6tables, jadi buat aturan yang sama dengan sudo ip6tables -I DOCKER-USER ... dan lakukan pengujian dari luar menggunakan curl terhadap alamat IPv6 publik server Anda, misalnya curl -6 http://[2001:db8:2a::1]:8080/.

Pada jaringan tanpa IPv6, klien IPv6 ditangani oleh docker-proxy, yaitu proses user-space biasa yang mendengarkan pada [::]:8080 dan meneruskan trafik ke container melalui IPv4. Trafik menuju proses pada host memang melewati INPUT, sehingga UFW dapat memfilter jalur tersebut, tetapi hanya jika UFW mengelola IPv6. Apakah UFW mengelola IPv6, serta cara lain celah IPv6 dapat muncul pada VPS, dibahas dalam panduan UFW dan IPv6.

Publikasi pada loopback menghindari seluruh pertanyaan ini: -p 127.0.0.1:8080:80 hanya mengikat loopback IPv4, sehingga tidak ada listener IPv6 dan tidak ada layanan yang dapat dijangkau dari luar melalui stack mana pun.

Pola yang dapat diandalkan

  • Publikasikan setiap port internal pada 127.0.0.1 agar port tersebut tidak pernah terekspos sejak awal.
  • Serahkan sisi publik kepada satu reverse proxy yang menggunakan port 80 dan 443.
  • Pertahankan kebijakan default deny UFW pada host, dengan mengizinkan SSH serta port proxy.
  • Filter port container yang benar-benar publik di DOCKER-USER, berdasarkan port tujuan asli, lalu persistensikan di /etc/ufw/after.rules.
  • Biarkan integrasi iptables Docker tetap aktif.

Setelah dikonfigurasi sekali, pola ini menghilangkan kejutan: ufw status menjelaskan host, sedangkan DOCKER-USER menjelaskan container. Tidak ada port yang dipublikasikan secara tidak sengaja, dan docker run -p berikutnya yang Anda ketik hanya mengekspos port yang memang Anda maksudkan.

FAQ

Mengapa saya dapat mengakses container Docker ketika UFW memblokir port tersebut?

Karena Docker menerbitkan port menggunakan aturan DNAT dalam chain PREROUTING. Aturan ini mengubah tujuan paket menjadi alamat container sebelum proses filtering dimulai. Paket kemudian melewati jalur FORWARD, sedangkan aturan UFW berada dalam INPUT, yaitu chain yang tidak pernah dimasuki paket tersebut. Firewall tidak pernah memeriksa paket itu, sehingga aturan penolakannya tidak berpengaruh pada port container yang diterbitkan.

Bagaimana cara membuat UFW memblokir port yang diterbitkan Docker?

UFW sendiri tidak dapat melakukannya karena aturannya berada dalam chain yang salah. Hentikan pemaparan port dengan menerbitkannya sebagai 127.0.0.1:8080:80 agar hanya host yang dapat mengaksesnya. Alternatifnya, lakukan filtering dalam chain DOCKER-USER menggunakan aturan iptables yang mencocokkan port tujuan awal melalui conntrack. Simpan aturan tersebut dalam /etc/ufw/after.rules agar tetap berlaku setelah reboot dan ufw reload.

Apakah saya harus menetapkan "iptables": false dalam daemon.json Docker?

Tidak. Pengaturan tersebut menghapus seluruh aturan firewall dan NAT milik Docker, sehingga dampaknya jauh lebih besar daripada sekadar melewati UFW. Container kehilangan akses Internet keluar karena aturan masquerade ikut terhapus. Port yang diterbitkan juga berhenti berfungsi karena aturan DNAT ikut terhapus. Gunakan loopback publishing dan chain DOCKER-USER. Keduanya mengatasi pemaparan port tanpa merusak jaringan container.

Apakah Docker juga melewati UFW pada IPv6?

Pada Docker Engine 27 dan versi yang lebih baru, pengelolaan ip6tables aktif secara default. Karena itu, port yang diterbitkan pada jaringan Docker yang mendukung IPv6 akan diubah arahnya untuk melewati UFW, sama seperti pada IPv4, dan memerlukan aturan DOCKER-USER yang dicerminkan dengan ip6tables. Pada jaringan tanpa IPv6, proses docker-proxy mendengarkan pada [::]. Trafik tersebut melewati INPUT, sehingga UFW dapat memfilternya jika UFW mengelola IPv6. Publishing pada 127.0.0.1 menghindari kedua kondisi tersebut karena tidak ada proses yang mendengarkan pada IPv6.