SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor

راهنمای پیاده‌سازی mTLS در Nginx با استفاده از OpenSSL

با استفاده از OpenSSL یک CA خصوصی بسازید و با تنظیم ssl_verify_client در Nginx، دسترسی به پنل مدیریت را فقط برای دارندگان گواهی معتبر ممکن کنید. رفع خطای 400 Bad Request.

مفهوم mTLS

پروتکل Mutual TLS که معمولاً mTLS نامیده می‌شود، Nginx را وادار می‌کند تا از هر کلاینت یک گواهی درخواست کند و در صورت نبود گواهی یا صادر نشدن آن توسط یک مرجع صدور گواهی (CA) تحت کنترل شما، درخواست را رد کند. این بررسی در حین handshake پروتکل TLS (امنیت لایه انتقال) انجام می‌شود، بنابراین کلاینتی که گواهی معتبر ندارد، هرگز به اپلیکیشن شما دسترسی پیدا نمی‌کند. جذابیت این روش در همین است: یک پنل مدیریت یا endpoint مربوط به متریک‌ها می‌تواند بدون صفحه ورود و بدون هیچ نقطه قابل حدسی برای ربات‌ها، روی اینترنت عمومی قرار بگیرد.

پیاده‌سازی آن ساده است. یک CA خصوصی که با openssl ساخته شده، یک گواهی برای هر شخص، و سه دستورالعمل در بلاک server در Nginx. کاری که تعیین می‌کند این سیستم یک سال دوام بیاورد یا خیر، جنبه عملیاتی آن است؛ بنابراین بخش عمده این راهنما به طول عمر گواهی‌ها، ابطال (revocation)، گواهی‌های اختصاصی برای هر فرد، و نحوه عیب‌یابی در زمانی که دسترسی کلاینتی رد می‌شود و دلیل آن مشخص نیست، می‌پردازد.

دو زنجیره، نه یکی

در یک پیکربندی mTLS دو زنجیره گواهی وجود دارد که هیچ ارتباطی با یکدیگر ندارند. ادغام این دو، نخستین اشتباهی است که تقریباً همه مرتکب می‌شوند.

زنجیره اول متعلق به سرور است. VPS شما گواهی مربوط به admin.example.com را که توسط یک CA عمومی مانند Let's Encrypt صادر شده است ارائه می‌دهد و مرورگر آن را با root store موجود در سیستم‌عامل تطبیق می‌دهد. mTLS هیچ تغییری در این بخش ایجاد نمی‌کند. اگر certbot امروز آن گواهی را برای شما صادر می‌کند، آن را دقیقاً به همان شکل حفظ کنید: به صدور گواهی Let's Encrypt برای nginx با certbot مراجعه کنید.

زنجیره دوم متعلق به کلاینت است. شما یک CA کوچک برای خود ایجاد می‌کنید، برای هر شخصی که نیاز به دسترسی دارد یک گواهی امضا می‌کنید و به nginx می‌گویید که هنگام بررسی کلاینت‌ها، فقط به آن CA اعتماد کند. هیچ root store عمومی، CA شما را نمی‌شناسد و نیازی هم به شناختن آن ندارد. تنها موجودیتی که باید به آن اعتماد کند، nginx است که این کار را از طریق فایل ssl_client_certificate انجام می‌دهد.

بنابراین ssl_client_certificate هرگز بر گواهی‌ای که nginx ارائه می‌دهد تأثیری ندارد و زنجیره Let's Encrypt نیز هرگز بر اینکه کدام کلاینت‌ها اجازه ورود دارند، تأثیرگذار نیست. اشاره کردن ssl_client_certificate به fullchain.pem آن کاری را که به نظر می‌رسد انجام نمی‌دهد: این دستور مشخص می‌کند که گواهی کلاینت ممکن است از چه صادرکنندگانی باشد، که این سمت دیگر اتصال است. اعتماد کردن خودِ سرور به CA شما برای فعالیت‌های خروجی‌اش، یک وظیفه جداگانه است که در افزودن CA شخصی به trust store در Ubuntu پوشش داده شده است؛ و trust store سیستم، همان چیزی نیست که nginx هنگام تأیید کلاینت می‌خواند.

ساخت CA کلاینت اختصاصی با openssl

CA را در مکانی غیر از وب‌سرور بسازید. nginx تنها به گواهی عمومی CA نیاز دارد. کلید خصوصی CA برای امضای گواهی‌های جدید کلاینت استفاده می‌شود؛ بنابراین، نگهداری آن روی سروری که در معرض اینترنت است، به این معناست که با یک نفوذ، مهاجم می‌تواند به‌دلخواه برای خود گواهی‌های کلاینت معتبر صادر کند.

