چرا Docker از فایروال UFW عبور میکند و راه حل آن
داکر به دلیل تغییر در قوانین iptables پورتهای کانتینر را بدون توجه به UFW باز میکند. در این مطلب نحوه عملکرد این فرآیند و روشهای ایمنسازی پورتها را بررسی میکنیم.
چرا Docker از UFW عبور میکند
Docker از UFW عبور میکند زیرا پورتهای منتشرشدهٔ کانتینر هرگز از قوانین فایروالی که UFW مدیریت میکند، عبور نمیکنند. هنگامی که شما docker run -p 8080:80 را اجرا میکنید، Docker یک قانون DNAT (ترجمه آدرس شبکه مقصد) را در زنجیره PREROUTING از جدول nat هسته مینویسد. آن قانون، مقصد هر بسته را پیش از آنکه هسته تصمیم بگیرد بسته به کجا میرود، به آدرس خصوصی کانتینر بازنویسی میکند. بستهٔ بازنویسیشده سپس از طریق زنجیره FORWARD که تحت کنترل Docker است، به داخل کانتینر هدایت میشود. قوانین UFW در زنجیره INPUT قرار دارند و بسته هرگز وارد آن نمیشود. بنابراین ufw status وضعیت پیشفرض deny را نشان میدهد، sudo ufw deny 8080 موفقیتآمیز بودن عملیات را گزارش میکند، اما پورت 8080 همچنان به کل اینترنت پاسخ میدهد.
این یک باگ در Docker نیست و UFW نیز خراب نشده است. هر دو ابزار یک فایروال هستهٔ یکسان را برنامهریزی میکنند. قوانین Docker بهسادگی در نقطهٔ زودتری از مسیر بسته عمل میکنند، بنابراین هرگز از UFW پرسشی نمیشود. این راهنما نحوهٔ این عبور را نشان میدهد، مکانیزم آن را توضیح میدهد و سپس دو راهکار عملی را پوشش میدهد: انتشار پورتها روی 127.0.0.1 و فیلتر کردن در زنجیره DOCKER-USER. اگر UFW برای شما جدید است، ابتدا آن را با راهنمای اصول فایروال UFW راهاندازی کنید، زیرا یک فایروال با سیاست پیشفرض deny، همچنان بهترین پایه برای هر چیز دیگری روی سرور است.
مشاهده دور زدن فایروال روی سرور شخصی
کار را از یک VPS شروع کنید که در آن UFW فعال است و سیاست پیشفرض برای ترافیک ورودی روی deny تنظیم شده است. یک کانتینر وب با پورتی منتشرشده اجرا کنید:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineدستور ufw 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 در زنجیرهای قرار دارد که بسته (packet) هرگز به آن نمیرسد. UFW شکست نخورده است. اصلاً از آن استعلام گرفته نشده است. به همین دلیل است که این مشکل به خوبی پنهان میماند: هیچ خطایی در هیچجا چاپ نمیشود، استقرار (deploy) کار میکند و وضعیت خروجی فایروال دقیقاً مانند یک سرور سالم و ایمن به نظر میرسد.
مکانیسم: PREROUTING پیش از INPUT اجرا میشود
هسته سیستمعامل بستههای ورودی را با ترتیبی ثابت پردازش میکند و کل مشکل در همین ترتیب نهفته است.
PREROUTINGابتدا اجرا میشود. قوانین در این بخش ممکن است مقصد بسته را بازنویسی کنند و قانون Docker برای پورتهای منتشر شده (published ports) دقیقاً همین کار را انجام میدهد.- تصمیمگیری برای مسیریابی (routing decision) در مرحله بعد انجام میشود. بستهای که خطاب به خود میزبان باشد به زنجیره
INPUTمیرود. بستهای که خطاب به هر ماشین دیگری باشد به زنجیرهFORWARDمیرود. - قوانین UFW در
INPUTقرار دارند. قوانین Docker درFORWARDقرار دارند.
به قانون Docker برای کانتینری که بهتازگی راهاندازی کردهاید نگاه کنید:
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:80خط DNAT تمام ماجراست. هر بستهای که برای پورت 8080 میرسد، مقصدش به 172.17.0.2:80 بازنویسی میشود که همان آدرس کانتینر در شبکه bridge خصوصی Docker است. پس از بازنویسی، بسته دیگر خطاب به میزبان نیست، بنابراین تصمیم مسیریابی آن را به مسیر FORWARD میفرستد؛ جایی که Docker قبلاً قوانینی را اضافه کرده است که ترافیک را به شبکههای خودش میپذیرد. قانون deny 8080/tcp شما در INPUT منتظر بستهای میماند که هرگز نمیرسد.
در Ubuntu 24.04 دستور iptables یک رابط کاربری (front end) روی nftables است، اما ترتیب زنجیرهها و نتیجه نهایی یکسان است. UFW و Docker هر دو در همان خط لوله بستههای هسته مینویسند و نقطه ورود Docker زودتر است. هیچکدام از این موارد مختص UFW نیست: firewalld روی یک VPS با سیستمعامل Rocky یا AlmaLinux نیز در همان نقطه از خط لوله فیلتر میکند و توسط همان قانون DNAT دور زده میشود، بنابراین راهحلهای زیر برای آنجا نیز کاربرد دارند.
راهکار روزمره: انتشار پورتها روی 127.0.0.1
بیشتر کانتینرها از ابتدا نیازی به عمومی بودن نداشتهاند. یک دیتابیس، یک اپلیکیشن سرور پشت یک reverse proxy، یک پنل مدیریتی یا یک endpoint برای متریکها؛ هیچکدام نباید مستقیماً به اینترنت پاسخ دهند. آنها را روی آدرس loopback منتشر کنید:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpineیا در یک فایل Compose:
services:
web:
image: nginx:1.29-alpine
ports:
- "127.0.0.1:8080:80"این روش کار میکند زیرا قانون DNAT در Docker اکنون فقط با بستههایی مطابقت دارد که مقصد آنها 127.0.0.1 است، و یک بسته از اینترنت هرگز نمیتواند بهطور قانونی چنین مقصدی داشته باشد، بنابراین هسته سیستمعامل پیش از اجرای هرگونه قانون فایروال، آن را حذف میکند. پورت از روی میزبان (host) قابل دسترسی است و از هیچ جای دیگر خیر. اتصال را بررسی کنید:
sudo ss -tlnp | grep 8080شما باید در خروجی 127.0.0.1:8080 را ببینید، نه 0.0.0.0:8080 یا [::]:8080. سپس از یک ماشین دیگر تأیید کنید که curl http://your-vps-ip:8080/ با رد اتصال (refused) مواجه میشود.
برای سرویسهایی که باید در معرض اینترنت باشند، یک reverse proxy اجرا کنید که مالک پورتهای 80 و 443 باشد و مسیریابی را بر اساس نام دامنه انجام دهد؛ هیچ چیز دیگری را منتشر نکنید. این همان الگویی است که راهنمای reverse proxy با Traefik بر اساس آن ساخته شده است و به همین دلیل است که یک اپلیکیشن self-hosted مانند Nextcloud روی VPS بهجز از طریق پروکسی خود، غیرقابل دسترس باقی میماند. نحوه تعریف ورودیهای ports: و سایر جزئیات گردش کار Compose در راهنمای اصول Docker Compose پوشش داده شده است.
با قرار دادن تمام کانتینرهای داخلی روی loopback، فایروال UFW به وظیفه اصلی خود بازمیگردد: محافظت از پورتهایی که خودِ میزبان ارائه میدهد. مجموعه قوانین را در اینجا بسازید و سپس دستورات را به ترتیب اجرا کنید:
فیلترینگ واقعی: زنجیره DOCKER-USER
گاهی اوقات لازم است پورت یک کانتینر روی شبکه منتشر بماند اما دسترسی به آن محدود شود؛ برای مثال، پورت یک دیتابیس replica که فقط یک آدرس IP خاص در دفتر کار باید به آن دسترسی داشته باشد. برای این منظور، Docker زنجیره DOCKER-USER را ارائه میدهد. هر بستهای که به سمت هر کانتینری میرود، پیش از اعمال قوانین پذیرش (accept) خودِ Docker، از DOCKER-USER عبور میکند و Docker هرگز هیچ قانونی در آن نمینویسد. این زنجیره متعلق به شماست و Docker محتویات آن را پس از راهاندازی مجدد daemon تغییر نمیدهد.
یک نکته مهم پیش از اجرای دستور: در لحظهای که بسته به DOCKER-USER میرسد، بازنویسی DNAT قبلاً انجام شده است. پورت مقصد بسته، پورت کانتینر (در مثال ما 80) است، نه پورت منتشرشده (8080). بنابراین، قانونی که با --dport 8080 مطابقت داده شود، هیچ بستهای را پیدا نمیکند. روش مطمئن، تطبیق با پورتی است که کلاینت در ابتدا به آن متصل شده است؛ اطلاعاتی که در 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 وارد شدهاند و متعلق به اتصالی هستند که پورت مقصد اصلی آن 8080 بوده است، هر بستهای که از 10.0.0.10 ارسال نشده باشد را drop کنید. تطبیق --ctdir ORIGINAL قانون را به جهت کلاینت به کانتینر محدود میکند تا بستههای پاسخ (reply) به اشتباه فیلتر نشوند. به جای eth0 رابط شبکه عمومی خود را قرار دهید؛ ip route | grep default نام آن است. تست را مانند قبل انجام دهید: curl از آدرس مجاز موفقیتآمیز است و از هر جای دیگر، اتصال با timeout مواجه میشود. این وقفه (hang)، نشانه عملکرد صحیح قانون DROP است و با پورتی که پشت آن سرویسی وجود ندارد تفاوت دارد؛ و تفاوت بین اتصال رد شده (refused) و اتصال دارای timeout سریعترین راه برای تشخیص پورت فیلترشده از سرویسی است که صرفاً گوش نمیدهد.
قوانینی که با دستور iptables اضافه میشوند، پس از reboot از بین میروند. از آنجا که 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 این فایل را در هر بار reload و هر بار boot مجدداً اعمال میکند، بنابراین فیلترینگ کانتینر شما اکنون در همان جایی قرار دارد که سایر تنظیمات فایروال شماست و هم پس از reboot و هم پس از ارتقای Docker باقی میماند.
چرا نباید یکپارچگی iptables در Docker را غیرفعال کنید
پاسخهای قدیمیتر برای این مشکل، تنظیم { "iptables": false } را در /etc/docker/daemon.json پیشنهاد میدهند. این کار را انجام ندهید. قوانین فایروال Docker کارهایی بسیار فراتر از انتشار پورتها انجام میدهند. قانون masquerade همان چیزی است که به کانتینرها دسترسی خروجی به اینترنت از طریق آدرس میزبان میدهد؛ بنابراین با خاموش بودن این یکپارچگی، کانتینرها نمیتوانند imageها را دریافت کنند، به mirrorهای پکیج دسترسی یابند یا هیچ API (رابط برنامهنویسی اپلیکیشن) خارجی را فراخوانی کنند. قوانین DNAT همان چیزی هستند که باعث میشوند -p اصلاً کار کند، بنابراین پورتهای منتشرشده بهطور کامل از کار میافتند. قوانین ایزولاسیونی که شبکههای مجزای Compose را از هم جدا نگه میدارند نیز از بین میروند. شما با شکستن شبکه کانتینر، این bypass را اصلاح میکنید و مسئولیت نوشتن و نگهداری دستی تکتک آن قوانین بر عهده شما خواهد بود. مستندات خود Docker این تنظیم را برای کسانی توصیف میکند که قصد انجام دقیقاً همین کار را دارند. زنجیره DOCKER-USER دقیقاً به همین دلیل وجود دارد که هیچکس نیازی به این سوئیچ نداشته باشد.
جنبه IPv6 همان مشکل
ابتدا بررسی کنید که پورت منتشرشده در IPv6 چگونه به نظر میرسد:
sudo ss -tlnp | grep 8080از نسخه 27 به بعد Docker Engine، داکر بهصورت پیشفرض ip6tables را مدیریت میکند. در یک شبکه داکر که IPv6 در آن فعال است، پورت منتشرشده همان رفتار DNAT را در جداول IPv6 دریافت میکند؛ بنابراین همان دور زدن (bypass) در اینجا نیز وجود دارد و همان راهحل اعمال میشود: زنجیره DOCKER-USER در ip6tables نیز موجود است، پس قانون خود را با sudo ip6tables -I DOCKER-USER ... بازتاب دهید و از خارج شبکه با استفاده از curl علیه آدرس IPv6 عمومی سرور خود تست کنید، برای مثال curl -6 http://[2001:db8:2a::1]:8080/.
در شبکهای بدون IPv6، کلاینتهای IPv6 توسط docker-proxy مدیریت میشوند؛ یک پردازش فضای کاربری (user-space) معمولی که روی [::]:8080 گوش میدهد و ترافیک را از طریق IPv4 به داخل کانتینر هدایت میکند. ترافیک به سمت یک پردازش میزبان از طریق INPUT عبور میکند، بنابراین UFW میتواند آن مسیر را فیلتر کند، اما تنها زمانی که UFW بهطور کلی در حال مدیریت IPv6 باشد. اینکه آیا این وضعیت برقرار است یا خیر، و سایر روشهایی که یک شکاف IPv6 روی یک VPS ایجاد میشود، موضوع راهنمای UFW و IPv6 است.
انتشار روی loopback کل این مسئله را دور میزند: -p 127.0.0.1:8080:80 فقط به IPv4 loopback متصل میشود، بنابراین هیچ listener برای IPv6 وجود ندارد و چیزی از خارج روی هیچکدام از پشتهها (stack) قابل دسترسی نیست.
الگویی که پایداری را تضمین میکند
- تمام پورتهای داخلی را روی
127.0.0.1منتشر کنید تا در وهلهٔ اول هیچگاه در معرض دسترسی عمومی قرار نگیرند. - سمت عمومی را به یک reverse proxy بسپارید که مالکیت پورتهای 80 و 443 را در اختیار دارد.
- سیاست پیشفرض UFW برای میزبان را روی deny قرار دهید و فقط اجازهٔ دسترسی به SSH و پورتهای proxy را بدهید.
- پورتهای کانتینری که واقعاً عمومی هستند را در
DOCKER-USERفیلتر کنید؛ این کار بر اساس پورت مقصد اصلی انجام شده و در/etc/ufw/after.rulesماندگار میشود. - یکپارچگی iptables در Docker را فعال نگه دارید.
با یکبار تنظیم، این روش غافلگیریها را حذف میکند: ufw status میزبان را توصیف میکند و DOCKER-USER کانتینرها را. هیچ موردی بهطور تصادفی منتشر نمیشود و دستور docker run -p بعدی که تایپ میکنید، دقیقاً همان چیزی را در معرض قرار میدهد که مد نظر شما بوده است.
FAQ
چرا با وجود مسدود بودن پورت در UFW، همچنان میتوانم به کانتینر Docker دسترسی داشته باشم؟
زیرا Docker پورت را با یک قانون DNAT در زنجیره PREROUTING منتشر میکند که مقصد بسته را پیش از هرگونه فیلترینگ، به آدرس کانتینر تغییر میدهد. بسته سپس مسیر FORWARD را طی میکند، در حالی که قوانین UFW در زنجیره INPUT قرار دارند؛ زنجیرهای که بسته هرگز وارد آن نمیشود. فایروال در این فرآیند دخالتی ندارد، بنابراین قوانین deny آن هیچ تأثیری بر پورتهای منتشرشده کانتینر ندارند.
چگونه میتوانم UFW را وادار کنم پورتهای منتشرشده Docker را مسدود کند؟
خود UFW نمیتواند این کار را انجام دهد، زیرا قوانین آن در زنجیره اشتباهی قرار دارند. یا انتشار پورت را متوقف کنید و آن را به صورت 127.0.0.1:8080:80 منتشر کنید تا فقط میزبان (host) به آن دسترسی داشته باشد، یا با استفاده از یک قانون iptables که پورت مقصد اصلی را از طریق conntrack مطابقت میدهد، در زنجیره DOCKER-USER فیلترینگ را اعمال کنید. این قانون را در /etc/ufw/after.rules پایدار کنید تا پس از راهاندازی مجدد و ufw reload باقی بماند.
آیا باید گزینه "iptables": false را در فایل daemon.json داکر تنظیم کنم؟
خیر. این تنظیم تمام قوانین فایروال و NAT داکر را حذف میکند که باعث بروز مشکلاتی بسیار فراتر از دور زدن فایروال میشود. کانتینرها دسترسی خروجی به اینترنت را از دست میدهند زیرا قانون masquerade حذف شده است و پورتهای منتشرشده نیز از کار میافتند زیرا قوانین DNAT حذف شدهاند. به جای این کار از انتشار روی loopback و زنجیره DOCKER-USER استفاده کنید؛ این روش مشکل دسترسی ناخواسته را بدون آسیب زدن به شبکه کانتینرها حل میکند.
آیا داکر UFW را در IPv6 نیز دور میزند؟
در Docker Engine 27 و نسخههای بعد از آن، مدیریت ip6tables بهصورت پیشفرض فعال است، بنابراین پورتی که روی یک شبکه Docker با قابلیت IPv6 منتشر میشود، دقیقاً مانند IPv4 از کنار UFW عبور داده میشود و نیاز به همان قانون DOCKER-USER دارد که با ip6tables بازتاب داده شده باشد. در شبکههای بدون IPv6، فرآیند docker-proxy روی [::] گوش میدهد و آن ترافیک از INPUT عبور میکند، جایی که اگر UFW مدیریت IPv6 را بر عهده داشته باشد، میتواند آن را فیلتر کند. انتشار روی 127.0.0.1 هر دو حالت را پوشش میدهد، زیرا در این صورت هیچ سرویسی روی IPv6 گوش نمیدهد.