رفتن به محتوا
LoopX

HTTPS با Let's Encrypt

توی این درس یاد می‌گیری سایتت را HTTPS کنی، با گواهی رایگان و تمدید خودکار. می‌فهمی گواهی TLS چیست و چه چیزی را ثابت می‌کند، با certbot و افزونه‌ی Nginx در یک دستور (sudo certbot --nginx -d example.com) گواهی می‌گیری و نصب می‌کنی، می‌بینی certbot چطور مالکیت دامنه را با چالش HTTP-01 ثابت می‌کند، همه‌ی ترافیک HTTP را به HTTPS منتقل می‌کنی، و با certbot.timer و sudo certbot renew --dry-run مطمئن می‌شوی گواهی هیچ‌وقت منقضی نمی‌شود. تنظیم دستی TLS در Nginx (listen 443 ssl، ssl_certificate) را هم می‌سازی تا بدانی certbot پشت صحنه چه می‌کند.

مسئله: HTTP یعنی کارت‌پستال

Section titled “مسئله: HTTP یعنی کارت‌پستال”

درخواست HTTP مثل کارت‌پستال است: هر کسی در مسیر (وای‌فای کافه، ارائه‌دهنده‌ی اینترنت، هر روتر میانی) می‌تواند بخواندش و حتی عوضش کند: رمز ورود، کوکی نشست، اطلاعات کارت. HTTPS همان HTTP است داخل یک تونل رمزنگاری‌شده (TLS)، و یک چیز مهم‌تر: گواهی (certificate) ثابت می‌کند که واقعاً با shop.ir حرف می‌زنی، نه با کسی که خودش را جای آن جا زده. امروز مرورگرها سایت HTTP را «Not secure» نشان می‌دهند، فرم‌ها هشدار می‌دهند، و موتورهای جست‌وجو HTTPS را ترجیح می‌دهند. خبر خوب: Let’s Encrypt گواهی را رایگان و خودکار می‌دهد.

تشبیه: کارت شناسایی و صادرکننده‌اش

Section titled “تشبیه: کارت شناسایی و صادرکننده‌اش”

گواهی TLS مثل کارت شناسایی سایت است: اسم (دامنه)، تاریخ اعتبار و مهر صادرکننده. مرورگر به خود سایت اعتماد ندارد؛ به صادرکننده (CA) اعتماد دارد، چون فهرست صادرکننده‌های معتبر از قبل در سیستم‌عامل و مرورگر هست. Let’s Encrypt یک CA معتبر است که قبل از صادر کردن کارت، فقط یک چیز را می‌سنجد: «آیا واقعاً این دامنه دست توست؟». روشش ساده است: «یک کاغذ با این رمز را پشت پنجره‌ی خانه‌ات (آدرس /.well-known/acme-challenge/... روی دامنه) بگذار، من از بیرون نگاه می‌کنم» (چالش HTTP-01).

گرفتن گواهی با ACME و HTTP-01

Section titled “گرفتن گواهی با ACME و HTTP-01”
روند گرفتن گواهی با certbot: certbot از CA درخواست می‌کند؛ CA یک توکن می‌دهد؛ certbot آن را موقتاً در Nginx زیر /.well-known/acme-challenge/ قرار می‌دهد؛ CA از اینترنت به http://دامنه/.well-known/acme-challenge/TOKEN درخواست می‌دهد و اگر جواب درست بود، گواهی را صادر می‌کند؛ certbot گواهی را در /etc/letsencrypt ذخیره و در تنظیم Nginx نصب می‌کند.

نکته‌ی مهم این نمودار: CA از اینترنت به پورت ۸۰ دامنه‌ی تو وصل می‌شود. پس سه شرط لازم است: DNS دامنه به IP سرور اشاره کند، پورت ۸۰ از بیرون باز باشد، و Nginx روی آن دامنه جواب بدهد.

محیط آزمایش این درس: Pebble

Section titled “محیط آزمایش این درس: Pebble”

گرفتن گواهی از Let’s Encrypt واقعی به یک دامنه‌ی واقعی و سروری که از اینترنت در دسترس باشد نیاز دارد؛ ماشین آزمایشی ما هیچ‌کدام را نداشت. پس از Pebble استفاده کردیم: سرور ACME آزمایشی رسمی که خود Let’s Encrypt برای آزمایش کلاینت‌ها ساخته و منتشر کرده است. همه‌چیز در این درس واقعی است (certbot واقعی از مخزن Ubuntu، Nginx واقعی، پروتکل ACME و چالش HTTP-01 واقعی)؛ فقط صادرکننده به‌جای Let’s Encrypt، Pebble است و گواهی‌هایش را مرورگرها نمی‌شناسند. روی سرور خودت، همین دستورها را بدون گزینه‌ی --server (و بدون متغیر REQUESTS_CA_BUNDLE) می‌زنی و certbot خودش به Let’s Encrypt وصل می‌شود.