mkdir -p ~/client-ca/certs ~/client-ca/newcerts ~/client-ca/private ~/client-ca/csr
cd ~/client-ca
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumber

فایل‌های index.txt، serial و crlnumber پایگاه‌داده CA هستند. openssl ca بدون وجود آن‌ها اجرا نمی‌شود. این فایل‌ها همچنین امکان ابطال گواهی در آینده را فراهم می‌کنند، زیرا لیست ابطال (CRL) بر اساس شماره سریال است و CA باید بداند هر شماره سریال به چه کسی اختصاص یافته است.

فایل ~/client-ca/openssl.cnf را بنویسید. مقدار dir را روی مسیر واقعی آن دایرکتوری تنظیم کنید، زیرا openssl ca متغیر ~ را بسط نمی‌دهد.

[ ca ]
default_ca = client_ca

[ client_ca ]
dir               = /home/you/client-ca
database          = $dir/index.txt
new_certs_dir     = $dir/newcerts
certificate       = $dir/ca.crt
private_key       = $dir/private/ca.key
serial            = $dir/serial
crlnumber         = $dir/crlnumber
default_md        = sha256
default_days      = 365
default_crl_days  = 30
policy            = policy_loose
rand_serial       = no
unique_subject    = no
email_in_dn       = no

[ policy_loose ]
commonName              = supplied
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
emailAddress            = optional

[ client_ext ]
basicConstraints        = CA:FALSE
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = clientAuth
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer

اکنون کلید CA و گواهی خودامضای آن را بسازید:

openssl genrsa -aes256 -out private/ca.key 4096
chmod 600 private/ca.key
openssl req -x509 -new -key private/ca.key -sha256 -days 3650 \
  -subj "/O=Example Ops/CN=Example Ops Client CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -out ca.crt

دستور -aes256 یک رمز عبور روی کلید CA قرار می‌دهد، بنابراین در هر بار عملیات امضا، رمز عبور درخواست می‌شود. هدف از این کار همین است. آنچه ساخته‌اید را بررسی کنید:

openssl x509 -in ca.crt -noout -subject -dates -ext basicConstraints

موضوع (Subject) باید CA شما باشد و اعتبار آن باید ده سال در نظر گرفته شود. خط extension باید شامل CA:TRUE, pathlen:0 باشد. مقدار pathlen:0 به این معناست که این CA می‌تواند گواهی‌های نهایی را امضا کند اما اجازه امضای یک CA دیگر را ندارد؛ این کار باعث می‌شود زنجیره دقیقاً در یک سطح باقی بماند و نیازی به تغییر ssl_verify_depth نداشته باشید.

صدور گواهی کلاینت برای هر شخص

برای هر نفر یک گواهی صادر کنید. هرگز از یک گواهی مشترک برای کل تیم استفاده نکنید، زیرا گواهی مشترک را نمی‌توان بدون مسدود کردن دسترسی همه افراد باطل کرد و همچنین مشخص نمی‌کند چه کسی درخواست را ارسال کرده است.

openssl genrsa -out private/alice.key 2048
openssl req -new -key private/alice.key -out csr/alice.csr \
  -subj "/O=Example Ops/CN=alice"
openssl ca -config openssl.cnf -extensions client_ext \
  -days 365 -notext -in csr/alice.csr -out certs/alice.crt

openssl ca گواهی مورد نظر برای امضا را نمایش می‌دهد، رمز عبور CA را می‌پرسد، دو بار برای تأیید درخواست می‌کند و سپس یک خط به index.txt اضافه می‌کند. هنگام اسکریپت‌نویسی، -batch را اضافه کنید. بخش client_ext به دلیل وجود یک خط در آن اهمیت دارد: extendedKeyUsage = clientAuth. گواهی‌ای که در بخش extended key usage فقط شامل serverAuth باشد، به عنوان گواهی نامناسب برای احراز هویت کلاینت رد می‌شود؛ بنابراین به جای حدس و گمان، هدف را صریحاً مشخص کنید.

پیش از تحویل هر چیزی، جفت گواهی را با CA بررسی کنید:

openssl verify -CAfile ca.crt certs/alice.crt

این دستور certs/alice.crt: OK را چاپ می‌کند. هر خروجی دیگری به این معناست که گواهی و CA با هم مطابقت ندارند و هیچ پیکربندی در nginx نمی‌تواند آن را اصلاح کند.

کلید و گواهی را در یک فایل واحد که مرورگر بتواند آن را import کند، بسته‌بندی کنید:

openssl pkcs12 -export -inkey private/alice.key -in certs/alice.crt \
  -name "alice at example ops" -out alice.p12

