پرش به محتوای اصلی
امنیت
۶ دقیقه مطالعه

گواهی SSL سرور مبدأ ماه‌ها منقضی بود و هیچ‌کس نفهمید

سایت پشت CDN سالم بود، اما گواهی سرور مبدأ ماه‌ها قبل منقضی شده بود. روایت یک تمدید خودکار ناقص و راه‌حل با webroot و install-cert.

سید محمد بهبهانی‌زاده
برنامه‌نویس فول‌استک

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

ماجرا و علامت‌ها

سایت پشت یک CDN بود و از بیرون همه‌چیز عادی به نظر می‌رسید. کاربرها گواهی معتبرِ لبه CDN را می‌دیدند، صفحه‌ها باز می‌شدند و مانیتورینگ ساده «بالا بودن سایت» که از بیرون می‌پرسید، همیشه جواب مثبت می‌گرفت.

ماجرا وقتی رو شد که برای کاری دیگر سراغ سرور رفتیم و گواهی نصب‌شده روی خودِ سرور مبدأ را بررسی کردیم. تاریخ انقضای آن چند ماه قبل گذشته بود.

نکته عجیب این بود که هیچ علامت قابل مشاهده‌ای وجود نداشت. نه خطای گواهی در مرورگر، نه قطعی، نه شکایت کاربر. همین نبودنِ علامت، خودش مشکل اصلی بود: یک گواهی منقضی که ماه‌ها در سکوت کنار سایت زنده مانده بود.

نکات کلیدی

  • کاربر گواهی لبه CDN را می‌بیند، نه گواهی سرور مبدأ
  • مانیتورینگ بیرونی «بالا بودن سایت» چیزی درباره سلامت مبدأ نمی‌گوید
  • نبودن خطا به معنی سالم بودن نیست

ردگیری علت

اولین سؤال این بود که چرا چیزی خراب نشده. جواب در تنظیم رمزنگاری بین CDN و مبدأ بود. در حالت Full، CDN با مبدأ از طریق HTTPS حرف می‌زند اما گواهی آن را اعتبارسنجی نمی‌کند. طبق مستندات، چنین گواهی‌ای می‌تواند منقضی یا خودامضا باشد، یا حتی نام دامنه‌اش با درخواست نخواند. پس انقضای گواهی مبدأ برای CDN اهمیتی نداشت و ارتباط همچنان برقرار می‌ماند.

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

نتیجه این بود که تمدیدها احتمالاً در دایرکتوری داخلی خود ابزار انجام می‌شد، اما فایلی که Nginx می‌خواند هیچ‌وقت عوض نمی‌شد و Nginx هم هیچ‌وقت reload نمی‌شد. ابزار از نگاه خودش کارش را درست انجام می‌داد و از دید سرویس وب چیزی تغییر نمی‌کرد.

نکات کلیدی

  • حالت Full گواهی مبدأ را اعتبارسنجی نمی‌کند، پس انقضا دیده نمی‌شود
  • صدور اولیه موفق بود؛ مشکل در مرحله «نصب و reload» بود
  • تمدید و نصب دو کار جدا هستند

علت اصلی و مفهوم فنی پشت آن

علت اصلی ترکیب دو چیز بود: یک زنجیره تمدید که آخرش به Nginx وصل نبود، و یک CDN که اجازه می‌داد این گسست دیده نشود.

پروتکل ACME (استاندارد RFC 8555) فقط درباره اثبات مالکیت دامنه و صدور گواهی است. در چالش http-01، سرور CA یک فایل را از مسیر /.well-known/acme-challenge/ روی دامنه شما می‌خواند. اگر محتوا درست باشد، گواهی صادر می‌شود. اینکه گواهی بعد از صدور کجا کپی شود و کدام سرویس دوباره بارگذاری شود، بیرون از پروتکل است و کار کلاینت است.

در کلاینت‌هایی مثل acme.sh این بخش با گزینه‌های --install-cert و --reloadcmd تعریف می‌شود. مستندات توضیح می‌دهد که install-cert گواهی صادرشده را به مسیرهای مشخص کپی می‌کند و reloadcmd بعد از هر صدور یا تمدید اجرا می‌شود تا سرور گواهی جدید را بخواند. بدون این دو، تمدید در عمل بی‌اثر است.

نکته مهم دیگر این است که Nginx گواهی را هنگام شروع یا reload در حافظه می‌گیرد. حتی اگر فایل روی دیسک عوض شود، تا reload نشود همان نسخه قدیمی را ارائه می‌دهد.

و اما سمت CDN: در حالت Full (strict) لازم است گواهی مبدأ منقضی نشده باشد، از یک CA مورد اعتماد عمومی (یا CA خود CDN) صادر شده باشد و CN یا SAN آن با نام میزبان بخواند. اگر تنظیم ما روی Full strict بود، همان روز انقضا خطا می‌دیدیم (در مستندات خطای 526 ذکر شده). چون روی حالتی بود که اعتبارسنجی نمی‌کرد، مشکل پنهان ماند.

