SSD Nodes Learn Hosting plans →
गाइड Matt Connorलेखक: Matt Connor

Docker, UFW को बायपास क्यों करता है और इसे कैसे ठीक करें

Docker द्वारा UFW को बायपास करने का कारण iptables और DNAT नियम हैं। जानें कि कैसे आपके कंटेनर पोर्ट्स सुरक्षित न होने के बावजूद इंटरनेट पर खुले रह जाते हैं और इन्हें कैसे फिक्स करें।

Docker, UFW को बायपास क्यों करता है

Docker, UFW को इसलिए बायपास करता है क्योंकि पब्लिश किए गए कंटेनर पोर्ट्स कभी भी उन फायरवॉल रूल्स से नहीं गुजरते जिन्हें UFW मैनेज करता है। जब आप docker run -p 8080:80 रन करते हैं, तो Docker कर्नल की nat टेबल में अपनी खुद की DOCKER चेन में एक DNAT (डेस्टिनेशन नेटवर्क एड्रेस ट्रांसलेशन) रूल लिख देता है, जिस पर PREROUTING चेन जंप करती है। वह रूल कर्नल द्वारा यह तय किए जाने से पहले कि पैकेट कहाँ जा रहा है, प्रत्येक पैकेट के डेस्टिनेशन को कंटेनर के प्राइवेट एड्रेस में बदल देता है। इसके बाद रीराइट किया गया पैकेट FORWARD चेन के माध्यम से कंटेनर में फॉरवर्ड कर दिया जाता है, जिसे Docker कंट्रोल करता है। UFW के रूल्स INPUT चेन में होते हैं और पैकेट कभी भी उसमें प्रवेश नहीं करता। इसलिए ufw status डिफ़ॉल्ट डिनाय (deny) दिखाता है, sudo ufw deny 8080 सफलता की रिपोर्ट देता है, और पोर्ट 8080 अभी भी पूरे इंटरनेट को रिस्पॉन्स देता है।

यह Docker का कोई बग नहीं है और न ही UFW खराब है। दोनों टूल्स एक ही कर्नल फायरवॉल को प्रोग्राम करते हैं। Docker के रूल्स पैकेट के पाथ पर पहले ही काम कर लेते हैं, इसलिए UFW से कभी पूछा ही नहीं जाता। यह गाइड इस बायपास को प्रदर्शित करती है, इसके मैकेनिज्म को समझाती है, और फिर काम करने वाले दो फिक्स कवर करती है: 127.0.0.1 पर पोर्ट्स पब्लिश करना, और DOCKER-USER चेन में फिल्टरिंग करना। यदि UFW आपके लिए नया है, तो पहले UFW फायरवॉल बेसिक्स गाइड के साथ इसे सेटअप करें, क्योंकि डिफ़ॉल्ट-डिनाय फायरवॉल अभी भी सर्वर पर बाकी सब चीजों के लिए सही आधार है।

अपने सर्वर पर बाईपास देखें

एक ऐसे VPS से शुरुआत करें जहाँ UFW सक्रिय है और incoming traffic के लिए default deny policy लागू है। एक published port के साथ एक web container चलाएँ:

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

ufw status verbose यह दिखाता है कि Default: deny (incoming), allow (outgoing) और port 8080 के लिए कोई rule नहीं है। firewall की अपनी रिपोर्ट के अनुसार, port बंद है। अब सर्वर से नहीं, बल्कि किसी अन्य मशीन से परीक्षण करें:

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

Container जवाब देता है। एक स्पष्ट deny rule जोड़ें और फिर से परीक्षण करें:

sudo ufw deny 8080/tcp

Port अभी भी जवाब देता है, क्योंकि deny rule ऐसी chain में स्थित है जहाँ packet कभी पहुँचता ही नहीं है। UFW विफल नहीं हुआ। उससे कभी परामर्श ही नहीं किया गया। यही कारण है कि यह समस्या इतनी अच्छी तरह से छिपी रहती है: कहीं कोई error print नहीं होता, deploy काम करता है, और firewall status output बिल्कुल एक सुरक्षित और स्वस्थ सर्वर जैसा दिखता है।

कार्यप्रणाली: PREROUTING, INPUT से पहले चलता है