عملیات export از شما یک رمز عبور می‌خواهد که از فایل در حین انتقال محافظت می‌کند. فایل و رمز عبور را از کانال‌های ارتباطی جداگانه ارسال کنید و به افراد فایل .p12 را تحویل دهید، نه یک .key خام. می‌توانید -certfile ca.crt را اضافه کنید تا CA نیز در بسته قرار بگیرد، اما nginx به آن نیازی ندارد: nginx در حال حاضر ca.crt را در اختیار دارد، بنابراین گواهی‌ای که مستقیماً توسط آن CA امضا شده باشد، به‌تنهایی تأیید می‌شود.

OpenSSL 3 که در Ubuntu 24.04 ارائه شده است، فایل‌های PKCS#12 را با الگوریتم‌های رمزنگاری مدرن می‌نویسد و مرورگرها و سیستم‌عامل‌های رایج تا اوت 2026 آن‌ها را می‌خوانند. اگر یک ابزار import قدیمی فایل را نپذیرفت، با افزودن -legacy دوباره export کنید تا از الگوریتم‌های قدیمی‌تری که آن ابزار انتظار دارد استفاده شود. پیش از استفاده از این فلگ، پیام خطای ابزار import را به‌دقت بخوانید.

پیکربندی Nginx با ssl_client_certificate و ssl_verify_client

گواهی CA را، و فقط همان گواهی CA را، به سرور کپی کنید.

scp ca.crt user@admin.example.com:/tmp/client-ca.crt
ssh user@admin.example.com \
  'sudo install -o root -g root -m 644 /tmp/client-ca.crt /etc/nginx/client-ca.crt'

حالت 644 در اینجا صحیح است. گواهی CA اطلاعات عمومی محسوب می‌شود. کلید CA باید روی ایستگاه کاری شما باقی بماند.

سپس سه دستورالعمل زیر را به بلوک server که در حال حاضر TLS را مدیریت می‌کند، اضافه کنید:

