SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor

آموزش ساخت گواهی Self-signed معتبر در Ubuntu 24.04

با استفاده از دستور openssl و تنظیمات SAN، گواهی TLS خودامضا بسازید که Chrome آن را بپذیرد. این راهنما نحوه پیکربندی nginx و افزودن گواهی به اعتماد سیستم را آموزش می‌دهد.

آنچه می‌سازید

یک گواهی TLS خودامضا (self-signed) که مرورگرها و کلاینت‌های مدرن واقعاً آن را می‌پذیرند، تنظیمات صحیح subjectAltName، مجوزهای دسترسی امن برای کلیدها، اتصال به nginx یا Apache، به‌علاوه بخشی که تقریباً در تمام راهنماها نادیده گرفته می‌شود: وادار کردن کلاینت‌ها به اعتماد صحیح به آن، به‌جای کلیک کردن روی هشدارهای امنیتی و هاردکد کردن curl -k در اسکریپت‌ها برای همیشه. در پایان، یک CA خصوصی با 5 دستور برای زمانی که یک سرویس داخلی به 6 سرویس تبدیل می‌شود.

ابتدا تصمیم‌گیری، زیرا گواهی خودامضا بسیار کمتر از آنچه استفاده می‌شود، ابزار مناسبی است. اگر سرویس از طریق اینترنت عمومی و با یک نام دامنه واقعی در دسترس است، خواندن را متوقف کنید و به‌جای آن یک گواهی رایگان Let's Encrypt با certbot روی nginx یا معادل آن برای Apache دریافت کنید. این کار هزینه‌ای ندارد، به‌طور خودکار تمدید می‌شود و مرورگرها و سیستم‌عامل‌های اصلی از قبل به آن اعتماد دارند. استفاده از گواهی خودامضا در یک سایت عمومی، کاربران شما را عادت می‌دهد که از هشدارهای امنیتی عبور کنند، که عادتی بدتر از استفاده از HTTP ساده است.

گواهی خودامضا زمانی ابزار مناسبی است که اینترنت عمومی در کار نباشد: یک پنل مدیریتی که به آدرس تونل WireGuard روی VPS شما متصل است، یک سرور staging در یک شبکه خصوصی، ترافیک بین سرویس‌های backend، یک دستگاه در آزمایشگاه خانگی، یا جایگزینی گواهی پیش‌فرضی که Webmin برای خود روی پورت 10000 تولید می‌کند. به هر حال Let's Encrypt نمی‌تواند برای 10.8.0.1 یا git.internal.lan گواهی صادر کند، هیچ CA عمومی یک IP خصوصی یا یک TLD ساختگی را در گواهی قرار نمی‌دهد. برای این نام‌ها، شما خودتان CA هستید.

تمام مراحل زیر روی یک سرور تازه Ubuntu 24.04 اجرا می‌شود که OpenSSL 3.0.x را ارائه می‌دهد (برای تأیید از openssl version استفاده کنید). هیچ‌کدام از این مراحل به دسترسی اینترنت نیاز ندارد؛ همه چیز در محیط air-gapped کار می‌کند.

چرا دستورات قدیمی، گواهی‌هایی تولید می‌کنند که Chrome آن‌ها را رد می‌کند

دستوری که در تمام آموزش‌های پیش از سال 2017 ارائه شده، به این شکل است:

# Do not run this: shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key -out selfsigned.crt

این دستور مجموعه‌ای از پرسش‌های تعاملی را مطرح می‌کند، نام میزبان (hostname) شما را در فیلد Common Name قرار می‌دهد و گواهی‌ای بدون پسوند subjectAltName تولید می‌کند. چنین گواهی‌ای از همان لحظه صدور غیرقابل استفاده است. مرورگر Chrome از نسخه 58 در آوریل 2017، خواندن Common Name را متوقف کرد. استاندارد RFC 2818 نیز تطبیق بر اساس CN را از سال 2000 منسوخ کرده بود و مرورگرهای Firefox و Safari نیز به همین شکل عمل می‌کنند. ابزار curl و ماژول ssl در پایتون، در صورتی که گواهی فاقد هرگونه SAN باشد، همچنان به Common Name بازمی‌گردند؛ به همین دلیل است که یک گواهی بدون SAN می‌تواند در یک تست سریع curl موفق شود اما در تمام مرورگرها با خطا مواجه گردد. از دیدگاه مرورگر، یک گواهی هویت سرور را تنها از طریق پسوند SAN تأیید می‌کند و در غیر این صورت، مرورگر دقیقاً با این پیام خطا به شما هشدار می‌دهد:

