همه سامانه‌ها عملیاتی هستند 6 منطقه آفشور پرداخت بدون KYC
Hands-on راهنمای میدانی

Docker روی VPS: فایروالی که بی‌سروصدا دور زده می‌شود

اولین ساعت را درست انجام دادید: یک کاربر نام‌دار، SSH فقط-با-کلید، یک فایروال default-deny روی هر دو خانوادهٔ IP، به‌روزرسانی‌های امنیتی خودکار. بعد یک پورت کانتینر منتشر کردید و یک پایگاه‌داده را روی اینترنت عمومی گذاشتید، چون Docker آن پورت را از راه قانونی تحویل می‌دهد که مدت‌ها پیش از آنکه از فایروال شما اصلاً نظر خواسته شود اجرا می‌شود — و ufw همچنان گزارش می‌دهد که همه‌چیز رد می‌شود. این راهنمای همان شکاف است: چرا وجود دارد، چطور بخوانید که کانتینرهای شما همین الان چه چیزی را در معرض گذاشته‌اند، و آن مشتی تغییر که باعث می‌شود فایروالی که نوشتید واقعاً همان معنایی را بدهد که فکر می‌کردید.

به‌روزرسانی‌شده در 2026-09-14 · 15 دقیقه مطالعه · عملیات ناوگان
در این صفحه
  1. قانونی که شما نوشتید، و قانونی که Docker نوشت
  2. یک پورت در معرض، از بیرون چه شکلی است
  3. بخوانید واقعاً چه چیزی را منتشر می‌کنید
  4. بایند به loopback، و تفاوت میان ports و expose
  5. DOCKER-USER: زنجیره‌ای که دقیقاً برای همین وجود دارد
  6. دروازهٔ دیگر: سوکت، و چیزی که با روت اجرا می‌شود
  7. ایمیج‌هایی که خودتان ننوشته‌اید، روی باکسی که نمی‌توانید از دستش بدهید
  8. لاگ‌ها، volumeها، و وضعیتی که از کانتینر بیشتر عمر می‌کند
  9. گام‌به‌گام
SP·01

قانونی که شما نوشتید، و قانونی که Docker نوشت

دو برنامه دارند یک فایروال یکسان را با دو مفروضهٔ متفاوت ویرایش می‌کنند، و فقط یکی از آن‌ها به شما خبر می‌دهد. ufw قوانین خودش را در زنجیرهٔ INPUT از جدول filter می‌نویسد — مسیری که یک بسته طی می‌کند وقتی مقصدش خودِ هاست است. Docker در جدول nat و در FORWARD می‌نویسد — مسیری که یک بسته طی می‌کند وقتی مقصدش جای دیگری است.

یک بسته را دنبال کنید تا شکاف کاملاً روشن شود. یک نفر در کشوری دیگر به آدرس شما روی پورت 5432 وصل می‌شود. بسته می‌رسد، و اولین چیزی که با آن روبه‌رو می‌شود nat PREROUTING است، که آن را به زنجیرهٔ DOCKER در Docker می‌فرستد. آن‌جا یک قانون DNAT مقصد را به 172.17.0.2:5432 بازمی‌نویسد — یعنی خودِ کانتینر. بسته دیگر به آدرس سرور شما نیست، پس کرنل آن را مسیریابی می‌کند به‌جای تحویل محلی: از FORWARD رد می‌شود، جایی که Docker از قبل یک ACCEPT برای ترافیکِ روبه‌سوی یک پورتِ منتشرشده نصب کرده است. در هیچ نقطه‌ای از این مسیر، بسته از INPUT رد نمی‌شود — همان تنها زنجیره‌ای که ufw فیلترش می‌کند. قانون شما هیچ‌وقت اشتباه نبود. فقط هیچ‌وقت از آن پرسیده نشد.

به همین دلیل هم این علامت این‌قدر گیج‌کننده است. ufw status verbose همچنان گزارش می‌دهد Default: deny (incoming)، با هیچ چیز مجاز جز 80، 443 و پورت SSH شما، درحالی‌که یک اسکنر در قاره‌ای دیگر یک نشست باز به پایگاه‌دادهٔ شما دارد. هر دو جمله درست‌اند. فایروال دقیقاً همان کاری را می‌کند که پیکربندی کرده‌اید، روی ترافیکی که به آن داده شده، و ترافیکی که اهمیت دارد اصلاً به آن داده نمی‌شود.

هیچ‌کدام این‌ها باگ نیست، و هیچ‌کدام هم بی‌دقتیِ Docker نیست. یک container engine مجبور است قوانین NAT و forwarding را برنامه‌ریزی کند وگرنه کانتینرها اصلاً نمی‌توانند به شبکه برسند، و نمی‌تواند با اطمینان حدس بزند کدام‌یک از قوانین فایروال هاست شما قرار بوده رویشان اعمال شود. پس کار صادقانه را انجام می‌دهد: زنجیره‌های خودش را مدیریت می‌کند، و یک زنجیرهٔ اختصاصی در اختیار شما می‌گذارد — DOCKER-USER، که پیش از هر چیز دیگری در FORWARD ارزیابی می‌شود — و قول می‌دهد هرگز آن را بازنویسی نکند. شکاف، وجودِ خودِ این مکانیزم نیست. شکاف این است که -p 5432:5432 شبیه "این را در دسترس بگذار" خوانده می‌شود اما معنایش این است: "این را به هر آدرسی که این ماشین به آن پاسخ می‌دهد منتشر کن، آن‌سوترِ همان فایروالی که یک ساعت وقت گذاشتی بنویسی".

SP·02

یک پورت در معرض، از بیرون چه شکلی است

اینترنت سریع‌تر از آن چیزی که فکر می‌کنید متوجه می‌شود. بازه‌های هاستینگ به‌طور پیوسته و کامل اسکن می‌شوند — نه از سوی کسی که به شما علاقه‌مند شده، بلکه توسط کراولرهای تجاری، پروژه‌های پژوهشی و بات‌نت‌های فرصت‌طلبی که هر آدرس قابل‌مسیریابی را روی هر پورت جالبی جارو می‌کنند و نتیجه را منتشر یا می‌فروشند. فاصلهٔ میان docker compose up -d و اولین اتصال ناخواستهٔ به یک پورتِ تازه‌منتشرشدهٔ پایگاه‌داده، با ساعت اندازه‌گیری می‌شود. هیچ‌کس لازم نبود hostname شما را حدس بزند. هیچ‌کس لازم نبود اسمتان را بداند. آدرس در همان بازه بود.

