HTTPS خودکار با Let’s Encrypt؛ صدور، بررسی و تمدید گواهی
در این درس ۸ بخش
در پایان درس قبل، صفحه و API از یک نشانی HTTP پاسخ میدادند. اکنون همان پروژه را پشت HTTPS قرار میدهیم؛ هدف این درس آن است که کاربر با نام دامنهٔ درست، ارتباط رمزگذاریشده و گواهی معتبر به صفحه برسد و تمدید گواهی هم قابل بررسی باشد. مسیر اصلی برای یک سایت عمومی کوچک، Let’s Encrypt همراه Certbot است. «خودکار» به معنی بینیازبودن از DNS درست، نگهداری سرور یا بررسی تمدید نیست؛ ابزار کار تکراری صدور و تمدید را انجام میدهد و ما باید شرایط اجرای آن را حفظ کنیم.
TLS، گواهی و کلید خصوصی را با یک مثال بشناسید
HTTP شیوهٔ فرستادن درخواست و پاسخ است. TLS لایهای است که در این سناریو ارتباط میان مرورگر و Nginx را رمزگذاری میکند و به مرورگر کمک میکند هویت نام دامنه را بررسی کند. HTTPS یعنی HTTP روی چنین ارتباطی. گواهی، اطلاعاتی دربارهٔ نامهای مجاز و کلید عمومی دارد و توسط یک مرجع صدور معتبر امضا میشود. کلید خصوصی بخش محرمانهٔ همان جفت کلید است؛ مالک سرور باید آن را نگه دارد. نمایش گواهی به بازدیدکننده طبیعی است، اما قراردادن کلید خصوصی در public، مخزن Git یا فایل دانلود پروژه اشتباه است. قفل مرورگر به معنی درستبودن همهٔ منطق برنامه یا قابلاعتمادبودن صاحب هر سایت نیست.
در معماری ما، مرورگر روی پورت ۴۴۳ به Nginx وصل میشود. Nginx بخش TLS را انجام میدهد و درخواست را روی 127.0.0.1:3000 به Node میفرستد. این ارتباط دوم در همان میزبان و روی لوپبک است؛ قرار نیست نام یا کلید TLS را به حساب helloapp بدهیم. مسیر /api/greet همان مسیر میماند و #fetch-http-json صفحه از نشانی نسبی استفاده میکند، پس پس از تغییر به HTTPS، درخواست API هم همان مبدأ را دارد. اگر پشت پراکسی دیگری یا روی دو میزبان جدا کار میکنید، این مدل ساده را بدون بازبینی مرز شبکه تعمیم ندهید؛ آن معماری، تنظیم و ارزیابی دیگری میخواهد.
برای مرور تفاوت DNS، درگاه عمومی و پورت داخلی، درس دامنه و فایروال دورهٔ قبلی را ببینید: https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=e9c6a3bd-ba35-4a0e-85d0-3d64757c2caa
برای توضیح مفصلتر گواهی و تمدید نیز درس HTTPS همان مسیر در دسترس است: https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=7b0cf12c-fec5-46da-9c8e-e62346417ead
در این پروژه، خروجی ملموس هر بررسی همان صفحهٔ «سلام، سرور!» و پاسخ #json آن است؛ لازم نیست برای آزمایش صدور گواهی، یک برنامهٔ پیچیدهتر بسازید.
پیشنیاز صدور؛ قبل از نصب ابزار این چهار مورد را اثبات کنید
اول، یک نام دامنهٔ واقعی انتخاب کنید و بدانید اختیار تغییر DNS آن را دارید. hello.example.com در متن فقط نمونه است؛ آن را در فرمانها و فایل Nginx با نام خود عوض کنید. دوم، رکورد A و در صورت وجود AAAA باید به همین سرور پاسخگو برسد؛ دسترسی درست IPv4، خراببودن IPv6 را جبران نمیکند. سوم، صفحه و /api/status با HTTP از اینترنت باز شوند. چهارم، ورودی پورتهای ۸۰ و ۴۴۳ در فایروال میزبان و ارائهدهنده مجاز باشد. بررسی از داخل سرور جای بررسی از اینترنت را نمیگیرد؛ curl با Host محلی فقط بخشی از مسیر را ثابت میکند.
# روی ویندوز، PowerShell؛ این نام را با دامنهٔ واقعی خود عوض کنید.
$domain = 'hello.example.com'
Resolve-DnsName -Name $domain -Type A
Resolve-DnsName -Name $domain -Type AAAA
curl.exe --fail --show-error --silent --max-time 15 "http://${domain}/api/status"
Test-NetConnection -ComputerName $domain -Port 80
Test-NetConnection -ComputerName $domain -Port 443در این نقطه، پاسخ HTTP باید ok برابر true و service برابر darsavand-hello داشته باشد. نبود رکورد AAAA وقتی عمداً IPv6 ندارید مشکل نیست، ولی وجود رکورد اشتباه مشکل است. پیش از راهاندازی HTTPS ممکن است بررسی اتصال ۴۴۳ ناموفق باشد، چون هنوز Nginx روی آن گوش نمیدهد؛ این نتیجه را با بستهبودن قانون فایروال یکی ندانید. از پنل فایروال، قانون را جدا بخوانید. ساعت سیستم نیز باید درست باشد؛ روی اوبونتو timedatectl status را بررسی کنید. اگر بهجای پروژه، صفحهٔ سرویس دیگری باز میشود، اول نگاشت نام دامنه به سایت درست Nginx را اصلاح کنید.
Let’s Encrypt یک مرجع صدور و Certbot برنامهای است که از طرف شما با آن ارتباط میگیرد. در مسیر انتخابی، افزونهٔ Nginx از یک بررسی HTTP برای اثبات کنترل نام دامنه استفاده میکند. سرور مرجع باید بتواند دادهٔ موقتی آزمون را روی پورت ۸۰ بخواند؛ بازبودن پورت ۳۰۰۰ جای آن را نمیگیرد. Certbot افزونهٔ Nginx را طوری به کار میگیرد که لازم نیست خودمان یک فایل آزمون را داخل کد Node اضافه کنیم. اگر سرویس عمومی روی چند میزبان پخش شده یا یک پراکسی ابری بین راه است، باید مطمئن شوید این بررسی به محل درست میرسد؛ برای شروع، مسیر مستقیم آزمایشگاه را ساده نگه دارید.
نصب Certbot از مسیر رسمی، بدون مخلوطکردن روشها
# روی اوبونتو، در حساب مدیریتی دارای sudo
snap version
command -v certbot || true
dpkg-query -W certbot python3-certbot-nginx 2>/dev/null || true
sudo nginx -t
sudo systemctl status nginx --no-pagerاگر snap شناخته نمیشود، از مخزن اوبونتو با sudo apt update و sudo apt install snapd آن را نصب و وضعیت سرویسش را بررسی کنید؛ سپس این مرحله را دوباره انجام دهید. اگر Certbot از قبل با apt یا روش دیگری نصب شده، خودکار آن را حذف نکنید. نسخه، گواهیها و برنامهٔ تمدید موجود را بررسی و راهنمای مهاجرت رسمی را دنبال کنید؛ ادامهٔ دستورهای این درس برای آزمایشگاهی است که نصب Certbot قبلی ندارد. داشتن چند نسخهٔ متفاوت میتواند باعث شود دستور تعاملی از یک نسخه و زمانبند از نسخهای دیگر اجرا شود. مسیر کامل /snap/bin/certbot در فرمانهای ما این ابهام را کاهش میدهد.
# روی اوبونتو، فقط در شاخهٔ نصب تازهٔ توضیحدادهشده
sudo snap install --classic certbot
/snap/bin/certbot --version
/snap/bin/certbot pluginsنسخه باید نمایش داده شود و افزونهٔ nginx در فهرست باشد. گزینهٔ classic بخشی از روش رسمی نصب این ابزار است و به آن امکان کار با تنظیمات سرور را میدهد؛ این را با مجوز اجرای برنامهٔ Node اشتباه نگیرید. Certbot برای تنظیم Nginx با sudo اجرا میشود، ولی سرویس helloapp همان کاربر محدود قبلی را حفظ میکند. اگر دانلود Snap به دلیل شبکه یا سیاست دستگاه انجام نشد، توقف و همان علت را بررسی کنید؛ اجرای یک اسکریپت ناشناس از سایت واسطه، جای تکمیل نصب رسمی نیست. فایلها و گواهیهای واقعی را نیز برای گرفتن کمک عمومی در متن خطا منتشر نکنید.
صدور و نصب خودکار؛ فقط یک نام واقعی و یک مسیر انتخابی
Certbot میتواند فایل Nginx را برای TLS تغییر دهد، بنابراین پیش از اجرای آن از تنظیم HTTP همین پروژه یک کپی بگیرید. این پشتیبان شامل کلید خصوصی نیست و فایل پروژهٔ دیگری را جابهجا نمیکند. اگر مقصد پشتیبان موجود باشد، بلوک متوقف میشود تا نسخهٔ قبلی بیخبر از بین نرود. در این درس مسیر خودکار را انتخاب میکنیم؛ مسیر دستی درس بعد یک گزینهٔ جایگزین است، نه مجموعهای از server blockها که باید همزمان روی این فایل بچسبانید. اگر قبلاً گواهی فعالی دارید، ابتدا مالک آن تنظیم و شیوهٔ تمدیدش را مشخص کنید.
# روی اوبونتو؛ پس از موفقیت nginx -t و بررسی HTTP عمومی
(
set -eu
BACKUP="$HOME/darsavand-hello.http.before-tls.conf"
if test -e "$BACKUP" || test -L "$BACKUP"; then
printf 'Backup exists; stop and inspect\n' >&2; exit 1
fi
sudo cat /etc/nginx/sites-available/darsavand-hello > "$BACKUP"
chmod 0600 "$BACKUP"
printf 'HTTP configuration backup: %s\n' "$BACKUP"
DOMAIN='hello.example.com'
sudo /snap/bin/certbot --nginx -d "$DOMAIN"
)DOMAIN را پیش از اجرای دستور با نام واقعی تنظیم کنید. ابزار ممکن است ایمیل تماس و پذیرش شرایط مرجع را تعاملی بپرسد؛ خود صاحب سرور باید پرسشها و توافق را بخواند و تصمیم بگیرد. در این آموزش پرچم پذیرش خودکار توافق قرار ندادهایم. تنظیم هدایت HTTP به HTTPS را با توجه به خروجی نسخهٔ نصبشده انتخاب و نتیجه را بعداً بررسی کنید؛ شکل پرسشها ممکن است تغییر کند. Certbot از اختیار دامنه آزمون میگیرد، گواهی و کلید را در مسیرهای مدیریتی نگه میدارد و تنظیم لازم را به Nginx میدهد. موفقیت نهایی را از خروجی واقعی بخوانید، نه فقط از تمامشدن اجرای فرمان.
اگر آزمون دامنه شکست خورد، نخستین پیام مرتبط را نگه دارید: اشتباه DNS، مسیر HTTP نامناسب، مسدودبودن ۸۰ یا رکورد IPv6 نادرست از علتهای رایجاند. چندبار زدن سریع همان فرمان بدون اصلاح علت کمکی نمیکند و ممکن است با محدودیت صدور روبهرو شوید. برای مسئلهٔ DNS به صفحهٔ سرور و رکوردها برگردید؛ برای خطای اتصال، مسیر فایروال را بررسی کنید. پس از اصلاح علت، اگر پشتیبان موفق همین سایت را از مرحلهٔ قبل دارید، آن را نگه دارید و فقط فرمان sudo /snap/bin/certbot --nginx -d "$DOMAIN" را با متغیر واقعی دوباره اجرا کنید؛ بلوک ساخت پشتیبان عمداً مقصد موجود را رد میکند. خطای نحوی Nginx را نیز با صدور دوبارهٔ گواهی درمان نمیکنیم. اگر به گزینهٔ DNS-01 نیاز دارید، آن را با راهنمای رسمی ارائهدهنده دنبال کنید؛ مرحلهٔ دستی TXT با تمدید خودکارِ آماده یکسان نیست.
نتیجه را از صفحه و API با هم بسنجید
# روی اوبونتو
sudo nginx -t
sudo systemctl status nginx --no-pager
sudo /snap/bin/certbot certificates# روی ویندوز، PowerShell؛ domain واقعی از مرحلهٔ قبل
curl.exe --fail --show-error --silent --max-time 15 "https://${domain}/api/status"
$body = @{ name = 'کاربر آزمایشی' } | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)
Invoke-RestMethod -Uri "https://${domain}/api/greet" -Method Post -ContentType 'application/json; charset=utf-8' -Body $bytesبلوک نخست برای Bash اوبونتو و بلوک دوم برای PowerShell ویندوز است؛ آنها را در ترمینال مربوط اجرا کنید. پاسخ وضعیت باید همان دادهٔ معتبر قبل و پاسخ فرم باید پیام فارسی باشد. سپس نشانی https را در مرورگر باز و دکمهٔ ارسال را امتحان کنید. در ابزار توسعه، درخواست /api/greet باید از همان مبدأ HTTPS باشد. اگر فقط صفحه امن است ولی کد، API را با http:// فراخوانی میکند، مرورگر ممکن است آن را بهعنوان محتوای ترکیبی مسدود کند. در پروژهٔ ما نشانی نسبی است؛ فایل app.js اشتباه یا نسخهٔ قدیمی را بررسی کنید. افزودن catch بیصدا به #try-catch یا نادیدهگرفتن هشدار مرورگر اصلاح این علت نیست.
در جزئیات گواهی، نام مورد استفاده، تاریخ اعتبار و صادرکننده را ببینید. اتصال با IP یا نام دیگری لزوماً با گواهی همین دامنه معتبر نیست. در فرمانهای بررسی، گزینهٔ ناامن برای نادیدهگرفتن اعتبار گواهی اضافه نکنید؛ موفقیت ارتباط در آن حالت، اعتماد صحیح را ثابت نمیکند. اگر curl خطای گواهی دارد، نام، زمان سیستم و زنجیره را بررسی کنید. ابزار #debugging-console میتواند خطای fetch یا محتوای ترکیبی را نشان دهد، اما وضعیت گواهی و شبکه را هم باید جدا بخوانید. آزمون دقیق به شما میگوید شکست مربوط به کدام لایه است و جلوی تغییر بیدلیل کد پاسخ سلام را میگیرد.
تمدید خودکار را مشاهده و آزمایش کنید
# روی اوبونتو؛ بررسی مسیر زمانی نصب Snap
sudo systemctl list-timers --all | grep -F certbot || true
sudo systemctl status snap.certbot.renew.timer --no-pager
sudo /snap/bin/certbot renew --dry-runگواهی تاریخ پایان دارد و نگهداری آن به یکبار سبزشدن صفحه محدود نمیشود. بستهٔ Snap معمولاً زمانبند systemd برای تلاشهای دورهای تمدید دارد؛ در خروجی list-timers، نام واحد نصبشده و زمان بعدی را بخوانید. اگر نام واحد در نسخهٔ شما متفاوت بود، وجود همان واحد واقعی را بررسی کنید و فرمان status را مطابق آن اجرا کنید؛ نبود یک نام نمونه را با نبود قطعی همهٔ زمانبندها یکی ندانید. dry-run مسیر تمدید را آزمایش میکند و باید نتیجهٔ موفق برای گواهی مربوط نشان دهد؛ این آزمایش برای مشاهده است و جای بررسی برنامهٔ زمانی و شرایط دسترسی آینده را نمیگیرد.
با افزونهٔ Nginx، چرخهٔ معمول تمدید شامل بارگذاری گواهی تازه برای سرویس مربوط است؛ بعد از تغییر روش نصب یا مسیر گواهی، این رفتار را دوباره بررسی کنید. فایلهای گواهی زیر /etc/letsencrypt را دستی جابهجا یا به public کپی نکنید. اگر تمدید شکست خورد، دسترسی HTTP، DNS و لاگهای Certbot را بررسی کنید و پیش از انقضا مشکل را حل کنید. صرف تنظیم یک cron با نام renew، بدون موفقیت آزمون یا شناخت روش اعتبارسنجی، تضمین تمدید نیست. برای این پروژه نیازی به دادن کلید خصوصی به Node، بازکردن پورت ۳۰۰۰ یا اجرای helloapp با root نداریم.
تمرین و پاسخ؛ چه چیزی را باید تحویل دهید؟
تحویل این درس شامل نشانی HTTPS واقعی، پاسخ وضعیت، پاسخ نام فارسی، نام و تاریخ گواهی، وضعیت زمانبند و نتیجهٔ dry-run است. تمرین اول: HTTP کار میکند ولی صدور در مسیر IPv6 شکست میخورد؛ آیا کد /api/greet را تغییر میدهید؟ پاسخ: خیر، رکورد AAAA و دسترسی همان نشانی را بررسی میکنیم. تمرین دوم: صفحه با HTTPS باز میشود اما فرم به آدرس HTTP قدیمی درخواست میفرستد؛ پاسخ، اصلاح نشانی یا فایل کلاینت است، نه خاموشکردن حفاظت مرورگر. تمرین سوم: آیا صدور موفق امروز به معنی تمدید موفق چند هفتهٔ بعد است؟ خیر؛ DNS، فایروال، زمانبند و خروجی آزمون باید قابل نگهداری باشند.
منابع رسمی و ادامهٔ مسیر دستی
Certbot — Nginx on Linux, Snap instructions: https://certbot.eff.org/instructions?os=snap&ws=nginx
Ubuntu — Obtain TLS certificates: https://ubuntu.com/server/docs/how-to/security/obtain-tls-certificates/
Let’s Encrypt — Challenge types: https://letsencrypt.org/docs/challenge-types/
Let’s Encrypt — IPv6 support: https://letsencrypt.org/docs/ipv6-support/
Certbot — Using Certbot and renewal: https://eff-certbot.readthedocs.io/en/stable/using.html
MDN — Mixed content: https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content
تألیف و مثالهای آموزشی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.