NET::ERR_CERT_COMMON_NAME_INVALID

This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.

هیچ تغییری در trust-store این خطا را برطرف نمی‌کند، زیرا گواهی عملاً هیچ نامی را معرفی نمی‌کند. اگر در حال حاضر با خطای NET::ERR_CERT_COMMON_NAME_INVALID مواجه هستید، گواهی شما فاقد SAN است (یا SAN اشتباهی دارد) و باید گواهی جدیدی صادر کنید. خوشبختانه، راه‌حل تنها با یک دستور انجام می‌شود.

صدور گواهی مورد تایید مرورگرها: یک دستور

OpenSSL از نسخه 1.1.1 به بعد، فلگ -addext را اضافه کرد که باعث می‌شود دیگر نیازی به دردسرهای فایل پیکربندی برای تزریق SAN که در راهنماهای قدیمی مرسوم بود، نداشته باشید. در Ubuntu 24.04:

sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
  -keyout /etc/ssl/private/git.internal.key \
  -out /etc/ssl/certs/git.internal.crt \
  -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"

عملکرد هر فلگ:

  • -x509 به‌جای ایجاد درخواست امضا (CSR)، مستقیماً یک گواهی خود-امضا (self-signed) صادر می‌کند.
  • -newkey rsa:4096 در همان مرحله یک کلید جدید تولید می‌کند. RSA 4096 با هیچ کلاینت قدیمی‌ای تداخل ندارد؛ اگر تمام کلاینت‌های متصل مدرن هستند، -newkey ec -pkeyopt ec_paramgen_curve:P-256 کوچک‌تر و سریع‌تر است.
  • -noenc معادل OpenSSL 3.x برای فلگ قدیمی -nodes است: بدون رمز عبور روی کلید. هر دو عبارت کار می‌کنند. کلید دارای رمز عبور باعث می‌شود nginx در هر بار بوت برای دریافت ورودی متوقف شود، بنابراین برای کلید سرور به این گزینه نیاز دارید.
  • -days 730، دو سال؛ در بخش انقضا درباره این عدد بیشتر توضیح داده شده است.
  • -subj به سوالات تعاملی به‌صورت خطی پاسخ می‌دهد. مقدار CN امروزه جنبه ظاهری دارد، اما به هر حال آن را روی نام اصلی تنظیم کنید؛ برخی ابزارها آن را نمایش می‌دهند.
  • -addext "subjectAltName=..." فلگ کلیدی و حیاتی است. تمام نام‌ها و تمام IPهایی که کلاینت‌ها تایپ می‌کنند را لیست کنید: ورودی‌های DNS: برای نام‌های میزبان (استفاده از wildcards مانند DNS:*.internal.lan مجاز است)، ورودی‌های IP: برای آدرس‌ها. اگر قرار است کسی به https://10.8.0.1 متصل شود، ورودی IP:10.8.0.1 باید وجود داشته باشد؛ در غیر این صورت، یک SAN فقط با DNS باعث نمایش مجدد خطای NET::ERR_CERT_COMMON_NAME_INVALID می‌شود.

پیش از اتصال هر چیزی، تایید کنید که SAN واقعاً اعمال شده است:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltName

خروجی صحیح:

X509v3 Subject Alternative Name:
    DNS:git.internal.lan, IP Address:10.8.0.1

اگر به جای آن No extensions in certificate چاپ شد، یعنی گواهی فاقد SAN است و مرورگرها آن را رد می‌کنند؛ به جای ادامه دادن، آن را دوباره تولید کنید.

ایمن‌سازی کلید

کلید خصوصی که توسط هر کاربری روی سیستم قابل خواندن باشد، دیگر کلید خصوصی نیست. در یک سیستم پیش‌فرض Ubuntu 24.04، مسیر /etc/ssl/private به‌طور پیش‌فرض 700 root:root است، بنابراین فقط root می‌تواند محتویات آن را فهرست کند؛ این مسیر تنها در صورتی 710 root:ssl-cert می‌شود که بسته‌ای مانند ssl-cert نصب شده باشد (که apache2 آن را پیشنهاد می‌دهد). در هر صورت، مجوز فایل را به‌طور صریح تنظیم کنید:

sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.key

سرویس‌های nginx و Apache هر دو گواهی‌ها را پیش از کاهش سطح دسترسی (drop privileges) با کاربر root می‌خوانند، بنابراین حالت root:root با مجوز 600 برای آن‌ها کار می‌کند. اگر کلید متعلق به سرویسی است که با کاربر مخصوص خود اجرا می‌شود و خودش کلید را بارگذاری می‌کند، مانند یک برنامه Node، Gitea یا یک دیمون Python، مالکیت آن را به همان کاربر سرویس تغییر دهید (chown)، اما همچنان از مجوز 600 استفاده کنید. کارهایی که هرگز نباید انجام دهید: استفاده از مجوز 644، قرار دادن کپی در مخزن git، یا نگهداری کپی در /tmp.

اتصال به nginx

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name git.internal.lan;

    ssl_certificate     /etc/ssl/certs/git.internal.crt;
    ssl_certificate_key /etc/ssl/private/git.internal.key;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}
sudo nginx -t && sudo systemctl reload nginx

دستور nginx -t باید پیش از آنکه reload اعمال شود، syntax is ok و test is successful را چاپ کند. اگر در عوض SSL_CTX_use_PrivateKey_file() failed ... key values mismatch چاپ شد، به این معناست که گواهی و کلید از دو اجرای متفاوت تولید شده‌اند؛ در این صورت به بخش failure-modes مراجعه کنید.

اتصال به Apache

sudo a2enmod ssl proxy proxy_http

استفاده از ssl به تنهایی در اینجا کافی نیست: vhost زیر از ProxyPass استفاده می‌کند و بدون mod_proxy و mod_proxy_http، تست پیکربندی با خطای Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration متوقف می‌شود. فایل vhost را با نام /etc/apache2/sites-available/git-internal.conf ذخیره کنید:

<VirtualHost *:443>
    ServerName git.internal.lan
    SSLEngine on
    SSLCertificateFile      /etc/ssl/certs/git.internal.crt
    SSLCertificateKeyFile   /etc/ssl/private/git.internal.key

    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2

configtest باید پاسخ Syntax OK را برگرداند. اکنون از یک ماشین کلاینت تست کنید:

curl -v https://git.internal.lan/

و با یک خطا مواجه خواهید شد:

curl: (60) SSL certificate problem: self-signed certificate

این یک باگ نیست. این عملکرد صحیح TLS است: curl گواهی شما را نمی‌شناسد و از برقراری ارتباط با سروری که نمی‌تواند آن را احراز هویت کند، خودداری می‌کند. بخش بعدی راه‌حل واقعی است و برخلاف تصور بسیاری از کاربران در اینترنت، روشی متفاوت دارد.

قابل‌اعتماد کردن کلاینت‌ها و الگوهای ضد‌موفقیت (Anti-patterns) که باید از آن‌ها اجتناب کرد

ابتدا به سراغ راه‌حل‌های نادرست می‌رویم که باید از آن‌ها پرهیز کرد. استفاده از curl -k (یا --insecure) در اسکریپت‌ها، verify=False در درخواست‌های Python، یا NODE_TLS_REJECT_UNAUTHORIZED=0 در Node، هیچ‌کدام گواهی شما را قابل‌اعتماد نمی‌کنند. این دستورات صرفاً تأیید گواهی را غیرفعال می‌کنند؛ به این معنی که کلاینت با هر سروری که هر نوع گواهی ارائه دهد (حتی گواهی جعلی که توسط مهاجم در مسیر قرار گرفته) ارتباط برقرار می‌کند. با این کار، سربار TLS را حفظ کرده اما احراز هویتی که هدف اصلی آن بوده را از دست می‌دهید. بدتر اینکه این پرچم‌ها گسترش می‌یابند: ابتدا در یک cron job کپی می‌شوند، سپس در اسکریپت deploy و در نهایت در کد production قرار می‌گیرند، تا جایی که دیگر کسی به یاد نمی‌آورد کدام اتصالات قرار بود موقتی باشند. اگر یک verify=False پس از پایان جلسه عیب‌یابی همچنان باقی بماند، یعنی طراحی سیستم شما ایراد دارد.

راه‌حل درست این است که به سیستم‌عامل هر کلاینت بیاموزید که این گواهی یک ریشه (root) قابل‌اعتماد است. در کلاینت‌های Ubuntu و Debian:

sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificates

خط مهم در خروجی (که یک بلوک Running hooks in /etc/ca-certificates/update.d... به دنبال آن می‌آید):

Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

دو نکته ظریف در این خطوط وجود دارد. فایل حتماً باید با .crt تمام شود؛ پسوند .pem نادیده گرفته می‌شود و شما بدون دریافت هیچ پیام خطایی، با 0 added مواجه می‌شوید. همچنین محتوا باید با فرمت PEM باشد و فایل با -----BEGIN CERTIFICATE----- شروع شود؛ اگر فایل باینری DER دارید، ابتدا آن را با openssl x509 -inform der -in file.der -out file.crt تبدیل کنید. افزودن گواهی self-signed به عنوان ریشه کارساز است، زیرا یک گواهی self-signed در واقع ریشه خودش محسوب می‌شود.

پس از این کار، curl، wget، git، apt و هر ابزار دیگری که از OpenSSL و مخزن گواهی‌های سیستم استفاده می‌کند، بدون نیاز به هیچ پرچمی به سرور اعتماد خواهد کرد. تعداد کمی از کلاینت‌ها مخزن اعتماد (trust store) اختصاصی خود را دارند و نیاز به تنظیمات جداگانه دارند:

  • Chrome/Chromium در لینوکس دیتابیس NSS را می‌خواند، نه مخزن سیستم: sudo apt install libnss3-tools و سپس certutil -d sql:$HOME/.local/share/pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt برای هر کاربر. این مسیر پیش‌فرض Chrome از نسخه 146 به بعد است؛ اگر کاربر همچنان یک ~/.pki/nssdb قدیمی از نصب‌های قبل داشته باشد، Chrome همچنان آن را می‌خواند، بنابراین در آن ماشین‌ها -d را به sql:$HOME/.pki/nssdb اشاره دهید.
  • Firefox مخزن اختصاصی خود را دارد: Settings → Privacy & Security → Certificates → Import. در ویندوز و macOS می‌توانید به جای آن، مقدار security.enterprise_roots.enabled را در about:config به true تغییر دهید تا مخزن سیستم‌عامل را بخواند؛ موزیلا این تنظیم را برای ویندوز و macOS مستند کرده است، نه لینوکس، بنابراین در Ubuntu از همان روش Import استفاده کنید.
  • Python requests مخزن CA اختصاصی خود (certifi) را همراه دارد و مخزن سیستم را نادیده می‌گیرد: از verify="/usr/local/share/ca-certificates/git.internal.crt" استفاده کنید یا REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt را export کنید.
  • Node.js به‌طور پیش‌فرض از ریشه‌های همراه خود استفاده می‌کند: NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt را export کنید، یا با استفاده از پرچم --use-system-ca (در Node 23.9 و بالاتر در لینوکس) یا NODE_USE_SYSTEM_CA=1 (در Node 24.6 و 22.19 و بالاتر) به آن بگویید مخزن سیستمی که به‌روزرسانی کرده‌اید را بخواند.

در کلاینت‌های ویندوز، روی .crt دوبار کلیک کنید و آن را در بخش Trusted Root Certification Authorities نصب کنید؛ در macOS، آن را به System keychain در Keychain Access اضافه کرده و گزینه Always Trust را برای آن فعال کنید.

یک ریشه برای چندین سرویس: یک CA خصوصی کوچک

اعتماد به گواهی‌های تکی به‌سرعت مقیاس‌پذیری را مختل می‌کند: شش سرویس ضرب‌در چهار ماشین کلاینت برابر است با 24 نصب گواهی، و هر سرویس جدید این تعداد را افزایش می‌دهد. راه‌حل، استفاده از یک CA خصوصی است؛ کلاینت‌ها به یک ریشه اعتماد می‌کنند و شما گواهی هر سرویس را با آن امضا می‌کنید.

گزینه کاربرپسند mkcert است که در مخازن Ubuntu 24.04 موجود است و مدیریت NSS storeها (فایرفاکس، و کروم با یک نکته که در ادامه می‌آید) را که update-ca-certificates نادیده می‌گیرد، انجام می‌دهد:

sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1

