گواهی wildcard روی یک سرور کار سادهای است. به چالش DNS-01 سوئیچ میکنید، certbot هر دو سه ماه یک بار تمدیدش میکند و کلاً یادتان میرود که وجود دارد. دردسر از وقتی شروع میشود که همان *.example.com باید روی سرور دوم هم باشد، بعد روی یک NAS، بعد روی یک VM ابری که جلوی همه چیز نشسته. تمدید روی یک ماشین درست کار میکند و روی بقیه بیسروصدا شکست میخورد، و شما وقتی متوجه میشوید که مرورگر هشدار قرمز نشان میدهد.
ما چند سرور پشت یک دامنه داریم، از جمله یک سرور ایمیل، و همین مسیر را رفتهایم. این مطلب توضیح میدهد چرا تمدید جداگانه روی هر سرور خراب میشود، الگوی «یک صادرکننده» که پایدار میماند چیست، و کدام جزئیات hookهای certbot تعیین میکند اسکریپت شما در لحظهٔ درست اجرا شود یا نه.
چرا تمدید مستقل روی هر سرور خراب میشود
در خود Let's Encrypt چیزی جلوی این را نمیگیرد که چند میزبان یک wildcard یکسان را درخواست کنند. مشکلها در اطراف این کار است.
محدودیت نرخ. Let's Encrypt اجازه میدهد برای دقیقاً یک مجموعه شناسهٔ یکسان، حداکثر ۵ گواهی در هر ۷ روز صادر شود، و این محدودیت سراسری است: سفارشهای همهٔ حسابها حساب میشوند. سه سرور که هر کدام یکی دو بار از نو نصب یا تست شده باشند، میتوانند در یک بعدازظهر این سهمیه را تمام کنند.
واگذاری چالش. مقصر اصلی ماجرای «فقط آخرین سرور تمدید میشود» همین است. در acme-dns هر ثبتنام یک زیردامنهٔ تصادفی مخصوص خودش برمیگرداند و شما _acme-challenge.example.com را با یک CNAME به آن اشاره میدهید. هر نام فقط یک CNAME میتواند داشته باشد. وقتی سرور سوم ثبتنام میکند و رکورد را بهروز میکنید، دو سرور اول همچنان مقدار TXT را در زیردامنههایی مینویسند که CA دیگر سراغشان نمیرود. تمدید آنها با خطای اعتبارسنجی DNS شکست میخورد.
کلید DNS همه جا پخش است. هر میزبانی که تمدید میکند به توکنی نیاز دارد که روی zone شما دسترسی نوشتن داشته باشد. سه میزبان یعنی سه نسخه از کلیدی که میتواند هر رکوردی را که دارید تغییر دهد.
HTTP-01 هم مقیاسپذیر نیست. پشت مودم خانگی یا یک IP عمومی، پورت ۸۰ فقط به یک ماشین forward میشود. در یکی از شبکههای ما پورت ورودی ۸۰ در بالادست فیلتر شده بود و تمدیدهای HTTP-01 هفتهها شکست خوردند تا کسی متوجه شد. DNS-01 به هیچ پورت ورودیای وابسته نیست و به همین دلیل پایهٔ درستی برای این کار است.
الگو: یک صادرکننده، بعد ارسال به بقیه
یک ماشین را صاحب گواهی کنید. توکن API مربوط به DNS فقط روی آن است، certbot یا acme.sh را اجرا میکند و تنها میزبانی است که با CA حرف میزند. بقیهٔ سرورها فقط فایل تحویل میگیرند.
- یک زمانبندی تمدید برای زیر نظر گرفتن، به جای سه تا.
- یک توکن DNS، روی یک ماشین.
- محدودیت نرخ دیگر نگرانی نیست، چون یک میزبان یک گواهی را تمدید میکند.
- اضافه کردن سرور پنجم یعنی یک خط بیشتر در اسکریپت، نه یک راهاندازی ACME دیگر.
سرورهای گیرنده فقط سه چیز لازم دارند: یک مسیر ثابت برای فایلها، کاربری که بتواند آنجا بنویسد، و اجازهٔ reload کردن وبسرور یا سرور ایمیل خودشان.
از deploy hook استفاده کنید، نه post hook
certbot سه نوع hook دارد و مستندات certbot دقیق گفته هر کدام کی اجرا میشود:
| Hook | certbot کی اجرایش میکند | مناسب برای |
|---|---|---|
--pre-hook | قبل از هر تلاش برای تمدید، فقط وقتی گواهی موعد تمدیدش رسیده | متوقف کردن سرویسی که پورت ۸۰ را گرفته |
--post-hook | بعد از هر تلاش برای تمدید، حتی تلاشهای ناموفق | راهاندازی دوبارهٔ همان سرویس |
--deploy-hook | فقط بعد از اینکه گواهی واقعاً تمدید شد | کپی فایلهای جدید و reload سرورها |
hookها فقط وقتی اجرا میشوند که موعد تمدید گواهی رسیده باشد، پس cron یا تایمر systemd میتواند certbot renew را مرتب اجرا کند بیآنکه اثر جانبی داشته باشد. تفاوت در این است که وقتی تمدید شکست میخورد چه اتفاقی میافتد. post hook در هر حال اجرا میشود. اگر اسکریپت کپی شما آنجا باشد، تمدید ناموفق باز هم فایلهای قدیمی را به همه میفرستد و همهٔ سرورها را reload میکند، و لاگها میگویند «deployed» در حالی که گواهی دارد به تاریخ انقضا نزدیکتر میشود. deploy hook فقط بعد از تمدید موفق اجرا میشود.
deploy hook دو متغیر هم دریافت میکند: $RENEWED_LINEAGE، یعنی دایرکتوری live که فایلهای جدید در آن است (مثلاً /etc/letsencrypt/live/example.com)، و $RENEWED_DOMAINS، یعنی نامهای روی گواهی تمدیدشده. هر فایل اجرایی در /etc/letsencrypt/renewal-hooks/deploy/ به عنوان deploy hook اجرا میشود، پس لازم نیست فلگ را به خاطر بسپارید.
یک نسخهٔ حداقلی:
#!/bin/sh
# /etc/letsencrypt/renewal-hooks/deploy/push-certs.sh
set -e
for host in web2 nas vm-proxy; do
rsync -aL "$RENEWED_LINEAGE/" "certsync@$host:/etc/ssl/wildcard/"
ssh "certsync@$host" 'sudo nginx -t && sudo systemctl reload nginx'
done
سه جزئیات مهم است. rsync -L خود فایلها را کپی میکند، چون دایرکتوری live/ فقط symlinkهایی به archive/ دارد. کاربر certsync روی هر مقصد باید کلید SSHای داشته باشد که فقط به همین کار محدود است، و دسترسی sudo فقط برای تست و reload کردن nginx. و nginx -t قبل از reload اجرا میشود، پس پیکربندی خراب روی یک میزبان سایت را از دسترس خارج نمیکند.
سرورهای ایمیل هم همین رسیدگی را لازم دارند. Postfix، Dovecot و بیشتر سرورهای دیگر گواهی را هنگام شروع بارگذاری میکنند، پس تا reload نشوند همان گواهی قدیمی را ارائه میدهند. اگر سرور ایمیل شما هم از wildcard استفاده میکند، دستور reload آن را به حلقه اضافه کنید. سمت سرور ایمیل را در مقایسهٔ Mailcow، Stalwart و Mailu در محیط عملیاتی بررسی کردهایم.
تست بدون دو ماه انتظار
اول روی صادرکننده certbot renew --dry-run -v را اجرا کنید. این دستور با سرور staging حرف میزند و اگر خطای چالشی وجود داشته باشد، خطای واقعی را نشان میدهد. لاگ کامل در /var/log/letsencrypt/letsencrypt.log است. یک نکتهٔ ظریف: طبق مستندات certbot، deploy hookها در dry run اجرا نمیشوند مگر اینکه --run-deploy-hooks را اضافه کنید. بدون آن، تمدید را تست کردهاید ولی توزیع را نه.
بعد بررسی کنید هر سرور واقعاً چه گواهیای ارائه میدهد، نه اینکه چه چیزی روی دیسکش است:
echo | openssl s_client -connect web2.example.com:443 -servername web2.example.com 2>/dev/null | openssl x509 -noout -enddate
اگر تاریخها بین میزبانها فرق دارد، کپی یا reload روی همان میزبان شکست خورده است. این بررسی را با هشدار در ۱۴ روز مانده به انقضا در مانیتورینگتان بگذارید. هزینهای ندارد و خطاهایی را میگیرد که hook نمیتواند ببیند. اگر میزبانها در کانتینر اجرا میشوند، خروجی hook را به stdout بفرستید تا کنار بقیهٔ لاگهایتان جمع شود، همانطور که در چرا لاگ کانتینر باید روی stdout باشد توضیح دادهایم.
گواهیهای کوتاهتر این کار را اجباری میکنند
Let's Encrypt جدول زمانی کوتاه شدن عمر گواهیها را، همراستا با Baseline Requirements انجمن CA/Browser، اعلام کرده است. در ۱۰ فوریهٔ ۲۰۲۷ پروفایل پیشفرض از گواهی ۹۰ روزه به ۶۴ روزه میرود. در ۱۶ فوریهٔ ۲۰۲۸ به ۴۵ روز میرسد و مدتی که میشود یک اعتبارسنجی دامنه را دوباره استفاده کرد از ۳۰ روز به ۷ ساعت کاهش پیدا میکند. قرار بود پروفایل اختیاری tlsserver از ۱۳ مه ۲۰۲۶ گواهیهای ۴۵ روزه صادر کند.
راهاندازیای که در آن یک نفر گواهی تمدیدشده را دستی روی بقیهٔ سرورها کپی میکند، با کمی شانس در ۹۰ روز جواب میدهد. در ۴۵ روز از کار میافتد. یک صادرکنندهٔ واحد با deploy hook، هر قدر هم عمر گواهی کوتاه شود، همچنان کار میکند.
چکلیست
- فقط یک میزبان wildcard را درخواست و تمدید میکند. توکن DNS فقط همانجا وجود دارد.
- اگر از acme-dns استفاده میکنید، برای هر نام دقیقاً یک ثبتنام و یک CNAME.
- اسکریپت کپی و reload در
renewal-hooks/deploy/است، نه در post hook. - برای هر مقصد یک کلید SSH محدود، و sudo فقط برای تست و reload.
nginx -t(یا بررسی معادل سرور ایمیل) قبل از هر reload.- یک بار بعد از راهاندازی، dry run با
--run-deploy-hooks. - بررسی بیرونی تاریخ انقضا برای همهٔ hostnameها، با هشدار در ۱۴ روز.
پرسشهای متداول
آیا هر سرور میتواند نسخهٔ خودش از همان گواهی wildcard را درخواست کند؟
بله، ولی هر نسخه از محدودیت Let's Encrypt کم میکند: ۵ گواهی برای دقیقاً یک مجموعه نام در هر ۷ روز، برای همهٔ حسابها روی هم. با سه چهار سرور و چند اجرای آزمایشی ممکن است به این سقف بخورید. یک صادرکنندهٔ واحد این مشکل را از اساس حذف میکند.
چرا تمدید فقط روی سروری کار میکند که آخر از همه راهاندازی کردهام؟
علت معمول، واگذاری چالش است. در acme-dns هر ثبتنام یک زیردامنهٔ جدید به شما میدهد و CNAME مربوط به _acme-challenge فقط به یکی از آنها میتواند اشاره کند. سرورهایی که زودتر ثبتنام کردهاند رکورد TXT خود را روی نامی مینویسند که CA هیچوقت بررسیاش نمیکند.
آیا certbot renew --dry-run deploy hook من را هم تست میکند؟
نه. مستندات certbot میگوید deploy hookها در dry run اجرا نمیشوند مگر اینکه --run-deploy-hooks را اضافه کنید. pre hook و post hook اجرا میشوند.
برای گواهیهای ۴۵ روزه باید چیزی را تغییر دهم؟
اگر تمدید و توزیع کاملاً خودکار است، نه. اگر حتی یک مرحله دستی است، بله: پروفایل پیشفرض Let's Encrypt در ۱۰ فوریهٔ ۲۰۲۷ به گواهی ۶۴ روزه و در ۱۶ فوریهٔ ۲۰۲۸ به ۴۵ روزه میرسد، پس کپی دستی هر دو ماه یک بار دیگر کافی نیست.