Terminal window
(PEBBLE_VA_NOSLEEP=1 exec /opt/pebble/pebble -config /opt/pebble/config.json) > /var/log/pebble.log 2>&1 &
sleep 2
grep -E 'Listening|ACME directory' /var/log/pebble.log | sed -E 's/^Pebble [0-9/]+ [0-9:]+ //'
# certbot must trust Pebble's own HTTPS certificate (test CA)
export REQUESTS_CA_BUNDLE=/opt/pebble/test/certs/pebble.minica.pem
echo "ACME_SERVER=https://localhost:14000/dir" > /root/acme.env
خروجی
Listening on: 0.0.0.0:14000
ACME directory available at: https://0.0.0.0:14000/dir

Pebble در پورت ۱۴۰۰۰ همان API را می‌دهد که Let’s Encrypt در https://acme-v02.api.letsencrypt.org/directory. دامنه‌ی آزمایشی shop.test در /etc/hosts به همین ماشین اشاره می‌کند، پس Pebble برای چالش HTTP-01 به Nginx همین ماشین وصل می‌شود؛ درست مثل Let’s Encrypt که از اینترنت به سرور تو وصل می‌شود.

این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24، certbot از مخزن Ubuntu) با کاربر root اجرا شده؛ روی سرور خودت جلوی دستورها sudo بگذار.

مثال ۱: سایت HTTP، نقطه‌ی شروع

Section titled “مثال ۱: سایت HTTP، نقطه‌ی شروع”
/etc/nginx/sites-available/shop.test
server {
listen 80;
server_name shop.test www.shop.test;
root /var/www/shop.test;
index index.html;
}
Terminal window
mkdir -p /var/www/shop.test && echo '<h1>LoopX shop</h1>' > /var/www/shop.test/index.html
ln -s /etc/nginx/sites-available/shop.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s http://shop.test/
curl -s -o /dev/null -w 'https: curl exit code %{exitcode}\n' https://shop.test/ || true
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
<h1>LoopX shop</h1>
https: curl exit code 7

سایت روی HTTP کار می‌کند؛ HTTPS هنوز هیچ چیزی روی پورت ۴۴۳ ندارد (curl کد ۷: اتصال برقرار نشد). قبل از certbot، روی سرور واقعی سه چیز را بسنج: dig +short shop.ir باید IP سرور را بدهد؛ sudo ufw status پورت ۸۰ و ۴۴۳ را باز نشان دهد؛ و server_name در Nginx دقیقاً همان دامنه‌ها باشد (certbot با همین پیدا می‌کند کدام فایل را ویرایش کند).

Terminal window
DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-nginx 2>&1 | grep -E '^(The following NEW|Setting up (certbot|python3-certbot-nginx))'
certbot --version
certbot plugins 2>/dev/null | grep -E '^\* |^Description'
systemctl list-timers certbot.timer --no-pager | head -n 2
خروجی
The following NEW packages will be installed:
Setting up certbot (2.9.0-1) ...
Setting up python3-certbot-nginx (2.9.0-1) ...
certbot 2.9.0
* nginx
Description: Nginx Web Server plugin
* standalone
Description: Runs an HTTP server locally which serves the necessary validation
* webroot
Description: Saves the necessary validation files to a
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-10-05 03:27:42 UTC 13h Sun 2026-10-04 13:18:15 UTC - certbot.timer certbot.service

روی سرور خودت: sudo apt install certbot python3-certbot-nginx. بسته‌ی دوم افزونه‌ی nginx است که هم چالش را از راه Nginx حل می‌کند و هم گواهی را در تنظیمات نصب می‌کند. همراه نصب، یک systemd timer (certbot.timer) هم فعال شد که روزی دو بار تمدید را بررسی می‌کند (مثال ۵). (روش جایگزین که مستندات certbot پیشنهاد می‌کند، نصب با snap است؛ نسخه‌اش جدیدتر است ولی روی سرور ایرانی دسترسی به مخزن snap ممکن است مشکل داشته باشد.)

مثال ۳: certbot --nginx، یک دستور برای همه‌چیز

Section titled “مثال ۳: certbot --nginx، یک دستور برای همه‌چیز”

شکل دستور روی سرور واقعی:

روی سرور خودت
sudo certbot --nginx -d shop.ir -d www.shop.ir