server {
    listen 443 ssl;
    server_name admin.example.com;

    ssl_certificate     /etc/letsencrypt/live/admin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;

    ssl_client_certificate /etc/nginx/client-ca.crt;
    ssl_verify_client      on;
    ssl_verify_depth       1;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

مقدار ssl_verify_depth 1 پیش‌فرض Nginx است و بیان می‌کند که گواهی کلاینت باید مستقیماً توسط CA موجود در آن فایل امضا شده باشد. تنها در صورتی این مقدار را افزایش دهید که یک CA میانی (intermediate) اضافه کرده باشید. Nginx همچنین نام‌های subject را از ssl_client_certificate در طول handshake برای کلاینت ارسال می‌کند؛ این همان روشی است که مرورگر متوجه می‌شود کدام‌یک از گواهی‌هایش را ارائه دهد. این رفتار دلیل استفاده از ssl_client_certificate به‌جای ssl_trusted_certificate است که اگرچه به همان شیوه احراز هویت می‌کند، اما هیچ لیستی ارسال نمی‌کند.

در Ubuntu 24.04، نسخه Nginx 1.24 ارائه شده است که در آن HTTP/2 در خط listen به صورت listen 443 ssl http2; تعریف می‌شود. در Nginx 1.25.1 و نسخه‌های بعد از آن، این فرم منسوخ شده و HTTP/2 دستورالعمل اختصاصی خود یعنی http2 on; را دارد. هیچ‌کدام از این دو انتخاب، تأثیری بر بررسی گواهی ندارند.

سرویس را Reload کرده و نتیجه را مشاهده کنید:

sudo nginx -t && sudo systemctl reload nginx
curl -i https://admin.example.com/

دستور nginx -t مقادیر syntax is ok و test is successful را چاپ می‌کند. فراخوانی curl هیچ گواهی‌ای به همراه ندارد، بنابراین باید با کد 400 Bad Request و بدنه No required SSL certificate was sent بازگردد. این یعنی Nginx در همان دروازه ورودی درخواست را رد کرده است، که نشان می‌دهد پیکربندی فعال است و درخواست هرگز به اپلیکیشن نرسیده است. حالا روش صحیح را امتحان کنید:

curl --cert certs/alice.crt --key private/alice.key https://admin.example.com/

این دستور باید خروجی اپلیکیشن شما را بازگرداند.

چرا گیت در بلاک server قرار می‌گیرد

گواهی در طول TLS handshake مبادله می‌شود، یعنی پیش از آنکه nginx خط درخواست را بخواند؛ بنابراین در آن لحظه nginx نمی‌داند درخواست به کدام location هدایت خواهد شد. قرار دادن ssl_verify_client on; درون یک location، کلاینت را وادار به مذاکره مجدد (renegotiation) در میانه اتصال می‌کند. پروتکل TLS 1.3 قابلیت renegotiation را حذف کرده و HTTP/2 نیز آن را ممنوع کرده است؛ در نتیجه در پشته‌های نرم‌افزاری امروزی، این الگو به‌جای درخواست گواهی، با شکست مواجه می‌شود.

محدوده‌بندی (scoping) را خودتان انجام دهید. گواهی را در سطح server درخواست کنید و سپس در سطح location تصمیم بگیرید:

ssl_verify_client optional;

location /metrics {
    if ($ssl_client_verify != SUCCESS) { return 403; }
    proxy_pass http://127.0.0.1:9090;
}

location /healthz {
    proxy_pass http://127.0.0.1:8080;
}

متغیر $ssl_client_verify حاوی SUCCESS است، یا اگر کلاینت چیزی ارسال نکند NONE، یا FAILED: به همراه دلیل آن. با استفاده از optional، nginx گواهی را درخواست کرده و تنها در صورت دریافت، آن را تایید می‌کند؛ این همان چیزی است که اجازه می‌دهد مسیر عمومی /healthz در بالا کار کند، در حالی که /metrics بسته باقی می‌ماند. گواهی‌ای که ارسال شده اما در تایید شکست می‌خورد، در همان نقطه توسط nginx رد می‌شود. اگر می‌خواهید خودتان گواهی‌های نامعتبر را بررسی کنید، از optional_no_ca استفاده کنید؛ در این صورت تست شما باید هر مقداری غیر از SUCCESS را به عنوان رد صلاحیت در نظر بگیرد.

nginx کدهای وضعیت غیر استانداردی برای این منظور دارد و error_page می‌تواند آن‌ها را دریافت کند تا بازدیدکننده رد شده، به‌جای یک خطای 400 ساده، توضیحی دریافت کند:

error_page 495 496 = @needcert;

location @needcert {
    default_type text/plain;
    return 200 "This host requires a client certificate. Ask ops for one.\n";
}

کد 495 به این معنی است که گواهی کلاینت در تایید شکست خورده است. کد 496 به این معنی است که کلاینت هیچ گواهی‌ای ارائه نکرده است. آن صفحه را به صورت متن ساده (plain text) نگه دارید، زیرا شخصی که آن را می‌خواند نه نشست (session) دارد و نه حساب کاربری.

چگونه گواهی کلاینت را در مرورگر نصب کنم؟

فایرفاکس از مخزن گواهی اختصاصی خود استفاده می‌کند: به بخش Settings، سپس Privacy and Security، و بعد View Certificates بروید. در تب Your Certificates، گزینه Import را انتخاب کرده و فایل .p12 را انتخاب کنید و رمز عبور آن را وارد نمایید.

مرورگرهای کروم و اج در ویندوز و macOS از مخزن گواهی سیستم‌عامل استفاده می‌کنند، بنابراین باز کردن فایل .p12، ویزارد وارد کردن گواهی سیستم را اجرا می‌کند. در لینوکس، کروم از یک دیتابیس جداگانه NSS (سرویس‌های امنیتی شبکه) در دایرکتوری home شما استفاده می‌کند و استفاده از ابزار خط فرمان، مطمئن‌ترین روش است:

sudo apt install -y libnss3-tools
pk12util -d sql:$HOME/.pki/nssdb -i alice.p12

پس از آن، سایت را باز کنید تا مرورگر از شما بپرسد کدام گواهی را ارسال کند. کروم این انتخاب را تا پایان نشست مرورگر به خاطر می‌سپارد، بنابراین اگر می‌خواهید دوباره از شما پرسیده شود، مرورگر را مجدداً راه‌اندازی کنید. گواهی در یک پروفایل مرورگر روی یک دستگاه ذخیره می‌شود؛ بنابراین گواهی وارد شده در فایرفاکس برای کروم قابل مشاهده نیست و هر دو برای گوشی شما نیز غیرقابل مشاهده هستند.

تست با curl --cert

برای عیب‌یابی از curl استفاده کنید، زیرا این ابزار گزارش می‌دهد که چه عملیاتی انجام داده است.

curl -v --cert certs/alice.crt --key private/alice.key https://admin.example.com/

شما می‌توانید گواهی و کلید را در یک فایل PEM واحد ترکیب کرده و آن را به عنوان --cert alice.pem ارسال کنید. اگر کلید دارای عبارت عبور (passphrase) باشد، curl آن را از شما می‌پرسد. همچنین می‌توانید از --cert alice.pem:passphrase استفاده کنید، اما این کار باعث می‌شود عبارت عبور در تاریخچه shell شما ذخیره شود؛ بنابراین بهتر است اجازه دهید curl آن را از شما بپرسد.

پیش از آنکه Nginx را مقصر بدانید، انجام دو بررسی ارزشمند است. نخست، گواهی و کلید باید یک جفت معتبر باشند:

openssl x509 -noout -pubkey -in certs/alice.crt | openssl sha256
openssl pkey -pubout -in private/alice.key | openssl sha256

دو هش یکسان به این معناست که فایل‌ها متعلق به یکدیگر هستند. دو هش متفاوت به این معناست که شما فایل‌های دو موجودیت مختلف را با هم اشتباه گرفته‌اید؛ هیچ کلاینتی دلیل این خطا را به شما اعلام نخواهد کرد.

دوم، سرور باید گواهی CA شما را درخواست کند:

openssl s_client -connect admin.example.com:443 -servername admin.example.com </dev/null

در خروجی به دنبال بلوک Acceptable client certificate CA names و موضوع (subject) مربوط به CA خود در داخل آن بگردید. اگر این بلوک کاملاً وجود ندارد، Nginx در آن server block که پاسخ داده است، درخواستی برای گواهی ارسال نمی‌کند؛ این یعنی دستورالعمل‌های شما در یک بلوک دیگر، که اغلب همان default server است، اعمال شده‌اند.

انتقال CN کلاینت به برنامه

گواهی مشخص می‌کند چه کسی درخواست را ارسال کرده است، اما برنامه‌ای که پشت پروکسی قرار دارد نمی‌تواند لایه TLS را ببیند؛ بنابراین nginx باید نام را به آن منتقل کند.

map $ssl_client_s_dn $client_cn {
    default              "";
    "~,?CN=(?<cn>[^,]+)" $cn;
}

$ssl_client_s_dn نام متمایز (distinguished name) سوژه را با فرمت RFC 2253 نگه می‌دارد که به شکل CN=alice,O=Example Ops است. این map فیلد CN را به $client_cn منتقل می‌کند. CN را به صورت یک نام کاربری ساده نگه دارید، زیرا کاما در داخل CN در آن فرمت escape می‌شود و عبارت منظم (regular expression) ساده‌ای که در بالا آمده، از پس آن برنمی‌آید.

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host                 $host;
    proxy_set_header X-Forwarded-For      $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto    $scheme;
    proxy_set_header X-Client-Cert-CN     $client_cn;
    proxy_set_header X-Client-Cert-Serial $ssl_client_serial;
}

