SSD Nodes Learn
Panduan Matt ConnorOleh Matt Connor · Diperbarui 2026-07-24

Cara mengatasi Docker bypass UFW

Docker melewati aturan UFW karena menggunakan iptables DNAT. Pelajari mekanisme penyebabnya dan cara memperbaikinya agar port kontainer tetap aman terjaga.

Mengapa Docker melewati UFW

Docker melewati UFW karena port kontainer 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 kernel. Aturan tersebut menulis ulang alamat tujuan setiap paket ke alamat privat kontainer sebelum kernel menentukan tujuan paket tersebut. Paket yang telah ditulis ulang kemudian diteruskan ke dalam kontainer melalui chain FORWARD yang dikendalikan oleh Docker. Aturan UFW berada di chain INPUT, dan paket tersebut tidak pernah masuk ke sana. Oleh karena itu, ufw status menunjukkan status deny secara default, sudo ufw deny 8080 melaporkan sukses, namun port 8080 tetap dapat diakses oleh seluruh internet.

Ini bukan bug Docker, dan UFW tidak rusak. Kedua alat tersebut memprogram firewall kernel yang sama. Aturan Docker bekerja pada titik yang lebih awal dalam jalur paket, sehingga UFW tidak pernah dipanggil. Panduan ini mendemonstrasikan bypass tersebut, menjelaskan mekanismenya, dan membahas dua solusi yang berhasil: memublikasikan port pada 127.0.0.1, dan melakukan filtering di chain DOCKER-USER. Jika Anda baru mengenal UFW, atur terlebih dahulu menggunakan panduan dasar firewall UFW, karena firewall dengan default-deny tetap merupakan dasar yang tepat untuk semua hal lainnya di server.

See the bypass on your own server

Start from a VPS where UFW is active with a default deny policy for incoming traffic. Run a web container with a published port:

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

ufw status verbose shows Default: deny (incoming), allow (outgoing) and no rule for port 8080. By the firewall's own report, the port is closed. Now test from a different machine, not from the server itself:

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

The container answers. Add an explicit deny rule and test again:

sudo ufw deny 8080/tcp

The port still answers, because the deny rule sits in a chain the packet never visits. UFW did not fail. It was never consulted. This is also why the problem hides so well: no error is printed anywhere, the deploy works, and the firewall status output looks exactly like a healthy locked-down server.

Mekanisme: PREROUTING berjalan sebelum INPUT

Kernel memproses paket yang masuk dalam urutan yang tetap, dan seluruh masalah terletak pada urutan tersebut.

  1. PREROUTING berjalan pertama kali. Aturan di sini dapat mengubah tujuan paket, dan aturan Docker untuk port yang dipublikasikan melakukan hal tersebut.
  2. Keputusan routing adalah langkah 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.

Perhatikan aturan Docker untuk kontainer 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 adalah inti masalahnya. Setiap paket yang tiba untuk port 8080 akan diubah tujuannya menjadi 172.17.0.2:80, yaitu alamat kontainer pada jaringan bridge privat Docker. Setelah perubahan tersebut, paket tidak lagi ditujukan ke host, sehingga keputusan routing mengirimkannya melalui jalur FORWARD, di mana Docker telah menambahkan aturan untuk menerima trafik ke dalam jaringannya sendiri. Aturan deny 8080/tcp Anda menunggu di INPUT untuk paket yang tidak pernah datang.

Pada Ubuntu 24.04, perintah iptables adalah front end untuk nftables, tetapi urutan chain dan hasilnya identik. UFW dan Docker sama-sama menulis ke dalam pipeline paket kernel yang sama, dan titik masuk Docker berada lebih awal.

Solusi harian: publikasikan port pada 127.0.0.1

Sebagian besar container sebenarnya tidak perlu bersifat publik. Database, server aplikasi di balik reverse proxy, panel admin, atau endpoint metrik: tidak ada satu pun dari komponen ini yang boleh merespons internet secara langsung. Publikasikan komponen 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"

