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

Docker pintas UFW: punca dan cara atasi

Docker menggunakan peraturan iptables yang memintas UFW. Pelajari mengapa port tetap terbuka walaupun telah disekat dan cara menyelesaikannya dengan betul.

Mengapa Docker memintas UFW

Docker memintas UFW kerana port kontena yang diterbitkan tidak melalui peraturan firewall yang diuruskan oleh UFW. Apabila anda menjalankan docker run -p 8080:80, Docker menulis peraturan DNAT (destination network address translation) ke dalam rantaian PREROUTING dalam jadual nat kernel. Peraturan tersebut menulis semula destinasi setiap paket ke alamat peribadi kontena sebelum kernel menentukan hala tuju paket tersebut. Paket yang telah ditulis semula itu kemudiannya dihantar ke dalam kontena melalui rantaian FORWARD yang dikawal oleh Docker. Peraturan UFW berada dalam rantaian INPUT, dan paket tersebut tidak pernah memasuki rantaian itu. Oleh itu, ufw status menunjukkan deny secara lalai, sudo ufw deny 8080 melaporkan kejayaan, tetapi port 8080 masih boleh diakses oleh seluruh internet.

Ini bukan pepijat Docker, dan UFW tidak rosak. Kedua-dua alatan ini memprogramkan firewall kernel yang sama. Peraturan Docker bertindak pada titik yang lebih awal dalam laluan paket, jadi UFW tidak pernah dipanggil. Panduan ini menunjukkan pemintasan tersebut, menjelaskan mekanismenya, dan kemudian membincangkan dua penyelesaian yang berkesan: menerbitkan port pada 127.0.0.1, dan menapis dalam rantaian DOCKER-USER. Jika anda baru mengenali UFW, tetapkan ia menggunakan panduan asas firewall UFW terlebih dahulu, kerana firewall deny-lalai tetap merupakan asas yang betul untuk semua perkara lain pada pelayan.

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 masuk mengikut urutan tetap, dan masalah utama berpunca daripada urutan tersebut.

  1. PREROUTING berjalan dahulu. Peraturan di sini boleh mengubah destinasi paket, dan peraturan Docker untuk port yang diterbitkan melakukan perkara tersebut.
  2. Keputusan penghalaan (routing decision) menyusul kemudian. Paket yang ditujukan kepada hos itu sendiri akan pergi ke rantaian INPUT. Paket yang ditujukan kepada mesin lain akan pergi ke rantaian FORWARD.
  3. Peraturan UFW berada dalam INPUT. Peraturan Docker berada dalam FORWARD.

Lihat peraturan Docker untuk kontena yang baru anda mulakan:

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 punca masalahnya. Sebarang paket yang tiba untuk port 8080 akan diubah destinasi kepada 172.17.0.2:80, iaitu alamat kontena pada rangkaian bridge peribadi Docker. Selepas perubahan tersebut, paket tidak lagi ditujukan kepada hos, maka keputusan penghalaan akan menghantarnya melalui laluan FORWARD, di mana Docker telah menambah peraturan untuk menerima trafik ke dalam rangkaiannya sendiri. Peraturan deny 8080/tcp anda menunggu di INPUT untuk paket yang tidak akan tiba.

Pada Ubuntu 24.04, arahan iptables adalah antaramuka bagi nftables, tetapi urutan rantaian dan hasilnya adalah sama. UFW dan Docker kedua-duanya menulis ke dalam saluran paip paket kernel yang sama, dan titik masuk Docker adalah lebih awal.

Penyelesaian harian: terbitkan port pada 127.0.0.1

Kebanyakan kontena sebenarnya tidak perlu dibuka kepada awam. Pangkalan data, pelayan aplikasi di belakang reverse proxy, panel admin, atau titik akhir metrik: tiada satu pun daripadanya harus menjawab permintaan internet secara langsung. Terbitkan mereka pada alamat loopback:

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

Atau dalam fail Compose:

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

Ini berfungsi kerana peraturan DNAT Docker kini hanya memadankan paket yang ditujukan kepada 127.0.0.1. Paket dari internet tidak boleh membawa destinasi tersebut secara sah, jadi kernel akan membuangnya sebelum sebarang peraturan firewall dijalankan. Port tersebut boleh dicapai dari hos, dan dari tiada yang lain. Sahkan pemautan (binding):

sudo ss -tlnp | grep 8080

Anda perlu melihat 127.0.0.1:8080 dalam output, bukan 0.0.0.0:8080 atau [::]:8080. Kemudian, sahkan dari mesin lain bahawa curl http://your-vps-ip:8080/ ditolak (refused).