कर्नेल एक आने वाले पैकेट को एक निश्चित क्रम में प्रोसेस करता है, और पूरी समस्या इसी क्रम में निहित है।

  1. PREROUTING सबसे पहले चलता है। यहाँ से पहुँचने वाले नियम पैकेट के डेस्टिनेशन को फिर से लिख (rewrite) सकते हैं, और पब्लिश किए गए पोर्ट के लिए Docker का नियम, जो PREROUTING द्वारा जंप किए गए DOCKER चेन में होता है, ठीक यही करता है।
  2. इसके बाद राउटिंग का निर्णय लिया जाता है। होस्ट को संबोधित पैकेट INPUT चेन में जाता है। किसी अन्य मशीन को संबोधित पैकेट FORWARD चेन में जाता है।
  3. UFW के नियम INPUT में होते हैं। Docker के नियम FORWARD में होते हैं।

अभी शुरू किए गए कंटेनर के लिए Docker का नियम देखें:

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

DNAT लाइन ही पूरी कहानी है। पोर्ट 8080 के लिए आने वाले किसी भी पैकेट का डेस्टिनेशन बदलकर 172.17.0.2:80 कर दिया जाता है, जो Docker के प्राइवेट ब्रिज नेटवर्क पर कंटेनर का पता है। रीराइट के बाद पैकेट अब होस्ट को संबोधित नहीं रहता, इसलिए राउटिंग निर्णय इसे FORWARD पथ पर भेज देता है, जहाँ Docker ने पहले ही ऐसे नियम जोड़ रखे हैं जो उसके अपने नेटवर्क में ट्रैफिक को स्वीकार करते हैं। आपका deny 8080/tcp नियम INPUT में उस पैकेट की प्रतीक्षा करता है जो कभी नहीं आता।

Ubuntu 24.04 पर iptables कमांड nftables के ऊपर एक फ्रंट एंड है, लेकिन चेन का क्रम और परिणाम समान हैं। UFW और Docker दोनों एक ही कर्नेल पैकेट पाइपलाइन में लिखते हैं, और Docker का एंट्री पॉइंट पहले आता है। इसमें से कुछ भी UFW तक सीमित नहीं है: Rocky या AlmaLinux VPS पर firewalld उसी पाइपलाइन के उसी बिंदु पर फिल्टर करता है और उसी DNAT नियम द्वारा इसे दरकिनार कर दिया जाता है, इसलिए नीचे दिए गए समाधान वहाँ भी काम आते हैं।

रोजमर्रा का समाधान: ports को 127.0.0.1 पर publish करें

ज्यादातर containers को कभी भी 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 अब केवल उन packets से मेल खाता है जिन्हें 127.0.0.1 पर भेजा गया है, और Docker Engine 28.0.0 और उसके बाद के संस्करणों में, Docker raw table की PREROUTING chain में एक rule भी जोड़ता है जो loopback के अलावा किसी अन्य interface पर आने वाले उस loopback port के किसी भी packet को drop कर देता है। उन releases पर, port host से तो पहुँच योग्य है, लेकिन किसी और चीज़ से नहीं। 28.0.0 से पुराने releases पर, Docker यह बताता है कि एक ही L2 segment के भीतर के hosts, उदाहरण के लिए एक ही network switch पर मौजूद machines, अभी भी localhost पर publish किए गए ports तक पहुँच सकते हैं, इसलिए इस पर भरोसा करने से पहले docker version की जाँच करें और upgrade करें। binding को verify करें:

sudo ss -tlnp | grep 8080

आप output में 127.0.0.1:8080 चाहते हैं, न कि 0.0.0.0:8080 या [::]:8080। फिर किसी दूसरी machine से पुष्टि करें कि curl http://your-vps-ip:8080/ को कोई जवाब नहीं मिलता है। आपके public address पर भेजे गए packet को Docker द्वारा फिर से नहीं लिखा जाता है, इसलिए यह INPUT तक पहुँचता है, जहाँ UFW की default deny policy और पहले का deny 8080/tcp rule उसे चुपचाप drop कर देते हैं, न कि reject। इसलिए curl hang हो जाता है और connection timeout में समाप्त होता है, "connection refused" में नहीं।

