رفع مشکل امنیت IPv6 در فایروال UFW
بررسی دلیل باز ماندن پورتها در VPS با وجود فعال بودن UFW. یاد بگیرید چگونه از نفوذ مهاجمان از طریق IPv6 که در IPv4 بسته است جلوگیری کنید.
تله فایروال IPv6 در یک جمله
فایروال شما از IPv4 محافظت میکند. VPS شما تقریباً قطعاً یک آدرس IPv6 عمومی نیز دارد و بسیاری از سرویسها بهصورت پیشفرض روی آن در حال شنود (listen) هستند. اگر فایروال شما فقط IPv4 را پوشش دهد، یا اگر از یک فایروال ابری استفاده کنید که فقط IPv4 را فیلتر میکند، تمام آن سرویسها از طریق IPv6 از کل اینترنت قابل دسترسی خواهند بود، در حالی که بخش IPv4 شما کاملاً بسته به نظر میرسد. شما یک پورت را با curl تست میکنید، پیام connection refused را دریافت میکنید و احساس امنیت میکنید. یک مهاجم از طریق IPv6 به همان پورت متصل میشود و وارد سیستم میشود.
این راهنما نشان میدهد که این شکاف در یک VPS معمولی با Ubuntu 24.04 از کجا نشأت میگیرد، چگونه دقیقاً آنچه را که در معرض قرار دادهاید مشاهده کنید و چگونه آن را ببندید. UFW در اینجا مقصر نیست. در یک نصب مدرن Ubuntu، سرویس UFW از قبل IPv6 را مدیریت میکند. این آسیبپذیری از لایههای اطراف آن و از سرویسهایی ناشی میشود که نمیدانستید در حال شنود هستند.
Why your VPS is on IPv6 in the first place
Nearly every VPS today ships with a public IPv6 address, often a whole /64, alongside its IPv4 address. Check yours:
ip -6 addr show scope global2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
inet6 2001:db8:2a::1/64 scope globalThat 2001:db8:2a::1 is routable from anywhere on the internet, exactly like your IPv4 address. Now look at what is listening:
sudo ss -tlnpState Recv-Q Local Address:Port Process
LISTEN 0 0.0.0.0:22 sshd
LISTEN 0 [::]:22 sshd
LISTEN 0 127.0.0.1:5432 postgres
LISTEN 0 [::]:8080 docker-proxyRead the Local Address column closely. 0.0.0.0:22 means "listen on every IPv4 address." [::]:22 means "listen on every IPv6 address." 127.0.0.1:5432 is bound to loopback and is not public at all, so the Postgres line is safe. The two [::] lines answer the whole internet over IPv6, and the docker-proxy one is the kind you forget you started.
Most daemons bind to :: out of the box, because on Linux a :: socket usually accepts IPv4 as well. So the default posture of a fresh server is "answer on both stacks, everywhere." Your firewall is the only thing standing in front of that, which is why a firewall that sees only one stack is a real problem.
منشأ واقعی شکاف IPv6 کجاست
چهار منبع رایج وجود دارد. در یک سرور مشخص، ممکن است یکی از این موارد یا چندین مورد را همزمان داشته باشید.
1. فایروال ابری که فقط IPv4 را فیلتر میکند. بسیاری از فایروالهای ارائهدهندگان و محصولات security-group بر پایه IPv4 توسعه یافتهاند؛ بنابراین یا IPv6 را نادیده میگیرند و یا نیاز به قوانین جداگانه IPv6 دارند که باید به صورت دستی اضافه شوند. اگر تنها فایروال شما همان فایروال موجود در پنل مدیریت ارائهدهنده باشد و IPv6 را پوشش ندهد، سرویسهای [::] شما باز خواهند بود، فارغ از اینکه وضعیت پورت 22 در IPv4 چگونه باشد. مستندات فایروال ارائهدهنده خود را بخوانید و بهطور مشخص به دنبال کلمه IPv6 باشید.
2. استفاده از iptables دستی بدون ip6tables. دستور iptables فقط جداول IPv4 را تغییر میدهد. IPv6 دارای یک دستور کاملاً مجزا به نام ip6tables با قوانین جداگانه خود است. اگر اسکریپت فایروال خود را با خطوط iptables -A INPUT ... نوشتهاید و هرگز قوانین مطابق با آن را در ip6tables تعریف نکردهاید، فایروال IPv6 شما خالی است؛ یک زنجیره INPUT خالی با سیاست پیشفرض ACCEPT، اجازه دسترسی به همه چیز را میدهد:
sudo ip6tables -L INPUT -nChain INPUT (policy ACCEPT)
target prot opt source destinationاین خروجی، تمام تله را در یک صفحه نشان میدهد. IPv4 فیلتر شده است، اما IPv6 به همه اجازه ورود میدهد.
3. انتشار پورتها توسط Docker مستقیماً از فایروال شما. وقتی docker run -p 8080:80 را اجرا میکنید، Docker قوانین خود را قبل از قوانین UFW درج میکند؛ بنابراین یک پورت منتشر شده قابل دسترسی است، حتی اگر ufw status بگوید آن پورت مسدود شده است. در نسخههای مدرن Docker، این موضوع برای IPv6 نیز صدق میکند. چرا Docker از UFW عبور میکند و چگونه پورتهای کانتینر را به درستی فیلتر کنیم این مکانیسم و روشهای اصلاح را توضیح میدهد. برای نحوه تعریف این پورتهای منتشر شده، مبانی Docker Compose در یک VPS را ببینید.
4. غیرفعال بودن IPv6 در UFW. UFW از IPv6 پشتیبانی میکند، اما فقط در صورتی که به آن دستور داده شود. وضعیت سوئیچ را بررسی کنید:
grep IPV6 /etc/default/ufwنسخههای مدرن Ubuntu دارای IPV6=yes هستند، بنابراین UFW هر قانون را برای هر دو پشته (stack) اعمال میکند. اگر در یک ایمج قدیمی یا راهنمای قدیمی با IPV6=no مواجه شدید، تمام قوانین UFW که نوشتهاید فقط برای IPv4 هستند و IPv6 بدون مدیریت باقی مانده است.
دقیقاً بررسی کنید چه چیزی را در معرض دسترسی قرار دادهاید
حدس نزنید. آن را از بیرون اندازهگیری کنید. ابتدا لیست listenerهای خود را استخراج کنید و هر موردی که به :: متصل شده است را یادداشت کنید:
sudo ss -tlnp | grep '::'سپس، از یک دستگاه دیگر، به آدرس IPv6 عمومی سرور متصل شوید و پورتای را که فکر میکنید بسته است، امتحان کنید:
curl -6 -v http://[2001:db8:2a::1]:8080/اگر این دستور صفحهای یا یک banner را برگرداند، یعنی پورت در IPv6 باز است. یک پورت بسته، خطای Connection refused یا timeout برمیگرداند. برای داشتن یک تصویر کامل، آدرس IPv6 را با استفاده از nmap از خارج از سرور اسکن کنید:
nmap -6 2001:db8:2a::1هر پورتی که nmap در IPv6 به عنوان open گزارش دهد، پورتی است که کل اینترنت میتواند به آن دسترسی داشته باشد، فارغ از اینکه اسکن IPv4 شما چه چیزی نشان داده است. مقایسه اسکنهای IPv4 و IPv6 در کنار هم، سریعترین راه برای یافتن شکاف است: هر چیزی که در -6 باز باشد اما در IPv4 بسته باشد، سرویسی است که فایروال شما آن را پوشش نداده است.
Close the gap
UFW را برای هر دو پشته (stack) فعال کنید و حالت پیشفرض را روی deny قرار دهید. تغییرات را تایید کنید، سپس یک سیاست default-deny برای ورودی (inbound) تنظیم کنید و فقط موارد مورد نیاز را مجاز (allow) کنید:
sudo sed -i 's/^IPV6=no/IPV6=yes/' /etc/default/ufw
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verboseاگر هنگام تغییر IPV6=yes، سرویس UFW فعال بوده است، تغییرات تا زمانی که sudo ufw reload را اجرا نکنید اعمال نمیشوند.
ufw status هر قانون را دو بار لیست میکند؛ یک بار به صورت ساده و یک بار با پسوند (v6). وقتی خطوط (v6) را مشاهده میکنید، یعنی UFW در حال فیلتر کردن IPv6 است:
22/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)اگر iptables را به صورت دستی مدیریت میکنید، تمام قوانین را در ip6tables نیز تکرار کنید، یا به nftables مهاجرت کنید؛ جداول inet در nftables پوشش IPv4 و IPv6 را در یک مکان فراهم میکنند و این نوع خطا را کاملاً از بین میبرند. اگر خودتان در حال نوشتن قوانین هستید، استفاده از یک جدول فیلتر nftables inet تمیزترین راه حل است.
سرویسهایی که نمیخواهید عمومی باشند را به loopback متصل کنید. یک پایگاه داده، پنل مدیریت، یا یک نقطه اتصال (endpoint) برای متریکها، معمولاً اصلاً به یک آدرس عمومی نیاز ندارند. آنها را به 127.0.0.1 و ::1 متصل کنید تا از همان ابتدا روی یک آدرس قابل مسیریابی (routable) گوش ندهند. برای Postgres، مقدار listen_addresses = 'localhost' را تنظیم کنید. برای یک سرور اپلیکیشن، آن را به 127.0.0.1 متصل کنید و یک reverse proxy در جلوی آن قرار دهید. بستن listener بسیار موثرتر از فیلتر کردن آن با فایروال است، زیرا در آن صورت چیزی برای دسترسی وجود نخواهد داشت.
به UFW برای محافظت از پورتهای منتشر شده (published ports) در Docker اعتماد نکنید. پورتهای کانتینر را به جای تمام اینترفیسها، روی یک آدرس مشخص منتشر کنید، مثلاً -p 127.0.0.1:8080:80؛ به این ترتیب پورت فقط از طریق host و هر چیزی که عمداً به آن پروکسی شده است، قابل دسترسی خواهد بود. وقتی یک کانتینر واقعاً باید عمومی باشد، آن را پشت یک reverse proxy از نوع Traefik قرار دهید و فقط پروکسی را منتشر کنید، نه تکتک اپلیکیشنها را.
قوانین IPv6 را به فایروال ارائهدهنده خود اضافه کنید، یا بپذیرید که آن فایروال مسئولیت IPv6 را بر عهده ندارد و اجازه دهید UFW یا nftables روی host این وظیفه را انجام دهند.
Verify you are actually closed
پس از اعمال تغییرات، دوباره همان تست خارجی را اجرا کنید:
curl -6 -v http://[2001:db8:2a::1]:8080/
nmap -6 2001:db8:2a::1پورتی که قبلاً پاسخ میداد، اکنون باید درخواست را رد کند (refuse) یا با timeout مواجه شود؛ در این حالت nmap باید وضعیت آن را به صورت filtered یا closed گزارش دهد. اگر پورتی همچنان open است، چهار منبع ذکر شده در بالا را بررسی کنید: سرویسی که همچنان به :: متصل است و قانونی جلوی آن نیست، یک Docker rule که قبل از UFW قرار دارد، یا یک provider firewall که اصلاً IPv6 را شناسایی نکرده است.
جداسازی کامل سرویسهای حساس از اینترنت عمومی، امنیت بیشتری فراهم میکند. Put SSH and admin panels behind a WireGuard VPN و پورتهای آنها را فایروال کنید تا فقط از طریق tunnel پاسخ دهند؛ در این صورت مسئله exposure در IPv6 برای آنها مطرح نخواهد بود. برای کاهش سرعت brute-force scanهایی که به سرویسهای عمومی حمله میکنند، Fail2ban in front of SSH را روی یک firewall با حالت default-deny قرار دهید.
اگر با مفهوم پورتها آشنایی ندارید، ابتدا what ports are and how services listen را مطالعه کنید.
FAQ
آیا UFW به صورت پیشفرض IPv6 را مسدود میکند؟
در نصبهای مدرن Ubuntu 24.04، پاسخ مثبت است. UFW تنظیمات IPV6=yes را از /etc/default/ufw میخواند و هر قانون را برای هر دو پروتکل IPv4 و IPv6 اعمال میکند؛ همچنین ufw status قوانین IPv6 را با پسوند (v6) نمایش میدهد. مشکل زمانی رخ میدهد که از IPV6=no (از یک ایمج قدیمی یا آموزش قدیمی) استفاده کنید، یا زمانی که به فایروال یک سرویسدهنده متکی باشید که فقط IPv4 را فیلتر میکند، و یا زمانی که Docker پورت را فراتر از UFW منتشر (publish) کند. وضعیت این سوئیچ را با grep IPV6 /etc/default/ufw بررسی کنید.
چگونه بفهمم VPS من چه مواردی را در IPv6 نمایش میدهد؟
دستور sudo ss -tlnp را اجرا کنید و هر Listener که آدرس محلی آن با [::] شروع میشود را یادداشت کنید؛ این یعنی آن سرویس به تمام اینترفیسهای IPv6 پاسخ میدهد. سپس، از یک دستگاه دیگر، آدرس IPv6 عمومی سرور را مستقیماً با curl -6 -v http://[YOUR:IPV6::ADDR]:PORT/ تست کنید یا با nmap -6 YOUR:IPV6::ADDR آن را اسکن کنید. هر پورتی که در اسکن IPv6 باز باشد اما در IPv4 بسته باشد، یک شکاف امنیتی است.
چرا وقتی UFW میگوید پورت Docker من مسدود است، همچنان میتوانم به آن دسترسی داشته باشم؟
وقتی با استفاده از -p پورت را منتشر میکنید، Docker قوانین فایروال خودش را قبل از قوانین UFW درج میکند؛ بنابراین پورت منتشر شده در دسترس است، حتی اگر ufw status آن را به عنوان denied لیست کرده باشد. این اتفاق در IPv4 و همچنین در IPv6 (زمانی که پشتیبانی IPv6 در Docker فعال باشد) رخ میدهد. پورت را فقط روی یک آدرس مشخص مانند -p 127.0.0.1:8080:80 منتشر کنید، یا کانتینر را پشت یک reverse proxy قرار دهید و فقط پورت پروکسی را منتشر کنید.
اگر فایروال IPv4 من قوی باشد، آیا هنوز به فایروال IPv6 نیاز دارم؟
بله. IPv4 و IPv6 پشتههای شبکه مجزا با قوانین فایروال مجزا هستند. یک مجموعه کامل از قوانین IPv4، هیچ تأثیری روی ترافیک IPv6 ندارد. اگر VPS شما دارای یک آدرس IPv6 عمومی باشد (که تقریباً همه دارند)، هر سرویسی که روی :: گوش دهد، تا زمانی که یک قانون فایروال IPv6 یا یک binding روی loopback آن را متوقف نکند، از طریق IPv6 در دسترس باقی میماند.
چگونه یک سرویس را فقط روی IPv4 یا فقط روی localhost محدود کنم؟
آدرس bind سرویس را در فایل کانفیگ خود آن تنظیم کنید. برای محدود کردن به IPv4 loopback فقط از 127.0.0.1 استفاده کنید، یا برای تمام آدرسهای IPv4 بدون Listener در IPv6 از 0.0.0.0 استفاده کنید. Postgres از listen_addresses، SSH از ListenAddress استفاده میکند و اکثر سرورهای اپلیکیشن یک پرچم host یا bind دارند. نتیجه را با sudo ss -tlnp تایید کنید و بررسی کنید که Local Address دیگر [::] را نشان ندهد.