چیزی که پیدا می‌کنند کاملاً به آنچه منتشر کرده‌اید بستگی دارد، و حالت‌های رایج دلگیرند. یک کانتینر PostgreSQL یا MySQL که از قطعه‌کد quick-start یک ایمیج راه افتاده، با همان رمز عبورِ بی‌اهمیتِ همان قطعه‌کد که هنوز سرجایش مانده. یک نمونهٔ Elasticsearch یا MongoDB که هرگز برای خواستنِ احراز هویت پیکربندی نشده، چون قرار بوده فقط از کانتینرِ اپلیکیشن در دسترس باشد. یک memcached که به هر کسی پاسخ می‌دهد — که نه‌فقط یک نشتِ داده، بلکه تقویت‌کننده‌ای است که می‌توان آن را به‌سمتِ یک هدفِ دیگر نشانه رفت و سرور شما را به یکی از شرکت‌کنندگانِ همان نوع سیلی تبدیل کرد که در راهنمای اولین ساعتِ حملهٔ DDoS ما شرح داده شده. یک پنلِ مدیریتی یا داشبوردِ صفِ پیام که فرض را بر یک شبکهٔ خصوصی گذاشته بود. یک endpoint متریک که بی‌سروصدا هر سرویسِ داخلی، hostname و نسخه‌ای را که اجرا می‌کنید فهرست می‌کند.

ارزش دارد آسیبِ درجه‌دوم را هم نام ببریم، چون آسیبِ درجه‌یک تمامِ صورت‌حساب نیست. یک پایگاه‌داده که از اینترنت قابل‌دسترسی است فقط قابل‌خواندن نیست — در بیشترِ موتورها قابل‌نوشتن هم هست، که یعنی نفوذگر برای نصبِ یک راهِ ماندگار به اکسپلویتِ بیشتری نیاز ندارد، و چند موتور را می‌شود وادار کرد از یک نشستِ دارای امتیاز، روی هاست فایل بنویسند یا دستور اجرا کنند. روی باکسی که دقیقاً به این دلیل انتخابش کردید که هیچ هویتی حمل نمی‌کند، یک نقطهٔ ورودِ بدون‌احرازهویت هم پلی است به هر چیزِ دیگری که آن باکس با آن سروکار دارد: مقصدِ بک‌آپی که می‌تواند به آن برسد، کلیدهای داخلِ متغیرهای محیطی‌اش، و کانتینرهای دیگر روی همان bridge.

بخشِ ناراحت‌کننده این است که هیچ‌کدام از این‌ها خودش را اعلام نمی‌کند. هیچ سطرِ لاگی نیست که بگوید "فایروال شما دور زده شد". سرویس کار می‌کند، اپلیکیشن وصل می‌شود، سایت بالاست، و تنها سیگنالِ بیرونی یک شمارندهٔ اتصال است که کسی تماشایش نمی‌کند. این افشا یا توسطِ خودتان، عمداً، در همان ده دقیقهٔ بعدی کشف می‌شود — یا توسطِ یک نفرِ دیگر، طبقِ برنامهٔ خودش.

SP·03

بخوانید واقعاً چه چیزی را منتشر می‌کنید

از آن‌جا شروع کنید که خودِ engine فکر می‌کند دارد چه‌کار می‌کند. docker ps یک ستونِ PORTS چاپ می‌کند، و تمایزِ داخل آن، کلِ موضوعِ این راهنماست: 0.0.0.0:5432->5432/tcp یعنی هر آدرسی روی ماشین، 127.0.0.1:5432->5432/tcp یعنی فقط loopback، و یک 5432/tcp سادهٔ بدونِ فلش یعنی پورت به کانتینرهای دیگر expose شده و هیچ‌جا منتشر نشده. پیش از آنکه چیزی را تغییر دهید، هر سطرِ آن ستون را روی هر کانتینر بخوانید.

بعد با ss -tulpen نگاهی به سوکت‌ها بیندازید. روی یک نصبِ پیش‌فرض می‌بینید docker-proxy پورت‌های منتشرشده را نگه داشته، چون Docker هنوز به ازای هر پورتِ منتشرشده یک پردازهٔ کوچک در فضای کاربر راه می‌اندازد. این‌جا همان تله‌ای است که یک بعدازظهرِ آدم را می‌گیرد: اگر userland-proxy روی دیمنِ شما غیرفعال باشد — که یک تغییرِ تنظیمِ رایج است، و بعضی توزیع‌ها همین‌طور عرضه‌اش می‌کنند — اصلاً هیچ سوکتِ listen‌کننده‌ای روی هاست وجود ندارد. ss هیچی نشان نمی‌دهد، lsof هیچی نشان نمی‌دهد، و پورت هنوز کاملاً باز است، چون قانونِ DNATِ کرنل دارد کار را انجام می‌دهد بدونِ آنکه هیچ پردازه‌ای لازم باشد آدرس را نگه دارد. خروجیِ ساکتِ ss مدرکی برای بسته‌بودنِ یک پورت نیست.

پس خودِ قوانین را بخوانید. iptables -t nat -S DOCKER به ازای هر پورتِ منتشرشده یک سطرِ DNAT فهرست می‌کند، و هر سطر همان جوابی را که می‌خواهید حمل می‌کند: قانونی با -d 127.0.0.1/32 یعنی انتشار روی loopback، و قانونی بدونِ محدودیتِ مقصد روی هر آدرسی که ماشین دارد اعمال می‌شود. همین کار را با ip6tables هم انجام دهید، چون این دو خانواده مستقل از هم پیکربندی می‌شوند و یک باکس می‌تواند روی یکی سفت و روی دیگری باز باشد.

و در آخر — و این تنها مرحله‌ای است که واقعاً چیزی را ثابت می‌کند — از جایی دیگر به ماشین نگاه کنید. هر دستوری که تا این‌جا اجرا کردید روی خودِ هاست اجرا می‌شود و دیدِ خودِ هاست از شبکه‌اش را به ارث می‌برد. ترافیکِ loopback از زنجیره‌های مهم رد نمی‌شود، پس موفق‌بودنِ curl 127.0.0.1:5432 هیچ چیزی دربارهٔ اینکه آیا یک غریبه می‌تواند همین کار را بکند به شما نمی‌گوید، و شکست‌خوردنش حتی کمتر می‌گوید. تستِ معتبر یک اسکن از یک ماشینِ دیگر روی یک شبکهٔ دیگر است، روی هر دو خانوادهٔ IP. هر چیزی پیش از آن فقط یک فرضیه است.