Hal ini berhasil karena aturan DNAT Docker sekarang hanya mencocokkan paket yang ditujukan ke 127.0.0.1. Paket dari internet tidak akan pernah memiliki tujuan tersebut secara sah, sehingga kernel akan membuangnya sebelum aturan firewall dijalankan. Port tersebut dapat diakses dari host, dan tidak dari tempat lain. Verifikasi binding:

sudo ss -tlnp | grep 8080

Anda harus melihat 127.0.0.1:8080 pada output, bukan 0.0.0.0:8080 atau [::]:8080. Kemudian, konfirmasi dari mesin lain bahwa curl http://your-vps-ip:8080/ ditolak.

Untuk layanan yang harus dapat diakses internet, jalankan satu reverse proxy yang menggunakan port 80 dan 443 serta melakukan routing berdasarkan hostname, dan jangan publikasikan port lainnya. Itulah pola yang digunakan dalam panduan reverse proxy Traefik, dan itulah cara aplikasi self-hosted seperti Nextcloud pada VPS tetap tidak dapat diakses kecuali melalui proxynya. Cara mendeklarasikan entri ports:, serta alur kerja Compose lainnya, dibahas dalam panduan dasar Docker Compose.

Dengan setiap container internal berada pada loopback, UFW kembali menjalankan tugas normalnya: menjaga port yang dilayani oleh host itu sendiri. Susun aturan tersebut di sini, lalu jalankan perintah berikut secara berurutan:

ToolUFW rule generator

Real filtering: the DOCKER-USER chain

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

Satu jebakan sebelum perintah dijalankan: saat paket mencapai DOCKER-USER, penulisan ulang DNAT sudah terjadi. Port tujuan paket adalah port kontainer (80 dalam contoh kita), bukan port yang dipublikasikan (8080). Aturan yang mencocokkan --dport 8080 oleh karena itu tidak akan mencocokkan apa pun. Cara yang andal adalah mencocokkan port yang awalnya dipanggil oleh klien, yang diingat 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 merupakan bagian dari koneksi dengan port tujuan asli 8080, hapus semua yang tidak dikirim dari 10.0.0.10. Pencocokan --ctdir ORIGINAL membatasi aturan pada arah klien-ke-kontainer, sehingga paket balasan tidak ikut terblokir secara tidak sengaja. Ganti eth0 dengan interface publik Anda; ip route | grep default adalah namanya. Uji dengan cara yang sama seperti sebelumnya: curl dari alamat yang diizinkan berhasil, dan dari tempat lain koneksi akan mengalami timeout.

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 blok berikut 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 memproses ulang file tersebut pada setiap reload dan setiap boot, sehingga penyaringan kontainer Anda kini berada di tempat yang sama dengan bagian firewall lainnya, dan aturan tersebut tetap ada setelah reboot maupun upgrade Docker.

Mengapa Anda tidak boleh menonaktifkan integrasi iptables Docker

Jawaban lama untuk masalah ini menyarankan untuk mengatur { "iptables": false } di dalam /etc/docker/daemon.json. Jangan lakukan itu. Aturan firewall Docker melakukan lebih dari sekadar mempublikasikan port. Aturan masquerade adalah fitur yang memberikan akses internet keluar bagi container melalui alamat host. Jika integrasi ini dimatikan, container tidak dapat menarik image, menjangkau mirror paket, atau memanggil API (application programming interface) eksternal apa pun. Aturan DNAT adalah fitur yang membuat -p dapat berfungsi; jika dinonaktifkan, port yang dipublikasikan akan berhenti berfungsi sepenuhnya. Aturan isolasi yang memisahkan jaringan Compose juga akan hilang. Anda akan memperbaiki bypass dengan merusak jaringan container, dan setiap aturan tersebut harus Anda tulis dan kelola secara manual. Dokumentasi Docker sendiri mendeskripsikan pengaturan ini sebagai opsi bagi pengguna yang memang berniat melakukan hal tersebut. Chain DOCKER-USER ada agar pengguna tidak perlu menggunakan switch ini.

Sisi IPv6 dari masalah yang sama

