SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor

UFW و IPv6: چرا VPS شما هنوز در معرض خطر است؟

در Ubuntu 24.04، فایروال ابری یا iptables دستی ممکن است فقط IPv4 را فیلتر کند. دلیل دسترسی IPv6 به سرویس‌ها و روش بستن این شکاف را ببینید.

دام فایروال IPv6 در یک جمله

فایروال شما از IPv4 محافظت می‌کند، اما VPS شما تقریباً به‌طور قطع دارای یک آدرس IPv6 عمومی نیز هست و بسیاری از سرویس‌ها به‌صورت پیش‌فرض روی آن گوش می‌دهند. اگر فایروال شما فقط IPv4 را پوشش دهد، یا اگر به فایروال ابری متکی باشید که فقط IPv4 را فیلتر می‌کند، تمام آن سرویس‌ها از طریق IPv6 برای کل اینترنت قابل‌دسترسی خواهند بود، در حالی که سمت IPv4 شما امن به نظر می‌رسد. شما یک پورت را با curl تست می‌کنید، با خطای refused connection مواجه می‌شوید و احساس امنیت می‌کنید؛ اما یک مهاجم از طریق IPv6 به همان پورت متصل شده و به‌راحتی وارد می‌شود.

این راهنما نشان می‌دهد که این شکاف امنیتی در یک VPS معمولی با Ubuntu 24.04 از کجا ناشی می‌شود، چگونه دقیقاً متوجه شوید چه چیزی را در معرض دید قرار داده‌اید و چگونه آن را مسدود کنید. UFW در اینجا مقصر نیست. در نصب مدرن Ubuntu، ابزار UFW از قبل IPv6 را مدیریت می‌کند. این نشت امنیتی از لایه‌های اطراف آن و از سرویس‌هایی ناشی می‌شود که نمی‌دانستید در حال گوش دادن هستند.

چرا VPS شما اساساً روی IPv6 قرار دارد

امروزه تقریباً هر VPS همراه با آدرس عمومی IPv6 ارائه می‌شود؛ این آدرس اغلب یک /64 کامل است و در کنار آدرس IPv4 قرار دارد. آدرس خود را بررسی کنید:

ip -6 addr show scope global
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
    inet6 2001:db8:2a::1/64 scope global

مقدار 2001:db8:2a::1 در این راهنما، یک placeholder از 2001:db8::/32 است؛ این prefix برای مستندات رزرو شده و هرگز در اینترنت route نمی‌شود. آدرس واقعی که سرور شما در آن خط نمایش می‌دهد، از هر نقطه‌ای در اینترنت قابل route است؛ درست مانند آدرس IPv4 شما. اکنون ببینید چه مواردی در حال listening هستند:

sudo ss -tlnp
State   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-proxy

ستون Local Address را با دقت بررسی کنید. 0.0.0.0:22 یعنی «روی همه آدرس‌های IPv4 در حالت listening قرار دارد». [::]:22 یعنی «روی همه آدرس‌های IPv6 در حالت listening قرار دارد». مقدار 127.0.0.1:5432 به loopback متصل است و اصلاً public نیست؛ بنابراین خط Postgres امن است. دو خط [::] از طریق IPv6 به کل اینترنت پاسخ می‌دهند و مورد docker-proxy همان سرویسی است که ممکن است فراموش کرده باشید آن را اجرا کرده‌اید.

بیشتر daemonها به‌صورت پیش‌فرض به :: متصل می‌شوند، زیرا در Linux، یک socket از نوع :: معمولاً IPv4 را نیز می‌پذیرد. بنابراین وضعیت پیش‌فرض یک سرور تازه این است: «در هر دو stack و در همه‌جا پاسخ بده». تنها چیزی که در برابر این وضعیت قرار می‌گیرد firewall شماست؛ به همین دلیل، firewallای که فقط یکی از این دو stack را مشاهده کند، یک مشکل واقعی است.

منشأ واقعی شکاف IPv6

این مشکل معمولاً از سه منبع ناشی می‌شود. در یک سرور مشخص ممکن است یکی از آن‌ها یا چند مورد از آن‌ها هم‌زمان وجود داشته باشد.