SP·04

بایند به loopback، و تفاوت میان ports و expose

کوچک‌ترین اصلاحِ مفید، یازده کاراکتر است. -p 127.0.0.1:5432:5432 به Docker می‌گوید قانونِ DNAT خودش را با یک محدودیتِ مقصد بنویسد، پس بازنویسی فقط روی ترافیکی اعمال می‌شود که از قبل محلی بوده. یک بستهٔ راه‌دور که به‌سمتِ آدرسِ عمومیِ شما نشانه رفته دیگر با این قانون جور درنمی‌آید، به کانتینر فوروارد نمی‌شود، و بالاخره همان‌جایی می‌رسد که همیشه فکر می‌کردید می‌رسد: INPUT، جایی که ufw آن را رد می‌کند. در یک فایلِ Compose همین کار می‌شود ports: ["127.0.0.1:5432:5432"]، و گیومه‌ها اهمیت دارند — یک مقدارِ بدونِ گیومه با دونقطه داخلش، تصادفِ parsing‌ای است که منتظرِ رخ‌دادن است.

اما سؤالِ بهتر این است که اصلاً چرا این پورت منتشر شده. کانتینرهایی که به یک شبکهٔ user-defined یکسان وصل‌اند، مستقیماً به هم می‌رسند، با نامِ سرویس، روی همان پورتِ خودِ کانتینر، بدونِ هیچ‌گونه انتشاری. اپلیکیشنِ شما به 127.0.0.1:5432 وصل نمی‌شود؛ به postgres:5432 وصل می‌شود، که DNS داخلیِ Docker آن را به آدرسی روی bridge خصوصی resolve می‌کند. یک پایگاه‌داده در چنین چینشی هیچ خطِ ports:ای لازم ندارد — نه از نوعِ loopback، نه هیچ نوعِ دیگر. امن‌ترین پورتِ منتشرشده، همان پورتی است که حذفش کرده‌اید. ports: را برای همان یکی‌دو سرویسی نگه دارید که واقعاً با عموم روبه‌رو هستند، و بگذارید بقیه روی شبکهٔ خصوصی حرف بزنند.

این‌جا هم همان جایی است که expose: بد فهمیده می‌شود. نه چیزی منتشر می‌کند و نه چیزی باز می‌کند؛ صرفاً مستندسازی است که ثبت می‌کند یک سرویس روی چه پورتی listen می‌کند، و در هیچ جهتی روی فایروال اثری ندارد. آدم‌ها آن را اضافه می‌کنند به این امید که نسخهٔ امنِ ports: باشد، که همین‌طور هم هست — دقیقاً به همان معنایی که یک کامنت نسخهٔ امنِ کد است. اگر می‌خواهید سرویسی فقط برای همسایه‌هایش قابل‌دسترسی باشد، به expose: نیازی ندارید؛ به نبودِ ports: نیاز دارید.

دو محدودیتِ صادقانه برای بایند به loopback وجود دارد. اول، این کار مرزِ هاست را محافظت می‌کند، نه همسایگی را: کانتینرهای روی یک bridge network یکسان همچنان آزادانه به هم می‌رسند، پس یک کانتینرِ front-end که به خطر افتاده، مسیری باز به یک پایگاه‌داده دارد که هیچ‌چیز منتشر نکرده. سرویس‌ها را روی شبکه‌های جدا از هم پخش کنید و شبکهٔ بک‌اند را وقتی شعاعِ انفجار اهمیت دارد internal: true علامت بزنید. دوم، 127.0.0.1 یک آدرسِ IPv4 است و فقط IPv4 را محدود می‌کند؛ اگر هاست یک /64 قابل‌مسیریابی دارد — که هر پلن این‌جا یکی می‌دهد — دربارهٔ v6 جداگانه فکر کنید، و جداگانه هم تستش کنید.

SP·05

DOCKER-USER: زنجیره‌ای که دقیقاً برای همین وجود دارد

بایند به loopback همان کانتینرهایی را درست می‌کند که به یادشان هستید. DOCKER-USER راهی است که با آن دیگر فقط یک -p با حادثهٔ بعدی فاصله نداشته باشید. Docker آن را به‌عنوانِ اولین jump در FORWARD نصب می‌کند، پیش از قوانینِ accept خودش، و — برخلافِ هر چیزِ دیگری در زنجیره‌هایش — محتوایش را در طولِ ری‌استارت‌ها، ارتقاها و کانتینرهای تازه دست‌نخورده رها می‌کند. این‌جا همان جای پشتیبانی‌شده برای سیاستی است که engine نمی‌تواند حدس بزند: اینکه اصلاً کدام منبع‌ها اجازه دارند به کانتینرهای این هاست برسند.

الگو سه قانون روی اینترفیسِ عمومی است، و ترتیب همه‌چیز است. اول، ترافیکِ established و related را برگردانید، تا پاسخِ اتصالاتی که کانتینرهای شما باز کرده‌اند همچنان جاری بماند. دوم، منبع‌هایی را که واقعاً می‌خواهید اجازهٔ ورود بدهید برگردانید — یک آدرسِ دفتر، یک هاستِ مانیتورینگ، یک سرورِ همتا. سوم، هر چیزِ دیگری را که از اینترنت می‌رسد drop کنید. اشتباه گرفتنِ قانونِ conntrack، همان شکستِ کلاسیکِ این‌جاست: اگر DROP را اول بگذارید، هر اتصالِ خروجیِ هر کانتینر روی بستهٔ برگشتی می‌میرد، که به شکلِ "Docker شبکهٔ DNS و نصبِ پکیج‌ها را خراب کرده" ظاهر می‌شود و آدم‌ها را کاملاً به سمتِ جای اشتباه می‌فرستد تا دنبالش بگردند.