ما همان را با --server (آدرس Pebble) و گزینه‌های غیرتعاملی اجرا می‌کنیم: --agree-tos (پذیرش شرایط)، -m (ایمیل برای هشدار انقضا)، --redirect (انتقال خودکار HTTP به HTTPS). بدون این‌ها، certbot همین‌ها را می‌پرسد:

Terminal window
source /root/acme.env
certbot --nginx -d shop.test -d www.shop.test --server "$ACME_SERVER" \
--agree-tos -m admin@shop.test --no-eff-email --non-interactive --redirect 2>&1 \
| grep -vE '^(- - -| \* Donating|If you like Certbot)' | grep -v '^$'
خروجی
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Account registered.
Requesting a certificate for shop.test and www.shop.test
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/shop.test/fullchain.pem
Key is saved at: /etc/letsencrypt/live/shop.test/privkey.pem
This certificate expires on 2027-01-02.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
Deploying certificate
Successfully deployed certificate for shop.test to /etc/nginx/sites-enabled/shop.test
Successfully deployed certificate for www.shop.test to /etc/nginx/sites-enabled/shop.test
Congratulations! You have successfully enabled HTTPS on https://shop.test and https://www.shop.test

certbot: یک حساب ساخت، گواهی را برای هر دو دامنه (shop.test و www.shop.test) گرفت (Successfully received certificate)، در /etc/letsencrypt/live/shop.test/ ذخیره کرد، در فایل Nginx نصب کرد (Successfully deployed certificate) و تاریخ انقضا را گفت (حدود ۹۰ روز). حالا ببینیم با فایل تنظیمات ما چه کرد:

Terminal window
cat /etc/nginx/sites-available/shop.test
خروجی
server {
server_name shop.test www.shop.test;
root /var/www/shop.test;
index index.html;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/shop.test/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/shop.test/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.shop.test) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = shop.test) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name shop.test www.shop.test;
return 404; # managed by Certbot
}