mkcert -install یک ریشه ایجاد کرده و آن را در سیستم store، در NSS storeهای مرورگرهایی که شناسایی می‌کند، و در صورت تنظیم JAVA_HOME، در Java store ثبت می‌کند. Node به‌صورت پیش‌فرض سیستم store را نمی‌خواند، بنابراین برنامه‌های Node را با --use-system-ca (یا NODE_USE_SYSTEM_CA=1) همان‌طور که در بالا گفته شد اجرا کنید، یا NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" را export نمایید. دستور سوم git.internal.lan+2.pem و git.internal.lan+2-key.pem را تولید می‌کند که آماده قرارگیری در قطعه‌کدهای nginx یا Apache در بالا هستند. فرض طراحی این ابزار، یک ماشین توسعه است؛ کلید ریشه روی هر سیستمی که -install را اجرا کرده باقی می‌ماند، بنابراین برای لپ‌تاپ توسعه عالی است اما برای ناوگان سرورها مناسب نیست.

نکته مهم تا اکتبر 2026: ابزار mkcert فقط دیتابیس NSS کروم را در مسیر قدیمی ~/.pki/nssdb جستجو می‌کند، نه در ~/.local/share/pki/nssdb که کروم از نسخه 146 به‌صورت پیش‌فرض از آن استفاده می‌کند. روی ماشینی که دیتابیس قدیمی را ندارد، ریشه را خودتان با دستور certutil از بخش قبلی و با استفاده از -i "$(mkcert -CAROOT)/rootCA.pem" به کروم اضافه کنید.

برای سرورها، OpenSSL ساده کل فرآیند CA را در پنج دستور انجام می‌دهد:

openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
  -out lab-ca.crt -subj "/CN=Lab Internal CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
  -CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crt

تله در دستور آخر است: openssl x509 -req به‌صورت پیش‌فرض تمام extensionها را از CSR حذف می‌کند، از جمله SAN که با دقت اضافه کرده بودید. -copy_extensions copy (یک گزینه در OpenSSL 3.x که در 24.04 کار می‌کند) آن‌ها را منتقل می‌کند؛ اگر آن را حذف کنید، گواهی امضا شده فاقد SAN خواهد بود و کروم دوباره با NET::ERR_CERT_COMMON_NAME_INVALID از شما استقبال می‌کند. با همان بررسی openssl x509 -noout -ext subjectAltName که قبلاً ذکر شد، صحت آن را تایید کنید.

فایل lab-ca.crt را از طریق مراحل trust-store که در بالا گفته شد، یک‌بار برای همیشه بین کلاینت‌ها توزیع کنید. از lab-ca.key مانند جواهرات سلطنتی محافظت کنید: با مجوز 600، و ترجیحاً روی سیستمی نگهداری شود که یکی از سرورهای امضا شده توسط آن نباشد؛ زیرا هر کسی که آن را در اختیار داشته باشد، می‌تواند برای هر نامی که کلاینت‌های شما باور می‌کنند، گواهی صادر کند.

انقضا و چرخش گواهی‌ها

طول عمر گواهی‌های صادرشده توسط CAهای عمومی رو به کاهش است. مجمع CA/Browser در مارس 2026 سقف اعتبار گواهی‌های عمومی جدید را به 200 روز (از 398 روز) کاهش داد و این مقدار در سال 2027 به 100 روز و تا مارس 2029 به 47 روز خواهد رسید. با این حال، این قوانین فقط برای CAهای عمومی و معتبر الزام‌آور است. CA خصوصی شما تحت این قوانین نیست و مرورگرها نیز این محدودیت‌ها را برای ریشه‌هایی (roots) که به‌صورت دستی نصب شده‌اند، اعمال نمی‌کنند. تنها محدودیت واقعی در دنیای واقعی این است که پلتفرم‌های اپل، هر گواهی سرور TLS با اعتبار بیش از 825 روز را بدون توجه به صادرکننده رد می‌کنند؛ بنابراین اگر قرار است آیفون‌ها یا مک‌ها به سرویس شما متصل شوند، اعتبار گواهی‌های نهایی (leaf certificates) را حداکثر دو سال یا کمتر در نظر بگیرید. -days 730 این استاندارد را در همه جا رعایت می‌کند؛ یک ریشه ده ساله با گواهی‌های نهایی دو ساله، ساختاری مناسب برای محیط‌های داخلی است.

گواهی‌های با عمر طولانی تنها به یک شکل شکست می‌خورند: بی‌سروصدا و به‌طور ناگهانی در تاریخی که هیچ‌کس آن را به خاطر نمی‌آورد. وضعیت فعلی خود را بررسی کنید:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate

تمدید گواهی را در یک تقویم واقعی ثبت کنید یا از cron بخواهید 30 روز پیش از انقضا به شما هشدار دهد؛ openssl x509 -checkend 2592000 -in cert.crt اگر زمان انقضا کمتر از آن تعداد ثانیه باشد، با کد خروجی غیر صفر متوقف می‌شود. اگر از قبل Uptime Kuma برای پایش وضعیت را اجرا می‌کنید، مانیتورهای HTTPS آن به‌صورت رایگان نزدیک شدن به زمان انقضای گواهی را به شما اطلاع می‌دهند.

چرخش (rotation) گواهی با یک CA خصوصی فرآیندی ساده و بدون دردسر است: دستورات CSR و امضا را دوباره اجرا کنید، فایل‌ها را جایگزین کرده و وب‌سرور را reload کنید. از آنجا که ریشه (root) تغییر نکرده است، هیچ کلاینتی متوجه تغییری نخواهد شد.

حالت‌های شکست و رشته‌هایی که مشاهده خواهید کرد

NET::ERR_CERT_AUTHORITY_INVALID، وضعیت مورد انتظار پیش از نصب اعتماد است و نقص در گواهی محسوب نمی‌شود. اگر این وضعیت پس از نصب ریشه (root) همچنان باقی ماند: در لینوکس، Chrome به‌جای مخزن سیستم از NSS استفاده می‌کند (مرحله certutil را ببینید)، و از نسخه 146 به بعد Chrome، این پایگاه داده در ~/.local/share/pki/nssdb قرار دارد مگر اینکه یک ~/.pki/nssdb قدیمی وجود داشته باشد، بنابراین ریشه‌ای که در مسیر دیگر اضافه شده دیده نمی‌شود؛ یا فایل کپی‌شده با .crt پایان نیافته و update-ca-certificates پیام 0 added را گزارش کرده است؛ یا سرور گواهی متفاوتی نسبت به آنچه شما به آن اعتماد کرده‌اید ارائه می‌دهد، اثر انگشت‌ها (fingerprints) را با openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256 مقایسه کنید.

NET::ERR_CERT_COMMON_NAME_INVALID، گواهی فاقد SAN است یا SAN نام موجود در نوار آدرس را پوشش نمی‌دهد. حالت کلاسیک: SAN شامل DNS:git.internal.lan است اما کاربر به https://10.8.0.1 مراجعه کرده است. تغییرات در مخزن اعتماد (trust-store) این مشکل را حل نمی‌کند؛ گواهی را با ورودی گمشده مجدداً صادر کنید.

curl: (60) SSL certificate problem: self-signed certificate، ابزار curl به گواهی اعتماد ندارد. گونه self-signed certificate in certificate chain برای گواهی امضاشده توسط CA خصوصی شما، همان معنا را دارد. راه‌حل موقت: curl --cacert lab-ca.crt https://...؛ راه‌حل دائمی: مخزن اعتماد. از -k استفاده نکنید.

unable to load certificate ... Expecting: TRUSTED CERTIFICATE (یا Expecting: CERTIFICATE REQUEST، یا no start line)، سردرگمی در فرمت PEM. شما نوع اشتباهی از فایل را به OpenSSL داده‌اید: یک کلید یا CSR در جایی که انتظار گواهی می‌رفت، یا یک فایل باینری DER در جایی که انتظار PEM می‌رفت. head -1 filename به شما می‌گوید واقعاً چه چیزی در اختیار دارید، یک گواهی با -----BEGIN CERTIFICATE----- شروع می‌شود. برای DER، با استفاده از openssl x509 -inform der -in file.der -out file.crt تبدیل را انجام دهید.

nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch)، گواهی و کلید به یکدیگر تعلق ندارند، معمولاً به این دلیل که دستور تولید دو بار اجرا شده و فایل‌ها با هم مخلوط شده‌اند. با مقایسه openssl x509 -in git.internal.crt -noout -pubkey | sha256sum و openssl pkey -in git.internal.key -pubout | sha256sum تأیید کنید؛ هش‌های یکسان به معنای جفت بودن آن‌هاست. اگر متفاوت هستند، هر دو را با هم مجدداً تولید کنید.

FAQ