دو نکتهٔ عملیاتی تعیین می‌کند که آیا این در تماس با واقعیت دوام می‌آورد یا نه. باید با نام به اینترفیسِ عمومی محدود شود — -i eth0، یا هر چیزی که ip route get 1.1.1.1 روی باکسِ شما گزارش می‌دهد — وگرنه ترافیکِ میانِ bridgeهای خودتان را هم drop می‌کنید. و باید بعد از هر ری‌استارت دوباره اعمال شود، بعد از آنکه دیمنِ Docker زنجیره را ساخته. یک واحدِ systemd از نوعِ oneshot که با After=docker.service ترتیب داده شده شکلِ قابل‌اعتمادِ این کار است؛ ذخیره‌کردن با netfilter-persistent هم کار می‌کند، تا وقتی بپذیرید که بازیابیِ یک ruleset کامل درحالی‌که Docker هم‌زمان دارد آن را از نو می‌سازد، یک race است که باید تأییدش کنید، نه فرضش بگیرید.

بعد نیمهٔ IPv6. اگر ip6tables -S DOCKER-USER یک زنجیره چاپ کند، هر قانون را عیناً در آن هم بسازید. اگر خطا بدهد، دیمنِ شما اصلاً قوانینِ v6 را مدیریت نمی‌کند — که هیچ چیزی دربارهٔ اینکه آیا ترافیکِ v6 به کانتینرهای شما می‌رسد یا نه نمی‌گوید، فقط می‌گوید Docker آن را فیلتر نمی‌کند. آن‌جا با استدلال به جواب نرسید؛ مسیرها بسته به نسخه، تنظیماتِ دیمن و توزیع فرق می‌کنند، و یک نتیجه‌گیریِ غلطِ پرادعا از هیچ نتیجه‌گیری‌ای بدتر است. خودتان را با nmap -6 اسکن کنید و به نتیجه‌اش اعتماد کنید.

یک کاری که نباید بکنید: دست بردن به‌سمتِ "iptables": false در /etc/docker/daemon.json. این کار جلوی دست‌زدنِ Docker به فایروال را می‌گیرد، و هم‌زمان جلوی این را هم می‌گیرد که NAT کانتینر، masquerading خروجی و ایزوله‌سازیِ بین‌شبکه‌ای توسطِ هر چیزی پیکربندی شوند. شما مشکل را حذف نکرده‌اید، کارش را به ارث برده‌اید — با دست، برای هر کانتینر، تا ابد. روی یک VPS تکی، انتخابِ صادقانه این است که بگذارید Docker زنجیره‌های خودش را مدیریت کند و شما DOCKER-USER را صاحب شوید.

SP·06

دروازهٔ دیگر: سوکت، و چیزی که با روت اجرا می‌شود

همهٔ آنچه تا این‌جا گفته شد دربارهٔ بسته‌هایی بود که می‌رسند. این بخش دربارهٔ چیزی است که آن بسته‌ها پیدا می‌کنند، و یک مورد آن‌قدر بدتر از بقیه است که سزاوار است جداگانه گفته شود: مانت‌کردنِ /var/run/docker.sock داخلِ یک کانتینر، معادلِ دادنِ دسترسیِ root روی هاست به همان کانتینر است. نه "نزدیک به". معادل. هر چیزی که بتواند با آن سوکت حرف بزند می‌تواند یک کانتینرِ privileged تازه راه بیندازد که فایل‌سیستمِ هاست داخلش مانت شده، و از آن‌جا هر کلیدی را بخواند، هر فایلی را بنویسد و هر چیزی را که بخواهد نصب کند. خیلی از ایمیج‌های به‌ظاهر راحت این را می‌خواهند — داشبوردها، auto-updaterها، reverse proxyهایی با service discovery. با آن درخواست طوری رفتار کنید که انگار تصمیم گرفته‌اید به آن ایمیج به همان اندازه‌ای اعتماد کنید که به root shellِ خودتان اعتماد دارید، و همان دقت را هم برای Docker API روی TCP داشته باشید: یک دیمنِ بدون‌احرازهویت روی پورتِ 2375 همان دروازه است، این‌بار رو به کلِ اینترنت باز.

بعد از آن، container runtime چهار کاهشِ کم‌هزینه به شما می‌دهد، و هیچ‌کدام نیاز به بازطراحیِ چیزی ندارد. با user: "1000:1000" به‌عنوانِ یک کاربرِ non-root اجرا کنید، چون پیش‌فرض، root داخلِ namespace است و همین نقطهٔ شروعِ هر فرار از کانتینر است. security_opt: ["no-new-privileges:true"] را تنظیم کنید، که جلوی یک باینریِ setuid داخلِ ایمیج را می‌گیرد از اینکه هیچ‌وقت بیشتر از همان پردازه‌ای که با آن شروع شده امتیاز بگیرد. همهٔ قابلیت‌ها را با cap_drop: [ALL] بیندازید و فقط همان چیزی را برگردانید که سرویس واقعاً لازم دارد — بیشترِ اپلیکیشن‌های وب هیچ‌کدام را لازم ندارند. فایل‌سیستمِ روت را read_only: true مانت کنید و یک tmpfs کوچک برایش برای فضای موقت بگذارید، که "انداختنِ یک وب‌شل در دایرکتوریِ اپ" را از یک قدم به یک بن‌بست تبدیل می‌کند.

همین منطق دربارهٔ چیزی هم که به کانتینر داده می‌شود صدق می‌کند. رازهایی که به‌شکلِ متغیرِ محیطی پاس داده می‌شوند برای هر چیزی که بتواند محیطِ پردازه را بخواند قابل‌دیدن‌اند و با وفاداریِ کامل در خروجیِ docker inspect و در هر لاگ یا گزارشِ کرشی که پیکربندی را dump می‌کند کپی می‌شوند؛ یک فایل که فقط-خواندنی در یک مسیرِ مشخص مانت شده کمتر راحت است اما به‌طرزِ قابل‌توجهی کمتر نشت می‌کند. و یک کانتینر به بیش از شبکه‌ای که کارش نیاز دارد نیازی ندارد: یک worker که فقط با پایگاه‌داده حرف می‌زند اصلاً نباید بتواند به اینترنت اتصال باز کند.