certbot همان فایل را ویرایش کرد (خط‌هایش با # managed by Certbot مشخص‌اند):

  • server block اصلی حالا روی listen 443 ssl است، با ssl_certificate (گواهی به‌علاوه‌ی زنجیره‌ی صادرکننده: fullchain.pem) و ssl_certificate_key (کلید خصوصی: privkey.pem)، و یک include برای تنظیم‌های امن TLS.
  • یک server block جدید روی پورت ۸۰ ساخت که هر دو دامنه را با 301 به https://$host$request_uri می‌فرستد (به‌خاطر --redirect).

آزمایش. چون صادرکننده Pebble است، به curl می‌گوییم ریشه‌ی Pebble را معتبر بداند (--cacert)؛ روی سایت واقعی با Let’s Encrypt این لازم نیست:

Terminal window
curl -sk https://localhost:15000/roots/0 > /root/pebble-root.pem
echo "=== HTTP:"
curl -s -I http://shop.test/products?id=3 | grep -iE '^(HTTP|location)'
echo "=== HTTPS:"
curl -s --cacert /root/pebble-root.pem https://shop.test/
echo "=== HTTPS بدون اعتماد به صادرکننده:"
curl -s -o /dev/null https://shop.test/ 2>&1; echo "curl exit code: $?"
curl -s -o /dev/null -w '%{errormsg}\n' https://shop.test/
خروجی
=== HTTP:
HTTP/1.1 301 Moved Permanently
Location: https://shop.test/products?id=3
=== HTTPS:
<h1>LoopX shop</h1>
=== HTTPS بدون اعتماد به صادرکننده:
curl exit code: 60
SSL certificate problem: unable to get local issuer certificate

HTTP با ۳۰۱ (و حفظ مسیر) به HTTPS رفت؛ HTTPS با گواهی معتبر جواب داد. بدون --cacert، curl گواهی را رد کرد (کد ۶۰، صادرکننده ناشناخته)؛ دقیقاً همان چیزی که مرورگر با گواهی خودامضا یا صادرکننده‌ی نامعتبر نشان می‌دهد.

مثال ۴: چالش HTTP-01 را در لاگ ببینیم

Section titled “مثال ۴: چالش HTTP-01 را در لاگ ببینیم”

لاگ دسترسی Nginx ردپای Pebble را دارد؛ همان درخواست‌هایی که CA برای سنجیدن مالکیت فرستاد:

Terminal window
grep 'acme-challenge' /var/log/nginx/access.log | awk '{print $1, $6, $7, $9}' | sed -E 's|challenge/[A-Za-z0-9_-]{10,}|challenge/<TOKEN>|' | tail -n 4
خروجی
127.0.0.1 "GET /.well-known/acme-challenge/<TOKEN> 200
127.0.0.1 "GET /.well-known/acme-challenge/<TOKEN> 200
127.0.0.1 "GET /.well-known/acme-challenge/<TOKEN> 200
127.0.0.1 "GET /.well-known/acme-challenge/<TOKEN> 200

برای هر دامنه یک درخواست GET /.well-known/acme-challenge/<TOKEN> با جواب ۲۰۰. (Let’s Encrypt واقعی برای امنیت بیشتر از چند نقطه‌ی مختلف جهان همین را درخواست می‌کند.) certbot بعد از گرفتن گواهی، location موقت چالش را از Nginx برداشت. یادت هست در درس سایت استاتیک، /.well-known/ را از قانون بستن فایل‌های مخفی استثنا کردیم؟ این دلیلش است.

مثال ۵: تمدید خودکار و certbot renew --dry-run

Section titled “مثال ۵: تمدید خودکار و certbot renew --dry-run”

گواهی‌های Let’s Encrypt ۹۰ روزه‌اند (عمداً کوتاه، تا تمدید حتماً خودکار باشد). certbot هر گواهی را که کمتر از ۳۰ روز به انقضایش مانده، تمدید می‌کند. چه کسی certbot را اجرا می‌کند؟ همان timer:

Terminal window
systemctl cat certbot.timer --no-pager | grep -E '^(OnCalendar|RandomizedDelaySec|Persistent)'
systemctl cat certbot.service --no-pager | grep -E '^ExecStart'
certbot certificates 2>/dev/null | grep -E 'Certificate Name|Domains|Expiry'
خروجی
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true
ExecStart=/usr/bin/certbot -q renew --no-random-sleep-on-renew
Certificate Name: shop.test
Domains: shop.test www.shop.test
Expiry Date: 2027-01-02 13:37:36+00:00 (VALID: 89 days)

روزی دو بار (۰۰:۰۰ و ۱۲:۰۰ با تأخیر تصادفی تا ۱۲ ساعت، تا همه‌ی سرورهای دنیا همزمان به Let’s Encrypt هجوم نیاورند) certbot -q renew اجرا می‌شود. حالا مهم‌ترین دستور این درس، آزمایش تمدید بدون تمدید واقعی:

Terminal window
source /root/acme.env
certbot renew --dry-run --no-random-sleep-on-renew --server "$ACME_SERVER" 2>&1 | grep -vE '^$|^-'
خروجی
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Processing /etc/letsencrypt/renewal/shop.test.conf
Simulating renewal of an existing certificate for shop.test and www.shop.test
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/shop.test/fullchain.pem (success)

--dry-run کل فرایند (چالش و گرفتن گواهی) را واقعاً اجرا می‌کند ولی چیزی ذخیره نمی‌کند. (--no-random-sleep-on-renew را به این دلیل گذاشتیم: وقتی certbot renew بدون ترمینال اجرا می‌شود، مثل همین آزمایش یا یک خط cron، اول یک تأخیر تصادفی تا ۸ دقیقه می‌گذارد تا همه‌ی سرورها همزمان سراغ CA نروند؛ لاگ certbot می‌گوید Non-interactive renewal: random delay of ... seconds. سرویس certbot.service که بالا دیدی همین گزینه را دارد، چون خود timer با RandomizedDelaySec تأخیر تصادفی‌اش را می‌گذارد. در ترمینال خودت این تأخیر نیست.) روی سرور خودت، بلافاصله بعد از گرفتن گواهی، sudo certbot renew --dry-run را بزن؛ اگر موفق بود، تمدید سه ماه بعد هم موفق است (به شرطی که بعداً پورت ۸۰ را نبندی یا DNS را عوض نکنی). certbot بعد از تمدید موفق، خودش Nginx را reload می‌کند تا گواهی تازه اعمال شود.

مثال ۶: تنظیم دستی TLS، بدون certbot

Section titled “مثال ۶: تنظیم دستی TLS، بدون certbot”

برای فهمیدن اینکه certbot دقیقاً چه کرد (و برای جاهایی که گواهی را از جای دیگری گرفته‌ای، مثلاً یک CA تجاری یا گواهی داخلی شرکت)، یک سایت دوم را دستی با یک گواهی خودامضا (self-signed) می‌سازیم. گواهی خودامضا را هیچ مرورگری قبول ندارد ولی برای سرویس‌های داخلی و آزمایش مفید است:

Terminal window
mkdir -p /etc/nginx/ssl
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout /etc/nginx/ssl/internal.key -out /etc/nginx/ssl/internal.crt \
-subj '/CN=internal.test' -addext 'subjectAltName=DNS:internal.test' 2>/dev/null
chmod 600 /etc/nginx/ssl/internal.key
chmod 644 /etc/nginx/ssl/internal.crt
ls -l /etc/nginx/ssl/
cat > /etc/nginx/sites-available/internal.test <<'EOF'
server {
listen 443 ssl;
server_name internal.test;
ssl_certificate /etc/nginx/ssl/internal.crt;
ssl_certificate_key /etc/nginx/ssl/internal.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
return 200 "internal dashboard over TLS ($ssl_protocol)\n";
}
}
EOF
echo '127.0.0.1 internal.test' >> /etc/hosts
ln -s /etc/nginx/sites-available/internal.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s --cacert /etc/nginx/ssl/internal.crt https://internal.test/
خروجی
total 8
-rw-r--r-- 1 root root 1159 Oct 4 13:37 internal.crt
-rw------- 1 root root 1704 Oct 4 13:37 internal.key
nginx: configuration file /etc/nginx/nginx.conf test is successful
internal dashboard over TLS (TLSv1.3)