Pertama, periksa tampilan port yang dipublikasikan pada 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. Oleh karena itu, celah yang sama juga ada di sana dan solusi yang sama berlaku: chain DOCKER-USER juga tersedia di ip6tables. Anda dapat menyalin aturan Anda menggunakan sudo ip6tables -I DOCKER-USER ..., lalu lakukan pengujian dari luar menggunakan curl ke 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 normal yang mendengarkan pada [::]:8080 dan meneruskan trafik ke dalam kontainer melalui IPv4. Trafik ke proses host melewati INPUT, sehingga UFW dapat memfilter jalur tersebut, namun hanya jika UFW mengelola IPv6. Mengenai apakah hal tersebut aktif, serta cara lain celah IPv6 terbuka pada VPS, dapat dipelajari dalam panduan UFW dan IPv6.

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

Pola yang menjaga keamanan

  • Publikasikan setiap port internal pada 127.0.0.1, sehingga port tersebut tidak pernah terekspos sejak awal.
  • Berikan akses publik hanya kepada satu reverse proxy yang menggunakan port 80 dan 443.
  • Gunakan aturan default deny pada UFW untuk host, dan hanya izinkan port SSH serta port proxy.
  • Filter port kontainer yang benar-benar publik di DOCKER-USER, berdasarkan port tujuan asli yang tersimpan di /etc/ufw/after.rules.
  • Biarkan integrasi iptables Docker tetap aktif.

Setelah dikonfigurasi, hal ini menghilangkan risiko kesalahan: ufw status menjelaskan host, dan DOCKER-USER menjelaskan kontainer. Tidak ada yang terpublikasi secara tidak sengaja, dan setiap docker run -p berikutnya yang Anda ketik akan mengekspos apa yang memang Anda maksudkan.

FAQ

Mengapa saya tetap bisa mengakses kontainer Docker saat UFW memblokir port tersebut?

Karena Docker mempublikasikan port menggunakan aturan DNAT pada chain PREROUTING, yang menulis ulang alamat tujuan paket ke alamat kontainer sebelum penyaringan dilakukan. Paket kemudian melewati jalur FORWARD, sedangkan aturan UFW berada di chain INPUT yang tidak pernah dimasuki oleh paket tersebut. Firewall tidak pernah diperiksa, sehingga aturan deny tidak berpengaruh pada port kontainer yang dipublikasikan.

Bagaimana cara membuat UFW memblokir port yang dipublikasikan Docker?

UFW tidak dapat melakukannya secara langsung karena aturan UFW berada pada chain yang salah. Anda dapat berhenti mengekspos port dengan mempublikasikannya sebagai 127.0.0.1:8080:80 agar hanya host yang dapat mengaksesnya, atau lakukan penyaringan pada chain DOCKER-USER menggunakan aturan iptables yang mencocokkan port tujuan asli melalui conntrack. Simpan aturan tersebut di /etc/ufw/after.rules agar tetap ada setelah reboot dan ufw reload.

Haruskah saya mengatur "iptables": false pada daemon.json Docker?

Tidak. Pengaturan tersebut menghapus semua aturan firewall dan NAT Docker, yang menyebabkan masalah lebih besar daripada sekadar bypass. Kontainer akan kehilangan akses internet keluar karena aturan masquerade hilang, dan port yang dipublikasikan akan berhenti berfungsi karena aturan DNAT hilang. Gunakan publikasi loopback dan chain DOCKER-USER sebagai gantinya; metode ini memperbaiki masalah eksposur tanpa merusak jaringan kontainer.

Apakah Docker juga melewati UFW pada IPv6?

Pada Docker Engine 27 dan versi terbaru, manajemen ip6tables aktif secara default. Oleh karena itu, port yang dipublikasikan pada jaringan Docker dengan IPv6 akan ditulis ulang melewati UFW sama seperti pada IPv4, dan memerlukan aturan DOCKER-USER yang sama yang dicerminkan dengan ip6tables. Pada jaringan tanpa IPv6, proses docker-proxy mendengarkan pada [::] dan trafik tersebut melewati INPUT, di mana UFW dapat menyaringnya jika UFW mengelola IPv6. Melakukan publikasi pada 127.0.0.1 menghindari kedua kasus tersebut, karena tidak ada yang mendengarkan pada IPv6 sama sekali.