proxy_set_header هر هدر با آن نام که توسط تماس‌گیرنده ارسال شده باشد را جایگزین می‌کند، بنابراین هیچ‌کس نمی‌تواند X-Client-Cert-CN را از طریق این location جعل کند. دو شرط این موضوع را تضمین می‌کنند. nginx مقدار proxy_set_header را تنها زمانی از سطح بیرونی به ارث می‌برد که سطح داخلی هیچ مقداری برای آن تعریف نکرده باشد؛ بنابراین یک location دوم با یک خط proxy_set_header، تمام هدرهای تنظیم‌شده در بالای خود (از جمله این مورد) را بی‌صدا از دست می‌دهد. همچنین برنامه باید به‌جز از طریق nginx غیرقابل دسترس باشد، که به معنای bind کردن آن روی 127.0.0.1 به‌جای 0.0.0.0 است؛ زیرا برنامه‌ای که روی یک پورت عمومی باشد، هدر جعل‌شده را مستقیماً از اینترنت می‌خواند. سمت پروکسی این موضوع در توضیح خط‌به‌خط پیکربندی reverse proxy در nginx پوشش داده شده است. اگر برنامه به‌جای نام، کل گواهی را بخواهد، $ssl_client_escaped_cert آن را به صورت URL-encoded و ایمن در داخل یک هدر حمل می‌کند.

چگونه می‌توانم یک گواهی کلاینت را ابطال کنم؟

فردی سازمان را ترک می‌کند یا لپ‌تاپی گم می‌شود. شما فقط همان یک گواهی را ابطال می‌کنید و بقیه کاربران به کار خود ادامه می‌دهند؛ این دقیقاً همان دلیلی است که برای هر شخص یک گواهی مجزا صادر می‌کنیم.

cd ~/client-ca
openssl ca -config openssl.cnf -revoke certs/alice.crt
openssl ca -config openssl.cnf -gencrl -out crl.pem

دستور اول، وضعیت خط مربوط به آن شماره سریال را در index.txt از V به R تغییر می‌دهد. دستور دوم یک لیست ابطال گواهی (CRL) ایجاد می‌کند که فایلی امضاشده شامل شماره سریال‌های ابطال‌شده است. این فایل را منتقل کنید و با استفاده از ssl_crl /etc/nginx/client-ca.crl; در کنار سایر دستورالعمل‌ها، آن را به nginx معرفی کنید.

