توی این درس یاد میگیری سایتت را 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”نکتهی مهم این نمودار: 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 وصل میشود.
(PEBBLE_VA_NOSLEEP=1 exec /opt/pebble/pebble -config /opt/pebble/config.json) > /var/log/pebble.log 2>&1 &sleep 2grep -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.pemecho "ACME_SERVER=https://localhost:14000/dir" > /root/acme.envListening on: 0.0.0.0:14000ACME directory available at: https://0.0.0.0:14000/dirPebble در پورت ۱۴۰۰۰ همان API را میدهد که Let’s Encrypt در https://acme-v02.api.letsencrypt.org/directory. دامنهی آزمایشی shop.test در /etc/hosts به همین ماشین اشاره میکند، پس Pebble برای چالش HTTP-01 به Nginx همین ماشین وصل میشود؛ درست مثل Let’s Encrypt که از اینترنت به سرور تو وصل میشود.
مثالهای عملی
Section titled “مثالهای عملی”این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24، certbot از مخزن Ubuntu) با کاربر root اجرا شده؛ روی سرور خودت جلوی دستورها sudo بگذار.
مثال ۱: سایت HTTP، نقطهی شروع
Section titled “مثال ۱: سایت HTTP، نقطهی شروع”server { listen 80; server_name shop.test www.shop.test; root /var/www/shop.test; index index.html;}mkdir -p /var/www/shop.test && echo '<h1>LoopX shop</h1>' > /var/www/shop.test/index.htmlln -s /etc/nginx/sites-available/shop.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s http://shop.test/curl -s -o /dev/null -w 'https: curl exit code %{exitcode}\n' https://shop.test/ || truenginx: 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 با همین پیدا میکند کدام فایل را ویرایش کند).
مثال ۲: نصب certbot
Section titled “مثال ۲: نصب certbot”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 --versioncertbot plugins 2>/dev/null | grep -E '^\* |^Description'systemctl list-timers certbot.timer --no-pager | head -n 2The 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* nginxDescription: Nginx Web Server plugin* standaloneDescription: Runs an HTTP server locally which serves the necessary validation* webrootDescription: Saves the necessary validation files to aNEXT LEFT LAST PASSED UNIT ACTIVATESMon 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 همینها را میپرسد:
source /root/acme.envcertbot --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.logAccount registered.Requesting a certificate for shop.test and www.shop.testSuccessfully received certificate.Certificate is saved at: /etc/letsencrypt/live/shop.test/fullchain.pemKey is saved at: /etc/letsencrypt/live/shop.test/privkey.pemThis 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 certificateSuccessfully deployed certificate for shop.test to /etc/nginx/sites-enabled/shop.testSuccessfully deployed certificate for www.shop.test to /etc/nginx/sites-enabled/shop.testCongratulations! You have successfully enabled HTTPS on https://shop.test and https://www.shop.testcertbot: یک حساب ساخت، گواهی را برای هر دو دامنه (shop.test و www.shop.test) گرفت (Successfully received certificate)، در /etc/letsencrypt/live/shop.test/ ذخیره کرد، در فایل Nginx نصب کرد (Successfully deployed certificate) و تاریخ انقضا را گفت (حدود ۹۰ روز). حالا ببینیم با فایل تنظیمات ما چه کرد:
cat /etc/nginx/sites-available/shop.testserver { 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 این لازم نیست:
curl -sk https://localhost:15000/roots/0 > /root/pebble-root.pemecho "=== 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 PermanentlyLocation: https://shop.test/products?id=3=== HTTPS:<h1>LoopX shop</h1>=== HTTPS بدون اعتماد به صادرکننده:curl exit code: 60SSL certificate problem: unable to get local issuer certificateHTTP با ۳۰۱ (و حفظ مسیر) به HTTPS رفت؛ HTTPS با گواهی معتبر جواب داد. بدون --cacert، curl گواهی را رد کرد (کد ۶۰، صادرکننده ناشناخته)؛ دقیقاً همان چیزی که مرورگر با گواهی خودامضا یا صادرکنندهی نامعتبر نشان میدهد.
مثال ۴: چالش HTTP-01 را در لاگ ببینیم
Section titled “مثال ۴: چالش HTTP-01 را در لاگ ببینیم”لاگ دسترسی Nginx ردپای Pebble را دارد؛ همان درخواستهایی که CA برای سنجیدن مالکیت فرستاد:
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 4127.0.0.1 "GET /.well-known/acme-challenge/<TOKEN> 200127.0.0.1 "GET /.well-known/acme-challenge/<TOKEN> 200127.0.0.1 "GET /.well-known/acme-challenge/<TOKEN> 200127.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:
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:00RandomizedDelaySec=43200Persistent=trueExecStart=/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 اجرا میشود. حالا مهمترین دستور این درس، آزمایش تمدید بدون تمدید واقعی:
source /root/acme.envcertbot renew --dry-run --no-random-sleep-on-renew --server "$ACME_SERVER" 2>&1 | grep -vE '^$|^-'Saving debug log to /var/log/letsencrypt/letsencrypt.logProcessing /etc/letsencrypt/renewal/shop.test.confSimulating renewal of an existing certificate for shop.test and www.shop.testCongratulations, 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) میسازیم. گواهی خودامضا را هیچ مرورگری قبول ندارد ولی برای سرویسهای داخلی و آزمایش مفید است:
mkdir -p /etc/nginx/sslopenssl 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/nullchmod 600 /etc/nginx/ssl/internal.keychmod 644 /etc/nginx/ssl/internal.crtls -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"; }}EOFecho '127.0.0.1 internal.test' >> /etc/hostsln -s /etc/nginx/sites-available/internal.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -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.keynginx: configuration file /etc/nginx/nginx.conf test is successfulinternal 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 گواهی درست را انتخاب کند:
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 60b1f41 s:CN = Pebble Intermediate CA 60b1f4i:CN = Pebble Root CA 289755New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384Verify return code: 0 (ok)--- محتوای گواهی:issuer=CN = Pebble Intermediate CA 60b1f4notBefore=Oct 4 13:37:37 2026 GMTnotAfter=Jan 2 13:37:36 2027 GMTX509v3 Subject Alternative Name: criticalDNS: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 نوشته شده هیچوقت عوض نمیشود:
ls -l /etc/letsencrypt/live/shop.test/ | awk 'NR > 1 {print $9, $10, $11}'READMEcert.pem -> ../../archive/shop.test/cert1.pemchain.pem -> ../../archive/shop.test/chain1.pemfullchain.pem -> ../../archive/shop.test/fullchain1.pemprivkey.pem -> ../../archive/shop.test/privkey1.pemجدولهای مرجع
Section titled “جدولهای مرجع”دستورهای 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 برای عیبیابی |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) DNS هنوز به سرور اشاره نمیکند یا پورت ۸۰ بسته است
Section titled “۱) DNS هنوز به سرور اشاره نمیکند یا پورت ۸۰ بسته است”CA از اینترنت به http://دامنه/.well-known/acme-challenge/... وصل میشود. اگر DNS به IP دیگری اشاره کند یا فایروال (سرور یا پنل ابری) پورت ۸۰ را ببندد، certbot شکست میخورد. این را واقعاً امتحان کنیم: دامنهی broken.test را به IP ماشین دیگری (اینجا میزبان داکر، 172.17.0.1، که روی پورت ۸۰ چیزی ندارد) میفرستیم؛ مثل دامنهای که DNS آن هنوز به سرور قبلی اشاره میکند:
source /root/acme.envecho '172.17.0.1 broken.test' >> /etc/hostscertbot 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 refusedHint: 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 کرد).
۳) آزمایش نکردن تمدید
Section titled “۳) آزمایش نکردن تمدید”راهحل: بلافاصله بعد از گرفتن گواهی sudo certbot renew --dry-run؛ و ایمیل درست با -m که هشدار انقضا را بگیری.
۴) cert.pem بهجای fullchain.pem
Section titled “۴) cert.pem بهجای fullchain.pem”در مرورگرهای دسکتاپ شاید کار کند (چون زنجیره را خودشان پیدا میکنند) ولی اپ موبایل، 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 منتقل میشود.
دیدن جواب
echo | openssl s_client -connect 127.0.0.1:443 -servername shop.test 2>/dev/null | openssl x509 -noout -issuer -enddatecurl -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 60b1f4notAfter=Jan 2 13:37:36 2027 GMTSSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKeyHTTP/1.1 301 Moved PermanentlyLocation: https://www.shop.test/cartopenssl s_client ... | openssl x509 گواهی را از سرور میگیرد و میخواند. curl -v در خط SSL connection using نسخهی TLS و الگوریتم رمزنگاری را میگوید. ریدایرکت با $host همان دامنهی درخواستی (با www) را نگه داشت.
تمرین اصلی درس: روند کامل را برای یک دامنهی تازه (blog.test) اجرا کن: سایت HTTP، گرفتن گواهی با certbot --nginx و ریدایرکت، آزمایش HTTPS، و renew --dry-run. روی سرور خودت، همین را با دامنهی واقعیات و بدون --server انجام بده.
دیدن جواب
source /root/acme.envecho '127.0.0.1 blog.test' >> /etc/hostsmkdir -p /var/www/blog.test && echo '<h1>LoopX blog</h1>' > /var/www/blog.test/index.htmlprintf 'server {\n listen 80;\n server_name blog.test;\n root /var/www/blog.test;\n}\n' > /etc/nginx/sites-available/blog.testln -s /etc/nginx/sites-available/blog.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1certbot --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 successfulHTTP/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) بفرستد و سایت اصلی را سرو کند. ثابت کن تمدید آزمایشی هنوز کار میکند.
دیدن جواب
source /root/acme.envcat > /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;}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for 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"donecertbot 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 successfulhttp://shop.test/a -> 301 https://shop.test/ahttp://www.shop.test/a -> 301 https://shop.test/ahttps://www.shop.test/a -> 301 https://shop.test/ahttps://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 است.
آزمونک
Section titled “آزمونک”certbot برای ثابت کردن مالکیت دامنه با چالش HTTP-01 چه چیزی لازم دارد؟
CA از اینترنت به http://دامنه/.well-known/acme-challenge/TOKEN درخواست میدهد.
گواهیهای Let's Encrypt چند روز اعتبار دارند و certbot کی تمدیدشان میکند؟
عمر کوتاه یعنی تمدید باید خودکار باشد.
در ssl_certificate کدام فایل را باید داد؟
بدون زنجیره، بعضی کلاینتها نمیتوانند گواهی را تأیید کنند.
بعد از گرفتن گواهی، اولین کاری که برای جلوگیری از انقضای ناگهانی میکنی؟
اگر dry-run موفق باشد، تمدید واقعی هم با همین شرایط موفق است.
چرا نباید بعد از فعال کردن HTTPS پورت ۸۰ را کامل ببندی؟
پورت ۸۰ باز با ریدایرکت به HTTPS، و /.well-known/acme-challenge/ قابل دسترس.
روی یک IP چند سایت HTTPS با گواهیهای مختلف داری. Nginx از کجا میفهمد کدام گواهی را بفرستد؟
هدر Host بعد از برقراری TLS خوانده میشود؛ گواهی قبلش لازم است.
curl خطای «SSL certificate problem: unable to get local issuer certificate» میدهد ولی مرورگر سایت را باز میکند. محتملترین علت؟
مرورگرها گاهی زنجیرهی گمشده را خودشان پیدا میکنند؛ بقیهی کلاینتها نه.
جمعبندی
Section titled “جمعبندی”- 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) و با--redirectHTTP را به 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' | گواهی خودامضا |