1. فایروال ابری که فقط IPv4 را فیلتر می‌کند. بسیاری از فایروال‌های ارائه‌دهندگان و محصولات security group بر اساس IPv4 توسعه یافته‌اند و IPv6 را نادیده می‌گیرند یا به ruleهای جداگانه IPv6 نیاز دارند که باید آن‌ها را دستی اضافه کنید. اگر تنها فایروال شما همان فایروال موجود در داشبورد ارائه‌دهنده باشد و IPv6 را پوشش ندهد، سرویس‌های [::] شما باز هستند؛ صرف‌نظر از این‌که درباره پورت 22 روی IPv4 چه وضعیتی اعلام شده باشد. مستندات فایروال ارائه‌دهنده را بخوانید و مشخصاً به دنبال واژه IPv6 بگردید.

2. iptables دستی بدون ip6tables. دستور iptables فقط جدول‌های IPv4 را تغییر می‌دهد. IPv6 دستور کاملاً جداگانه‌ای به نام ip6tables دارد که ruleهای مستقل خودش را استفاده می‌کند. اگر اسکریپت فایروال شما پر از خط‌های iptables -A INPUT ... است و ruleهای متناظر ip6tables را هرگز ننوشته‌اید، فایروال IPv6 شما خالی است و یک زنجیره INPUT خالی با policy پیش‌فرض ACCEPT، همه‌چیز را مجاز می‌کند:

sudo ip6tables -L INPUT -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

این خروجی کل مشکل را در یک صفحه نشان می‌دهد. IPv4 فیلتر شده است، اما IPv6 اتصال از هر مبدأیی را می‌پذیرد.

3. انتشار مستقیم پورت‌ها توسط Docker، بدون عبور از فایروال. وقتی docker run -p 8080:80 را اجرا می‌کنید، Docker ruleهای خودش را پیش از ruleهای UFW قرار می‌دهد؛ بنابراین یک پورت publish‌شده قابل دسترسی است، حتی اگر ufw status نشان دهد که آن پورت deny شده است. نسخه‌های فعلی Docker به‌طور پیش‌فرض ruleهای ip6tables را مدیریت می‌کنند. بنابراین اگر IPv6 در network مربوط به container فعال باشد، همین وضعیت برای IPv6 نیز برقرار است. چرا Docker از UFW عبور می‌کند و چگونه پورت‌های container را به‌درستی فیلتر کنیم سازوکار و راه‌حل‌ها را توضیح می‌دهد. برای آشنایی با نحوه تعریف این پورت‌های publish‌شده، به مبانی Docker Compose روی یک VPS مراجعه کنید.

نکته‌ای درباره سوییچ IPv6 در UFW. UFW از IPv6 پشتیبانی می‌کند و این سوییچ در یک فایل قرار دارد. آن را بررسی کنید:

grep IPV6 /etc/default/ufw

Ubuntu مدرن با IPV6=yes عرضه می‌شود؛ بنابراین UFW هر rule را روی هر دو stack اعمال می‌کند. اگر IPV6=no را مشاهده کردید، احتمالاً از یک image قدیمی یا یک راهنمای قدیمی استفاده می‌کنید؛ در این حالت IPv6 باز رها نشده است. با این تنظیم، UFW تمام ترافیک IPv6 را به‌جز روی loopback interface drop می‌کند. بنابراین مشکل در جهت مخالف ایجاد می‌شود: هر چیزی را که قصد داشتید از طریق IPv6 ارائه کنید، دیگر پاسخ نمی‌دهد. اگر می‌خواهید UFW به‌جای مسدودکردن کامل IPv6، آن را با ruleهای شما فیلتر کند، مقدار را روی yes تنظیم کنید.

دقیقاً ببینید چه چیزی را در معرض دید قرار داده‌اید

حدس نزنید. آن را از بیرون اندازه‌گیری کنید. ابتدا لیست listenerهای خود را تهیه کنید و هر موردی که به :: متصل است را یادداشت کنید:

sudo ss -tlnp | grep '::'

سپس، از یک ماشین دیگر، به آدرس IPv6 عمومی سرور متصل شوید و پورتی را که فکر می‌کنید بسته است امتحان کنید:

curl -6 -v http://[2001:db8:2a::1]:8080/

اگر این دستور یک صفحه یا بنر برگرداند، پورت روی IPv6 باز است. پورت بسته به شما Connection refused یا یک timeout می‌دهد. این دو خطا سیگنال‌های یکسانی نیستند و تفاوت بین refused و timed out به شما می‌گوید که آیا میزبان پاسخ داده و شما را رد کرده است یا یک فایروال بسته شما را در سکوت دور انداخته است. برای داشتن تصویری کامل، آدرس IPv6 را با nmap از خارج از سرور اسکن کنید:

nmap -6 2001:db8:2a::1