Untuk perkhidmatan yang perlu dibuka kepada internet, jalankan satu reverse proxy yang memegang port 80 dan 443 serta melakukan penghalaan mengikut nama hos, dan jangan terbitkan apa-apa yang lain. Itulah corak yang dibina dalam panduan reverse proxy Traefik, dan itulah cara aplikasi hos sendiri seperti Nextcloud pada VPS kekal tidak boleh dicapai kecuali melalui proxy tersebut. Cara entri ports: diisytiharkan, dan aliran kerja Compose yang lain, dibincangkan dalam panduan asas Docker Compose.

Dengan setiap kontena dalaman pada loopback, UFW kembali menjalankan tugas normalnya: melindungi port yang dilayani oleh hos itu sendiri. Bina set peraturan tersebut di sini, kemudian jalankan arahan mengikut urutan:

ToolUFW rule generator

Penapisan sebenar: rantaian DOCKER-USER

Kadangkala port kontena mesti kekal diterbitkan ke rangkaian tetapi dihadkan, contohnya port replika pangkalan data yang hanya boleh dicapai oleh satu alamat pejabat. Untuk tujuan itu, Docker menyediakan rantaian DOCKER-USER. Setiap paket yang menuju ke mana-mana kontena akan melalui DOCKER-USER sebelum peraturan accept milik Docker sendiri, dan Docker tidak pernah menulis peraturan ke dalamnya. Rantaian ini disediakan untuk anda, dan Docker tidak mengubah kandungannya walaupun daemon dimulakan semula.

Satu perangkap sebelum arahan: apabila paket sampai ke DOCKER-USER, penulisan semula DNAT telah pun berlaku. Port destinasi paket tersebut ialah port kontena (80 dalam contoh kita), bukan port yang diterbitkan (8080). Oleh itu, peraturan yang memadankan --dport 8080 tidak akan memadankan apa-apa. Cara yang boleh dipercayai adalah dengan memadankan port yang didail oleh klien pada asalnya, yang diingati 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

Baca sebagai: untuk paket yang masuk melalui eth0 dan milik sambungan yang mempunyai port destinasi asal 8080, jatuhkan semua yang tidak dihantar dari 10.0.0.10. Padanan --ctdir ORIGINAL mengehadkan peraturan kepada arah klien-ke-kontena, supaya paket balasan tidak terperangkap secara tidak sengaja. Gantikan eth0 dengan antara muka awam anda; ip route | grep default menamakan ia. Uji dengan cara yang sama seperti sebelum ini: curl dari alamat yang dibenarkan berjaya, dan dari mana-mana tempat lain sambungan akan tamat tempoh (timeout).

Peraturan yang ditambah dengan arahan iptables akan hilang semasa but semula. Memandangkan UFW sudah menguruskan tembok api ini, tempat yang sesuai untuk menyimpannya secara kekal ialah /etc/ufw/after.rules. Tambahkan blok di akhir fail:

*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 akan memainkan semula fail tersebut pada setiap muat semula dan setiap but, jadi penapisan kontena anda kini berada di tempat yang sama dengan bahagian tembok api yang lain, dan ia kekal selamat semasa but semula mahupun naik taraf Docker.

Mengapa anda tidak patut menyahaktifkan integrasi iptables Docker

Jawapan lama bagi masalah ini mencadangkan penetapan { "iptables": false } dalam /etc/docker/daemon.json. Jangan lakukan perkara tersebut. Peraturan firewall Docker melakukan lebih daripada sekadar menerbitkan port. Peraturan masquerade membolehkan kontena mendapat akses internet keluar melalui alamat hos. Jika integrasi ini dimatikan, kontena tidak dapat menarik imej, mencapai cermin pakej, atau memanggil mana-mana API (application programming interface) luaran. Peraturan DNAT adalah punca -p berfungsi; jika dimatikan, port yang diterbitkan akan gagal berfungsi sepenuhnya. Peraturan pengasingan yang memisahkan rangkaian Compose juga akan hilang. Anda akan cuba membaiki pintasan dengan merosakkan rangkaian kontena, dan setiap peraturan tersebut perlu ditulis serta diselenggara secara manual oleh anda. Dokumentasi Docker sendiri menyifatkan tetapan ini sebagai pilihan untuk pengguna yang memang berniat melakukan perkara tersebut. Rantai DOCKER-USER wujud supaya tiada sesiapa perlu menggunakan suis ini.

Bahagian IPv6 bagi masalah yang sama

Pertama, semak rupa port yang diterbitkan pada IPv6:

sudo ss -tlnp | grep 8080