اگر همهٔ این‌ها حسِ جنگیدن با پیش‌فرض را می‌دهد، این برداشتِ منصفانه‌ای است، و همین استدلالِ رفتن به‌سمتِ rootless Docker یا Podman است — جایی که دیمن و کانتینرها به‌عنوانِ یک کاربرِ بدون‌امتیاز اجرا می‌شوند، یک فرار از کانتینر شما را همان کاربر می‌رساند نه root، و پورت‌های منتشرشده توسطِ یک پردازهٔ عادیِ سطح‌کاربر روی هاست نگه داشته می‌شوند، که یعنی قوانینِ ufw شما به شکلِ معمول رویشان اعمال می‌شود. معامله واقعی است: بعضی قابلیت‌ها، بعضی storage driverها و بعضی ترفندهای شبکه‌ای متفاوت رفتار می‌کنند یا اصلاً کار نمی‌کنند. ارزش دارد بدانید این انتخاب وجود دارد و چه چیزی می‌خرد، تا اینکه بعد از یک حادثه کشف کنید که پیش‌فرض هم خودش یک انتخاب بوده.

SP·07

ایمیج‌هایی که خودتان ننوشته‌اید، روی باکسی که نمی‌توانید از دستش بدهید

یک ایمیجِ کانتینر، فایل‌سیستمِ یک نفرِ دیگر است که روی ماشینِ شما اجرا می‌شود، از لایه‌هایی سوارشده که نخوانده‌اید. این استدلالی علیهِ استفاده از آن‌ها نیست — استدلالی است برای دانستنِ اینکه کدام‌ها را اجرا می‌کنید و چقدر قدیمی‌اند. دو شکستی که واقعاً روی زیرساخت‌های کوچک اتفاق می‌افتد هر دو پیش‌پاافتاده‌اند: یا ایمیج هیچ‌وقت قابل‌اعتماد نبوده، یا در ماهِ مارس قابل‌اعتماد بوده و از آن موقع کسی دوباره نساخته‌اش.

شکستِ اول را بیشتر با نظم دربارهٔ اینکه ایمیج‌ها از کجا می‌آیند حل می‌کنید. مخزن‌های رسمی یا منتشرشده توسطِ vendor را به یک فورکِ راحت با سه ستاره ترجیح دهید، و خصوصاً به ایمیج‌هایی که جذابیت‌شان این است که شش سرویس را در یک خطِ YAML بسته‌بندی می‌کنند مشکوک باشید. جایی که اهمیت دارد، به‌جایِ tag، با digest pin کنید: postgres:17 یک هدفِ متحرک است که می‌تواند بینِ دو اجرای docker compose pull زیرِ پایتان عوض شود، درحالی‌که postgres@sha256:… همان فایل‌سیستمِ دقیقی است که تست کرده‌اید. Pin‌کردن، رفعِ خودکارِ مشکلات را با reproducibility معامله می‌کند، که وقتی عادت به rebuild‌کردن دارید معاملهٔ درستی است و وقتی ندارید معاملهٔ غلط.

دومی همان چیزی است که بی‌سروصدا نیش می‌زند. به‌روزرسانی‌های unattended به کانتینرهای شما دست نمی‌زنند. همان به‌روزرسانی‌های امنیتیِ خودکاری که در اولین ساعت پیکربندی کردید، پکیج‌های هاست را patch می‌کنند و هیچ دیدی به فضای کاربرِ داخلِ یک ایمیج ندارند — پس باکسی که خودش را کاملاً patch‌شده گزارش می‌دهد می‌تواند وب‌سروری را از یک ایمیجِ پایه با یک سال آسیب‌پذیریِ رفع‌نشده اجرا کند. کانتینرها به‌روزرسانی نمی‌شوند، جایگزین می‌شوند: pull کنید، دوباره بسازید، و هر چیزی را که دیگر ارجاعی به آن نیست حذف کنید، با ریتمی که واقعاً رعایتش می‌کنید. یک‌بار در ماه، یادداشت‌شده، از یک نیتِ کامل بهتر است.

همین باعث می‌شود فایلِ Compose باارزش‌ترین شیء روی سرور باشد. تنها توصیفِ کاملِ اینکه آن ماشین چیست همین است، و بازسازی از رویش باید یک کارِ روتین باشد نه یک پروژهٔ باستان‌شناسی — آن را در version control نگه دارید، فایل‌های environment را کنارش نگه دارید، و هر دو را در همان بک‌آپ رمزنگاری‌شده و خارج از سروری نگه دارید که داده‌های شما را دارد. تستِ یک هاستِ کانتینر این نیست که آیا در حال اجراست. این است که آیا می‌توانید آن را دوباره، عیناً، روی یک VPS تازه در حدود 15 min سرپا کنید.

SP·08

لاگ‌ها، volumeها، و وضعیتی که از کانتینر بیشتر عمر می‌کند

کانتینرها به‌عنوانِ چیزی یک‌بارمصرف تبلیغ می‌شوند، که این دربارهٔ خودِ پردازه درست است و دربارهٔ هر چیزی که پشتِ سر می‌گذارد غلط. دو نوع وضعیت روی یک هاستِ Docker انباشته می‌شود، و هر دو آدم‌ها را در بدترین لحظه غافلگیر می‌کنند.

اولی لاگ‌هاست. درایورِ پیش‌فرضِ json-file هر خطی را که کانتینرهای شما به stdout و stderr می‌نویسند ضبط می‌کند، و — مگر اینکه خلافش را بگویید — هیچ‌وقت آن‌ها را rotate نمی‌کند. یک اپلیکیشنِ پرحرف می‌تواند طیِ چند هفته یک دیسک را همین‌طوری پر کند، و یک دیسکِ پر روی یک هاستِ پایگاه‌داده خودش یک حادثه است. یک‌بار یک سقف تعیین کنید، در /etc/docker/daemon.json، تا روی هر چیزی که بعداً راه می‌اندازید اعمال شود.

یک خوانشِ حریمِ خصوصی هم از همین تنظیم وجود دارد، و روی سروری که برای حریمِ خصوصی انتخابش کرده‌اید احتمالاً همین خوانش مهم‌تر است. یک access log که rotate نشده، رکوردی دائمی و ایندکس‌نشده از آدرسِ IP، user agent و مسیرِ درخواستِ هر بازدیدکننده است، روی دیسکی که از نظرِ فیزیکی کنترلش نمی‌کنید. شما تصمیم نگرفتید آن را نگه دارید. پیش‌فرض به‌جای شما تصمیم گرفت. تصمیمِ آگاهانه یعنی سقف‌گذاری روی مدتِ نگه‌داری، و — بهتر از آن — کم‌کردنِ چیزی که از همان اول نوشته می‌شود: یک فرمتِ لاگ روی پروکسی که آدرسِ کلاینت را حذف یا کوتاه می‌کند، ارزشِ عملیاتی را نگه می‌دارد و مسئولیت را می‌اندازد. داده‌ای که هرگز ننوشته‌اید نه قابل‌توقیف است، نه قابل‌احضاریه، نه قابل‌لو‌رفتن — همان استدلالی که آدم‌ها را از همان اول به دور نگه‌داشتنِ نامشان از یک سرور می‌رساند.

