Konfiguracja mTLS w nginx: certyfikaty klienckie
Zabezpiecz panel administracyjny za pomocą mTLS. Poradnik tworzenia własnego CA w openssl, generowania certyfikatów użytkownika oraz konfiguracji dyrektyw ssl_verify_client.
Na czym polega mTLS
Mutual TLS, w skrócie mTLS, wymusza na nginx żądanie certyfikatu od każdego klienta i odrzucenie połączenia, jeśli certyfikat nie istnieje lub nie został wystawiony przez kontrolowany przez administratora urząd certyfikacji (CA). Weryfikacja odbywa się podczas nawiązywania sesji TLS (transport layer security), dzięki czemu klient bez ważnego certyfikatu nie uzyskuje dostępu do aplikacji. Jest to kluczowa zaleta: panel administracyjny lub punkt końcowy metryk może być dostępny w publicznym Internecie bez strony logowania, co eliminuje ryzyko ataków typu brute-force.
Implementacja jest prosta. Wymaga jednego prywatnego CA utworzonego za pomocą openssl, jednego certyfikatu na użytkownika oraz trzech dyrektyw w bloku serwera nginx. Trwałość tego rozwiązania zależy od działań operacyjnych, dlatego większość tego przewodnika poświęcono cyklom życia certyfikatów, ich unieważnianiu, przypisywaniu certyfikatów do użytkowników oraz diagnostyce sytuacji, w których dostęp jest odmawiany, a przyczyna błędu nie jest oczywista.
Dwa łańcuchy, nie jeden
W konfiguracji mTLS występują dwa łańcuchy certyfikatów, które są od siebie całkowicie niezależne. Łączenie ich to pierwszy błąd, który popełnia niemal każdy.
Pierwszy łańcuch należy do serwera. Twój VPS prezentuje certyfikat dla admin.example.com wydany przez publiczny urząd certyfikacji (CA), taki jak Let's Encrypt, a przeglądarka weryfikuje go względem głównego magazynu certyfikatów dostarczanego wraz z systemem operacyjnym. Mechanizm mTLS nie zmienia tej części. Jeśli certbot wydaje obecnie ten certyfikat, zachowaj go bez zmian: zobacz wydawanie certyfikatu Let's Encrypt dla nginx za pomocą certbot.
Drugi łańcuch należy do klienta. Tworzysz własny, niewielki urząd certyfikacji (CA), podpisujesz po jednym certyfikacie dla każdej osoby wymagającej dostępu i instruujesz nginx, aby ufał tylko temu konkretnemu CA podczas weryfikacji klientów. Żaden publiczny magazyn certyfikatów nie zna Twojego CA i nie musi go znać. Jedynym podmiotem, który musi mu ufać, jest nginx, poprzez plik ssl_client_certificate.
Zatem ssl_client_certificate nigdy nie wpływa na certyfikat prezentowany przez nginx, a łańcuch Let's Encrypt nigdy nie wpływa na to, którzy klienci uzyskują dostęp. Wskazanie ssl_client_certificate na fullchain.pem nie działa w sposób, na jaki wskazywałaby nazwa: ta dyrektywa określa wystawców, od których może pochodzić certyfikat klienta, co stanowi drugi koniec połączenia. Sprawienie, by sam serwer ufał Twojemu CA w ramach własnych połączeń wychodzących, to oddzielne zadanie, opisane w dodawanie własnego CA do magazynu zaufanych certyfikatów Ubuntu, a systemowy magazyn zaufania nie jest tym, co nginx odczytuje podczas weryfikacji klienta.
Budowa własnego CA dla klientów przy użyciu openssl
Zbuduj CA w lokalizacji innej niż serwer WWW. nginx wymaga jedynie publicznego certyfikatu CA. Klucz prywatny CA służy do podpisywania nowych certyfikatów klienckich, więc pozostawienie go na maszynie wystawionej na działanie Internetu oznacza, że włamanie daje atakującemu możliwość samodzielnego generowania ważnych certyfikatów klienckich.
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 > crlnumberindex.txt, serial oraz crlnumber stanowią bazę danych CA. openssl ca odmawia działania bez tych plików. Są one również niezbędne do późniejszego unieważniania certyfikatów, ponieważ lista unieważnień zawiera numery seryjne, więc CA musi przechowywać informację o tym, który numer seryjny został przypisany do konkretnego użytkownika.
Utwórz plik ~/client-ca/openssl.cnf. Ustaw dir na rzeczywistą ścieżkę do tego katalogu, ponieważ openssl ca nie rozwija zmiennej ~.
[ 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,issuerTeraz utwórz klucz CA oraz jego certyfikat z podpisem własnym:
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 nakłada hasło na klucz CA, więc każde podpisanie certyfikatu będzie wymagało jego podania. Na tym polega zabezpieczenie. Sprawdź utworzone pliki:
openssl x509 -in ca.crt -noout -subject -dates -ext basicConstraintsPole subject powinno wskazywać na Twoje CA, a ważność certyfikatu powinna wynosić dziesięć lat. Linia rozszerzeń powinna zawierać CA:TRUE, pathlen:0. pathlen:0 oznacza, że to CA może podpisywać certyfikaty końcowe, ale nie może podpisywać innych CA, co utrzymuje łańcuch na głębokości dokładnie jednego poziomu i pozwala pozostawić ssl_verify_depth bez zmian.
Wydawanie certyfikatu klienckiego dla każdej osoby
Jeden certyfikat na osobę. Nigdy nie należy używać współdzielonego certyfikatu dla zespołu, ponieważ takiego certyfikatu nie można unieważnić bez zablokowania dostępu wszystkim użytkownikom, a ponadto nie pozwala on zidentyfikować osoby wywołującej usługę.
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.crtopenssl ca wyświetla certyfikat przeznaczony do podpisania, prosi o hasło CA, wymaga dwukrotnego potwierdzenia, a następnie dopisuje linię do index.txt. W przypadku automatyzacji skryptowej należy dodać -batch. Sekcja client_ext jest istotna ze względu na jedną linię: extendedKeyUsage = clientAuth. Certyfikat zawierający rozszerzone użycie klucza (extended key usage) ograniczające się wyłącznie do serverAuth zostanie odrzucony jako nieprzydatny do uwierzytelniania klienta, dlatego należy jawnie określić przeznaczenie zamiast polegać na domyślnych ustawieniach.
Przed przekazaniem jakichkolwiek plików należy zweryfikować parę względem CA:
openssl verify -CAfile ca.crt certs/alice.crtPolecenie to wyświetla certs/alice.crt: OK. Każdy inny wynik oznacza, że certyfikat i CA nie są ze sobą zgodne, a żadna konfiguracja nginx nie naprawi tego błędu.
Połącz klucz oraz certyfikat w jeden plik, który można zaimportować do przeglądarki:
openssl pkcs12 -export -inkey private/alice.key -in certs/alice.crt \
-name "alice at example ops" -out alice.p12Eksport wymaga podania hasła, które zabezpiecza plik podczas przesyłania. Plik oraz hasło należy wysłać różnymi kanałami komunikacji, a użytkownikom przekazywać plik .p12 zamiast surowego .key. Można dodać -certfile ca.crt, aby dołączyć CA do pakietu, jednak nginx tego nie wymaga: nginx posiada już ca.crt, więc certyfikat podpisany bezpośrednio przez to CA weryfikuje się samodzielnie.
OpenSSL 3, dostarczany w Ubuntu 24.04, zapisuje pliki PKCS#12 przy użyciu współczesnych metod szyfrowania, które są obsługiwane przez przeglądarki i systemy operacyjne używane w sierpniu 2026 roku. Jeśli starszy importer odrzuca plik, należy przeprowadzić eksport ponownie z dodaną flagą -legacy, co wymusi użycie starszych algorytmów oczekiwanych przez to oprogramowanie. Przed użyciem tej flagi należy zapoznać się z komunikatem błędu zwracanym przez importera.
Konfiguracja nginx z użyciem ssl_client_certificate oraz ssl_verify_client
Skopiuj certyfikat CA, i wyłącznie certyfikat CA, na serwer.
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'Uprawnienia 644 są tutaj poprawne. Certyfikat CA jest informacją publiczną. Klucz CA pozostaje na stacji roboczej.
Następnie dodaj trzy dyrektywy do bloku server, który już obsługuje terminację 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 to wartość domyślna nginx, która oznacza, że certyfikat klienta musi być podpisany bezpośrednio przez CA znajdujące się w tym pliku. Zwiększ ją tylko w przypadku dodania certyfikatu pośredniego. nginx wysyła również nazwy podmiotów z ssl_client_certificate do klienta podczas uzgadniania połączenia, dzięki czemu przeglądarka wie, który ze swoich certyfikatów zaoferować. To zachowanie jest powodem, dla którego należy używać ssl_client_certificate zamiast ssl_trusted_certificate, który weryfikuje w ten sam sposób, ale nie wysyła listy.
Ubuntu 24.04 dostarcza nginx 1.24, w którym HTTP/2 definiuje się w linii listen jako listen 443 ssl http2;. W nginx 1.25.1 i nowszych ten zapis jest przestarzały, a HTTP/2 stanowi osobną dyrektywę http2 on;. Żaden z tych wyborów nie wpływa na proces sprawdzania certyfikatu.
Przeładuj konfigurację i sprawdź wynik:
sudo nginx -t && sudo systemctl reload nginx
curl -i https://admin.example.com/nginx -t wyświetla syntax is ok oraz test is successful. Wywołanie curl nie zawiera certyfikatu, więc powinno zwrócić 400 Bad Request wraz z treścią No required SSL certificate was sent. Oznacza to, że nginx odrzuca połączenie na własnym poziomie, co potwierdza, że konfiguracja działa, a żądanie nie dotarło do aplikacji. Teraz wykonaj poprawne zapytanie:
curl --cert certs/alice.crt --key private/alice.key https://admin.example.com/Powinno to zwrócić odpowiedź serwowaną przez aplikację.
Dlaczego dyrektywa musi znajdować się w bloku server
Certyfikat jest wymieniany podczas uzgadniania TLS, zanim nginx odczyta linię żądania, więc w tym momencie nginx nie wie, w którym location wyląduje żądanie. Umieszczenie ssl_verify_client on; wewnątrz location wymusza na kliencie renegocjację w trakcie trwania połączenia. Protokół TLS 1.3 usunął renegocjację, a HTTP/2 jej zabrania, więc w obecnym stosie technologicznym ten wzorzec kończy się błędem zamiast wyświetleniem monitu.
Należy samodzielnie zarządzać zakresem obowiązywania. Należy zażądać certyfikatu na poziomie serwera, a następnie podjąć decyzję dla każdej lokalizacji:
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 przechowuje SUCCESS, lub NONE, gdy klient niczego nie przesłał, albo FAILED: wraz z przyczyną. Dzięki optional nginx żąda certyfikatu i weryfikuje go tylko wtedy, gdy zostanie dostarczony; pozwala to na działanie publicznej ścieżki /healthz, podczas gdy /metrics pozostaje zamknięta. Certyfikat, który został przesłany, ale nie przeszedł weryfikacji, jest w tym momencie odrzucany przez nginx. Jeśli zamiast tego użytkownik chce samodzielnie sprawdzić błędny certyfikat, należy użyć optional_no_ca, a następnie własny test musi potraktować każdą wartość inną niż SUCCESS jako odmowę.
nginx posiada niestandardowe kody statusu dla tej sytuacji, a error_page pozwala je przechwycić, dzięki czemu odrzucony użytkownik otrzymuje wyjaśnienie zamiast surowego błędu 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";
}Kod 495 oznacza, że certyfikat klienta nie przeszedł weryfikacji. Kod 496 oznacza, że klient nie przedstawił żadnego certyfikatu. Należy zachować tę stronę w formacie zwykłego tekstu, ponieważ osoba ją czytająca nie posiada sesji ani konta.
Jak zainstalować certyfikat klienta w przeglądarce?
Firefox korzysta z własnego magazynu certyfikatów: należy przejść do Ustawień, następnie Prywatność i bezpieczeństwo, wybrać Wyświetl certyfikaty, przejść do karty Twoje certyfikaty, kliknąć Importuj, a następnie wskazać plik .p12 i wpisać jego hasło.
Chrome i Edge korzystają z magazynu systemowego w systemach Windows i macOS, dlatego otwarcie pliku .p12 uruchamia systemowy kreator importu. W systemie Linux przeglądarka Chrome odczytuje oddzielną bazę danych NSS (network security services) w katalogu domowym użytkownika, więc najpewniejszą metodą jest użycie narzędzia wiersza poleceń:
sudo apt install -y libnss3-tools
pk12util -d sql:$HOME/.pki/nssdb -i alice.p12Po załadowaniu strony przeglądarka zapyta, który certyfikat wysłać. Chrome zapamiętuje ten wybór do końca sesji przeglądarki, więc w celu ponownego wywołania pytania należy zrestartować aplikację. Certyfikat jest przypisany do jednego profilu przeglądarki na jednym urządzeniu, dlatego certyfikat zaimportowany do przeglądarki Firefox nie jest widoczny w Chrome, a oba są niewidoczne dla telefonu.
Testowanie za pomocą curl --cert
Debugowanie za pomocą curl jest zalecane, ponieważ narzędzie to raportuje wykonane operacje.
curl -v --cert certs/alice.crt --key private/alice.key https://admin.example.com/Można połączyć certyfikat oraz klucz w jeden plik PEM i przekazać go za pomocą --cert alice.pem. Jeśli klucz posiada hasło, curl poprosi o jego podanie. Narzędzie akceptuje również --cert alice.pem:passphrase, jednak powoduje to zapisanie hasła w historii powłoki, dlatego lepiej skorzystać z interaktywnego monitu.
Przed przypisaniem winy serwerowi nginx warto wykonać dwa testy. Po pierwsze, certyfikat i klucz muszą stanowić parę:
openssl x509 -noout -pubkey -in certs/alice.crt | openssl sha256
openssl pkey -pubout -in private/alice.key | openssl sha256Dwa identyczne skróty oznaczają, że pliki do siebie pasują. Dwa różne skróty oznaczają, że pliki zostały pomylone, a żaden klient nie wskaże tej przyczyny wprost.
Po drugie, serwer powinien żądać certyfikatu CA:
openssl s_client -connect admin.example.com:443 -servername admin.example.com </dev/nullNależy wyszukać blok Acceptable client certificate CA names w danych wyjściowych oraz sprawdzić, czy zawiera on podmiot (subject) Twojego CA. Jeśli ten blok w ogóle nie występuje, oznacza to, że nginx nie żąda certyfikatu w bloku serwera, który odpowiedział na zapytanie. Dyrektywy zostały prawdopodobnie umieszczone w innym bloku, często w domyślnym serwerze (default server).
Przekazywanie nazwy CN klienta do aplikacji
Certyfikat informuje, kto wywołał usługę, jednak aplikacja znajdująca się za proxy nie ma wglądu w warstwę TLS, dlatego nginx musi przekazać tę nazwę dalej.
map $ssl_client_s_dn $client_cn {
default "";
"~,?CN=(?<cn>[^,]+)" $cn;
}$ssl_client_s_dn przechowuje nazwę wyróżniającą (distinguished name) podmiotu w formacie RFC 2253, który wygląda jak CN=alice,O=Example Ops. Mapowanie wyodrębnia pole CN do zmiennej $client_cn. Należy zachować CN jako zwykłą nazwę użytkownika, ponieważ przecinek wewnątrz CN jest w tym formacie znakowany sekwencją ucieczki, a powyższe proste wyrażenie regularne nie obsługuje tego znaku.
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 zastępuje każdy nagłówek o tej nazwie wysłany przez wywołującego, dzięki czemu nikt nie może sfałszować X-Client-Cert-CN za pośrednictwem tej lokalizacji. Dwa warunki zapewniają utrzymanie tego stanu. nginx dziedziczy proxy_set_header z poziomu nadrzędnego tylko wtedy, gdy poziom podrzędny nie definiuje żadnego własnego, więc druga lokalizacja z jedną linią proxy_set_header pomija wszystkie nagłówki ustawione powyżej, w tym ten. Ponadto aplikacja musi być nieosiągalna inaczej niż przez nginx, co oznacza powiązanie jej z 127.0.0.1 zamiast 0.0.0.0, ponieważ aplikacja na publicznym porcie odczyta sfałszowany nagłówek bezpośrednio z Internetu. Strona proxy tego zagadnienia została omówiona w wyjaśnieniu konfiguracji reverse proxy nginx linia po linii. Jeśli aplikacja wymaga całego certyfikatu zamiast samej nazwy, $ssl_client_escaped_cert przenosi go w formie zakodowanej URL i bezpiecznie wewnątrz nagłówka.
Jak unieważnić pojedynczy certyfikat klienta?
Pracownik odchodzi lub laptop zostaje zgubiony. Unieważnienie tego jednego certyfikatu pozwala pozostałym użytkownikom na dalszą pracę, co stanowi główny powód wydawania osobnych certyfikatów dla każdej osoby.
cd ~/client-ca
openssl ca -config openssl.cnf -revoke certs/alice.crt
openssl ca -config openssl.cnf -gencrl -out crl.pemPierwsze polecenie zmienia status wiersza z danym numerem seryjnym w pliku index.txt z V na R. Drugie polecenie generuje listę unieważnionych certyfikatów (CRL), czyli podpisany plik zawierający numery seryjne certyfikatów, które straciły ważność. Należy go udostępnić i wskazać w konfiguracji nginx za pomocą dyrektywy ssl_crl /etc/nginx/client-ca.crl; obok pozostałych ustawień.
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'Oto pułapka, która może zablokować dostęp wszystkim użytkownikom. Lista CRL posiada datę nextUpdate, określaną przez parametr default_crl_days, który w powyższej konfiguracji wynosi 30. Po przekroczeniu tej daty OpenSSL uznaje listę za przestarzałą i odrzuca weryfikację każdego certyfikatu klienta z błędem CRL has expired, a nie tylko tego unieważnionego. nginx wczytuje plik podczas ładowania konfiguracji, więc samo nadpisanie pliku CRL na dysku nie przyniesie efektu bez przeładowania usługi. Należy generować i przeładowywać listę w harmonogramie zapewniającym odpowiedni margines czasowy (na przykład co tydzień przy 30-dniowym okresie ważności) oraz sprawdzać daty przed skopiowaniem pliku:
openssl crl -in crl.pem -noout -lastupdate -nextupdateDla niewielkiej liczby użytkowników istnieje prostsze rozwiązanie. Ponieważ CA należy do administratora, nginx może bezpośrednio odrzucić konkretny numer seryjny, pomijając mechanizm CRL:
map $ssl_client_serial $revoked {
default 0;
"1002" 1;
}Należy to połączyć z dyrektywą if ($revoked) { return 403; } w bloku location. To rozwiązanie nie posiada daty wygaśnięcia, o której można zapomnieć. Nie jest też przenoszone, więc inne systemy ufające temu CA nie otrzymają informacji o tym unieważnieniu. W przypadku jednego serwera nginx przed jedną aplikacją jest to najbardziej przejrzyste rozwiązanie. Do mechanizmu CRL warto przejść dopiero wtedy, gdy liczba punktów dostępowych wzrośnie.
Jak długo powinny być ważne certyfikaty klienckie?
Certyfikaty klienckie należy wydawać na rok lub krócej, jeśli nakład pracy związany z ponownym wydawaniem jest akceptowalny. Wygasanie certyfikatu jest awarią cichą, ponieważ użytkownik nie otrzymuje wcześniejszych ostrzeżeń. Po otwarciu panelu nginx odrzuci połączenie, a przeglądarka wyświetli komunikat o błędzie, który rzadko zawiera informację o wygaśnięciu. Okres ważności CA należy ustawić na 10 lat, a datę wygaśnięcia zapisać w miejscu, które będzie regularnie sprawdzane. Gdy certyfikat CA wygaśnie, wszystkie certyfikaty podrzędne przestaną być weryfikowane tego samego dnia.
Dwa polecenia pozwalają zachować kontrolę nad tym procesem:
openssl x509 -in certs/alice.crt -noout -subject -serial -enddate
awk -F'\t' '{print $1, $2, $4}' ~/client-ca/index.txtPierwsza kolumna pliku index.txt określa status: V oznacza ważny, R unieważniony, a E wygasły. Druga kolumna zawiera datę wygaśnięcia w formacie YYMMDDHHMMSSZ, a czwarta numer seryjny. Ten plik jest jedynym rejestrem posiadaczy certyfikatów, dlatego należy go archiwizować wraz z kluczem CA i traktować oba elementy jako dane poufne.
Odnowienie polega na wydaniu nowego certyfikatu, a nie na przedłużeniu ważności starego. Należy wygenerować nowy klucz oraz CSR (certificate signing request), podpisać go, przekazać użytkownikowi, a po potwierdzeniu poprawnego działania nowego certyfikatu, unieważnić stary.
Przed czym chroni mTLS, a przed czym nie
mTLS eliminuje nieautoryzowany dostęp. Skaner, który odnajdzie nazwę hosta, zostaje odrzucony już na etapie uzgadniania połączenia (handshake). Nie wysyła więc żądania HTTP, nie widzi formularza logowania i nie może przetestować skradzionego hasła. Ataki typu credential stuffing stają się bezużyteczne. Luka w mechanizmie logowania aplikacji jest niedostępna dla nikogo, kto nie posiada certyfikatu. Rozwiązanie to eliminuje również problem współdzielonych haseł przesyłanych na komunikatorach, ponieważ klucz prywatny jest plikiem, którego nie da się przypadkowo skopiować.
mTLS nie chroni przed zainfekowanym klientem. Złośliwe oprogramowanie na laptopie ma dostęp do pliku klucza oraz do hasła w momencie, gdy użytkownik je wpisuje. Dla serwera atakujący wygląda jak uprawniony użytkownik, ponieważ certyfikat potwierdza posiadanie pliku, a nie obecność człowieka. Hasło .p12 oraz szyfrowanie całego dysku nadal mają kluczowe znaczenie.
Mechanizm ten nie zastępuje autoryzacji. Każdy posiadacz ważnego certyfikatu uzyskuje dostęp do wszystkiego, co serwuje dany blok serwera, chyba że sprawdzisz $client_cn i podejmiesz odpowiednie kroki. Domyślnie wszyscy posiadacze certyfikatów mają identyczne uprawnienia.
mTLS zabezpiecza jedynie ścieżkę przez Nginx. Jeśli aplikacja nasłuchuje również na publicznym porcie, mTLS przed nią jest tylko dekoracją: powiąż aplikację z 127.0.0.1 i zamknij port na firewallu. Drugą drogą dostępu do tego samego serwera jest SSH, które wymaga takiej samej uwagi, co opisano w zabezpieczanie dostępu SSH na VPS.
Ostatnie ograniczenie ujawnia się w dniu wdrożenia. Każde narzędzie, które nie potrafi przedstawić certyfikatu, przestaje działać: monitor dostępności, webhook od dostawcy płatności, czytnik RSS czy aplikacja mobilna bez dostępu do magazynu certyfikatów. Decyzje w tej sprawie należy podjąć przed ustawieniem ssl_verify_client on, ponieważ awaria będzie całkowita i – z perspektywy zewnętrznych usług – niewidoczna.
Gdy klient otrzymuje odmowę, sprawdź zgłaszany przez niego komunikat
Komunikat wyświetlany przez odrzuconego klienta zależy od przeglądarki, wersji curl oraz użytej biblioteki TLS. Z tego powodu należy analizować dane wyjściowe własnego klienta, zamiast porównywać je z opisami znalezionymi w innych źródłach. Istotne szczegóły znajdują się po stronie serwera.
sudo tail -n 50 /var/log/nginx/error.logOdrzucony certyfikat powoduje zapisanie wiersza zawierającego client SSL certificate verify error, po którym następuje przyczyna podana przez OpenSSL. To właśnie na tę przyczynę należy zareagować. Zazwyczaj problem sprowadza się do kilku kwestii. Certyfikat pochodzi z innego urzędu certyfikacji (CA) niż ten wskazany w pliku ssl_client_certificate. Certyfikat wykracza poza daty swojej ważności. Lista CRL na serwerze przekroczyła swój nextUpdate, przez co odrzuca wszystkich klientów zamiast jednego.
Jeśli przeglądarka w ogóle nie oferuje certyfikatu, problem występuje przed etapem weryfikacji. nginx wysyła akceptowalne nazwy wystawców podczas uzgadniania (handshake), a przeglądarka nie znalazła w swoim magazynie niczego pasującego, więc nie miała czego zaoferować. Zaimportuj ponownie .p12 do profilu, z którego faktycznie korzystasz podczas przeglądania.
Warto wspomnieć o jeszcze jednym przypadku. Jeśli testy przeprowadzono z użyciem pojedynczego certyfikatu klienta z podpisem własnym (self-signed), zamiast certyfikatu podpisanego przez własne CA, weryfikacja nie może się powieść. nginx sprawdza podpis względem pliku CA, a certyfikat z podpisem własnym nie znajduje się w tym pliku. Mechanika tworzenia certyfikatu jest taka sama jak w generowaniu certyfikatu z podpisem własnym w systemie Ubuntu. mTLS wymaga jedynie dodatkowego kroku, w którym Twoje CA podpisuje certyfikat.
FAQ
Czy nadal potrzebuję certyfikatu Let's Encrypt, jeśli używam mTLS?
Tak. Te dwa certyfikaty nie są ze sobą powiązane. Serwer prezentuje własny certyfikat, aby przeglądarka mogła zweryfikować nazwę hosta, więc musi on pochodzić z urzędu certyfikacji (CA) zaufanego przez przeglądarkę. Twój CA dla klientów to osobny, prywatny łańcuch, służący wyłącznie do sprawdzania tożsamości łączącego się urządzenia. Ustawienie ssl_client_certificate nie zmienia certyfikatu prezentowanego przez nginx i nie może wskazywać na łańcuch Let's Encrypt.
Dlaczego przeglądarka nie prosi mnie o wybór certyfikatu?
Podczas nawiązywania połączenia nginx wysyła listę akceptowalnych wystawców, zbudowaną na podstawie pliku w ssl_client_certificate. Przeglądarka oferuje tylko te certyfikaty, których wystawca znajduje się na tej liście. Brak monitu oznacza, że w przeglądarce nie ma certyfikatu z Twojego CA: został zaimportowany do innego profilu lub certyfikat został podpisany przez inny urząd niż ten zainstalowany na serwerze. Uruchom openssl s_client -connect admin.example.com:443 i sprawdź w danych wyjściowych nazwy akceptowalnych urzędów CA, aby zweryfikować, o jaki certyfikat serwer faktycznie pyta.
Czy mogę wymagać certyfikatu klienta tylko dla jednego adresu URL?
Nie przy użyciu ssl_verify_client on wewnątrz location. Certyfikat jest wymieniany podczas nawiązywania połączenia, zanim nginx pozna ścieżkę żądania, a renegocjacja, która mogłaby to obejść, została usunięta w TLS 1.3 i jest zabroniona w HTTP/2. Ustaw ssl_verify_client optional; w bloku serwera, a następnie w każdej chronionej lokalizacji przetestuj $ssl_client_verify i zwróć błąd 403, jeśli wartość nie jest równa SUCCESS.
Jak cofnąć dostęp dla jednej osoby?
Unieważnij certyfikat za pomocą openssl ca -revoke, wygeneruj ponownie listę openssl ca -gencrl, skopiuj ją na serwer i przeładuj nginx, aby wczytał nowy plik. Pozostali użytkownicy nie odczują zmian, pod warunkiem, że każda osoba posiada własny certyfikat, a nie współdzielony. Monitoruj datę nextUpdate listy CRL, ponieważ wygasła lista CRL powoduje odrzucenie wszystkich klientów, a nie tylko tych unieważnionych.
Czy mTLS zastępuje stronę logowania?
W kwestii dostępności – tak: bez certyfikatu żądanie w ogóle nie dociera do aplikacji, więc nie ma formularza do ataku ani hasła do odgadnięcia. W kwestii tożsamości wewnątrz aplikacji – nie. Certyfikat potwierdza jedynie, że osoba wywołująca posiada plik klucza, więc skradziony laptop jest traktowany jako uprawniony użytkownik. Przekazuj wartość CN do backendu, zachowaj istniejące w aplikacji konta i uprawnienia, a certyfikat traktuj jako bramę zabezpieczającą dostęp do nich.