Docker কেন UFW বাইপাস করে এবং কীভাবে এটি সমাধান করবেন
Docker সরাসরি iptables ব্যবহার করায় UFW রুল কাজ করে না। আপনার কন্টেইনার পোর্টগুলো ইন্টারনেটে উন্মুক্ত থাকার কারণ এবং DOCKER-USER চেইনে ফিল্টারিং করার সঠিক পদ্ধতিটি এখানে জানুন।
Docker কীভাবে UFW বাইপাস করে
Docker UFW বাইপাস করে, কারণ প্রকাশিত container port কখনো UFW পরিচালিত firewall rule-এর মধ্য দিয়ে যায় না। আপনি docker run -p 8080:80 চালালে Docker kernel-এর nat table-এ নিজের DOCKER chain-এর মধ্যে একটি DNAT (destination network address translation) rule লেখে। PREROUTING chain এই chain-এ jump করে। ওই rule প্রতিটি packet-এর destination container-এর private address-এ পরিবর্তন করে, kernel packet-টি কোথায় যাবে তা নির্ধারণ করার আগে। এরপর পরিবর্তিত packet FORWARD chain-এর মাধ্যমে container-এ forward হয়। এই chain Docker নিয়ন্ত্রণ করে। UFW-এর rule INPUT chain-এ থাকে, কিন্তু packet কখনো সেখানে প্রবেশ করে না। তাই ufw status default deny দেখায়, sudo ufw deny 8080 সাফল্য জানায়, এবং port 8080 এখনও সমগ্র Internet থেকে অনুরোধ গ্রহণ করে।
এটি Docker-এর bug নয়, এবং UFW-ও নষ্ট নয়। উভয় tool একই kernel firewall-এ rule প্রয়োগ করে। Docker-এর rule packet-এর পথে আরও আগের পর্যায়ে কার্যকর হয়, তাই UFW-কে কখনো জিজ্ঞাসাই করা হয় না। এই guide-এ bypass প্রদর্শন করা হয়েছে, প্রক্রিয়াটি ব্যাখ্যা করা হয়েছে, এবং এরপর কার্যকর দুটি সমাধান আলোচনা করা হয়েছে: port 127.0.0.1-এ publish করা এবং DOCKER-USER chain-এ filtering করা। UFW আপনার জন্য নতুন হলে আগে UFW firewall-এর মৌলিক বিষয়ের guide অনুসরণ করে এটি সেট আপ করুন, কারণ server-এর অন্যান্য সব কিছুর জন্যও default-deny firewall এখনও সঠিক ভিত্তি।
আপনার নিজের সার্ভারে বাইপাসটি দেখুন
এমন একটি VPS থেকে শুরু করুন যেখানে UFW সক্রিয় আছে এবং ইনকামিং ট্র্যাফিকের জন্য ডিফল্ট পলিসি হিসেবে deny সেট করা আছে। একটি প্রকাশিত পোর্টসহ একটি ওয়েব কন্টেইনার চালান:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose কমান্ডটি Default: deny (incoming), allow (outgoing) দেখায় এবং পোর্ট 8080-এর জন্য কোনো রুল নেই। ফায়ারওয়ালের নিজস্ব রিপোর্ট অনুযায়ী, পোর্টটি বন্ধ। এখন সার্ভার থেকে নয়, বরং অন্য একটি মেশিন থেকে পরীক্ষা করুন:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKকন্টেইনারটি উত্তর দিচ্ছে। একটি স্পষ্ট deny রুল যোগ করুন এবং আবার পরীক্ষা করুন:
sudo ufw deny 8080/tcpপোর্টটি এখনও উত্তর দিচ্ছে, কারণ deny রুলটি এমন একটি চেইনে রয়েছে যেখানে প্যাকেটটি কখনোই পৌঁছায় না। UFW ব্যর্থ হয়নি। এটি কখনোই যাচাই করা হয়নি। এই কারণেই সমস্যাটি খুব ভালোভাবে লুকিয়ে থাকে: কোথাও কোনো ত্রুটি প্রদর্শিত হয় না, ডিপ্লয়মেন্ট কাজ করে এবং ফায়ারওয়ালের স্ট্যাটাস আউটপুট দেখতে একটি সুরক্ষিত সার্ভারের মতোই মনে হয়।
প্রক্রিয়াটি: PREROUTING, INPUT-এর আগে চলে
কার্নেল নির্দিষ্ট ক্রমে incoming packet প্রক্রিয়া করে, এবং পুরো সমস্যাটি এই ক্রমের মধ্যেই রয়েছে।
PREROUTINGপ্রথমে চলে। এখান থেকে কার্যকর হওয়া rule packet-এর destination পুনর্লিখতে পারে। Docker-এর published port-এর জন্য থাকা rule-টি,DOCKERchain-এ যাPREROUTING-এ jump করে, ঠিক সেটিই করে।- এরপর routing decision হয়। Host নিজে যে packet-এর destination, সেটি
INPUTchain-এ যায়। অন্য কোনো machine-এর destination হওয়া packetFORWARDchain-এ যায়। - UFW-এর rule
INPUT-এ থাকে। Docker-এর ruleFORWARD-এ থাকে।
আপনি যে container-টি সদ্য চালু করেছেন, তার জন্য Docker-এর rule দেখুন:
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:80DNAT line-টিই পুরো বিষয়টি ব্যাখ্যা করে। Port 8080-এর জন্য আসা যেকোনো packet-এর destination পুনর্লিখে 172.17.0.2:80 করা হয়, যা Docker-এর private bridge network-এ container-এর address। পুনর্লিখনের পরে packet-টির destination আর host থাকে না। তাই routing decision packet-টিকে FORWARD path-এ পাঠায়, যেখানে Docker ইতিমধ্যেই নিজের network-এ traffic গ্রহণ করার rule যোগ করেছে। আপনার deny 8080/tcp rule INPUT-এ এমন একটি packet-এর অপেক্ষায় থাকে, যা আর সেখানে পৌঁছায় না।
Ubuntu 24.04-এ iptables command-টি nftables-এর উপর একটি front end হিসেবে কাজ করে, তবে chain-এর ক্রম এবং ফলাফল একই থাকে। UFW এবং Docker উভয়ই একই kernel packet pipeline-এ rule লেখে, আর Docker-এর entry point আগে আসে। এটি শুধু UFW-এর ক্ষেত্রে প্রযোজ্য নয়: Rocky বা AlmaLinux VPS-এ firewalld একই pipeline-এর একই স্তরে filter করে এবং একই DNAT rule-এর কারণে সেই স্তর এড়িয়ে যায়। তাই নিচের fix-গুলো সেখানেও প্রয়োগ করতে হবে।
প্রতিদিনের সমাধান: 127.0.0.1-এ port publish করুন
বেশিরভাগ container শুরু থেকেই public হওয়ার প্রয়োজন ছিল না। Database, reverse proxy-এর পেছনে থাকা app server, admin panel বা metrics endpoint—এগুলোর কোনোটি সরাসরি Internet-এর অনুরোধের উত্তর দেওয়া উচিত নয়। এগুলো loopback address-এ publish করুন:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpineঅথবা একটি Compose file-এ:
services:
web:
image: nginx:1.29-alpine
ports:
- "127.0.0.1:8080:80"এটি কাজ করে, কারণ Docker-এর DNAT rule এখন শুধু 127.0.0.1-এ পাঠানো packet-এর সঙ্গে মেলে। এছাড়া Docker Engine 28.0.0 এবং পরবর্তী সংস্করণে Docker raw table-এর PREROUTING chain-এ একটি rule যোগ করে। এই rule loopback port-এর উদ্দেশ্যে পাঠানো, কিন্তু loopback ছাড়া অন্য interface দিয়ে আসা যেকোনো packet drop করে। ওই release-গুলোতে port-টি host থেকে reachable থাকে, অন্য কোথাও থেকে নয়। 28.0.0-এর আগের release-গুলোতে Docker-এর documentation অনুযায়ী একই L2 segment-এর host, যেমন একই network switch-এ থাকা machine, localhost-এ publish করা port-এ এখনও পৌঁছাতে পারে। তাই এর ওপর নির্ভর করার আগে docker version পরীক্ষা করে upgrade করুন। Binding যাচাই করুন:
sudo ss -tlnp | grep 8080Output-এ 127.0.0.1:8080 থাকা উচিত, 0.0.0.0:8080 বা [::]:8080 নয়। এরপর অন্য একটি machine থেকে নিশ্চিত করুন যে curl http://your-vps-ip:8080/ কোনো উত্তর পায় না। আপনার public address-এ পাঠানো packet Docker আর rewrite করে না। তাই এটি INPUT-এ পৌঁছায়, যেখানে UFW-এর default deny policy এবং আগের deny 8080/tcp rule packet-টি reject না করে নীরবে drop করে। ফলে curl অপেক্ষা করতে থাকে এবং শেষে connection timeout হয়, "connection refused" নয়।
যেসব service Internet-এর জন্য উন্মুক্ত রাখা দরকার, সেগুলোর জন্য একটি reverse proxy চালান। সেটি 80 এবং 443 port-এর দায়িত্ব নেবে এবং hostname অনুযায়ী routing করবে। অন্য কোনো port publish করবেন না। Traefik reverse proxy guide-এ এই pattern-ই তৈরি করা হয়েছে। VPS-এ Nextcloud-এর মতো self-hosted app কীভাবে তার proxy ছাড়া unreachable থাকে, এটিও সেই পদ্ধতি। ports: entry কীভাবে declare করতে হয় এবং Compose workflow-এর বাকি অংশ Docker Compose basics guide-এ ব্যাখ্যা করা হয়েছে।
প্রতিটি internal container loopback-এ থাকলে UFW আবার তার স্বাভাবিক কাজ করতে পারে: host নিজে যে port-গুলো serve করে, সেগুলো সুরক্ষিত রাখা। এখানে সেই rule set তৈরি করুন। এরপর command-গুলো ক্রমানুসারে চালান:
প্রকৃত filtering: DOCKER-USER chain
কখনো কোনো container port network-এ published রেখেও সেটিকে সীমাবদ্ধ করতে হয়। যেমন, একটি database replica port-এ শুধু একটি office address থেকে পৌঁছানো যাবে। এ কাজের জন্য Docker DOCKER-USER chain দেয়। যেকোনো container-এ যাওয়া প্রতিটি packet Docker-এর নিজস্ব accept rule-এর আগে DOCKER-USER-এর মধ্য দিয়ে যায়। এই chain আপনার rule-এর জন্য নির্দিষ্ট। daemon restart হলেও Docker এগুলো delete বা modify করে না।
এই chain Docker-এর default iptables firewall backend-এর অংশ। Docker Engine 29-এ experimental nftables backend যোগ হয়েছে। এটি firewall-backend daemon option দিয়ে চালু করতে হয়। এতে DOCKER-USER chain নেই। Custom filtering আপনার নিজস্ব আলাদা nftables table-এ রাখতে হবে, যেখানে forward hook-এ একটি base chain থাকবে। আপনি ওই option পরিবর্তন না করে থাকলে iptables ব্যবহৃত হচ্ছে এবং নিচের সবকিছু প্রযোজ্য। দুটি tool-এর পার্থক্য iptables বনাম nftables guide-এ ব্যাখ্যা করা হয়েছে।
Command চালানোর আগে একটি বিষয় মনে রাখুন: packet DOCKER-USER-এ পৌঁছানোর সময় DNAT rewrite ইতিমধ্যে হয়ে যায়। Packet-এর destination port তখন published port (8080) নয়, container port (আমাদের উদাহরণে 80)। তাই --dport 8080 মেলানো rule কোনো packet match করবে না। নির্ভরযোগ্য পদ্ধতি হলো client যে port-এ মূলত সংযোগ করেছিল, সেটি match করা। Kernel-এর connection tracker এই তথ্য সংরক্ষণ করে:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPএর অর্থ হলো: eth0 দিয়ে প্রবেশ করা এবং যার original destination port 8080 এমন connection-এর packet-গুলোর মধ্যে 10.0.0.10 থেকে পাঠানো নয়—এমন সব packet drop করো। --ctdir ORIGINAL match rule-টিকে শুধু client-to-container direction-এ সীমাবদ্ধ করে। ফলে ভুল করে reply packet ধরা পড়ে না। eth0-এর জায়গায় আপনার public interface বসান। ip route | grep default সেটির নাম দেখায়। আগের মতো পরীক্ষা করুন: অনুমোদিত address থেকে curl সফল হবে। অন্য যেকোনো স্থান থেকে connection timeout হবে। এই hang দেখায় যে DROP rule কাজ করছে। এর পেছনে কিছু নেই এমন port-এর ক্ষেত্রে এমন হয় না। কোনো port filtered কি না, নাকি service আসলে listening করছে না, তা বোঝার দ্রুততম উপায় হলো refused connection এবং timeout হওয়া connection-এর পার্থক্য দেখা।
iptables command দিয়ে যোগ করা rule reboot হলে হারিয়ে যায়। UFW যেহেতু ইতিমধ্যে এই firewall manage করছে, তাই rule persist করার সঠিক স্থান হলো /etc/ufw/after.rules। File-এর শেষে একটি block যোগ করুন:
*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এরপর sudo ufw reload চালান। প্রতিটি reload এবং boot-এর সময় UFW ওই file আবার প্রয়োগ করে। ফলে container filtering firewall-এর বাকি অংশের সঙ্গে একই জায়গায় থাকে এবং reboot ও Docker upgrade—উভয়ের পরেও কার্যকর থাকে।
Docker-এর iptables integration নিষ্ক্রিয় করা উচিত নয়
এই সমস্যার পুরোনো সমাধানগুলোতে { "iptables": false }-কে /etc/docker/daemon.json-এ সেট করার পরামর্শ দেওয়া হয়। এটি করবেন না। Docker-এর firewall rule শুধু port publish করে না, এর চেয়ে অনেক বেশি কাজ করে। masquerade rule-এর মাধ্যমে container host-এর address ব্যবহার করে outbound Internet access পায়। তাই integration নিষ্ক্রিয় করলে container package mirror-এ পৌঁছাতে বা কোনো external API (application programming interface) কল করতে পারে না। DNAT এবং isolation rule-ও সরিয়ে যায়। Docker-এর documentation সতর্ক করে যে বিকল্প rule না থাকলে container-এর port স্থানীয় network-এর host-গুলোর কাছে accessible হয়ে যায়। bypass বন্ধ করতে গেলে container networking নষ্ট হবে। এরপর প্রতিটি rule আপনাকেই হাতে লিখে রক্ষণাবেক্ষণ করতে হবে। Docker-এর নিজস্ব documentation অনুযায়ী, এই setting অধিকাংশ ব্যবহারকারীর জন্য উপযুক্ত নয়। পরে rule তৈরি করাকে documentation-এ অত্যন্ত জটিল কাজ হিসেবে বর্ণনা করা হয়েছে। DOCKER-USER chain-এর উদ্দেশ্যই হলো, যাতে কারও এই switch-এর প্রয়োজন না হয়।
একই সমস্যার IPv6 দিক
প্রথমে IPv6-এ published port-টি কেমন দেখায় তা পরীক্ষা করুন:
sudo ss -tlnp | grep 8080Docker Engine 27 থেকে Docker ডিফল্টভাবে ip6tables পরিচালনা করে। IPv6 সক্রিয় থাকা কোনো Docker network-এ published port IPv6 table-এও একই DNAT প্রক্রিয়ার মধ্য দিয়ে যায়। তাই একই bypass সেখানে কার্যকর থাকে এবং একই সমাধান প্রযোজ্য হয়: ip6tables-এও DOCKER-USER chain থাকে। তাই sudo ip6tables -I DOCKER-USER ... ব্যবহার করে একই rule যোগ করুন এবং আপনার server-এর public IPv6 address-এ curl চালিয়ে বাইরে থেকে পরীক্ষা করুন, যেমন curl -6 http://[2001:db8:2a::1]:8080/। এটি Docker-এর ডিফল্ট iptables backend-এর ক্ষেত্রে প্রযোজ্য। পরীক্ষামূলক nftables backend-এ কোনো stack-এই DOCKER-USER chain থাকে না। সেখানে IPv6 filtering আপনার নিজস্ব ip6 nftables table-এ করতে হবে।
IPv6 ছাড়া কোনো network-এ IPv6 client-দের পরিবর্তে docker-proxy পরিচালনা করে। এটি একটি সাধারণ user-space process, যা [::]:8080-এ listen করে এবং IPv4-এর মাধ্যমে container-এ traffic forward করে। কোনো host process-এর উদ্দেশ্যে পাঠানো traffic INPUT-এর মধ্য দিয়ে যায়। তাই UFW সেই পথ filter করতে পারে, তবে কেবল UFW যখন IPv6 পরিচালনা করছে তখনই। UFW তা করছে কি না এবং VPS-এ IPv6-এর security gap আর কোন উপায়ে তৈরি হয়, তা UFW এবং IPv6 guide-এ ব্যাখ্যা করা হয়েছে।
loopback-এ publish করলে পুরো বিষয়টি এড়ানো যায়: -p 127.0.0.1:8080:80 শুধু IPv4 loopback-এ bind করে। তাই কোনো IPv6 listener থাকে না। Docker Engine 28.0.0 এবং পরবর্তী সংস্করণে বাইরে থেকে কোনো stack-এই পৌঁছানোর মতো কিছু থাকে না।
যে পদ্ধতি নির্ভরযোগ্যভাবে কাজ করে
127.0.0.1-এ প্রতিটি অভ্যন্তরীণ port publish করুন, যাতে সেটি শুরু থেকেই কখনো exposed না হয়। এটি Docker Engine 28.0.0 বা পরবর্তী সংস্করণে প্রযোজ্য; পুরোনো release-গুলোতে একই L2 segment-এর host-গুলো সেই port-এ পৌঁছাতে পারত।- Public দিকের জন্য একটি reverse proxy ব্যবহার করুন, যা 80 এবং 443 port-এর দায়িত্বে থাকবে।
- Host-এর জন্য UFW-এর default deny চালু রাখুন এবং SSH ও proxy port অনুমোদন করুন।
- Docker-এর default iptables backend-এ প্রকৃতপক্ষে public container port-গুলো
DOCKER-USER-এ filter করুন। Original destination port মিলিয়ে নিয়ম প্রয়োগ করুন এবং/etc/ufw/after.rules-এ তা persist করুন। - Docker-এর iptables integration চালু রাখুন।
একবার সেটআপ করলে এই পদ্ধতিতে অপ্রত্যাশিত exposure থাকে না: ufw status host-এর বর্ণনা দেয়, আর DOCKER-USER container-গুলোর বর্ণনা দেয়। কোনো কিছু ভুল করে publish হয় না, এবং পরের docker run -p টাইপ করলে আপনি যা প্রকাশ করতে চেয়েছেন, ঠিক সেটিই exposed হবে।
FAQ
Docker-এর port-এ UFW block থাকা সত্ত্বেও container-এ কেন পৌঁছানো যায়?
কারণ Docker তার DOCKER chain-এর nat table-এ একটি DNAT rule দিয়ে port publish করে। এই chain-এ PREROUTING থেকে পৌঁছানো যায়। Filtering শুরু হওয়ার আগেই rule-টি packet-এর destination container-এর address-এ পরিবর্তন করে। এরপর packet FORWARD path দিয়ে যায়। UFW-এর rules থাকে INPUT-এ, যেখানে packet কখনো প্রবেশ করে না। তাই firewall-কে কখনো পরামর্শ করা হয় না এবং published container port-এ তার deny rules কোনো প্রভাব ফেলে না।
Docker-এর published port-গুলো UFW দিয়ে কীভাবে block করব?
UFW নিজে তা করতে পারে না, কারণ তার rules ভুল chain-এ থাকে। হয় port publish করা বন্ধ করুন, নয়তো Docker Engine 28.0.0 এবং পরবর্তী সংস্করণে এটিকে 127.0.0.1:8080:80 হিসেবে publish করুন, যাতে শুধু host সেটিতে পৌঁছাতে পারে। অন্যথায় conntrack-এর মাধ্যমে মূল destination port মিলিয়ে iptables rule দিয়ে DOCKER-USER chain-এ filtering করুন। Reboot এবং ufw reload-এর পরেও rule টিকে থাকার জন্য সেটি /etc/ufw/after.rules-এ স্থায়ী করুন।
Docker-এর daemon.json-এ "iptables": false সেট করা উচিত কি?
না। এই setting Docker-এর অধিকাংশ firewall এবং NAT rules সরিয়ে দেয়। এতে bypass সমস্যার চেয়ে অনেক বেশি ক্ষতি হয়। Masquerade rule না থাকায় container-গুলোর outbound Internet access বন্ধ হয়ে যায়। Docker সতর্ক করে যে replacement rules না থাকলে local network-এর host-গুলো container port-এ পৌঁছাতে পারবে। এর বদলে loopback publishing এবং DOCKER-USER chain ব্যবহার করুন। এতে container networking নষ্ট না করে exposure ঠিক করা যায়।
IPv6-এর ক্ষেত্রেও কি Docker UFW bypass করে?
Docker Engine 27 এবং পরবর্তী সংস্করণে ip6tables management default হিসেবে চালু থাকে। তাই IPv6-enabled Docker network-এ publish করা port IPv4-এর মতোই UFW-এর বাইরে rewrite হয়। Docker-এর default iptables backend-এ একই DOCKER-USER rule-কে ip6tables-এও প্রয়োগ করতে হয়। Engine 29-এর experimental nftables backend-এ কোনো DOCKER-USER chain নেই। তাই সেখানে আপনার নিজস্ব nftables table-এ filtering করতে হবে। IPv6 ছাড়া network-এ docker-proxy process [::]-এ listen করে। সেই traffic INPUT-এর মধ্য দিয়ে যায়, যেখানে UFW IPv6 manage করলে এটি filter করতে পারে। 127.0.0.1-এ publish করলে উভয় ক্ষেত্রই এড়ানো যায়, কারণ IPv6-এ কোনো কিছু listen করে না।