دومی volumeهاست، و لبهٔ تیز یک flag است. docker compose down کانتینرها را متوقف و حذف می‌کند و volumeهای نام‌دار را دست‌نخورده می‌گذارد؛ docker compose down -v آن‌ها را برای همیشه حذف می‌کند، با همان اطمینان. پایگاه‌داده‌ها داخلِ همان volumeها زندگی می‌کنند. هر چیزِ دیگری هم که دلتان برایش تنگ می‌شود همین‌طور. داده‌هایتان را در volumeهای نام‌دار نگه دارید نه در volumeهای بی‌نام، بدانید ساعتِ دو بامداد دارید کدام دستور را تایپ می‌کنید، و به‌یاد داشته باشید که docker system prune برای پس‌گرفتنِ فضا وجود دارد و با کمالِ میل فضای شما را هم پس می‌گیرد.

می‌ماند خودِ بک‌آپ، جایی که انتزاعِ کانتینر برای آخرین بار گمراه‌تان می‌کند. کپی‌کردنِ دایرکتوریِ volume یک پایگاه‌دادهٔ زنده، بک‌آپ نیست؛ کپیِ فایل‌هایی است که همان لحظه‌ای که شما می‌خواندیدشان داشتند نوشته می‌شدند، و دقیقاً همان زمانی که بیشتر از همیشه لازمش دارید، به‌شکلِ یک پایگاه‌دادهٔ خراب بازیابی می‌شود. از راهِ خودِ engine بک‌آپ بگیرید — pg_dump، mysqldump، یا API اسنپ‌شاتِ خودِ engine — و بعد نتیجه را رمزنگاری کنید و به جایی بفرستید که سرورتان نتواند به آن برسد. همان بندِ آخر جایی است که وقتی نفوذگر از قبل داخلِ کانتینر است اهمیت پیدا می‌کند: بک‌آپی که هاستِ به‌خطرافتادهٔ شما بتواند حذفش کند، اصلاً بک‌آپ نیست.

SP·09