نکات کلیدی

  • ACME فقط صدور را پوشش می‌دهد؛ نصب و reload وظیفه کلاینت است
  • Nginx تا reload نشود گواهی قدیمی را در حافظه نگه می‌دارد
  • Full (strict) انقضای مبدأ را فوراً آشکار می‌کند؛ Full نه

راه‌حل

سه کار انجام دادیم. اول، گواهی را دوباره با روش webroot صادر کردیم. مسیر .well-known از طریق خود CDN قابل دسترس بود و CA می‌توانست فایل چالش را بخواند. دوم، مسیر نصب و فرمان reload را صریحاً تعریف کردیم و در reloadcmd قبل از reload، تست کانفیگ با nginx -t را گذاشتیم تا کانفیگ خراب باعث قطع سرویس نشود. سوم، یک چک ساده برای تاریخ انقضای گواهی خودِ مبدأ اضافه کردیم.

نمونه زیر عمومی است و مسیرها و دامنه‌ها ساختگی‌اند:

بعد از آن، بررسی انقضا را مستقل از CDN و مستقیم روی مبدأ انجام می‌دهیم. چک زیر با نمونه آدرس مستندات (203.0.113.10) نشان داده شده و باید با مقدار واقعی جایگزین شود:

bash
# 1) صدور با webroot
acme.sh --issue -d example.com -w /var/www/example

# 2) تعریف نصب و reload (با تست کانفیگ قبل از reload)
acme.sh --install-cert -d example.com \
  --key-file       /etc/ssl/example/example.com.key \
  --fullchain-file /etc/ssl/example/fullchain.pem \
  --reloadcmd      "nginx -t && systemctl reload nginx"

# 3) چک انقضای گواهی مبدأ (مستقیم، بدون عبور از CDN)
if ! echo | openssl s_client -connect 203.0.113.10:443 -servername example.com 2>/dev/null \
     | openssl x509 -noout -checkend $((14*86400)); then
  echo "origin certificate expires within 14 days" >&2
  exit 1
fi

نکات کلیدی

  • روش webroot وقتی مسیر .well-known از CDN رد می‌شود قابل استفاده است
  • nginx -t قبل از reload جلوی اعمال کانفیگ خراب را می‌گیرد
  • چک انقضا باید مستقیم به مبدأ وصل شود، نه به آدرس عمومی پشت CDN

درس‌ها و چک‌لیست

مهم‌ترین درس این است که «سایت بالا است» یعنی لبه سالم است، نه مبدأ. اگر روزی حالت CDN به Full (strict) تغییر کند، یا CDN برداشته شود، یا ترافیک مستقیم به مبدأ برسد، سایت ناگهان با خطای گواهی می‌خوابد. در آن لحظه معلوم نیست مشکل چند ماه قدمت دارد.

درس دوم این است که صدور موفق گواهی هنوز به معنی تمدید کامل نیست. فقط وقتی مطمئنیم که فایل جدید نصب و سرویس reload شود، چرخه کامل شده است. بهتر است یک بار تمدید را عمداً اجرا کنیم و ببینیم گواهی ارائه‌شده واقعاً عوض می‌شود.

چک‌لیستی که از این ماجرا برای خودمان نگه داشتیم:

۱) برای هر گواهی، مسیر نصب و reloadcmd صریح تعریف شود.

۲) reloadcmd شامل nginx -t باشد.

۳) انقضای گواهی را مستقیم روی مبدأ بسنجیم، نه از روی آدرس عمومی.

۴) برای چک انقضا هشدار بگذاریم، نه فقط گزارش.

۵) حالت رمزنگاری CDN را بشناسیم و بدانیم مبدأ در آن اعتبارسنجی می‌شود یا نه.

۶) هر چند وقت یک بار تمدید را آزمایشی اجرا کنیم و تاریخ گواهی ارائه‌شده را بررسی کنیم.

نکات کلیدی

  • سلامت لبه CDN دلیل سلامت مبدأ نیست
  • تمدید یعنی صدور + نصب + reload، نه فقط صدور
  • مانیتورینگ باید مبدأ را مستقیم ببیند

جمع‌بندی

گواهی منقضی روی مبدأ تا وقتی CDN آن را اعتبارسنجی نمی‌کرد هیچ خرابی‌ای نشان نداد، و همین آن را خطرناک می‌کرد. بعد از این ماجرا تمدید را زنجیره‌ای کامل می‌بینیم: صدور، نصب، reload و در نهایت مشاهده گواهیِ واقعاً ارائه‌شده از خودِ مبدأ.

منابع

  1. RFC 8555 - Automatic Certificate Management Environment (ACME)datatracker.ietf.org
  2. acme.sh(1) - Debian trixie manpagemanpages.debian.org
  3. Full (strict) - SSL/TLS encryption modesdevelopers.cloudflare.com