जो services internet के सामने होनी चाहिए, उनके लिए एक reverse proxy चलाएँ जो ports 80 और 443 का स्वामी हो और hostname के आधार पर route करे, और कुछ भी publish न करें। यही वह pattern है जिसे Traefik reverse proxy guide बनाता है, और इसी तरह Nextcloud on a VPS जैसा self-hosted app अपने proxy के अलावा कहीं और से पहुँच योग्य नहीं रहता है। ports: entries को कैसे घोषित किया जाता है, और Compose workflow का बाकी हिस्सा, the Docker Compose basics guide में कवर किया गया है।

loopback पर हर internal container के साथ, UFW अपना सामान्य काम फिर से कर रहा है: उन ports की सुरक्षा करना जिन्हें host स्वयं serve करता है। यहाँ वह rule set बनाएँ, फिर commands को क्रम में चलाएँ:

ToolUFW rule generator

वास्तविक फ़िल्टरिंग: DOCKER-USER चेन

कभी-कभी किसी कंटेनर पोर्ट को नेटवर्क पर पब्लिश रखना जरूरी होता है, लेकिन उसे प्रतिबंधित करना भी आवश्यक हो सकता है। उदाहरण के लिए, एक डेटाबेस रेप्लिका पोर्ट जिसे केवल एक ऑफिस एड्रेस से ही एक्सेस किया जा सके। इसके लिए, Docker DOCKER-USER चेन प्रदान करता है। किसी भी कंटेनर की ओर जाने वाला प्रत्येक पैकेट Docker के अपने accept नियमों से पहले DOCKER-USER से होकर गुजरता है। यह चेन आपके नियमों के लिए है, और Docker इन्हें डिलीट या मॉडिफाई नहीं करता है, यहाँ तक कि daemon के रीस्टार्ट होने पर भी ये सुरक्षित रहते हैं।

यह चेन Docker के डिफ़ॉल्ट iptables फ़ायरवॉल बैकएंड का हिस्सा है। Docker Engine 29 ने एक प्रायोगिक nftables बैकएंड जोड़ा है जिसे आपको firewall-backend डेमन विकल्प के साथ चालू करना होगा, और इसमें कोई DOCKER-USER चेन नहीं होती है: कस्टम फ़िल्टरिंग आपकी अपनी एक अलग nftables टेबल में होती है, जिसमें forward हुक पर एक बेस चेन होती है। यदि आपने उस विकल्प को नहीं बदला है, तो आप iptables का उपयोग कर रहे हैं और नीचे दी गई सभी बातें आप पर लागू होती हैं। इन दोनों टूल्स के बीच का अंतर iptables बनाम nftables गाइड में कवर किया गया है।

कमांड से पहले एक सावधानी: जब तक पैकेट DOCKER-USER तक पहुँचता है, DNAT रीराइट पहले ही हो चुका होता है। पैकेट का डेस्टिनेशन पोर्ट कंटेनर पोर्ट (हमारे उदाहरण में 80) होता है, न कि पब्लिश किया गया पोर्ट (8080)। इसलिए --dport 8080 से मेल खाने वाला नियम किसी भी चीज़ से मैच नहीं करेगा। इसका विश्वसनीय तरीका उस पोर्ट से मैच करना है जिसे क्लाइंट ने मूल रूप से डायल किया था, जिसे कर्नल का कनेक्शन ट्रैकर याद रखता है:

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

इसे इस तरह पढ़ें: उन पैकेटों के लिए जो eth0 पर आए हैं और एक ऐसे कनेक्शन से संबंधित हैं जिसका मूल डेस्टिनेशन पोर्ट 8080 था, 10.0.0.10 से न भेजे गए सभी पैकेटों को ड्रॉप करें। --ctdir ORIGINAL मैच नियम को क्लाइंट-से-कंटेनर दिशा तक सीमित करता है, ताकि रिप्लाई पैकेट गलती से न पकड़े जाएँ। eth0 को अपने पब्लिक इंटरफ़ेस से बदलें; ip route | grep default इसका नाम है। इसे पहले की तरह ही टेस्ट करें: अनुमत एड्रेस से curl सफल होता है, और कहीं और से कनेक्शन टाइम आउट हो जाता है। वह हैंग होना इस बात का संकेत है कि एक DROP नियम अपना काम कर रहा है, न कि यह कि पोर्ट के पीछे कुछ भी नहीं है, और refused कनेक्शन और टाइम आउट होने वाले कनेक्शन के बीच का अंतर यह बताने का सबसे तेज़ तरीका है कि पोर्ट फ़िल्टर किया गया है या सर्विस बस लिसन नहीं कर रही है।