گام‌به‌گام

  1. 01

    فهرست‌برداری از آنچه باکس همین الان منتشر می‌کند

    پیش از تغییردادنِ هر چیزی، وضعیتِ فعلی را یادداشت کنید — بعداً برای diff‌گرفتن بهش نیاز خواهید داشت. ستونِ PORTS را برای هر کانتینر بخوانید، بعد قوانینِ NAT‌ای را که Docker واقعاً نصب کرده، روی هر دو خانوادهٔ IP، بخوانید. هر چیزی که بدونِ مقصدِ 127.0.0.1 چاپ شود از اینترنت قابل‌دسترسی است، هرچه ufw status ادعا کند.

    docker ps --format 'table {{.Names}}\t{{.Ports}}'
    ss -tulpen | grep -E 'docker|LISTEN'
    sudo iptables  -t nat -S DOCKER      # -d 127.0.0.1/32 = loopback only
    sudo ip6tables -t nat -S DOCKER      # may not exist; that is an answer too
    sudo ufw status verbose              # what you believed was true

    حالا که این‌جایید، نامِ اینترفیسِ عمومی را هم یادداشت کنید — قوانینِ فایروال بعداً به آن نیاز دارند، و روی هر ایمیجی eth0 نیست.

    ip route get 1.1.1.1 | awk '{print $5; exit}'
  2. 02

    خودتان را از جایی دیگر اسکن کنید

    هاست نمی‌تواند خودش را ممیزی کند: ترافیکِ loopback هرگز از زنجیره‌هایی که می‌خواهید تستشان کنید رد نمی‌شود. این کار را از یک ماشینِ دیگر روی یک شبکهٔ دیگر انجام دهید — یک VPS دوم از $8.00 در ماه در یک منطقهٔ دیگر، صادقانه‌ترین محلِ تست است، و بعد از این هم به کارتان می‌آید. هر دو خانواده را اسکن کنید، چون جداگانه پیکربندی می‌شوند.

    # from ANOTHER machine, against your server
    nmap -Pn -sS -p- --min-rate 1000 203.0.113.10
    nmap -Pn -6 -p 22,80,443,3306,5432,6379,9200,27017 2001:db8::1
    # no nmap? one port at a time is enough to settle an argument
    nc -zv 203.0.113.10 5432

    هر پورتِ بازی که 80، 443 یا پورتِ SSH شما نباشد، یک یافته است. خروجی را نگه دارید؛ همان نیمهٔ "پیش از"ِ مدرکی است که آخرِ کار به آن نیاز خواهید داشت.

  3. 03

    چیزی را که لازم نیست عمومی باشد، منتشر نکنید

    حالا آن را از سرچشمه، در فایلِ Compose درست کنید. سرویس‌های بک‌اند به یک شبکهٔ خصوصی منتقل می‌شوند و خطِ ports:شان را کاملاً از دست می‌دهند — اپلیکیشن با نامِ سرویس به آن‌ها می‌رسد. هر چیزی که باید از خودِ هاست قابل‌دسترسی باشد، به‌جایِ یک پورتِ ساده، یک بایندِ صریح به loopback می‌گیرد. شبکهٔ بک‌اند را internal علامت بزنید تا هیچ‌چیز رویش بدونِ اجازه با اینترنت حرف نزند.

    services:
      db:
        image: postgres:17
        # ports: ["5432:5432"]   # deleted: the app reaches it as db:5432
        networks: [back]
        volumes: [dbdata:/var/lib/postgresql/data]
    
      app:
        image: myapp:1.4
        environment:
          DATABASE_URL: postgres://app@db:5432/app
        ports: ["127.0.0.1:8080:8080"]   # loopback only; proxy sits in front
        networks: [back, front]
    
    networks:
      front: {}
      back:
        internal: true
    
    volumes:
      dbdata: {}

    استک را دوباره بسازید و تأیید کنید شکلِ قوانینِ NAT عوض شده — خطِ DNAT برای اپلیکیشن حالا باید یک مقصدِ loopback داشته باشد، و پایگاه‌داده اصلاً نباید هیچ خطی داشته باشد.

    docker compose up -d
    docker ps --format 'table {{.Names}}\t{{.Ports}}'
    sudo iptables -t nat -S DOCKER
  4. 04

    دقیقاً یک چیز را جلوی دیدِ عموم بگذارید

    وقتی همه‌چیز روی loopback باشد، یک reverse proxy تنها سطحِ عمومی می‌شود — تنها جایی که TLS در آن terminate می‌شود، تنها جایی که آدرسِ کلاینت اصلاً دیده می‌شود، و تنها جایی که فرمتِ لاگ تصمیم گرفته می‌شود. آن را روی هاست یا در کانتینری اجرا کنید که فقط 80 و 443 را منتشر می‌کند و هیچ چیزِ دیگری را؛ اگر در یک کانتینر اجرا شود، به شبکهٔ front می‌پیوندد و به‌جایِ loopback به نامِ سرویس‌ها proxy می‌کند.

    # in the http{} block of /etc/nginx/nginx.conf — no client address recorded
    log_format privacy '- - [$time_local] "$request" $status $body_bytes_sent';
    
    # /etc/nginx/conf.d/app.conf  (proxy on the host)
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name example.com;
    
        access_log /var/log/nginx/app.log privacy;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }

    اگر کلِ هدفِ این تمرین این است که هیچ‌کس نفهمد ماشین کجاست، جایِ این proxy اصلاً روی یک باکسِ دیگر است — همان چینشی که در پنهان کردن IP مبدأ پشت یک reverse proxy آمده، و کاملاً با هر چیزی که این‌جا گفتیم جفت می‌شود.

  5. 05

    شکاف را با DOCKER-USER ببندید، روی هر دو خانواده

    بایندهای loopback کانتینرهای امروز را درست می‌کنند؛ این یکی کانتینرهایی را درست می‌کند که هنوز ننوشته‌اید. سه قانون روی اینترفیسِ عمومی، اول conntrack تا خروجیِ کانتینر کارکردنش را ادامه بدهد، بعد منبع‌های مجازتان، بعد یک drop. بلافاصله بعد از اعمال‌کردن، اتصالِ خروجیِ یک کانتینر را تست کنید — همان قانونی که آدم‌ها برعکس می‌گیرندش.

    PUB=$(ip route get 1.1.1.1 | awk '{print $5; exit}')
    
    sudo iptables -I DOCKER-USER 1 -i "$PUB" -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
    sudo iptables -I DOCKER-USER 2 -i "$PUB" -s 203.0.113.7/32 -j RETURN   # your admin address
    sudo iptables -I DOCKER-USER 3 -i "$PUB" -j DROP
    
    sudo iptables -S DOCKER-USER
    docker run --rm alpine sh -c 'apk update' >/dev/null && echo 'egress OK'

    اگر دیمن قوانینِ v6 را مدیریت می‌کند، همین کار را برای IPv6 هم عیناً تکرار کنید، بعد با واحدی که بعد از آنکه Docker زنجیره‌هایش را از نو ساخت اجرا می‌شود، کاری کنید از یک ری‌استارت هم جان سالم به در ببرد.

    sudo ip6tables -S DOCKER-USER || echo 'no v6 chain: verify v6 reachability by scanning'
    
    # /etc/systemd/system/docker-user-rules.service
    [Unit]
    After=docker.service
    Requires=docker.service
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/sbin/docker-user-rules.sh
    RemainAfterExit=yes
    
    [Install]
    WantedBy=multi-user.target
  6. 06

    امتیاز را داخل کانتینرها کم کنید، و سقفی برای لاگ‌ها بگذارید

    کاری را که یک ایمیجِ به‌خطرافتاده می‌تواند بکند کم کنید، و جلویِ این را بگیرید که هاست از رکوردی پر شود که کسی تصمیم نگرفته نگهش دارد. آن چهار تنظیمِ کانتینر روی یک سرویسِ وبِ معمولی هیچ هزینه‌ای ندارند؛ سقفِ لاگ هم روی هر کانتینری که بعد از reload‌شدنِ دیمن راه می‌افتد اعمال می‌شود.

    services:
      app:
        image: myapp:1.4
        user: "1000:1000"
        read_only: true
        tmpfs: [/tmp]
        cap_drop: [ALL]
        security_opt: ["no-new-privileges:true"]
        # never: volumes: ["/var/run/docker.sock:/var/run/docker.sock"]
    # /etc/docker/daemon.json
    {
      "log-driver": "json-file",
      "log-opts": { "max-size": "10m", "max-file": "3" }
    }
    sudo systemctl reload docker
    docker inspect --format '{{.HostConfig.LogConfig}}' app
  7. 07

    از بیرون تأیید کنید، بعد runbook را بنویسید

    عمداً ری‌استارت کنید، درحالی‌که هیچ‌چیز به جوابش وابسته نیست، و ثابت کنید وضعیتی که ساختید همان وضعیتی است که برمی‌گردد. بعد اسکنِ بیرونیِ مرحلهٔ دو را تکرار کنید و آن را با خروجی‌ای که نگه داشته بودید diff بگیرید — همان diff محصولِ نهایی است، نه دستورها.

    sudo reboot
    
    # after it returns:
    sudo iptables -S DOCKER-USER          # rules reapplied?
    docker ps --format 'table {{.Names}}\t{{.Ports}}'
    
    # from ANOTHER machine again:
    nmap -Pn -p- 203.0.113.10
    nmap -Pn -6 -p- 2001:db8::1

    پنج خط را جایی بنویسید که پیدایشان کنید: قوانین به کدام اینترفیس بایند می‌شوند، واحد کجا زندگی می‌کند، کدام سرویس‌ها عمداً عمومی‌اند، آخرین بار کِی ایمیج‌ها را دوباره ساختید، و چطور volumeها را بازیابی کنید. یک هاستِ کانتینر فقط به‌اندازهٔ توصیفی خوب است که بتواند بعد از بدترین روز دوباره بسازدش.

SP·10 — پرسش‌های متداول

پاسخ‌های سریع

آیا ufw اصلاً روی یک هاستِ Docker کاری انجام می‌دهد؟