سه directive اصلی: listen 443 ssl (TLS روی پورت ۴۴۳)، ssl_certificate و ssl_certificate_key. ssl_protocols نسخه‌های قدیمی و ناامن TLS (۱.۰ و ۱.۱) را خاموش می‌کند. کلید خصوصی (.key) فقط برای root خواندنی است (600): master Nginx که root است آن را می‌خواند. ($ssl_protocol نسخه‌ی TLS همین اتصال است.)

پشت پرده: گواهی را باز کنیم

Section titled “پشت پرده: گواهی را باز کنیم”

openssl s_client مثل مرورگر به سرور TLS وصل می‌شود و همه‌چیز را نشان می‌دهد: گواهی، زنجیره‌ی صادرکننده و نسخه‌ی TLS. -servername همان SNI است (درس server block): اسم دامنه که در ابتدای اتصال فرستاده می‌شود تا Nginx گواهی درست را انتخاب کند:

Terminal window
echo | openssl s_client -connect 127.0.0.1:443 -servername shop.test -CAfile /root/pebble-root.pem 2>/dev/null \
| grep -E '^ *[0-9] s:|^ *i:|^(Protocol|Verify return code)|Cipher is' | sed 's/^ *//'
echo "--- محتوای گواهی:"
openssl x509 -in /etc/letsencrypt/live/shop.test/cert.pem -noout -issuer -dates -ext subjectAltName | sed 's/^ *//'
echo "--- همان IP، SNI دیگر (internal.test):"
echo | openssl s_client -connect 127.0.0.1:443 -servername internal.test 2>/dev/null | grep -E '^subject='
خروجی
0 s:
i:CN = Pebble Intermediate CA 60b1f4
1 s:CN = Pebble Intermediate CA 60b1f4
i:CN = Pebble Root CA 289755
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)
--- محتوای گواهی:
issuer=CN = Pebble Intermediate CA 60b1f4
notBefore=Oct 4 13:37:37 2026 GMT
notAfter=Jan 2 13:37:36 2027 GMT
X509v3 Subject Alternative Name: critical
DNS:shop.test, DNS:www.shop.test
--- همان IP، SNI دیگر (internal.test):
subject=CN = internal.test
  • زنجیره: گواهی سایت (s:) توسط صادرکننده‌ی میانی (i: Pebble Intermediate CA) امضا شده و آن توسط ریشه. fullchain.pem همین زنجیره است؛ اگر فقط cert.pem را به Nginx بدهی، بعضی کلاینت‌ها نمی‌توانند زنجیره را کامل کنند.
  • Verify return code: 0 (ok) یعنی با ریشه‌ای که دادیم معتبر است.
  • SAN (Subject Alternative Name): دامنه‌هایی که این گواهی برایشان معتبر است: shop.test و www.shop.test.
  • با همان IP و پورت ولی SNI دیگر، Nginx گواهی دیگری (خودامضای internal.test) داد.

فایل‌های certbot در /etc/letsencrypt/live/DOMAIN/ در واقع لینک به آخرین نسخه در archive/ هستند؛ هر تمدید نسخه‌ی تازه می‌سازد و لینک‌ها را جابه‌جا می‌کند، پس مسیری که در Nginx نوشته شده هیچ‌وقت عوض نمی‌شود:

Terminal window
ls -l /etc/letsencrypt/live/shop.test/ | awk 'NR > 1 {print $9, $10, $11}'
خروجی
README
cert.pem -> ../../archive/shop.test/cert1.pem
chain.pem -> ../../archive/shop.test/chain1.pem
fullchain.pem -> ../../archive/shop.test/fullchain1.pem
privkey.pem -> ../../archive/shop.test/privkey1.pem

دستورهای certbot:

