Samopodpisany certyfikat Ubuntu Chrome akceptuje
Jedno polecenie openssl z SAN, konfiguracja nginx/Apache i zaufanie bez curl -k na Ubuntu 24.04. Działa w Chrome i nowoczesnych przeglądarkach.
Co budujesz
Samopodpisany certyfikat TLS, który akceptują współczesne przeglądarki i klienci, z poprawnym subjectAltName, właściwymi uprawnieniami klucza, podłączony do nginx lub Apache, oraz element, który pomija prawie każdy poradnik: sprawienie, by klienci ufali mu prawidłowo, zamiast klikać ostrzeżenia i na stałe wpisywać curl -k do skryptów. Na końcu prywatny urząd certyfikacji w pięciu poleceniach, na wypadek gdy z jednej usługi wewnętrznej zrobi się sześć.
Najpierw decyzja, ponieważ samopodpisany certyfikat jest właściwym narzędziem znacznie rzadziej, niż jest używany. Jeśli usługa jest dostępna z publicznego internetu pod prawdziwą nazwą DNS, należy przerwać lekturę i zamiast tego uzyskać darmowy certyfikat Let's Encrypt z certbotem na nginx lub odpowiednik dla Apache. Nic nie kosztuje, odnawia się sam, a każda przeglądarka na świecie już mu ufa. Samopodpisany certyfikat na publicznej stronie uczy użytkowników klikać ostrzeżenia bezpieczeństwa, co jest gorszym nawykiem niż zwykły HTTP.
Samopodpisany jest właściwym narzędziem, gdy w grę nie wchodzi publiczny internet: panel administracyjny dowiązany do adresu tunelu WireGuard na VPS, serwer testowy w sieci prywatnej, ruch między usługami zaplecza, urządzenie w laboratorium domowym lub zastąpienie zastępczego certyfikatu, który Webmin generuje dla siebie na porcie 10000. Let's Encrypt i tak nie może wystawić certyfikatu dla 10.8.0.1 ani git.internal.lan, żaden publiczny urząd certyfikacji nie umieści prywatnego adresu IP ani wymyślonej domeny najwyższego poziomu w certyfikacie. Dla tych nazw to użytkownik jest urzędem certyfikacji.
Wszystko poniżej uruchamiane jest na świeżej instancji Ubuntu 24.04, która dostarcza OpenSSL 3.0.x (openssl version w celu potwierdzenia). Nic tutaj nie wymaga dostępu do internetu; wszystko działa w izolacji sieciowej.
Dlaczego stara jednolinijkowa komenda generuje certyfikaty odrzucane przez Chrome
Komenda, którą podaje każdy poradnik sprzed 2017 roku, wygląda tak:
# 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.crtZadaje serię interaktywnych pytań, umieszcza nazwę hosta w polu Common Name i tworzy certyfikat bez rozszerzenia subjectAltName. Taki certyfikat jest martwy od razu. Chrome przestał odczytywać Common Name w wersji 58, w kwietniu 2017 roku, RFC 2818 wycofał dopasowywanie CN już w roku 2000, a Firefox, Safari, curl i Python zachowują się tak samo. Certyfikat identyfikuje swój serwer przez rozszerzenie SAN albo nie identyfikuje go wcale, a przeglądarka informuje o tym dokładnie tymi słowami:
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.Żadne manipulacje przy magazynie zaufania nie naprawią tego błędu, ponieważ certyfikat faktycznie niczego nie nazywa. Jeśli widzisz teraz komunikat NET::ERR_CERT_COMMON_NAME_INVALID, Twój certyfikat nie ma SAN (albo ma błędny) i musisz wygenerować nowy. Na szczęście rozwiązaniem jest jedna komenda.
Wystaw certyfikat akceptowany przez przeglądarki: jedno polecenie
OpenSSL zyskał flagę -addext w wersji 1.1.1, co oznacza, że nie są już potrzebne gimnastyki z plikiem konfiguracyjnym, które stare poradniki stosowały do wstrzykiwania SAN. Na 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"Znaczenie poszczególnych flag:
-x509generuje bezpośrednio certyfikat z podpisem własnym zamiast żądania podpisania.-newkey rsa:4096tworzy świeży klucz w tym samym kroku. RSA 4096 nie sprawia problemów żadnemu starszemu klientowi; jeśli wszystko, co się łączy, jest nowoczesne,-newkey ec -pkeyopt ec_paramgen_curve:P-256jest mniejsze i szybsze.-noencto zapis w OpenSSL 3.x starej flagi-nodes: brak hasła do klucza. Oba zapisy działają. Klucz z hasłem powoduje, że nginx zawiesza się, czekając na dane wejściowe przy każdym uruchomieniu, więc dla klucza serwera jest to pożądane.-days 730, dwa lata; więcej o tej liczbie w sekcji o wygaśnięciu.-subjodpowiada na pytania interaktywne w linii. CN jest obecnie kwestią kosmetyczną, ale należy je ustawić na nazwę główną; niektóre narzędzia ją wyświetlają.-addext "subjectAltName=..."to flaga kluczowa. Należy wymienić każdą nazwę i każdy adres IP, który wpiszą klienci: wpisyDNS:dla nazw hostów (wieloznaczniki jakDNS:*.internal.lansą dozwolone), wpisyIP:dla adresów. Jeśli ktokolwiek będzie przeglądałhttps://10.8.0.1, wpisIP:10.8.0.1musi być obecny; SAN zawierający tylko DNS ponownie wywołuje błądNET::ERR_CERT_COMMON_NAME_INVALID.
Przed podłączeniem czegokolwiek należy potwierdzić, że SAN faktycznie się znalazł:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltNamePoprawne wyjście:
X509v3 Subject Alternative Name:
DNS:git.internal.lan, IP Address:10.8.0.1Jeśli zamiast tego wypisze No extensions in certificate, certyfikat nie ma SAN i przeglądarki go odrzucą; należy wygenerować go ponownie, zamiast kontynuować.
Zabezpieczenie klucza
Klucz prywatny czytelny dla każdego użytkownika w systemie nie jest kluczem prywatnym. W Ubuntu /etc/ssl/private ma już ustawione 710 root:ssl-cert, co chroni przed przypadkowym dostępem, ale należy jawnie ustawić uprawnienia samego pliku:
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keynginx i Apache odczytują certyfikaty jako root przed zrzuceniem uprawnień, więc root:root tryb 600 jest dla nich odpowiedni. Jeśli klucz jest przeznaczony dla usługi działającej jako własny użytkownik i ładującej klucz samodzielnie, aplikacji Node, Gitea, demona Pythona, należy chown go na tego użytkownika usługi, nadal z trybem 600. Czego nigdy nie robić: tryb 644, kopia w repozytorium git ani kopia w /tmp.
Podłączenie do 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 nginxnginx -t musi wyświetlić syntax is ok oraz test is successful, zanim ponowne przeładowanie konfiguracji przyniesie jakikolwiek skutek. Jeśli zamiast tego wyświetli SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, certyfikat i klucz pochodzą z dwóch różnych przebiegów generowania — szczegóły w sekcji dotyczącej trybów awarii.
Podłączenie do Apache
sudo a2enmod ssl proxy proxy_httpSam ssl nie wystarczy: poniższy vhost używa ProxyPass, a bez mod_proxy i mod_proxy_http test konfiguracji kończy się błędem Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Zapisz vhost jako /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 apache2configtest powinien odpowiedzieć Syntax OK. Teraz przetestuj z maszyny klienckiej:
curl -v https://git.internal.lan/i otrzymasz błąd:
curl: (60) SSL certificate problem: self-signed certificateTo nie jest błąd. To działanie TLS: curl nigdy nie słyszał o Twoim certyfikacie i odmawia komunikacji z serwerem, którego nie może uwierzytelnić. Następna sekcja to właściwe rozwiązanie, i nie jest nim to, co w tym momencie robi połowa internetu.
Jak sprawić, by klienci ufali certyfikatowi, oraz antywzorce, których należy odmówić
Najpierw błędne rozwiązania, nazwane wprost. curl -k (lub --insecure) wstawione do skryptu, verify=False w Python requests, NODE_TLS_REJECT_UNAUTHORIZED=0 w Node – żadne z nich nie sprawia, że certyfikat staje się zaufany. Wyłączają one weryfikację certyfikatu, co oznacza, że klient bez problemu połączy się z dowolnym serwerem przedstawiającym jakikolwiek certyfikat, w tym taki, który podstawił atakujący. Zachowuje się narzut TLS, tracąc uwierzytelnienie, które było jego celem. Gorzej, flagi te ulegają przerzutom: wklejone do jednego zadania crona, potem do skryptu wdrożeniowego, następnie do kodu produkcyjnego, aż nikt nie pamięta, które połączenia miały być tymczasowe. Jeśli verify=False przetrwa sesję debugowania, która go zrodziła, projekt jest błędny.
Właściwym rozwiązaniem jest nauczenie systemu operacyjnego każdego klienta, że ten certyfikat jest zaufanym urzędem głównym. Na klientach z Ubuntu i Debianem:
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificatesWiersz, który ma znaczenie w wyniku (następuje po nim blok Running hooks in /etc/ca-certificates/update.d...):
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.W tych wierszach kryją się dwie pułapki. Plik musi kończyć się rozszerzeniem .crt, rozszerzenie .pem jest po cichu ignorowane i otrzymuje się 0 added bez komunikatu błędu. Zawartość musi być w formacie PEM, plik zaczyna się od -----BEGIN CERTIFICATE-----; binarny DER należy najpierw przekonwertować za pomocą openssl x509 -inform der -in file.der -out file.crt. Dodanie samego certyfikatu z podpisem własnym jako urzędu głównego działa, ponieważ certyfikat z podpisem własnym jest swoim własnym urzędem głównym.
Po wykonaniu tej czynności curl, wget, git, apt i wszystko inne, co używa OpenSSL względem systemowego pakietu, ufa serwerowi bez żadnych flag. Kilka klientów posiada własne magazyny zaufania i wymaga indywidualnego traktowania:
- Chrome/Chromium na Linuksie odczytuje bazę NSS, a nie magazyn systemowy:
sudo apt install libnss3-tools, następniecertutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtdla każdego użytkownika. - Firefox ma własny magazyn: Ustawienia → Prywatność i bezpieczeństwo → Certyfikaty → Importuj lub zmień
security.enterprise_roots.enablednatruewabout:config, aby odczytywał magazyn systemowy. - Python requests dostarcza własny pakiet CA (certifi) i ignoruje magazyn systemowy: należy przekazać
verify="/usr/local/share/ca-certificates/git.internal.crt"lub wyeksportowaćREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt. - Node.js: należy wyeksportować
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.
Na klientach Windows należy dwukrotnie kliknąć .crt i zainstalować go w magazynie Zaufane główne urzędy certyfikacji; na macOS dodać go do pęku kluczy System w Dostępie do pęku kluczy i oznaczyć jako Zawsze ufaj.
Jeden główny urząd certyfikacji dla wielu usług: mały prywatny CA
Zaufanie oparte na pojedynczych certyfikatach szybko przestaje się skalować: sześć usług razy cztery maszyny klienckie daje dwadzieścia cztery instalacje zaufania, a każda nowa usługa dodaje kolejne. Rozwiązaniem jest prywatny CA – klienci ufają jednemu głównemu certyfikatowi, a każdy certyfikat usługi jest nim podpisywany.
Przyjazną opcją jest mkcert, które znajduje się w repozytoriach Ubuntu 24.04 i obsługuje magazyny NSS (Chrome, Firefox), pomijane przez update-ca-certificates:
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -install tworzy główny certyfikat i rejestruje go we wszystkich magazynach zaufania na danej maszynie; trzecie polecenie generuje git.internal.lan+2.pem oraz git.internal.lan+2-key.pem, gotowe do wstawienia w przytoczone wcześniej fragmenty konfiguracji nginx lub Apache. Założeniem projektowym jest maszyna deweloperska – klucz główny pozostaje na tym samym komputerze, na którym uruchomiono -install, co jest idealne dla laptopa programisty i nieodpowiednie dla floty serwerów.
W przypadku serwerów zwykły OpenSSL realizuje cały proces CA za pomocą pięciu poleceń:
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.crtPułapka kryje się w ostatnim poleceniu: openssl x509 -req domyślnie odrzuca wszystkie rozszerzenia z CSR, w tym starannie dodany SAN. -copy_extensions copy (opcja dostępna w OpenSSL 3.x, działa więc na 24.04) przenosi je; pominięcie jej skutkuje podpisanym certyfikatem bez SAN, a Chrome ponownie wita komunikatem NET::ERR_CERT_COMMON_NAME_INVALID. Należy to zweryfikować za pomocą tego samego sprawdzenia openssl x509 -noout -ext subjectAltName, co poprzednio.
lab-ca.crt dystrybuuje się do klientów, wykonując opisane wyżej kroki dla magazynu zaufania – jednorazowo na każdą maszynę. lab-ca.key należy chronić jak najcenniejszy skarb: tryb 600, najlepiej przechowywany na komputerze, który nie jest jednym z serwerów, dla których wystawia certyfikaty, ponieważ każdy, kto go posiada, może wygenerować certyfikat dla dowolnej nazwy, której zaufają klienci.
Wygaśnięcie i rotacja
Okresy ważności certyfikatów publicznych urzędów certyfikacji ulegają skróceniu. CA/Browser Forum ograniczyło nowo wydawane certyfikaty zaufane publicznie do 200 dni w marcu 2026 roku (z 398 dni), z dalszym skróceniem do 100 dni w 2027 roku i 47 dni do marca 2029 roku. Reguły te dotyczą jednak wyłącznie publicznie zaufanych urzędów certyfikacji. Prywatny urząd certyfikacji nie podlega tym ograniczeniom, a przeglądarki nie egzekwują ich wobec ręcznie zainstalowanych certyfikatów głównych. Obowiązuje jedno rzeczywiste ograniczenie: platformy Apple odrzucają każdy certyfikat serwera TLS ważny dłużej niż 825 dni, niezależnie od wystawcy. Jeśli więc połączenia będą nawiązywane z iPhone'ów lub komputerów Mac, certyfikaty końcowe należy utrzymywać na poziomie dwóch lat lub krótszym. -days 730 spełnia to kryterium wszędzie. Dziesięcioletni certyfikat główny z dwuletnimi certyfikatami końcowymi stanowi wygodną konfigurację wewnętrzną.
Długoterminowe certyfikaty zawodzą w jeden konkretny sposób: po cichu, wszystkie naraz, w dniu, którego nikt nie pamięta. Należy sprawdzić stan posiadanych certyfikatów:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddateOdnowienie należy wpisać do kalendarza lub skonfigurować powiadomienie z crona na 30 dni przed terminem. openssl x509 -checkend 2592000 -in cert.crt zwraca niezerowy kod wyjścia, gdy do wygaśnięcia pozostaje mniej sekund niż podana wartość. Jeśli używane jest już narzędzie Uptime Kuma do monitorowania statusu, jego monitory HTTPS bezpłatnie sygnalizują zbliżające się wygaśnięcie certyfikatu.
Rotacja z prywatnym urzędem certyfikacji jest przyjemnie prosta: należy ponownie wykonać polecenia CSR i podpisywania, podmienić pliki i przeładować serwer WWW. Certyfikat główny nie uległ zmianie, więc żaden klient nie zauważa różnicy.
Tryby awarii wraz z komunikatami, które zostaną wyświetlone
NET::ERR_CERT_AUTHORITY_INVALID, stan oczekiwany przed instalacją zaufania, a nie wada certyfikatu. Jeśli utrzymuje się po instalacji certyfikatu głównego: w systemie Linux Chrome odczytuje NSS zamiast magazynu systemowego (patrz krok certutil); lub skopiowany plik nie miał rozszerzenia .crt, a update-ca-certificates zgłosił 0 added; lub serwer przedstawia inny certyfikat niż ten, któremu zaufano – należy porównać odciski za pomocą openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256.
NET::ERR_CERT_COMMON_NAME_INVALID, certyfikat nie zawiera SAN lub SAN nie obejmuje nazwy z paska adresu. Klasyczny przypadek: SAN zawiera wpis DNS:git.internal.lan, ale użytkownik przeszedł pod adres https://10.8.0.1. Zmiany w magazynie zaufania nie rozwiążą tego problemu; konieczne jest ponowne wystawienie certyfikatu z brakującym wpisem.
curl: (60) SSL certificate problem: self-signed certificate, curl nie ufa certyfikatowi. Wariant self-signed certificate in certificate chain oznacza to samo dla certyfikatu podpisanego przez prywatny urząd CA. Obejście jednorazowe: curl --cacert lab-ca.crt https://...; rozwiązanie trwałe: magazyn zaufania. Nie -k.
unable to load certificate ... Expecting: TRUSTED CERTIFICATE (lub Expecting: CERTIFICATE REQUEST, lub no start line), nieprawidłowy format PEM. Do OpenSSL przekazano plik niewłaściwego typu: klucz lub CSR tam, gdzie oczekiwano certyfikatu, albo plik binarny DER tam, gdzie oczekiwano PEM. head -1 filename informuje, co faktycznie znajduje się w pliku; certyfikat zaczyna się od -----BEGIN CERTIFICATE-----. Dla formatu DER należy dokonać konwersji za pomocą openssl x509 -inform der -in file.der -out file.crt.
nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch), certyfikat i klucz nie pasują do siebie, zwykle dlatego, że polecenie generowania zostało uruchomione dwukrotnie i pliki uległy pomieszaniu. Należy to potwierdzić, porównując openssl x509 -in git.internal.crt -noout -pubkey | sha256sum z openssl pkey -in git.internal.key -pubout | sha256sum; zgodne skróty oznaczają pasującą parę. Jeśli się różnią, należy ponownie wygenerować oba jednocześnie.
FAQ
Dlaczego Chrome nadal wyświetla komunikat „Niebezpieczna” po utworzeniu certyfikatu z podpisem własnym?
Jeśli błąd to NET::ERR_CERT_AUTHORITY_INVALID, certyfikat jest prawidłowy, Chrome po prostu nie ma jeszcze powodu, by mu ufać. Zainstaluj go (lub główny urząd prywatnego CA) w magazynie zaufania klienta i pamiętaj, że w systemie Linux Chrome korzysta z bazy NSS za pośrednictwem certutil, a nie z magazynu systemowego. Jeśli błąd to NET::ERR_CERT_COMMON_NAME_INVALID, certyfikat nie zawiera nazwy alternatywnej podmiotu (Subject Alternative Name) zgodnej z adresem URL i musi zostać ponownie wystawiony z -addext "subjectAltName=...".
Jak sprawić, by curl ufał certyfikatowi z podpisem własnym bez użycia -k?
Skopiuj certyfikat (format PEM, rozszerzenie .crt) do /usr/local/share/ca-certificates/ i uruchom sudo update-ca-certificates, wynik musi wskazywać 1 added. Od tego momentu curl weryfikuje go jak każdy certyfikat publiczny. W przypadku jednorazowego żądania bez modyfikowania ustawień systemowych, curl --cacert /path/to/cert.crt weryfikuje wyłącznie względem tego pliku; -k całkowicie wyłącza weryfikację i nie powinno go być w żadnych skryptach.
Jak długo może być ważny certyfikat z podpisem własnym?
Technicznie rzecz biorąc, tak długo, jak sobie życzysz; limity CA/Browser Forum (obecnie 200 dni, 47 do 2029 roku) wiążą publicznie zaufane urzędy certyfikacji, a nie zaufanie prywatne. W praktyce należy ograniczyć ważność certyfikatów serwerowych do 825 dni, ponieważ urządzenia Apple odrzucają wszystkie dłuższe, niezależnie od wystawcy. Dziesięcioletni klucz główny prywatnego urzędu z dwuletnimi (-days 730) certyfikatami końcowymi to rozsądna wartość domyślna; wystarczy zapisać w kalendarzu termin odnowienia, ponieważ wygasły certyfikat wewnętrzny powoduje ciche zatrzymanie wszystkiego w dniu, którego nikt nie pamięta.
Czy powinienem użyć certyfikatu z podpisem własnym czy Let's Encrypt?
Jeśli usługa ma publiczną nazwę DNS i jest osiągalna z internetu, zawsze Let's Encrypt – darmowy, zautomatyzowany, już zaufany przez każdego klienta. Certyfikat z podpisem własnym (lub prywatny urząd certyfikacji) jest przeznaczony do zastosowań, dla których Let's Encrypt nie może wystawić certyfikatu: prywatne adresy IP, nazwy hostów wyłącznie wewnętrzne, takie jak .lan, sieci odizolowane oraz usługi celowo ukryte za siecią VPN. Decyzja dotyczy osiągalności i nazewnictwa, a nie siły zabezpieczeń – kryptografia jest identyczna.
Dlaczego mój certyfikat jest odrzucany nawet po dodaniu go do /usr/local/share/ca-certificates?
Sprawdź trzy kwestie. Plik musi mieć rozszerzenie .crt, rozszerzenie .pem jest pomijane bez komunikatu, a update-ca-certificates zgłasza 0 added. Zawartość musi być tekstem PEM zaczynającym się od -----BEGIN CERTIFICATE-----, a nie binarnym DER. Ponadto aplikacja musi faktycznie korzystać z magazynu systemowego – Chrome na Linuksie, Firefox, Python requests, Node.js i Java przechowują własne, prywatne magazyny zaufania i wymagają oddzielnego dodania certyfikatu.