هر پورتی که nmap آن را روی IPv6 باز گزارش می‌کند، پورتی است که کل اینترنت می‌تواند به آن دسترسی داشته باشد، فارغ از اینکه اسکن IPv4 شما چه چیزی را نشان داده است. مقایسه اسکن‌های IPv4 و IPv6 در کنار هم، سریع‌ترین راه برای یافتن شکاف است: هر چیزی که روی -6 باز است اما روی IPv4 بسته است، سرویسی است که فایروال شما آن را نادیده گرفته است.

پر کردن شکاف امنیتی

تنظیمات UFW را برای پوشش هر دو پشته اعمال کنید و سیاست پیش‌فرض را روی deny قرار دهید. تغییر را تأیید کنید، سپس سیاست ورودی پیش‌فرض را روی deny تنظیم کرده و فقط دسترسی‌های ضروری را مجاز کنید:

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 آن، IPv4 و IPv6 را در یک مکان پوشش می‌دهند و این دسته از خطاها را به‌طور کامل حذف می‌کنند. هنگام نوشتن دستی قوانین، استفاده از یک جدول فیلتر inet در nftables تمیزترین راهکار است. اگر VPS شما به جای Ubuntu از Rocky یا AlmaLinux استفاده می‌کند، ابزار UFW برای پیکربندی وجود ندارد و در عوض firewalld رابط مدیریتی شما خواهد بود که قوانین zone خود را به‌طور همزمان روی هر دو پشته اعمال می‌کند.

سرویس‌هایی که نمی‌خواهید عمومی باشند را روی loopback محدود کنید. یک پایگاه داده، پنل مدیریت یا endpoint مربوط به متریک‌ها، به‌ندرت به آدرس عمومی نیاز دارند. آن‌ها را روی 127.0.0.1 و ::1 bind کنید تا از همان ابتدا روی آدرس‌های قابل مسیریابی گوش ندهند. برای Postgres، مقدار listen_addresses = 'localhost' را تنظیم کنید. برای یک سرور برنامه، آن را روی 127.0.0.1 bind کرده و یک reverse proxy در مقابل آن قرار دهید. بستن listener بهتر از فایروال کردن آن است، زیرا در این صورت هیچ چیزی برای دسترسی وجود نخواهد داشت.

برای محافظت از پورت‌های منتشر شده توسط Docker به UFW اعتماد نکنید. پورت‌های container را به جای همه رابط‌ها، روی یک آدرس خاص منتشر کنید (مثلاً -p 127.0.0.1:8080:80) تا پورت فقط از طریق میزبان و هر آنچه که عمداً به آن proxy می‌کنید، قابل دسترسی باشد. زمانی که یک container حتماً باید عمومی باشد، آن را پشت یک reverse proxy مانند Traefik قرار دهید و فقط proxy را منتشر کنید، نه تک‌تک برنامه‌ها را.

قوانین IPv6 را به فایروال ارائه‌دهنده خود اضافه کنید، یا بپذیرید که آن فایروال برای IPv6 مناسب نیست و اجازه دهید UFW یا nftables روی میزبان این وظیفه را انجام دهند.

بررسی کنید که واقعاً بسته شده است

پس از اعمال تغییرات، همان آزمون خارجی را دوباره اجرا کنید:

curl -6 -v http://[2001:db8:2a::1]:8080/
nmap -6 2001:db8:2a::1

پورتی که قبلاً پاسخ می‌داد، اکنون باید اتصال را رد کند یا timeout شود و nmap باید وضعیت آن را filtered یا closed گزارش کند. اگر پورتی همچنان باز است، سه منبع بالا را دوباره بررسی کنید: سرویسی که همچنان روی :: در حال listen است و هیچ ruleای جلوی آن قرار ندارد، rule مربوط به Docker که پیش از UFW اعمال می‌شود، یا firewall ارائه‌دهنده‌ای که اساساً ترافیک IPv6 را دریافت نکرده است.

ایمن‌تر آن است که سرویس‌های حساس را کاملاً از اینترنت عمومی خارج کنید. SSH و پنل‌های مدیریتی را پشت یک VPN از نوع WireGuard قرار دهید و پورت‌های آن‌ها را طوری در firewall تنظیم کنید که فقط روی tunnel پاسخ دهند؛ در این حالت، مسئله exposure از طریق IPv6 دیگر برای آن سرویس‌ها مطرح نیست. برای کاهش سرعت اسکن‌های brute-force که به هر سرویس عمومی باقی‌مانده برخورد می‌کنند، Fail2ban را جلوی SSH قرار دهید و آن را روی یک firewall با سیاست default-deny اعمال کنید.