دستور کار
sudo apt install certbot python3-certbot-nginx نصب certbot و افزونه‌ی nginx
sudo certbot --nginx -d example.com -d www.example.com گرفتن و نصب گواهی (تعاملی)
... --redirect / --no-redirect انتقال HTTP به HTTPS، بله/نه (بدون سؤال)
sudo certbot certonly --nginx -d example.com فقط گرفتن گواهی، بدون ویرایش Nginx
sudo certbot certificates فهرست گواهی‌ها و تاریخ انقضا
sudo certbot renew --dry-run آزمایش تمدید
sudo certbot renew تمدید گواهی‌هایی که کمتر از ۳۰ روز مانده
sudo certbot delete --cert-name example.com حذف گواهی
systemctl list-timers certbot.timer زمان بعدی بررسی تمدید

directive های TLS در Nginx:

directive کار
listen 443 ssl; پذیرش TLS روی پورت ۴۴۳
ssl_certificate /path/fullchain.pem; گواهی + زنجیره‌ی صادرکننده
ssl_certificate_key /path/privkey.pem; کلید خصوصی
ssl_protocols TLSv1.2 TLSv1.3; نسخه‌های مجاز TLS
return 301 https://$host$request_uri; انتقال HTTP به HTTPS
listen 443 ssl http2; (یا در ۱.۲۵+: http2 on;) فعال کردن HTTP/2

فایل‌های certbot:

مسیر محتوا
/etc/letsencrypt/live/DOMAIN/fullchain.pem گواهی + زنجیره (برای ssl_certificate)
/etc/letsencrypt/live/DOMAIN/privkey.pem کلید خصوصی (هرگز به کسی نده)
/etc/letsencrypt/renewal/DOMAIN.conf تنظیم تمدید (روش چالش، سرور ACME)
/var/log/letsencrypt/letsencrypt.log لاگ کامل certbot برای عیب‌یابی

۱) DNS هنوز به سرور اشاره نمی‌کند یا پورت ۸۰ بسته است

Section titled “۱) DNS هنوز به سرور اشاره نمی‌کند یا پورت ۸۰ بسته است”

CA از اینترنت به http://دامنه/.well-known/acme-challenge/... وصل می‌شود. اگر DNS به IP دیگری اشاره کند یا فایروال (سرور یا پنل ابری) پورت ۸۰ را ببندد، certbot شکست می‌خورد. این را واقعاً امتحان کنیم: دامنه‌ی broken.test را به IP ماشین دیگری (اینجا میزبان داکر، 172.17.0.1، که روی پورت ۸۰ چیزی ندارد) می‌فرستیم؛ مثل دامنه‌ای که DNS آن هنوز به سرور قبلی اشاره می‌کند:

Terminal window
source /root/acme.env
echo '172.17.0.1 broken.test' >> /etc/hosts
certbot certonly --nginx -d broken.test --server "$ACME_SERVER" --agree-tos -m admin@shop.test \
--non-interactive 2>&1 | grep -E 'Domain:|Type:|Detail:|Hint:'
خروجی
Domain: broken.test
Type: connection
Detail: Get "http://broken.test:80/.well-known/acme-challenge/NJ_Bt5roIcRVMHr_EoazVfyMAV5g59KJjDgdaapkpJo": dial tcp 172.17.0.1:80: connect: connection refused
Hint: The Certificate Authority failed to verify the temporary nginx configuration changes made by Certbot. Ensure the listed domains point to this nginx server and that it is accessible from the internet.

پیام دقیقاً می‌گوید کدام دامنه (broken.test)، چه نوع مشکلی (connection) و جزئیات: CA به 172.17.0.1:80 وصل شد و connection refused گرفت. روی سرور واقعی، شکل‌های رایج این خطا Timeout during connect (likely firewall problem) (پورت ۸۰ بسته) یا Invalid response from ... (DNS به سرور دیگری اشاره می‌کند) است. راه‌حل: dig +short دامنه (باید IP همین سرور باشد) و sudo ufw allow 'Nginx Full'.

۲) بستن پورت ۸۰ بعد از HTTPS

Section titled “۲) بستن پورت ۸۰ بعد از HTTPS”

«حالا که HTTPS داریم، پورت ۸۰ را می‌بندیم». سه ماه بعد تمدید (با چالش HTTP-01) شکست می‌خورد. راه‌حل: پورت ۸۰ را باز بگذار و فقط ریدایرکت کن (همان کاری که certbot کرد).

راه‌حل: بلافاصله بعد از گرفتن گواهی sudo certbot renew --dry-run؛ و ایمیل درست با -m که هشدار انقضا را بگیری.

در مرورگرهای دسکتاپ شاید کار کند (چون زنجیره را خودشان پیدا می‌کنند) ولی اپ موبایل، curl و سرویس‌های دیگر خطای «unable to get local issuer certificate» می‌دهند. راه‌حل: همیشه fullchain.pem.