Sejak Docker Engine 27, Docker menguruskan ip6tables secara lalai. Pada rangkaian Docker dengan IPv6 diaktifkan, port yang diterbitkan menerima rawatan DNAT yang sama dalam jadual IPv6. Oleh itu, pintasan yang sama wujud di sana dan penyelesaian yang sama terpakai: rantaian DOCKER-USER juga wujud dalam ip6tables. Sila salin peraturan anda dengan sudo ip6tables -I DOCKER-USER ... dan uji dari luar menggunakan curl terhadap alamat IPv6 awam pelayan anda, contohnya curl -6 http://[2001:db8:2a::1]:8080/.

Pada rangkaian tanpa IPv6, klien IPv6 dikendalikan oleh docker-proxy, iaitu proses ruang pengguna biasa yang mendengar pada [::]:8080 dan menghantar trafik ke dalam kontena melalui IPv4. Trafik ke proses hos memang melalui INPUT, jadi UFW boleh menapis laluan tersebut, tetapi hanya apabila UFW menguruskan IPv6. Sama ada ia menguruskan IPv6, dan cara lain jurang IPv6 terbuka pada VPS, adalah subjek dalam panduan UFW dan IPv6.

Menerbitkan pada loopback mengelakkan keseluruhan isu ini: -p 127.0.0.1:8080:80 hanya mengikat loopback IPv4, jadi tiada pendengar IPv6 dan tiada apa yang boleh dicapai dari luar pada mana-mana stack.

Corak yang menyokong sistem

  • Terbitkan setiap port dalaman pada 127.0.0.1, supaya ia tidak terdedah kepada umum.
  • Berikan akses awam kepada satu reverse proxy yang mengawal port 80 dan 443.
  • Tetapkan UFW kepada deny secara lalai untuk host, dan hanya benarkan port SSH dan proxy.
  • Tapis port container yang benar-benar awam dalam DOCKER-USER, berdasarkan port destinasi asal yang disimpan dalam /etc/ufw/after.rules.
  • Biarkan integrasi iptables Docker diaktifkan.

Tetapkan sekali, dan ini akan mengelakkan ralat: ufw status menerangkan host, dan DOCKER-USER menerangkan container. Tiada apa yang diterbitkan secara tidak sengaja, dan docker run -p seterusnya yang anda taip akan mendedahkan apa yang anda maksudkan sahaja.

FAQ

Mengapa saya boleh mengakses kontena Docker walaupun UFW menyekat port tersebut?

Ini berlaku kerana Docker menerbitkan port menggunakan peraturan DNAT dalam rantaian PREROUTING, yang menulis semula destinasi paket ke alamat kontena sebelum sebarang penapisan berlaku. Paket tersebut kemudian melalui laluan FORWARD, manakala peraturan UFW berada dalam INPUT, iaitu rantaian yang tidak pernah dimasuki oleh paket tersebut. Firewall tidak dirujuk, jadi peraturan deny tidak memberi kesan kepada port kontena yang diterbitkan.

Bagaimanakah cara untuk membuat UFW menyekat port yang diterbitkan oleh Docker?

UFW sendiri tidak boleh melakukannya kerana peraturan UFW berada dalam rantaian yang salah. Anda boleh berhenti mendedahkan port tersebut dengan menerbitkannya sebagai 127.0.0.1:8080:80 supaya hanya hos sahaja yang boleh mengaksesnya, atau lakukan penapisan dalam rantaian DOCKER-USER menggunakan peraturan iptables yang memadankan port destinasi asal melalui conntrack. Simpan peraturan tersebut dalam /etc/ufw/after.rules supaya ia kekal selepas but semula dan ufw reload.

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

Tidak. Tetapan tersebut membuang semua peraturan firewall dan NAT Docker, yang menyebabkan lebih banyak masalah berbanding isu pintasan tersebut. Kontena akan kehilangan akses internet keluar kerana peraturan masquerade telah hilang, dan port yang diterbitkan akan berhenti berfungsi kerana peraturan DNAT telah hilang. Gunakan penerbitan loopback dan rantaian DOCKER-USER sebagai ganti; kaedah ini membaiki pendedahan tanpa merosakkan rangkaian kontena.

Adakah Docker turut memintas UFW pada IPv6?

Pada Docker Engine 27 dan versi kemudian, pengurusan ip6tables diaktifkan secara lalai, jadi port yang diterbitkan pada rangkaian Docker yang menyokong IPv6 akan ditulis semula untuk memintas UFW sama seperti IPv4, dan memerlukan peraturan DOCKER-USER yang sama dicerminkan dengan ip6tables. Pada rangkaian tanpa IPv6, proses docker-proxy mendengar pada [::] dan trafik tersebut akan melalui INPUT, di mana UFW boleh menapisnya jika UFW menguruskan IPv6. Penerbitan pada 127.0.0.1 mengelakkan kedua-dua kes tersebut, kerana tiada apa-apa yang mendengar pada IPv6.