iptables कमांड के साथ जोड़े गए नियम रीबूट पर गायब हो जाते हैं। चूँकि UFW पहले से ही इस फ़ायरवॉल को मैनेज करता है, इसलिए इन्हें स्थायी बनाने के लिए सबसे सही जगह /etc/ufw/after.rules है। फ़ाइल के अंत में एक ब्लॉक जोड़ें:

*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 चलाएँ। UFW हर रीलोड और हर बूट पर उस फ़ाइल को फिर से लागू करता है, इसलिए आपकी कंटेनर फ़िल्टरिंग अब आपके फ़ायरवॉल के बाकी हिस्सों के साथ एक ही जगह पर रहती है, और यह रीबूट और Docker अपग्रेड दोनों के बाद भी सुरक्षित रहती है।

Docker के iptables integration को disable क्यों नहीं करना चाहिए

इस समस्या के पुराने समाधानों में { "iptables": false } को /etc/docker/daemon.json में सेट करने का सुझाव दिया जाता है। ऐसा न करें। Docker के firewall rules केवल ports publish करने से कहीं अधिक काम करते हैं। Masquerade rule ही वह नियम है जो containers को host के address के माध्यम से outbound internet access प्रदान करता है, इसलिए integration बंद होने पर containers package mirrors तक नहीं पहुँच पाएंगे और न ही किसी external API (application programming interface) को call कर पाएंगे। DNAT और isolation rules भी हट जाते हैं, और Docker का documentation चेतावनी देता है कि यदि कोई replacement rules न हों, तो containers के ports local network पर मौजूद hosts के लिए सुलभ हो जाते हैं। आप container networking को तोड़कर इस bypass को ठीक करेंगे, और उन सभी नियमों को आपको स्वयं लिखकर maintain करना होगा। Docker का अपना documentation इस setting को अधिकांश users के लिए अनुपयुक्त बताता है और बाद में नियम बनाने की प्रक्रिया को अत्यंत जटिल बताता है। DOCKER-USER chain का अस्तित्व ही इसलिए है ताकि किसी को भी इस switch की आवश्यकता न पड़े।

IPv6 के संदर्भ में यही समस्या

सबसे पहले जाँचें कि IPv6 पर published port कैसा दिखता है:

sudo ss -tlnp | grep 8080

Docker Engine 27 के बाद से, Docker डिफ़ॉल्ट रूप से ip6tables को manage करता है। IPv6 सक्षम Docker network पर, एक published port को IPv6 tables में भी वही DNAT treatment मिलता है, इसलिए वही bypass वहाँ भी मौजूद है और वही समाधान लागू होता है: DOCKER-USER chain ip6tables में भी मौजूद होती है, इसलिए अपने rule को sudo ip6tables -I DOCKER-USER ... के साथ mirror करें और अपने सर्वर के public IPv6 address पर curl का उपयोग करके बाहर से test करें, उदाहरण के लिए curl -6 http://[2001:db8:2a::1]:8080/। यह Docker के डिफ़ॉल्ट iptables backend पर लागू होता है; experimental nftables backend पर किसी भी stack में DOCKER-USER chain नहीं होती है, और IPv6 filtering आपकी अपनी ip6 nftables table में जाती है।

बिना IPv6 वाले network पर, IPv6 clients को docker-proxy द्वारा संभाला जाता है, जो एक सामान्य user-space process है जो [::]:8080 पर listen करती है और traffic को IPv4 के माध्यम से container में forward करती है। Host process पर जाने वाला traffic INPUT से होकर गुजरता है, इसलिए UFW उस path को filter कर सकता है, लेकिन केवल तभी जब UFW IPv6 को manage कर रहा हो। क्या यह ऐसा कर रहा है, और VPS पर IPv6 gap खुलने के अन्य तरीके क्या हैं, यह the UFW and IPv6 guide का विषय है।

Loopback पर publish करना इस पूरी समस्या से बचाता है: -p 127.0.0.1:8080:80 केवल IPv4 loopback को bind करता है, इसलिए कोई IPv6 listener नहीं होता है, और Docker Engine 28.0.0 और उसके बाद के संस्करणों में किसी भी stack पर बाहर से पहुँचने के लिए कुछ भी उपलब्ध नहीं होता है।