scp crl.pem user@admin.example.com:/tmp/client-ca.crl
ssh user@admin.example.com 'sudo install -m 644 /tmp/client-ca.crl /etc/nginx/client-ca.crl && sudo nginx -t && sudo systemctl reload nginx'

در اینجا تله‌ای وجود دارد که دسترسی همه را مسدود می‌کند. یک CRL دارای تاریخ nextUpdate است که توسط default_crl_days تعیین می‌شود (در پیکربندی بالا 30 روز). پس از گذشت این تاریخ، OpenSSL لیست را منقضی تلقی کرده و احراز هویت تمام گواهی‌های کلاینت را با خطای CRL has expired رد می‌کند، نه فقط گواهی ابطال‌شده را. nginx فایل را هنگام بارگذاری پیکربندی می‌خواند، بنابراین وجود یک CRL جدید روی دیسک تا زمانی که nginx مجدداً بارگذاری (reload) نشود، تغییری ایجاد نمی‌کند. لیست را در بازه‌های زمانی مطمئن (مثلاً هفتگی برای بازه 30 روزه) بازتولید و بارگذاری کنید و پیش از کپی کردن، تاریخ‌ها را بررسی کنید:

openssl crl -in crl.pem -noout -lastupdate -nextupdate

برای تعداد کمی از کاربران، گزینه ساده‌تری وجود دارد. از آنجا که CA متعلق به شماست، nginx می‌تواند مستقیماً یک شماره سریال را رد کند و از دردسرهای CRL صرف‌نظر کند:

map $ssl_client_serial $revoked {
    default 0;
    "1002"  1;
}

این مورد را با if ($revoked) { return 403; } در بلاک location جفت کنید. این روش تاریخ انقضایی ندارد که فراموش شود. همچنین این تنظیمات منتقل نمی‌شود، بنابراین هر سرویس دیگری که به CA شما اعتماد دارد، از این ابطال بی‌خبر خواهد بود. برای یک nginx که جلوی یک اپلیکیشن قرار دارد، این صادقانه‌ترین و ساده‌ترین پاسخ است. زمانی که بیش از یک دروازه ورودی داشتید، به سراغ استفاده از CRL بروید.

مدت اعتبار گواهی‌های کلاینت چقدر باید باشد؟

به گواهی‌های کلاینت یک سال اعتبار بدهید، یا اگر زحمت صدور مجدد برایتان مقدور است، کمتر از یک سال در نظر بگیرید. انقضای گواهی در اینجا یک شکست خاموش محسوب می‌شود، زیرا هیچ هشداری پیش از موعد به دارنده گواهی داده نمی‌شود. کاربر یک روز صبح پنل را باز می‌کند، Nginx اتصال را رد می‌کند و مرورگر امتناع از اتصال را با عبارات خاص خود توصیف می‌کند که به‌ندرت شامل کلمه expired (منقضی‌شده) است. اعتبار CA را روی 10 سال تنظیم کنید و تاریخ انقضای آن را جایی یادداشت کنید که واقعاً آن را می‌خوانید؛ زیرا وقتی گواهی CA منقضی شود، تمام گواهی‌های زیرمجموعه آن در همان روز از اعتبار می‌افتند.

دو دستور زیر به شما کمک می‌کند تا از این وضعیت عقب نمانید:

openssl x509 -in certs/alice.crt -noout -subject -serial -enddate
awk -F'\t' '{print $1, $2, $4}' ~/client-ca/index.txt

ستون اول در index.txt نشان‌دهنده وضعیت است: V برای معتبر، R برای ابطال‌شده، E برای منقضی‌شده. ستون دوم تاریخ انقضا به فرمت YYMMDDHHMMSSZ و ستون چهارم شماره سریال است. آن فایل تنها سابقه شما از این است که چه کسی چه گواهی‌ای در اختیار دارد؛ بنابراین از آن به همراه کلید CA نسخه پشتیبان تهیه کنید و هر دو را به عنوان اطلاعات محرمانه در نظر بگیرید.

تمدید گواهی به معنای صدور یک گواهی جدید است، نه تمدید اعتبار گواهی قبلی. یک کلید و CSR (درخواست امضای گواهی) جدید تولید کنید، آن را امضا کرده و تحویل دهید، سپس پس از اینکه شخص تأیید کرد گواهی جدید کار می‌کند، گواهی قدیمی را ابطال کنید.

محافظت‌های mTLS و محدودیت‌های آن