اگر مفهوم port برای شما جدید است، ابتدا port چیست و سرویس‌ها چگونه روی آن listen می‌کنند را بخوانید.

FAQ

آیا UFW به‌طور پیش‌فرض IPv6 را مسدود می‌کند؟

در یک نصب جدید Ubuntu 24.04، پاسخ مثبت است. UFW فایل IPV6=yes را از /etc/default/ufw می‌خواند و هر rule را برای IPv4 و IPv6 اعمال می‌کند. همچنین ufw status، ruleهای IPv6 را با پسوند (v6) نمایش می‌دهد. مشکل زمانی ایجاد می‌شود که به firewall ارائه‌دهنده‌ای متکی باشید که فقط IPv4 را فیلتر می‌کند، ruleهای iptables را دستی نوشته باشید اما همتای ip6tables آن‌ها وجود نداشته باشد، یا Docker پورتی را خارج از مسیر UFW منتشر کند. IPV6=no، که ممکن است از یک image قدیمی یا tutorial قدیمی باقی مانده باشد، IPv6 را باز نمی‌کند. با فعال بودن UFW، همهٔ ترافیک IPv6، به‌جز loopback، با آن مسدود می‌شود. وضعیت این گزینه را با grep IPV6 /etc/default/ufw بررسی کنید.

چگونه بررسی کنم VPS من در IPv6 چه چیزهایی را در معرض دسترسی قرار داده است؟

sudo ss -tlnp را اجرا کنید و همهٔ listenerهایی را پیدا کنید که آدرس محلی آن‌ها با [::] شروع می‌شود. این وضعیت یعنی سرویس روی همهٔ interfaceهای IPv6 پاسخ می‌دهد. سپس از یک ماشین دیگر، آدرس عمومی IPv6 سرور را مستقیماً با curl -6 -v http://[YOUR:IPV6::ADDR]:PORT/ آزمایش کنید یا با nmap -6 YOUR:IPV6::ADDR آن را scan کنید. هر پورتی که در scan مربوط به IPv6 باز باشد اما در IPv4 بسته باشد، شکاف پیکربندی شماست.

چرا وقتی UFW می‌گوید پورت مسدود است، می‌توانم به پورت container مربوط به Docker دسترسی داشته باشم؟

وقتی پورتی را با -p منتشر می‌کنید، Docker ruleهای firewall خودش را پیش از ruleهای UFW وارد می‌کند. بنابراین پورت منتشرشده قابل دسترسی است، حتی اگر ufw status آن را denied نشان دهد. این وضعیت در IPv4 رخ می‌دهد و زمانی که پشتیبانی IPv6 در Docker فعال باشد، در IPv6 نیز رخ می‌دهد. پورت را به یک آدرس مشخص مانند -p 127.0.0.1:8080:80 bind کنید، یا container را پشت یک reverse proxy قرار دهید و فقط پورت proxy را منتشر کنید.

اگر firewall مربوط به IPv4 من به‌درستی تنظیم شده باشد، آیا هنوز به firewall برای IPv6 نیاز دارم؟

بله. IPv4 و IPv6 دو stack شبکهٔ جداگانه با ruleهای firewall جدا هستند. مجموعهٔ کامل ruleهای IPv4 هیچ اثری بر ترافیک IPv6 ندارد. اگر VPS شما یک آدرس عمومی IPv6 داشته باشد، که تقریباً همهٔ VPSها دارند، هر سرویسی که روی :: گوش دهد تا زمانی که یک rule مربوط به firewall IPv6 یا bind شدن به loopback آن را متوقف نکند، از طریق IPv6 قابل دسترسی باقی می‌ماند.

چگونه یک سرویس را فقط روی IPv4 یا فقط روی localhost به گوش‌دادن وادار کنم؟

آدرس bind سرویس را در فایل configuration خودش تنظیم کنید. برای loopback فقط در IPv4، آن را به 127.0.0.1 bind کنید. برای همهٔ آدرس‌های IPv4 و بدون listener مربوط به IPv6، از 0.0.0.0 استفاده کنید. Postgres از listen_addresses استفاده می‌کند، SSH از ListenAddress استفاده می‌کند و بیشتر app serverها یک host یا bind flag ارائه می‌دهند. نتیجه را با sudo ss -tlnp تأیید کنید و بررسی کنید که Local Address دیگر [::] را نشان ندهد.