بله، و باید نگهش دارید. همچنان هر چیزی را که مستقیم به خودِ هاست آدرس‌دهی شده کنترل می‌کند — SSH، یک پروکسی که بیرون از Docker اجرا می‌شود، هر دیمنی که با package manager نصب کرده‌اید — و این سهمِ بزرگی از چیزی است که به یک سرورِ کوچک حمله می‌کند. چیزی که کنترل نمی‌کند ترافیکی است که Docker با DNAT به یک کانتینر می‌فرستد، چون آن ترافیک فوروارد می‌شود نه تحویلِ محلی، و هرگز به زنجیره‌ای که ufw فیلتر می‌کند نمی‌رسد. دو ابزار، دو مسیر: ufw برای هاست، DOCKER-USER برای کانتینرها.

آیا بایند به 127.0.0.1 به‌تنهایی کافی است؟

برای همان پورت، روی همان خانواده، روی همان کانتینر کافی است — که دقیقاً همین، ضعفش به‌عنوانِ یک استراتژی است. به این بستگی دارد که هر -pی در آینده، توسطِ هر آدمی و هر قطعه‌کدِ کپی‌پیست‌شده‌ای، درست نوشته شود، و فقط IPv4 را محدود می‌کند. بین کانتینرها هم هیچ کاری نمی‌کند: هر چیزی روی همان bridge network همچنان به همسایهٔ بایندشده‌به‌loopback می‌رسد. آن را به‌عنوانِ عادتِ پیش‌فرض به‌کار ببرید، DOCKER-USER را پشتش به‌عنوانِ سیاستی بگذارید که به یادآوردنِ کسی وابسته نیست، و وقتی شعاعِ انفجار اهمیت دارد شبکه‌ها را از هم جدا کنید.

آیا می‌توانم فقط "iptables": false را تنظیم کنم و خودم فایروال را مدیریت کنم؟

می‌توانید، و روی یک VPS تکی تقریباً مطمئناً نباید. آن سوییچ یک صفحهٔ تمیز به شما نمی‌دهد — جلویِ پیکربندیِ قوانینِ NAT، masquerading و ایزوله‌سازی توسطِ Docker را می‌گیرد، همان قوانینی که اصلاً باعث می‌شوند شبکهٔ کانتینر کار کند، پس آن کار را با دست، برای هر کانتینر و هر شبکه‌ای که تا به‌حال ساخته‌اید، به ارث می‌برید. نتیجه معمولاً یا خروجیِ شکسته است یا یک ruleset که بی‌سروصدا از آنچه واقعاً در حال اجراست فاصله می‌گیرد. بگذارید دیمن صاحبِ زنجیره‌های خودش باشد و شما صاحبِ DOCKER-USER باشید، همان درزی که عمداً برایتان باز گذاشته.

آیا rootless Docker یا Podman این مشکل را دور می‌زنند؟

تا حدِ زیادی، و به دو دلیلِ جدا از هم. پورت‌های منتشرشده توسطِ یک پردازهٔ عادیِ سطح‌کاربر نگه داشته می‌شوند که به یک کاربرِ بدون‌امتیاز تعلق دارد، پس به‌شکلِ ترافیکِ رسیده به هاست ظاهر می‌شوند و قوانینِ ufw شما همان‌طور که همیشه انتظار داشتید رویشان اعمال می‌شود. و یک فرار از کانتینر، مهاجم را همان کاربرِ بدون‌امتیاز می‌رساند نه root. هزینه‌اش این است که بعضی قابلیت‌ها، storage driverها و چینش‌های شبکه‌ای متفاوت رفتار می‌کنند یا اصلاً در دسترس نیستند، و پورت‌های زیرِ 1024 به پیکربندیِ اضافه نیاز دارند. اگر دارید یک باکسِ تازه راه می‌اندازید و بارِکاری‌تان سرویس‌های وبِ معمولی است، این یک گزینهٔ جدی است. اگر دارید یک سیستمِ در حالِ اجرا را ممیزی می‌کنید، اول زنجیره را درست کنید و مهاجرت را جداگانه در نظر بگیرید.

VPS من پشتِ فیلترینگِ شبکهٔ شما نشسته. آیا این کافی نیست؟

نه، و ارزش دارد دربارهٔ این تمایز دقیق باشیم. هر ماشین در ناوگان پشتِ 1.5 Tbps از mitigationِ همیشه‌روشن نشسته، که برای جذبِ حملاتِ حجمی وجود دارد — شکلِ ترافیک را می‌خواند، نه نیت را، و نمی‌تواند بداند پورتِ پایگاه‌داده‌ای که منتشر کرده‌اید قرار نبوده عمومی باشد. یک اتصالِ تک و خوش‌ساخت به یک PostgreSQLِ در معرض، در آن لایه از ترافیکِ مشروع قابل‌تشخیص نیست، چون تا جایی که به شبکه مربوط می‌شود، این هست ترافیکِ مشروع: شما پورت را منتشر کرده‌اید. پاک‌سازیِ بالادستی سرورِ شما را قابل‌دسترسی نگه می‌دارد. اینکه چه چیزی رویش قابل‌دسترسی است، پیکربندیِ خودتان است.

از کجا بفهمم که واقعاً الان بسته است؟

فقط یک تست حساب می‌شود: از یک ماشینی که مالِ شما نیست، روی شبکه‌ای که مالِ شما نیست، روی هر دو خانوادهٔ IP، بعد از یک ری‌استارت وصل شوید. بررسی‌های محلی سیستماتیک خوش‌بینانه‌اند — ترافیکِ loopback از زنجیره‌های موردنظر رد نمی‌شود، و یک خروجیِ ساکتِ ss وقتی userland proxy غیرفعال است و کرنل دارد بدونِ هیچ سوکتِ listen‌کننده‌ای فورواردینگ را انجام می‌دهد، هیچ معنایی ندارد. کلِ بازهٔ پورت را اسکن کنید نه فقط پورت‌هایی که انتظارشان را دارید، خروجی را کنارِ runbook نگه دارید، و بعد از هر تغییری که یک کانتینر اضافه می‌کند، دوباره تکرارش کنید.

آن را به عمل درآورید

VPS ظرف 15 min آنلاین، اختصاصی ظرف 2–12 h تحویل داده می‌شود. از $30.00 با کریپتو شارژ کنید — بدون هویت پیوست‌شده.

استقرار یک VPS