۵) گرفتن گواهی‌های زیاد در آزمایش

Section titled “۵) گرفتن گواهی‌های زیاد در آزمایش”

Let’s Encrypt برای هر دامنه محدودیت تعداد دارد (مثلاً چند گواهی تکراری در هفته). اگر در حال آزمایش تنظیماتی، با --dry-run یا --test-cert (سرور staging) کار کن تا به سقف نخوری.

✎ تمرینآسان

با curl و openssl سه چیز را درباره‌ی https://shop.test گزارش کن: صادرکننده‌ی گواهی، تاریخ انقضا، و نسخه‌ی TLS که برای اتصال استفاده شد. بعد ثابت کن http://www.shop.test/cart به https://www.shop.test/cart منتقل می‌شود.

دیدن جواب
Terminal window
echo | openssl s_client -connect 127.0.0.1:443 -servername shop.test 2>/dev/null | openssl x509 -noout -issuer -enddate
curl -s -v --cacert /root/pebble-root.pem https://shop.test/ 2>&1 | grep -E 'SSL connection using' | sed 's/^\* //'
curl -s -I http://www.shop.test/cart | grep -iE '^(HTTP|location)'
خروجی
issuer=CN = Pebble Intermediate CA 60b1f4
notAfter=Jan 2 13:37:36 2027 GMT
SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKey
HTTP/1.1 301 Moved Permanently
Location: https://www.shop.test/cart

openssl s_client ... | openssl x509 گواهی را از سرور می‌گیرد و می‌خواند. curl -v در خط SSL connection using نسخه‌ی TLS و الگوریتم رمزنگاری را می‌گوید. ریدایرکت با $host همان دامنه‌ی درخواستی (با www) را نگه داشت.

✎ تمرینمتوسط

تمرین اصلی درس: روند کامل را برای یک دامنه‌ی تازه (blog.test) اجرا کن: سایت HTTP، گرفتن گواهی با certbot --nginx و ریدایرکت، آزمایش HTTPS، و renew --dry-run. روی سرور خودت، همین را با دامنه‌ی واقعی‌ات و بدون --server انجام بده.

دیدن جواب
Terminal window
source /root/acme.env
echo '127.0.0.1 blog.test' >> /etc/hosts
mkdir -p /var/www/blog.test && echo '<h1>LoopX blog</h1>' > /var/www/blog.test/index.html
printf 'server {\n listen 80;\n server_name blog.test;\n root /var/www/blog.test;\n}\n' > /etc/nginx/sites-available/blog.test
ln -s /etc/nginx/sites-available/blog.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
certbot --nginx -d blog.test --server "$ACME_SERVER" --agree-tos -m admin@blog.test \
--no-eff-email --non-interactive --redirect 2>&1 | grep -E 'Successfully|expires'
curl -s -I http://blog.test/ | grep -iE '^(HTTP|location)'
curl -s --cacert /root/pebble-root.pem https://blog.test/
certbot renew --dry-run --no-random-sleep-on-renew --server "$ACME_SERVER" --cert-name blog.test 2>&1 | grep -E 'success|fail'
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
HTTP/1.1 200 OK

چهار قدم، چهار ثابت: گواهی گرفته و نصب شد، HTTP ریدایرکت شد، HTTPS با گواهی معتبر جواب داد، و تمدید آزمایشی موفق بود. روی سرور خودت دستور اصلی فقط sudo certbot --nginx -d blog.example.com است.

✎ تمرینسخت

certbot ریدایرکت را با if ($host = ...) نوشت. فایل shop.test را دستی بازنویسی کن به شکل تمیزتر و رایج: یک server block روی ۸۰ که همه‌چیز (به‌جز /.well-known/acme-challenge/ برای تمدید) را با return 301 https://$host$request_uri منتقل کند، و یک server block روی ۴۴۳ که www.shop.test را به shop.test (بدون www) بفرستد و سایت اصلی را سرو کند. ثابت کن تمدید آزمایشی هنوز کار می‌کند.