آنچه mTLS حذف می‌کند، دسترسی‌های احرازنشده است. اسکنری که نام دامنه شما را پیدا می‌کند، در مرحله handshake رد می‌شود؛ بنابراین هرگز درخواست HTTP ارسال نمی‌کند، فرم ورود را نمی‌بیند و نمی‌تواند رمز عبور سرقت‌شده‌ای را امتحان کند. در این حالت، حملات credential stuffing عملاً بی‌اثر می‌شوند. آسیب‌پذیری‌های موجود در فرآیند ورود به برنامه نیز برای هر کسی که گواهی معتبر نداشته باشد، غیرقابل‌دسترسی است. همچنین، این روش مشکل به اشتراک‌گذاری رمزهای عبور در محیط‌های چت را حل می‌کند، زیرا کلید خصوصی فایلی است که کپی کردن تصادفی آن دشوار است.

mTLS در برابر کلاینت‌های آلوده هیچ محافظتی ارائه نمی‌دهد. بدافزار موجود روی لپ‌تاپ به فایل کلید دسترسی دارد و به محض اینکه کاربر رمز عبور (passphrase) را وارد کند، آن را در اختیار می‌گیرد. برای سرور، آن مهاجم دقیقاً مانند یک کاربر قانونی به نظر می‌رسد، زیرا گواهی فقط مالکیت یک فایل را اثبات می‌کند، نه حضور فیزیکی یک شخص را. بنابراین، استفاده از .p12 و رمزنگاری کامل دیسک همچنان اهمیت حیاتی دارند.

این پروتکل همچنین جایگزین مجوزدهی (authorization) نیست. هر گواهی معتبری به تمام محتوای آن بلوک سرور دسترسی دارد، مگر اینکه $client_cn را بررسی کرده و بر اساس مقدار آن عمل کنید. به‌طور پیش‌فرض، دو دارنده گواهی دسترسی یکسانی دارند.

علاوه بر این، mTLS فقط مسیر عبور از طریق Nginx را ایمن می‌کند. اگر برنامه شما روی یک پورت عمومی نیز گوش می‌دهد، mTLS در مقابل آن صرفاً جنبه تزئینی دارد: برنامه را به 127.0.0.1 متصل کنید و پورت آن را در فایروال ببندید. راه دیگر ورود به همان سرور، SSH است که نیازمند توجه مشابهی است و در ایمن‌سازی دسترسی SSH روی VPS به آن پرداخته شده است.

یک محدودیت نهایی وجود دارد که دقیقاً در روز فعال‌سازی با آن مواجه می‌شوید: هر سرویسی که نتواند گواهی ارائه دهد، از کار می‌افتد؛ مانند سرویس‌های مانیتورینگ uptime، وب‌هوک‌های درگاه پرداخت، RSS readerها یا اپلیکیشن‌های موبایلی که به مخزن گواهی‌های شما دسترسی ندارند. پیش از تنظیم ssl_verify_client on در مورد این موارد تصمیم‌گیری کنید، زیرا خرابی ناشی از آن کامل است و در سمت آن‌ها، هیچ خطای مشخصی گزارش نمی‌شود.

هنگامی که کلاینت رد می‌شود، گزارش کلاینت را بخوانید

پیامی که کلاینتِ ردشده نمایش می‌دهد به مرورگر، نسخه curl و کتابخانه TLS زیرساختی بستگی دارد؛ بنابراین به‌جای تطبیق آن با پیامی که در جای دیگری نوشته شده است، آنچه کلاینت خودتان چاپ می‌کند را بخوانید. جزئیات مفید در سمت سرور قرار دارد.

sudo tail -n 50 /var/log/nginx/error.log

یک گواهی ردشده، خطی شامل client SSL certificate verify error را به همراه دلیلی که OpenSSL ارائه کرده است، بر جای می‌گذارد. آن دلیل، واقعیتی است که باید بر اساس آن اقدام کنید. معمولاً این مورد یکی از چند مسئله زیر است: گواهی از یک CA متفاوت نسبت به فایلی که در ssl_client_certificate نام‌برده شده صادر شده است؛ گواهی خارج از تاریخ‌های اعتبار خود است؛ یا CRL موجود در سرور از nextUpdate خود عبور کرده است و در نتیجه، به‌جای یک کلاینت، همه کلاینت‌ها را رد می‌کند.

هنگامی که مرورگر اصلاً گواهی ارائه نمی‌دهد، مشکل پیش از مرحله احراز هویت رخ داده است. nginx نام‌های صادرکننده (issuer) قابل‌قبول را در طول handshake ارسال می‌کند و مرورگر چیزی در مخزن خود پیدا نکرده که با آن‌ها مطابقت داشته باشد، بنابراین چیزی برای ارائه به شما نداشته است. .p12 را مجدداً در پروفایلی که واقعاً با آن مرور می‌کنید، وارد (Import) کنید.