چرا با وجود ایجاد گواهی خودامضا (self-signed)، کروم همچنان پیام "Not secure" را نمایش می‌دهد؟

اگر خطا NET::ERR_CERT_AUTHORITY_INVALID باشد، گواهی مشکلی ندارد و کروم صرفاً دلیلی برای اعتماد به آن ندارد. گواهی (یا ریشه CA خصوصی خود) را در trust store کلاینت نصب کنید. به یاد داشته باشید که در لینوکس، کروم از دیتابیس NSS از طریق certutil (در مسیر ~/.local/share/pki/nssdb از نسخه 146 کروم به بعد، یا ~/.pki/nssdb قدیمی در صورت وجود) استفاده می‌کند و نه از store سیستم. اگر خطا NET::ERR_CERT_COMMON_NAME_INVALID باشد، گواهی فاقد Subject Alternative Name منطبق با URL است و باید با -addext "subjectAltName=..." مجدداً صادر شود.

چگونه curl را وادار کنم بدون استفاده از -k به یک گواهی خودامضا اعتماد کند؟

گواهی (با فرمت PEM و پسوند .crt) را در /usr/local/share/ca-certificates/ کپی کنید و sudo update-ca-certificates را اجرا کنید؛ خروجی باید 1 added باشد. از آن پس، curl گواهی را مانند هر گواهی عمومی دیگری تأیید می‌کند. برای یک درخواست تکی بدون تغییر در سیستم، curl --cacert /path/to/cert.crt فقط همان فایل را بررسی می‌کند؛ -k تأیید را به‌طور کامل غیرفعال می‌کند و نباید در هیچ اسکریپتی استفاده شود.

اعتبار یک گواهی خودامضا حداکثر چقدر می‌تواند باشد؟

از نظر فنی هر چقدر که بخواهید؛ محدودیت‌های CA/Browser Forum (اکنون 200 روز، و 47 روز تا سال 2029) فقط برای CAهای عمومی معتبر است، نه اعتماد خصوصی. در عمل، اعتبار گواهی‌های سرور را به 825 روز محدود کنید، زیرا دستگاه‌های اپل هر گواهی طولانی‌تری را بدون توجه به صادرکننده رد می‌کنند. یک ریشه خصوصی ده ساله با گواهی‌های نهایی دو ساله (-days 730) یک انتخاب منطقی است؛ فقط تمدید آن را در تقویم ثبت کنید، زیرا یک گواهی داخلی منقضی‌شده، همه چیز را در تاریخی که کسی به یاد نمی‌آورد، بی‌سروصدا از کار می‌اندازد.

آیا باید از گواهی خودامضا استفاده کنم یا Let's Encrypt؟

اگر سرویس دارای نام DNS عمومی است و از اینترنت در دسترس است، همیشه از Let's Encrypt استفاده کنید؛ رایگان، خودکار و مورد اعتماد اکثر مرورگرها و سیستم‌عامل‌ها است (اگر دستگاه‌های بسیار قدیمی باید متصل شوند، لیست سازگاری Let's Encrypt را بررسی کنید). گواهی خودامضا (یا CA خصوصی) برای مواردی است که Let's Encrypt نمی‌تواند صادر کند: IPهای خصوصی، نام‌های میزبان صرفاً داخلی مانند .lan، شبکه‌های ایزوله (air-gapped) و سرویس‌هایی که عمداً پشت VPN مخفی شده‌اند. این تصمیم مربوط به دسترسی‌پذیری و نام‌گذاری است، نه قدرت امنیتی؛ رمزنگاری در هر دو حالت یکسان است.

چرا گواهی من حتی پس از افزودن به /usr/local/share/ca-certificates رد می‌شود؟

سه مورد را بررسی کنید. فایل باید با .crt تمام شود؛ پسوند .pem نادیده گرفته می‌شود و update-ca-certificates خطای 0 added را گزارش می‌کند. محتوا باید متن PEM باشد که با -----BEGIN CERTIFICATE----- شروع می‌شود، نه باینری DER. همچنین برنامه باید واقعاً از store سیستم استفاده کند؛ کروم در لینوکس، فایرفاکس، کتابخانه requests در پایتون، Node.js (مگر اینکه با --use-system-ca اجرا شود) و جاوا، هر کدام یک trust store خصوصی دارند و نیاز دارند گواهی به‌طور جداگانه به آن‌ها اضافه شود.