دیدن جواب
Terminal window
source /root/acme.env
cat > /etc/nginx/sites-available/shop.test <<'EOF'
server {
listen 80;
server_name shop.test www.shop.test;
location /.well-known/acme-challenge/ {
root /var/www/shop.test;
}
location / {
return 301 https://shop.test$request_uri;
}
}
server {
listen 443 ssl;
server_name www.shop.test;
ssl_certificate /etc/letsencrypt/live/shop.test/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.test/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
return 301 https://shop.test$request_uri;
}
server {
listen 443 ssl;
server_name shop.test;
ssl_certificate /etc/letsencrypt/live/shop.test/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.test/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
root /var/www/shop.test;
index index.html;
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for u in http://shop.test/a http://www.shop.test/a https://www.shop.test/a https://shop.test/; do
printf '%-26s -> ' "$u"
curl -s -o /dev/null --cacert /root/pebble-root.pem -w '%{http_code} %{redirect_url}\n' "$u"
done
certbot renew --dry-run --no-random-sleep-on-renew --server "$ACME_SERVER" --cert-name shop.test 2>&1 | grep -E 'success|fail'
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
http://shop.test/a -> 301 https://shop.test/a
http://www.shop.test/a -> 301 https://shop.test/a
https://www.shop.test/a -> 301 https://shop.test/a
https://shop.test/ -> 200
/etc/letsencrypt/live/shop.test/fullchain.pem (success)

هر آدرسی، با حداکثر یک ریدایرکت، به https://shop.test/... می‌رسد (کمتر ریدایرکت یعنی سرعت بیشتر و SEO بهتر). /.well-known/acme-challenge/ روی HTTP باز ماند، پس تمدید (که افزونه‌ی nginx موقتاً همان مسیر را تنظیم می‌کند) موفق بود. options-ssl-nginx.conf تنظیم‌های امن TLS پیشنهادی certbot است.

⚡ بررسی سریع

certbot برای ثابت کردن مالکیت دامنه با چالش HTTP-01 چه چیزی لازم دارد؟

؟ آزمونک
  1. گواهی‌های Let's Encrypt چند روز اعتبار دارند و certbot کی تمدیدشان می‌کند؟

  2. در ssl_certificate کدام فایل را باید داد؟

  3. بعد از گرفتن گواهی، اولین کاری که برای جلوگیری از انقضای ناگهانی می‌کنی؟

  4. چرا نباید بعد از فعال کردن HTTPS پورت ۸۰ را کامل ببندی؟

  5. روی یک IP چند سایت HTTPS با گواهی‌های مختلف داری. Nginx از کجا می‌فهمد کدام گواهی را بفرستد؟

  6. curl خطای «SSL certificate problem: unable to get local issuer certificate» می‌دهد ولی مرورگر سایت را باز می‌کند. محتمل‌ترین علت؟

  • HTTPS = HTTP داخل TLS؛ گواهی ثابت می‌کند با دامنه‌ی درست حرف می‌زنی. صادرکننده (CA) باید معتبر باشد؛ Let’s Encrypt رایگان و خودکار است.
  • certbot: sudo apt install certbot python3-certbot-nginx و sudo certbot --nginx -d example.com -d www.example.com؛ گواهی را می‌گیرد، در Nginx نصب می‌کند (listen 443 ssl، ssl_certificate با fullchain.pem) و با --redirect HTTP را به HTTPS منتقل می‌کند.
  • چالش HTTP-01: CA به http://دامنه/.well-known/acme-challenge/TOKEN سر می‌زند؛ DNS درست و پورت ۸۰ باز لازم است (حتی بعد از HTTPS).
  • تمدید: گواهی ۹۰ روزه؛ certbot.timer روزی دو بار certbot renew را اجرا می‌کند؛ با sudo certbot renew --dry-run آزمایشش کن. فایل‌های live/ لینک‌اند و مسیرشان ثابت می‌ماند.
  • بررسی: openssl s_client -connect IP:443 -servername دامنه (زنجیره، نسخه‌ی TLS، SNI) و openssl x509 -noout -dates -ext subjectAltName.
  • برای سرویس داخلی، گواهی خودامضا با openssl req -x509 کافی است (مرورگرها قبولش ندارند).
برگه‌ی تقلب این درس
دستورکاری که می‌کند
sudo apt install certbot python3-certbot-nginxنصب certbot و افزونه‌ی nginx
sudo certbot --nginx -d example.com -d www.example.comگرفتن و نصب گواهی
sudo certbot --nginx -d example.com --redirectبه‌علاوه‌ی انتقال HTTP به HTTPS
sudo certbot renew --dry-runآزمایش تمدید
sudo certbot certificatesفهرست و تاریخ انقضا
systemctl list-timers certbot.timerتمدید خودکار کی اجرا می‌شود
listen 443 ssl; ssl_certificate .../fullchain.pem; ssl_certificate_key .../privkey.pem;TLS دستی در Nginx
return 301 https://$host$request_uri;انتقال به HTTPS
echo | openssl s_client -connect IP:443 -servername example.comدیدن گواهی و زنجیره
openssl x509 -in cert.pem -noout -issuer -dates -ext subjectAltNameخواندن گواهی
openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout k.key -out c.crt -subj '/CN=name'گواهی خودامضا