یک مورد دیگر که ارزش ذکر کردن دارد: اگر با یک گواهی کلاینت self-signed تکی تست کرده‌اید و نه گواهی‌ای که توسط CA شما امضا شده باشد، احراز هویت نمی‌تواند انجام شود؛ زیرا nginx امضا را با فایل CA تطبیق می‌دهد و یک گواهی self-signed در آن فایل وجود ندارد. مکانیسم ساخت گواهی همانند تولید گواهی self-signed در Ubuntu است. mTLS فقط به یک مرحله اضافی نیاز دارد که در آن CA شما گواهی را امضا کند.

FAQ

آیا در صورت استفاده از mTLS همچنان به گواهی Let's Encrypt نیاز دارم؟

بله. این دو گواهی هیچ ارتباطی به هم ندارند. سرور شما گواهی خود را ارائه می‌دهد تا مرورگر به نام دامنه اعتماد کند و این گواهی همچنان باید از یک CA صادر شده باشد که مرورگر از قبل آن را می‌شناسد. CA کلاینت شما یک زنجیره خصوصی مجزا است که فقط برای بررسی هویت اتصال‌دهنده استفاده می‌شود. تنظیم ssl_client_certificate هیچ تغییری در گواهی ارائه‌شده توسط nginx ایجاد نمی‌کند و نباید به زنجیره Let's Encrypt شما اشاره داشته باشد.

چرا مرورگر من هرگز از من نمی‌خواهد گواهی انتخاب کنم؟

nginx در طول فرآیند handshake لیستی از صادرکنندگان قابل‌قبول را ارسال می‌کند که از فایل موجود در ssl_client_certificate ساخته شده است. مرورگر فقط گواهی‌هایی را پیشنهاد می‌دهد که صادرکننده آن‌ها در آن لیست باشد. بنابراین، عدم نمایش درخواست به این معناست که مرورگر هیچ گواهی‌ای از CA شما در اختیار ندارد: یا وارد پروفایل مرورگر دیگری شده است، یا گواهی توسط CA متفاوتی نسبت به آنچه روی سرور نصب شده، امضا شده است. دستور openssl s_client -connect admin.example.com:443 را اجرا کنید و به دنبال نام‌های CA قابل‌قبول برای گواهی کلاینت در خروجی بگردید تا ببینید سرور در واقع چه CA را درخواست می‌کند.

آیا می‌توانم گواهی کلاینت را فقط برای یک URL خاص الزامی کنم؟

خیر، با استفاده از ssl_verify_client on در داخل یک location این کار ممکن نیست. گواهی در طول handshake و پیش از آنکه nginx مسیر درخواست را بداند مبادله می‌شود؛ همچنین قابلیت renegotiation که می‌توانست این محدودیت را دور بزند، در TLS 1.3 حذف شده و در HTTP/2 ممنوع است. دستور ssl_verify_client optional; را در بلاک server تنظیم کنید، سپس در هر location محافظت‌شده، مقدار $ssl_client_verify را تست کرده و در صورتی که برابر با SUCCESS نبود، خطای 403 برگردانید.

چگونه دسترسی یک نفر را لغو کنم؟

آن گواهی را با openssl ca -revoke باطل کنید، لیست را با openssl ca -gencrl دوباره تولید کنید، آن را به سرور کپی کنید و nginx را reload کنید تا فایل جدید را بخواند. دسترسی سایرین تحت تأثیر قرار نمی‌گیرد؛ این روش تنها در صورتی کار می‌کند که هر شخص گواهی اختصاصی خود را داشته باشد و از گواهی مشترک استفاده نشود. تاریخ nextUpdate مربوط به CRL را زیر نظر داشته باشید، زیرا CRL منقضی‌شده باعث شکست در احراز هویت برای همه کلاینت‌ها می‌شود، نه فقط برای موارد باطل‌شده.

آیا mTLS جایگزین صفحه ورود (login) می‌شود؟

از نظر دسترسی، بله: بدون گواهی، هیچ درخواستی به برنامه نمی‌رسد، بنابراین فرمی برای حمله یا رمز عبوری برای حدس زدن وجود ندارد. اما برای احراز هویت در داخل برنامه، خیر. گواهی فقط ثابت می‌کند که تماس‌گیرنده فایل کلید را در اختیار دارد، بنابراین یک لپ‌تاپ سرقت‌شده به عنوان کاربر معتبر شناخته می‌شود. مقدار CN را به سمت upstream ارسال کنید، حساب‌ها و مجوزهای موجود در برنامه را حفظ کنید و گواهی را به عنوان دروازه‌ای در مقابل آن‌ها در نظر بگیرید.

#tls#mtls#nginx#openssl#access-control