वह पैटर्न जो प्रभावी रहता है

  • हर internal port को 127.0.0.1 पर publish करें, ताकि वह शुरुआत से ही expose न हो। यह Docker Engine 28.0.0 या उसके बाद के वर्ज़न पर लागू होता है (पुराने releases में एक ही L2 segment पर मौजूद hosts इसे access कर सकते थे)।
  • Public side को एक ऐसे reverse proxy को सौंपें जो ports 80 और 443 को manage करता हो।
  • Host के लिए UFW को default deny पर रखें और केवल SSH तथा proxy ports को allow करें।
  • Docker के default iptables backend में DOCKER-USER के माध्यम से वास्तविक public container ports को filter करें। इन्हें original destination port के आधार पर match करें और /etc/ufw/after.rules में persist करें।
  • Docker के iptables integration को on रहने दें।

एक बार setup करने के बाद, यह अनपेक्षित समस्याओं को दूर करता है: ufw status host का वर्णन करता है, और DOCKER-USER containers का। कोई भी port गलती से publish नहीं होता, और अगली बार जब आप docker run -p टाइप करेंगे, तो वही expose होगा जिसे आप वास्तव में करना चाहते थे।

FAQ

UFW द्वारा port block किए जाने के बावजूद मैं अपने Docker container तक क्यों पहुँच सकता हूँ?

क्योंकि Docker port को DOCKER chain में एक DNAT rule के साथ publish करता है, जो nat table का हिस्सा है और PREROUTING से होकर पहुँचता है। यह किसी भी filtering से पहले packet के destination को container के address में बदल देता है। इसके बाद packet FORWARD path से गुजरता है, जबकि UFW के rules INPUT में होते हैं, जहाँ packet कभी नहीं पहुँचता। Firewall से कभी संपर्क नहीं किया जाता, इसलिए इसके deny rules का published container ports पर कोई प्रभाव नहीं पड़ता।

मैं UFW से Docker के published ports को कैसे block करूँ?

UFW स्वयं ऐसा नहीं कर सकता, क्योंकि इसके rules गलत chain में होते हैं। या तो port को expose करना बंद करें, इसे 127.0.0.1:8080:80 के रूप में publish करके ताकि Docker Engine 28.0.0 और उसके बाद के versions पर केवल host ही उस तक पहुँच सके, या फिर DOCKER-USER chain में एक iptables rule का उपयोग करके filter करें जो conntrack के माध्यम से मूल destination port से मेल खाता हो। उस rule को /etc/ufw/after.rules में persist करें ताकि वह reboots और ufw reload के बाद भी बना रहे।

क्या मुझे Docker के daemon.json में "iptables": false सेट करना चाहिए?

नहीं। यह setting Docker के अधिकांश firewall और NAT rules को हटा देती है, जिससे bypass से कहीं अधिक चीजें खराब हो जाती हैं। Masquerade rule हट जाने के कारण containers की outbound internet access खत्म हो जाती है, और Docker चेतावनी देता है कि replacement rules के बिना, container ports स्थानीय network पर मौजूद hosts के लिए सुलभ हो जाते हैं। Loopback publishing और DOCKER-USER chain का उपयोग करें; वे container networking को तोड़े बिना exposure की समस्या को ठीक कर देते हैं।

क्या Docker IPv6 पर भी UFW को bypass करता है?

Docker Engine 27 और उसके बाद के versions पर, ip6tables management डिफ़ॉल्ट रूप से चालू रहता है, इसलिए IPv6-enabled Docker network पर publish किया गया port IPv4 की तरह ही UFW के चारों ओर rewrite हो जाता है। Docker के डिफ़ॉल्ट iptables backend पर, इसे ip6tables के साथ mirror किए गए उसी DOCKER-USER rule की आवश्यकता होती है। Engine 29 के experimental nftables backend पर कोई DOCKER-USER chain नहीं होती, इसलिए वह filtering आपके स्वयं के nftables table में जाती है। IPv6 के बिना networks पर, docker-proxy process [::] पर listen करती है और वह traffic INPUT से होकर गुजरता है, जहाँ UFW उसे filter कर सकता है यदि UFW IPv6 को manage करता हो। 127.0.0.1 पर publish करने से दोनों स्थितियाँ टल जाती हैं, क्योंकि IPv6 पर कुछ भी listen नहीं कर रहा होता।