TLS دستی؛ دریافت گواهی معتبر و نصب آن روی صفحه و API
در این درس ۶ بخش
در مسیر خودکار، Certbot بسیاری از کارهای صدور و نصب گواهی را هماهنگ میکرد. مسیر دستی برای وقتی است که گواهی را از مرجع دیگری میگیرید یا میخواهید فایلها و زمان تعویضشان را خودتان مدیریت کنید. نتیجهٔ ظاهری برای بازدیدکننده میتواند همان صفحهٔ «سلام، سرور!» با HTTPS معتبر باشد، اما مسئولیت شما بیشتر میشود: نام دامنه، زنجیرهٔ گواهی، کلید متناظر، مجوز فایل و تاریخ انقضا را باید بررسی کنید. این درس شاخهٔ جایگزین درس ششم است. اگر مسیر خودکار شما سالم است، برای تمرین آن را خراب نکنید؛ این بخش را بخوانید یا روی محیط آزمایشی مستقل، از پایان مرحلهٔ HTTP درس پنجم شروع کنید.
چه فایلهایی داریم و هرکدام چه چیزی را اثبات میکنند؟
گواهی سرور یک سند عمومی دیجیتال است که نامهای مجاز، کلید عمومی و بازهٔ اعتبار را در خود دارد. مرجع صدور گواهی یا CA، پس از بررسی کنترل دامنه طبق روش خودش، آن را امضا میکند. کلید خصوصی فایل دیگری است که باید فقط نزد صاحب سرویس بماند؛ آن را به پنل عمومی، مخزن Git یا گفتوگوی پشتیبانی نفرستید. مرورگر در برقراری TLS فقط وجود یک فایل را نمیبیند؛ نام مقصد، اعتبار زمانی و زنجیرهٔ اعتماد را نیز ارزیابی میکند. بنابراین صفحهای که «رمزگذاری میشود» لزوماً هویت معتبر برای همان دامنه ندارد.
CSR یعنی درخواست امضای گواهی. این فایل از کلید عمومی و اطلاعات درخواستی ساخته و با کلید خصوصی امضا میشود؛ خود کلید خصوصی داخل CSR نیست. در این پروژه SAN فهرست نامهایی است که گواهی باید پوشش دهد؛ برای نمونه hello.example.com یک نام نمونه است و باید با دامنهٔ واقعی شما عوض شود. داشتن گواهی برای www.example.com بهخودیخود hello.example.com را پوشش نمیدهد. زنجیره نیز معمولاً شامل گواهی سرور و گواهیهای میانی است تا مشتری بتواند به ریشهٔ مورد اعتماد برسد. ترتیب فایل fullchain از گواهی سرور شروع میشود و سپس گواهیهای میانی میآیند.
همانطور که در #validation-contracts دادهٔ ورودی را با قراردادش میسنجیم، اینجا هم چند پرسش جدا داریم: نام درست است؟ هنوز معتبر است؟ زنجیره قابل اعتماد است؟ کلید متعلق به همین گواهی است؟ هیچکدام جای دیگری را نمیگیرد. پاسخ API به شکل #json پس از برقراری اتصال HTTPS منتقل میشود؛ JSON خودش رمزگذاری یا احراز هویت TLS را انجام نمیدهد. این تفکیک کمک میکند خطای گواهی را با خراببودن تابع پاسخگویی اشتباه نگیرید و با تغییر کد برنامه دنبال اصلاح فایل گواهی نروید.
پیش از ادامه، صفحهٔ پروژه باید از HTTP کار کند، DNS به همان سرور برسد و تنظیم Nginx درس پنجم حاضر باشد. برای مرور نقش پراکسی و گواهی، این دو درس قبلی را ببینید:
https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=9cbb5041-c878-43cc-b361-2eb8ea492cff
https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=7b0cf12c-fec5-46da-9c8e-e62346417ead
این ارجاعها برای جزئیات بیشترند؛ مراحل همین درس مشخص میکنند چه فایلهایی بسازید. تمام فرمانهای زیر در Bash اوبونتوی آزمایشی اجرا میشوند، نه PowerShell ویندوز. روی سروری که سایت واقعی دارد، این نمونه جای برنامهٔ تغییر و پشتیبان اختصاصی آن نیست.
گام اول؛ کلید و درخواست را در پوشهٔ شخصی تازه بسازید
در این بلوک فقط DOMAIN را با دامنهٔ واقعی عوض کنید. نام نمونه عمداً رد میشود تا بهجای دامنهٔ خودتان برای یک مثال اینترنتی درخواست ندهید. پوشه با mktemp تازه ساخته میشود و umask دسترسی پیشفرض فایلهای تازه را محدود میکند. RSA با اندازهٔ ۳۰۷۲ در این تمرین انتخاب مشخصی است؛ لازم نیست این عدد را به قانون همهٔ پروژهها تبدیل کنید. بلوک در پرانتز اجرا میشود تا توقف آن، نشست اصلی SSH را نبندد. مسیر چاپشدهٔ آخر را یادداشت کنید؛ پس از بازکردن ترمینال تازه، متغیرهای نشست قبلی الزاماً وجود ندارند.
# روی اوبونتو، Bash؛ DOMAIN را واقعی کنید.
(
set -eu
umask 077
DOMAIN='hello.example.com'
test "$DOMAIN" != 'hello.example.com' || { printf 'Set your real domain first\n' >&2; exit 1; }
printf '%s\n' "$DOMAIN" | grep -Eq '^([A-Za-z0-9][A-Za-z0-9-]*\.)+[A-Za-z]{2,}$'
TLSWORK=$(mktemp -d "$HOME/hello-manual.XXXXXX")
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out "$TLSWORK/privkey.pem"
openssl req -new -sha256 -key "$TLSWORK/privkey.pem" -out "$TLSWORK/request.csr" \
-subj "/CN=$DOMAIN" -addext "subjectAltName=DNS:$DOMAIN"
openssl req -in "$TLSWORK/request.csr" -noout -verify
printf 'CSR directory: %s\n' "$TLSWORK"
)انتظار، ساختهشدن privkey.pem و request.csr و پیام موفقیت بررسی امضای درخواست است. نبودن فایل خروجی، خطای دسترسی یا پیام نامعتبر بودن درخواست یعنی هنوز به مرحلهٔ بعد نروید. فقط request.csr را طبق روش رسمی مرجع انتخابی ارسال کنید و کنترل دامنه را انجام دهید. گرفتن فایل کلید خصوصی توسط یک فرم ناشناس شرط صدور نیست. دریافت گواهی در این درس خودکار شبیهسازی نمیشود: تا وقتی مرجع واقعاً گواهی امضاشده نداده، فایل ساختگی را جای پاسخ آن نگذارید. هزینه و روش تأیید مرجع را پیش از انتخاب بررسی کنید.
فایل گواهی صادرشده را در همان پوشه با نام certificate.pem و مجموعهٔ گواهیهای میانی اعلامشدهٔ مرجع را با نام intermediates.pem ذخیره کنید. این نامها قرارداد همین تمریناند؛ برخی مراجع نام دیگری میدهند و باید محتوای درست را به این نامها منتقل کنید. پسوند pem معمولاً نشاندهندهٔ قالب متنی با سطرهای BEGIN و END است، اما تغییر نام یک فایل، قالب واقعی آن را تبدیل نمیکند. این مسیر آموزشی فرض میکند مرجع زنجیرهٔ میانی PEM داده است. اگر فایل PFX یا قالب دیگری دارید، ابتدا راهنمای رسمی تبدیل همان مرجع را دنبال کنید، نه اینکه فایل را فقط rename کنید.
گام دوم؛ نام، اعتماد، زمان و کلید متناظر را جدا بررسی کنید
دو مقدار اول بلوک بعد را واقعی کنید: مسیر TLSWORK باید همان پوشهٔ مرحلهٔ قبل باشد و DOMAIN همان نام داخل CSR. دستور نمایش مشخصات برای خواندن است. بررسی اصلی با openssl verify هم زنجیره و هم نام میزبان را میسنجد و به ریشههای نصبشدهٔ اوبونتو تکیه دارد؛ گواهی ناشناس را برای سبزشدن نتیجه به مخزن اعتماد سیستم اضافه نکنید. بررسی زمان صفر، منقضینبودن را کنترل میکند؛ هشدار هفتروزه فقط سیاست هشدار این تمرین است و عمر ثابت تمام گواهیها نیست. در پایان، چکیدهٔ کلیدهای عمومی باید برابر باشد؛ هیچ بخش خصوصی چاپ نمیشود.
# روی اوبونتو، Bash؛ هر دو مقدار نخست را واقعی کنید.
(
set -euo pipefail
TLSWORK='/home/labadmin/hello-manual.ABC123'
DOMAIN='hello.example.com'
test "$DOMAIN" != 'hello.example.com'
test -f "$TLSWORK/privkey.pem"
test -s "$TLSWORK/certificate.pem"
test -s "$TLSWORK/intermediates.pem"
openssl x509 -in "$TLSWORK/certificate.pem" -noout -subject -issuer -dates -ext subjectAltName
openssl verify -purpose sslserver -verify_hostname "$DOMAIN" -CApath /etc/ssl/certs \
-untrusted "$TLSWORK/intermediates.pem" "$TLSWORK/certificate.pem"
openssl x509 -in "$TLSWORK/certificate.pem" -noout -checkend 0
if openssl x509 -in "$TLSWORK/certificate.pem" -noout -checkend 604800; then
printf 'More than seven days remain\n'
else
printf 'Expiry warning: inspect the real notAfter date\n'
fi
CERT_PUBLIC=$(openssl x509 -in "$TLSWORK/certificate.pem" -pubkey -noout | openssl pkey -pubin -outform DER | sha256sum | awk '{print $1}')
KEY_PUBLIC=$(openssl pkey -in "$TLSWORK/privkey.pem" -pubout -outform DER | sha256sum | awk '{print $1}')
test "$CERT_PUBLIC" = "$KEY_PUBLIC" || { printf 'Key mismatch\n' >&2; exit 1; }
if test -e "$TLSWORK/fullchain.pem" || test -L "$TLSWORK/fullchain.pem"; then
printf 'Full chain output exists; stop before writing\n' >&2; exit 1
fi
cat "$TLSWORK/certificate.pem" "$TLSWORK/intermediates.pem" > "$TLSWORK/fullchain.pem"
printf 'Verified full chain is ready\n'
)خروجی verify باید برای گواهی شما OK باشد. خطای نام معمولاً یعنی نام دیگری در SAN آمده یا دامنه را اشتباه نوشتهاید؛ خطای issuer میتواند ناشی از زنجیرهٔ ناقص یا مرجع نامعتبر باشد. زمان سیستم هم در اعتبار زمانی اثر دارد. برابری چکیدهها فقط متناظربودن کلید را نشان میدهد، نه معتبر بودن مرجع؛ به همین دلیل بررسیهای قبلی حذف نشدهاند. اگر fullchain از قبل هست، بلوک متوقف میشود؛ ابتدا فایل موجود را بررسی کنید. پیام هشدار انقضا را نیز با موفقیت طولانیمدت یکی ندانید و تاریخ واقعی را در برنامهٔ نگهداری ثبت کنید.
گام سوم؛ کلید را فقط برای Nginx نصب کنید
برنامهٔ Node پشت Nginx همچنان روی لوپبک HTTP اجرا میشود و کاربر helloapp نباید کلید خصوصی TLS را بخواند. در نصب استاندارد اوبونتو، فرایند اصلی Nginx با دسترسی مدیریتی فایل کلید را میخواند. پوشهٔ زیر root-only و کلید ۶۰۰ است؛ گواهی عمومی میتواند ۶۴۴ باشد، ولی پوشه همچنان دسترسی را محدود میکند. بلوک فقط برای اولین نصب روی مسیر تازه است و اگر مقصد موجود باشد توقف میکند. این توقف را با chmod 777 یا حذف پوشهٔ موجود دور نزنید؛ ممکن است نسخهٔ سالم فعلی در همان مسیر باشد.
# روی اوبونتو، Bash؛ TLSWORK را با مسیر بررسیشدهٔ خود عوض کنید.
(
set -eu
TLSWORK='/home/labadmin/hello-manual.ABC123'
DEST='/etc/ssl/darsavand-hello'
test -s "$TLSWORK/privkey.pem"
test -s "$TLSWORK/fullchain.pem"
if sudo test -e "$DEST" || sudo test -L "$DEST"; then
printf 'TLS destination exists; stop and inspect\n' >&2; exit 1
fi
sudo install -d -o root -g root -m 0700 "$DEST"
sudo install -o root -g root -m 0600 "$TLSWORK/privkey.pem" "$DEST/privkey.pem"
sudo install -o root -g root -m 0644 "$TLSWORK/fullchain.pem" "$DEST/fullchain.pem"
sudo stat -c '%a %U:%G %n' "$DEST" "$DEST/privkey.pem" "$DEST/fullchain.pem"
)تنظیم کامل جایگزین را در فایل شخصی ~/hello-manual-tls.nginx.conf ذخیره کنید. هر سه بار hello.example.com را با دامنهٔ واقعی عوض کنید: دو server_name و یک نشانی هدایت. بلوک پورت ۸۰ فقط به HTTPS همان دامنه هدایت میکند؛ ۳۰۸ روش درخواست را حفظ میکند، اما دادهٔ فرستادهشدهٔ نخست را به عقب برنمیگرداند و رمزگذاری نمیکند. از ابتدا درخواست عمومی API را به HTTPS بفرستید. قسمت پراکسی مسیر را تغییر نمیدهد، پس /api/greet همچنان به همان مسیر Node میرسد و فایلهای صفحه و API از یک مبدأ باقی میمانند.
server {
listen 80;
listen [::]:80;
server_name hello.example.com;
return 308 https://hello.example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name hello.example.com;
ssl_certificate /etc/ssl/darsavand-hello/fullchain.pem;
ssl_certificate_key /etc/ssl/darsavand-hello/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 2k;
access_log /var/log/nginx/darsavand-hello.access.log;
error_log /var/log/nginx/darsavand-hello.error.log;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3s;
proxy_read_timeout 15s;
proxy_send_timeout 15s;
}
}اکنون فقط تنظیم متعلق به همین پروژه را جایگزین میکنیم. این بلوک وجود سایت HTTP قبلی را میخواهد و اگر نشانهٔ TLS قبلی ببیند متوقف میشود؛ بنابراین نسخهٔ تحت مدیریت Certbot را بیبرنامه بازنویسی نمیکند. نسخهٔ پشتیبان کنار فایل اصلی با نام یکتا ساخته میشود و مسیرش چاپ میشود. با nginx -t پیش از بارگذاری، نحو و امکان خواندن گواهی کنترل میشود؛ این فرمان بهتنهایی اعتماد مرورگر یا دسترسی از اینترنت را اثبات نمیکند. فایلهای دیگر سایت و پیوند فعالشدهٔ درس پنجم را تغییر ندهید.
# روی اوبونتو، Bash؛ فایل شخصیِ دامنهگذاریشده باید آماده باشد.
(
set -eu
SITE='/etc/nginx/sites-available/darsavand-hello'
CANDIDATE="$HOME/hello-manual-tls.nginx.conf"
test -f "$CANDIDATE"
if grep -q 'hello.example.com' "$CANDIDATE"; then
printf 'Replace every example domain first\n' >&2; exit 1
fi
sudo test -f "$SITE"
if sudo grep -Eq 'ssl_certificate|listen[[:space:]].*443' "$SITE"; then
printf 'Existing TLS configuration: use a separate lab or plan migration\n' >&2; exit 1
fi
BACKUP=$(sudo mktemp /etc/nginx/sites-available/darsavand-hello.before-manual.XXXXXX)
sudo cp -p -- "$SITE" "$BACKUP"
printf 'Config backup: %s\n' "$BACKUP"
sudo install -o root -g root -m 0644 "$CANDIDATE" "$SITE"
if sudo nginx -t && sudo systemctl reload nginx; then
printf 'Manual TLS configuration loaded\n'
else
sudo cp -p -- "$BACKUP" "$SITE"
sudo nginx -t && sudo systemctl reload nginx
printf 'Candidate failed; previous config restored; inspect errors\n' >&2
exit 1
fi
)پس از موفقیت، از دستگاه دیگری https://دامنهٔواقعی/ و مسیر /api/status را بررسی کنید و یک نام ساختگی در فرم بفرستید. نتیجه باید همان صفحه و پیام، بدون اخطار نام یا زنجیره باشد. اگر TLS درست است ولی ۵۰۲ میبینید، سراغ سرویس Node و پورت ۳۰۰۰ بروید؛ اگر نام گواهی اشتباه است، تغییر #try-catch برنامه علت را رفع نمیکند. برای تشخیص لایهها، راهنمای قبلی را ببینید:
https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=4529b889-fc8c-493e-bf2b-fd47eb5c9231
تمدید دستی و تمرین پایان درس
در مسیر دستی، تاریخ notAfter را ثبت و پیش از آن زمان برای صدور، بررسی، نصب و آزمون نسخهٔ تازه در نظر بگیرید. عمر گواهی را از خود فایل بخوانید؛ عددی مثل نود روز قانون همیشگی نیست. فرمان نصب نخستین بار را دوباره بیفکر اجرا نکنید: برای تعویض، فایلهای تازه را در پوشهٔ جدا بررسی کنید، از کلید و زنجیرهٔ فعلی در پشتیبان محافظتشده نسخه بگیرید، جفت تازه را جایگزین کنید و فقط پس از nginx -t بارگذاری کنید. اگر از Certbot با چالش دستی DNS استفاده میکنید، خود نام Let’s Encrypt تمدید را خودکار نمیکند؛ بدون hook مناسب، تأیید دستی باید تکرار شود و افزودن cron کافی نیست.
تمرین: مرجع گواهی جدیدی داده که از نظر زمان معتبر است، اما نامش api.example.com است و صفحهٔ شما hello.example.com. آیا میتوان با حذف بررسی نام ادامه داد؟ پاسخ: خیر؛ باید گواهی شامل نام درست صادر شود. تمرین دوم: کلیدها متناظرند ولی verify خطای زنجیره میدهد. پاسخ: جفتبودن کلید مشکل مرجع یا گواهی میانی را حل نمیکند؛ فایلهای رسمی مرجع و اعتماد سیستم را بررسی کنید. معیار تحویل، ثبت دامنه، تاریخ انقضا، نتیجهٔ بررسی زنجیره و نام، مجوز فایلها و پاسخ واقعی صفحه و API است. این ثبت نباید محتوی کلید خصوصی باشد.
منابع رسمی انگلیسی
OpenSSL — CSR and SAN: https://docs.openssl.org/3.0/man1/openssl-req/
OpenSSL — certificate checking: https://docs.openssl.org/3.0/man1/openssl-x509/
OpenSSL — public key operations: https://docs.openssl.org/3.0/man1/openssl-pkey/
OpenSSL — chain verification: https://docs.openssl.org/3.0/man1/openssl-verify/
Nginx — HTTPS and certificate chains: https://nginx.org/en/docs/http/configuring_https_servers.html
Certbot — manual validation and renewal: https://eff-certbot.readthedocs.io/en/stable/using.html#manual
تألیف و سناریوهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.