Bakit binabypass ng Docker ang UFW?
Ang Docker ay gumagamit ng iptables rules na lumalagpas sa UFW. Alamin ang mekanismo nito at ang dalawang siguradong fix para maayos ang iyong firewall.
Bakit binabypass ng Docker ang UFW
Binabypass ng Docker ang UFW dahil ang mga published container ports ay hindi dumadaan sa mga firewall rules na pinamamahalaan ng UFW. Kapag ni-run ang docker run -p 8080:80, nagsusulat ang Docker ng DNAT (destination network address translation) rule sa PREROUTING chain ng nat table ng kernel. Binabago ng rule na iyon ang destination ng bawat packet patungo sa private address ng container bago pa magdesisyon ang kernel kung saan pupunta ang packet. Ang rewritten na packet ay ipinapasa sa container sa pamamagitan ng FORWARD chain na kontrolado ng Docker. Ang mga rules ng UFW ay nasa INPUT chain, at hindi kailanman pumapasok ang packet dito. Kaya ang ufw status ay nagpapakita ng default deny, ang sudo ufw deny 8080 ay nag-uulat ng success, pero ang port 8080 ay sumasagot pa rin sa buong internet.
Hindi bug ng Docker ito, at hindi sira ang UFW. Parehong pino-program ng dalawang tool ang parehong kernel firewall. Ang mga rules ng Docker ay gumagana sa mas maagang bahagi ng path ng packet, kaya hindi na tinatanong ang UFW. Ipinapakita ng guide na ito ang bypass, ipinapaliwanag ang mekanismo, at tatalakayin ang dalawang gumaganang fix: ang pag-publish ng ports sa 127.0.0.1, at ang pag-filter sa DOCKER-USER chain. Kung bago pa sa iyo ang UFW, i-setup muna ito gamit ang the UFW firewall basics guide, dahil ang default-deny firewall ay mahalaga pa ring base para sa lahat ng iba pang serbisyo sa server.
Tingnan ang bypass sa sarili mong server
Magsimula sa isang VPS kung saan active ang UFW na may default deny policy para sa incoming traffic. Mag-run ng web container na may published port:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineIpinapakita ng ufw status verbose ang Default: deny (incoming), allow (outgoing) at walang rule para sa port 8080. Ayon sa report ng firewall, closed ang port. Ngayon, i-test ito mula sa ibang machine, hindi mula sa mismong server:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKSumasagot ang container. Magdagdag ng explicit deny rule at i-test muli:
sudo ufw deny 8080/tcpSumasagot pa rin ang port dahil ang deny rule ay nasa chain na hindi nararating ng packet. Hindi nag-fail ang UFW. Hindi ito tinanong. Ito rin ang dahilan kung bakit mahirap itong matuklasan: walang error na lumalabas kahit saan, gumagana ang deployment, at ang output ng firewall status ay mukhang isang healthy at locked-down server.
Ang mekanismo: nauuna ang PREROUTING bago ang INPUT
Pinoproseso ng kernel ang mga incoming packet sa isang fixed na pagkakasunod-sunod, at ang problema ay nagmumula sa pagkakasunod-sunod na ito.
- Unang tumatakbo ang
PREROUTING. Maaaring baguhin ng mga rule dito ang destination ng packet, at iyon ang ginagawa ng rule ng Docker para sa isang published port. - Kasunod nito ang routing decision. Ang packet na nakatutok sa host mismo ay papunta sa
INPUTchain. Ang packet na nakatutok sa ibang machine ay papunta saFORWARDchain. - Ang mga rule ng UFW ay nasa
INPUT. Ang mga rule ng Docker ay nasaFORWARD.
Tingnan ang rule ng Docker para sa container na sinimulan mo:
sudo iptables -t nat -L DOCKER -nChain 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:80Ang DNAT line ang dahilan ng lahat. Ang anumang packet na darating para sa port 8080 ay babaguhin ang destination tungo sa 172.17.0.2:80, ang address ng container sa private bridge network ng Docker. Pagkatapos ng rewrite, hindi na nakatutok ang packet sa host, kaya ipadadala ito ng routing decision sa FORWARD path. Dito ay mayroon nang mga rule ang Docker na tumatanggap ng traffic sa sarili nitong mga network. Ang deny 8080/tcp rule mo ay naghihintay sa INPUT para sa isang packet na hindi naman darating.
Sa Ubuntu 24.04, ang iptables command ay isang front end para sa nftables, ngunit ang chain order at ang resulta ay pareho lang. Parehong sumusulat ang UFW at Docker sa iisang kernel packet pipeline, at mas maaga ang entry point ng Docker.
Ang pangkaraniwang solusyon: i-publish ang mga port sa 127.0.0.1
Karamihan sa mga container ay hindi naman talaga kailangang maging public. Ang database, app server sa likod ng reverse proxy, admin panel, o metrics endpoint: wala sa mga ito ang dapat direktang tumutugon sa internet. I-publish ang mga ito sa loopback address:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpineO sa isang Compose file:
services:
web:
image: nginx:1.29-alpine
ports:
- "127.0.0.1:8080:80"Gumagana ito dahil ang DNAT rule ng Docker ay tumutugon na lamang sa mga packet na nakatarget sa 127.0.0.1. Ang mga packet mula sa internet ay hindi maaaring magkaroon ng ganitong destination, kaya itinatapon ito ng kernel bago pa man tumakbo ang anumang firewall rule. Ang port ay ma-a-access lamang mula sa host at wala nang iba. I-verify ang binding:
sudo ss -tlnp | grep 8080Dapat ay 127.0.0.1:8080 ang makikita sa output, hindi 0.0.0.0:8080 o [::]:8080. Pagkatapos, kumpirmahin mula sa ibang machine na ang curl http://your-vps-ip:8080/ ay refused.
Para sa mga service na dapat harapin ang internet, gumamit ng isang reverse proxy na may hawak sa mga port na 80 at 443 at nagro-route base sa hostname, at huwag mag-publish ng iba pa. Ito ang pattern na ginagamit sa Traefik reverse proxy guide, at ito ang paraan kung paano nananatiling unreachable ang mga self-hosted app gaya ng Nextcloud on a VPS maliban kung sa pamamagitan ng proxy nito. Ang pag-declare ng mga ports: entry at ang iba pang Compose workflow ay tatalakayin sa the Docker Compose basics guide.
Dahil ang bawat internal container ay nasa loopback, babalik sa normal na trabaho ang UFW: ang bantayan ang mga port na ang host mismo ang nagse-serve. I-build ang rule set na ito, pagkatapos ay patakbuhin ang mga command nang sunod-sunod:
Real filtering: ang DOCKER-USER chain
Minsan, kailangang naka-publish ang port ng container sa network pero dapat itong i-restrict. Halimbawa, ang port ng isang database replica na dapat ay isang office address lang ang makaka-access. Para dito, may ibinibigay ang Docker na DOCKER-USER chain. Ang bawat packet na papunta sa anumang container ay dumadaan sa DOCKER-USER bago ang sariling accept rules ng Docker, at hindi kailanman nagsusulat ang Docker ng rules sa chain na ito. Ang chain na ito ay para sa user, at hindi binabago ng Docker ang laman nito kahit mag-restart ang daemon.
Isang babala bago ang command: kapag ang packet ay nasa DOCKER-USER na, tapos na ang DNAT rewrite. Ang destination port ng packet ay ang container port (80 sa ating halimbawa), hindi ang published port (8080). Dahil dito, walang match ang rule na tumutukoy sa --dport 8080. Ang tamang paraan ay i-match ang port na orihinal na tinawag ng client, na natatandaan ng connection tracker ng kernel:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPBasahin ito nang ganito: para sa mga packet na pumasok sa eth0 at bahagi ng isang connection na ang orihinal na destination port ay 8080, i-drop ang lahat ng hindi galing sa 10.0.0.10. Ang --ctdir ORIGINAL match ay nililimitahan ang rule sa client-to-container direction, kaya hindi aksidenteng mahuhuli ang mga reply packets. Palitan ang eth0 ng iyong public interface; ang ip route | grep default ang nagpapangalan dito. I-test ito gaya ng dati: magiging successful ang curl mula sa allowed address, pero mag-ti-timeout ang connection mula sa ibang lugar.
Ang mga rules na idinagdag gamit ang iptables command ay mawawala pagkatapos ng reboot. Dahil ang UFW na ang nagma-manage sa firewall na ito, ang pinakamainam na lugar para i-save ang mga ito ay sa /etc/ufw/after.rules. Magdagdag ng block sa dulo ng 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
COMMITPagkatapos ay i-run ang sudo ufw reload. I-re-replay ng UFW ang file na iyon sa bawat reload at bawat boot. Dahil dito, ang container filtering mo ay nasa parehong lugar na gaya ng iba pang firewall rules, at mananatili ito kahit mag-reboot o mag-upgrade ng Docker.
Bakit hindi dapat i-disable ang iptables integration ng Docker
Ang mga lumang sagot sa problemang ito ay nagmumungkahi na i-set ang { "iptables": false } sa /etc/docker/daemon.json. Huwag itong gawin. Higit pa sa pag-publish ng mga port ang ginagawa ng firewall rules ng Docker. Ang masquerade rule ang nagbibigay sa mga container ng outbound internet access gamit ang address ng host. Kapag naka-off ang integration, hindi makakapag-pull ng images ang mga container, hindi makakaabot sa mga package mirror, at hindi makakatawag sa anumang external API (application programming interface). Ang mga DNAT rule ang nagpapagana sa -p, kaya tuluyan na hindi gagana ang mga published port. Mawawala rin ang mga isolation rule na naghihiwalay sa mga Compose network. Masosolusyunan mo ang bypass sa pamamagitan ng pagkasira ng container networking, at kailangan mo nang isulat at i-maintain ang bawat rule na iyon nang manual. Inilalarawan ng dokumentasyon ng Docker ang setting na ito para sa mga taong ang intensyon ay gawin ang eksaktong iyon. Ang DOCKER-USER chain ay umiiral para hindi na kailanganin ang switch na ito.
Ang IPv6 na aspeto ng parehong problema
I-check muna kung ano ang hitsura ng published port sa IPv6:
sudo ss -tlnp | grep 8080Simula sa Docker Engine 27, default na ang pag-manage ng Docker sa ip6tables. Sa isang Docker network na may enabled na IPv6, ang published port ay sumasailalim sa parehong DNAT treatment sa IPv6 tables. Dahil dito, mayroon ding parehong bypass at parehong fix ang kailangan: ang DOCKER-USER chain ay naroon din sa ip6tables, kaya i-mirror ang iyong rule gamit ang sudo ip6tables -I DOCKER-USER .... I-test ito mula sa labas gamit ang curl sa public IPv6 address ng iyong server, halimbawa ay curl -6 http://[2001:db8:2a::1]:8080/.
Sa network na walang IPv6, ang mga IPv6 client ay hinahawakan ng docker-proxy. Ito ay isang normal na user-space process na nakikinig sa [::]:8080 at nagfo-forward ng traffic sa container gamit ang IPv4. Ang traffic papunta sa isang host process ay dumadaan sa INPUT, kaya maaaring i-filter ng UFW ang path na iyon, pero mangyayari lang ito kung ang UFW ang nagma-manage ng IPv6. Ang mga detalye kung paano ito nangyayari, at ang iba pang paraan kung bakit nagkakaroon ng IPv6 gap sa isang VPS, ay matatagpuan sa UFW and IPv6 guide.
Ang pag-publish sa loopback ay umiiwas sa buong isyu: ang -p 127.0.0.1:8080:80 ay nagba-bind sa IPv4 loopback lamang, kaya walang IPv6 listener at walang anumang pwedeng ma-reach mula sa labas sa alinmang stack.
Ang pattern na nagpapanatili ng seguridad
- I-publish ang lahat ng internal port sa
127.0.0.1, para hindi ito agad na-e-expose. - Ibigay ang public side sa isang reverse proxy na may hawak ng ports 80 at 443.
- Panatilihin ang default deny ng UFW para sa host, at payagan lamang ang SSH at proxy ports.
- I-filter ang mga tunay na public container ports sa
DOCKER-USER, base sa original destination port na naka-persist sa/etc/ufw/after.rules. - Panatilihing naka-on ang iptables integration ng Docker.
Kapag na-set up na ito, maiiwasan ang mga hindi inaasahang error: inilalarawan ng ufw status ang host, at inilalarawan ng DOCKER-USER ang mga container. Walang mapo-publish nang hindi sinasadya, at sa susunod na docker run -p na i-type mo, eksaktong ang dapat i-expose ang lalabas.
FAQ
Bakit ko pa rin naa-access ang Docker container kahit naka-block ang port sa UFW?
Dahil ang Docker ay naglalathala (publish) ng port gamit ang DNAT rule sa PREROUTING chain. Binabago nito ang destination address ng packet patungo sa address ng container bago pa man magkaroon ng filtering. Ang packet ay dadaan sa FORWARD path, samantalang ang mga rules ng UFW ay nasa INPUT chain, na hindi pinapasukan ng packet. Hindi kinokonsulta ang firewall, kaya walang epekto ang mga deny rules nito sa mga published container ports.
Paano ko mapapa-block ng UFW ang mga published ports ng Docker?
Hindi ito kayang gawin ng UFW mismo dahil nasa maling chain ang mga rules nito. Maaari mong itigil ang pag-expose ng port sa pamamagitan ng pag-publish nito bilang 127.0.0.1:8080:80 para ang host lang ang makaka-access, o kaya ay mag-filter sa DOCKER-USER chain gamit ang iptables rule na tumutugma sa original destination port sa pamamagitan ng conntrack. I-save ang rule na ito sa /etc/ufw/after.rules para manatili ito pagkatapos ng reboot at ufw reload.
Dapat ko bang i-set ang "iptables": false sa daemon.json ng Docker?
Hindi. Tinatanggal ng setting na ito ang lahat ng firewall at NAT rules ng Docker, na mas maraming nasisira kaysa sa bypass na nararanasan. Mawawalan ng outbound internet access ang mga container dahil wala na ang masquerade rule, at hindi na gagana ang mga published ports dahil wala na ang mga DNAT rules. Gamitin ang loopback publishing at ang DOCKER-USER chain sa halip; inaayos nito ang exposure nang hindi nasisira ang container networking.
Bina-bypass din ba ng Docker ang UFW sa IPv6?
Sa Docker Engine 27 at mas bago, naka-on ang ip6tables management by default. Ang port na naka-publish sa isang IPv6-enabled Docker network ay binabago ang address sa paligid ng UFW gaya ng sa IPv4, at kailangan ng kaparehong DOCKER-USER rule na naka-mirror sa ip6tables. Sa mga network na walang IPv6, ang docker-proxy process ay nakikinig sa [::] at ang traffic na iyon ay dumadaan sa INPUT, kung saan maaaring i-filter ng UFW ang traffic kung ang UFW ang nagma-manage ng IPv6. Ang pag-publish sa 127.0.0.1 ay umiiwas sa parehong kaso dahil walang nakikinig sa IPv6.