Jak skonfigurować SSL w lokalnym developmencie
Ustawienie połączenia https na domenie localhost może być wyzwaniem jeśli robimy to pierwszy raz. Ten wpis jest bardzo szczegółowym tutorialem ze wszystkimi komendami i screenshotami.
Ustawianie certyfikatu ssl podczas developmentu może być poważnym wyzwaniem. Nie wynika to ze złożoności tego zadania, ale z pułapek, w jakie można wpaść jeśli posiada się luki w wiedzy na temat sieci, certyfikatów i protokołu https.
W tym wpisie pokażę jak krok po kroku przejść przez proces ustawiania lokalnego developmentu z https. Zrobimy przy tym kilka dygresji dotyczących problemów jakie mogą się pojawić.
W tym wpisie opisuję jak nadpisać zewnętrzne DNS, utworzyć organizację certyfikującą, przygotować żądanie utworzenia certyfikatu dla domeny, spełnić je, zaufać tej organizacji, skonfigurować serwer nginx do proxowania ruchu i używania certyfikatu domeny i finalnie cieszyć się połączeniem https.
Lokalna domena
Zaczniemy od podstaw. Czyli lokalnego przekierowania domeny na nasz lokalny komputer. Zwykle kiedy pytamy przeglądarki o domenę serwery DNS ustawione w naszym komputerze dostarczają na numer IP na który należy wysłać żądanie.
Ustawienia DNS
dla naszego komputera możemy sprawdzić w pliku resolv.conf
cat /etc/resolv.conf
Nie zawsze jednak musimy pytać o IP serwerów DNS
. Zapytania do zewnętrznych serwerów DNS możemy przesłonić naszymi wpisami w pliku /etc/hosts
.
Zawartość tego pliku może u Ciebie wyglądać tak:
# Static table lookup for hostnames.
# See hosts(5) for details.
127.0.0.1 localhost
::1 localhost
127.0.1.1 hp-1589
Aby dodać do niego naszą domenę musimy edytować plik hosts
sudo nvim /etc/hosts
Dodajemy na końcu linię:
127.0.0.1 local.dev
Aby przetestować tą konfigurację napiszemy prostą stronę w php
. Do pliku index.php
zapisujemy:
<?php
header('Content-Type: application/json');
echo '{"status":"ok"}';
Możemy ją hostować poleceniem
php -S localhost:8000 index.php
Naturalnie polecenie:
http http://localhost:8000/
zwróci nam {"status": "ok"}
. Niestety zapytanie o domenę:
http http://local.dev:8000/
pokarze błąd:
http: error: ConnectionError: HTTPConnectionPool(host='local.dev', port=8000): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fbaa2dfcc40>: Failed to establish a new connection: [Errno 111] Connection refused')) while doing a GET request to URL: http://local.dev:8000/
Domena kieruje nas w odpowiednie miejsce co może potwierdzić getent
getent hosts local.dev
127.0.0.1 local.dev
Problem leży w ograniczeniu hostów na których jest ustawiony serwer.
Pułapka 1: sprawdź jakiego hosta ma twój lokalny server.
W komendzie php -S localhost:8000 index.php
nie powinniśmy używać localhost
. Jest to częsty przypadek również w innych językach, gdzie frameworki serwują domyślnie na hoście localhost a powinny na 0.0.0.0
.
Aby naprawić problem wyłączamy serwer i stawiamy go komendą
php -S 0.0.0.0:8000 index.php
Tym razem działa on poprawnie.
Dlaczego tak jest? Sam localhost stanowi tylko alias względem adresu 127.0.0.1
. Nasza domena local.dev
też jest aliasem do 127.0.0.1
ale już nie do localhost
. Ustawiając serwer komendą: php -S 127.0.0.1:8000 index.php
, też uzyskaliśmy pożądany wynik. Chyba, że pracowali byśmy z adresacją ipv6, wtedy zamiast lub obok 127.0.0.1
w /etc/hosts
ustawili byśmy ::1
. Jeśli temat różnic między localhost
a 127.0.0.1
jest dla Ciebie nowy polecam Ci artykuł:
Instalacja Nginx
Pokazane tu rozwiązanie, to nie jedyna droga, bo wiele serwerów i frameworków ma swoje własne rozwiązania do ssl w lokalnym developmencie. Zaletą mojego podejścia jest uniwersalność.
Instalacja serwera nginx
yay -S nginx
Włączamy go:
sudo systemctl start nginx.service
Jeśli chcemy, żeby startował przy każdym włączeniu systemy dodajemy:
sudo systemctl enable nginx.service
Sam nie używał bym tego drugiego polecenia na komputerze lokalnym, ponieważ niepotrzebnie blokuje port 80
.
Po instalacji nginx
przywitał nas swoją stroną startową na porcie 80
.
Nginx w Mac OS
Na systemie Mac OS
ngnix domyślnie startuje na porcie 8080
https://www.javatpoint.com/installing-nginx-on-mac
Możemy to zmienić edytując plik /usr/local/etc/nginx/nginx.conf
a sam serwer włączyć poleceniem
launchctl load /usr/local/Cellar/nginx/1.21.4/homebrew.mxcl.nginx.plist
przy czym wersja w Twoim systemie może różnić się od tej podanej przeze mnie. Odpowiednikiem archowego enable
jest opcjonalna flaga -w
Przygotowanie certyfikatu self-signed
Aby móc posługiwać się certyfikatem SSL podczas lokalnego developmentu aplikacji należy posłużyć się certyfikatem self-signed
.
Jego utworzenie opisano w dokumentacji archa:
Interesujące nas polecenia to:
sudo mkdir /etc/nginx/ssl
cd /etc/nginx/ssl
sudo openssl req -new -x509 -nodes -newkey rsa:4096 -keyout server.key -out server.crt -days 1095
sudo chmod 400 server.key
sudo chmod 444 server.crt
Na Mac OS
lepszą lokalizacją będzie katalog /usr/local/etc/nginx/ssl
.
Dołączenie certyfikatu do Nginx
W tej chwili nasz certyfikat nie jest jeszcze podłączony do serwera. Nginx nie nasłuchuje na porcie 443
i przez to zapytanie o https://localhost
kończy się niepowodzeniem:
http --verify no https://localhost
http: error: ConnectionError: HTTPSConnectionPool(host='localhost', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f089f77fee0>: Failed to establish a new connection: [Errno 111] Connection refused')) while doing a GET request to URL: https://localhost/
Zmienimy teraz ustawienia nginx
sudo nvim /etc/nginx/nginx.conf
lub na Mac OS
sudo nano /usr/local/etc/nginx/nginx.conf
dodając pod kluczem http
wpis:
server {
listen 443 ssl;
server_name localhost;
ssl_certificate ssl/server.crt;
ssl_certificate_key ssl/server.key;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
Po przeładowaniu serwisu komendą:
sudo systemctl reload nginx.service
lub na Mac OS
sudo brew services restart nginx
zobaczymy, że domyślna strona nginx jest dostępna pod https
:
http --verify no -h https://localhost
HTTP/1.1 200 OK
Zaletą takiej konfiguracji jest to, że https działa, ale certyfikaty samopodpisane nie są obsługiwane przez httpie
a przeglądarka też może mieć z nimi problemy.
Aby przejść do kolejnego kroku skasujemy te certyfikaty. Nie będziemy ich więcej używać. Zamiast certyfikatów samo-podpisanych stworzymy organizację, która podpisze nam certyfikat domeny.
Przekierowanie ssl do aplikacji
Stajemy się weryfikatorem certyfikatów (CA)
Weryfikator Certyfikatów (CA)
Generowanie klucza prywatnego bez hasła
openssl genrsa -out myCA.key 2048
To polecenie tworzy plik myCA.key
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA+aKMj19W37DjX3nrQ7XTjP3trXXK5hLvByRDKL/QsMGOrxac
...
Xt0itnAcq1vPqqRcsV+YPAE8oyAOXHM1aaTQIH5mp5jHySOqZtSFca8=
-----END RSA PRIVATE KEY-----
Generowanie certyfikatu root
.
openssl req -x509 -new -nodes -key myCA.key -sha256 -days 825 -out myCA.pem
Dostajemy pytania o dane instytucji certyfikującej. Na pytania odpowiedziałem w następujący sposób:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:PL
State or Province Name (full name) [Some-State]:Mazovian
Locality Name (eg, city) []:Warsaw
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Precise Lab CA
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:PL_CA
Email Address []:gustaw.daniel@gmail.com
dostaliśmy plik myCA.pem
o zawartości
-----BEGIN CERTIFICATE-----
MIID5zCCAs+gAwIBAgIUUfo+Snobo0e/HXHJm5Hf4B0TvGEwDQYJKoZIhvcNAQEL
...
7ntEpRg3YZUdDtM0ptDvETM8+H35V9aZtUo1/e2136x459pGZd1aJz+Hhg==
-----END CERTIFICATE-----
Certyfikat podpisany
Tworzymy CA-signed
certyfikat (już nie samo-podpisany)
Definiujemy zmienną z zapisaną domeną:
NAME=local.dev
Generujemy klucz prywatny
openssl genrsa -out $NAME.key 2048
dostajemy plik local.dev.key
o treści
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApvXY4EiWGELQuVTEH9YZ8Qoi0Owq39cQ+g93e7EaKlMzx1fU
...
VburjZcC/InypDy0ZChc6tC0z5A6qkWlLA+3eGs8ADtvQ4qtCS9+Aw==
-----END RSA PRIVATE KEY-----
Następnie tworzymy żądanie jego podpisania.
openssl req -new -key local.dev.key -out local.dev.csr
Ponownie jesteśmy pytani o dane. Tym razem są to dane organizacji chcącej podpisać certyfikat. Nie możemy podać tej samej Common Name
. Moje odpowiedzi:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:PL
State or Province Name (full name) [Some-State]:Mazovian
Locality Name (eg, city) []:Warsaw
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Precise Lab Org
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:PL
Email Address []:gustaw.daniel@gmail.com
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
Po wykonaniu tego polecenia dostajemy plik local.dev.csr
o treści:
-----BEGIN CERTIFICATE REQUEST-----
MIICxjCCAa4CAQAwgYAxCzAJBgNVBAYTAlBMMREwDwYDVQQIDAhNYXpvdmlhbjEP
...
9f1qkg6LHapOjzevheKWEjWG1hnJjBOj42mmIDBVZBHVszP7rrfiRMma
-----END CERTIFICATE REQUEST-----
Teraz utworzymy plik konfiguracyjny rozszerzenia. Zapisujemy do pliku $NAME.ext
zawartość
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = local.dev
Tworzymy podpisany certyfikat
openssl x509 -req -in $NAME.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial -out $NAME.crt -days 825 -sha256 -extfile $NAME.ext
Jeśli wszystko się powiodło powinniśmy zobaczyć:
Signature ok
subject=C = PL, ST = Mazovian, L = Warsaw, O = Precise Lab, emailAddress = gustaw.daniel@gmail.com
Getting CA Private Key
Sprawdzenie czy poprawnie zbudowaliśmy certyfikat możemy wykonać komendą:
openssl verify -CAfile myCA.pem -verify_hostname local.dev local.dev.crt
local.dev.crt: OK
lub na Mac OS
openssl verify -CAfile myCA.pem local.dev.crt
Podsumujmy kroki, które wykonaliśmy:
- zostaliśmy organizacją certyfikującą "Precise Lab CA", która ma klucz
myCA.key
i certyfikatmyCA.pem
- podpisaliśmy certyfikat domeny używając certyfikatu i klucza organizacji certyfikującej dla domeny. Był do tego potrzebny jej klucz
local.dev.key
, żądanie jego podpisanialocal.dev.csr
wystawione przez "Precise Lab Org" i plik konfiguracyjny rozszerzenialocal.dev.ext
- podpisany certyfikat znajduje się w pliku
local.dev.crt
.
Zaufanie organizacji certyfikującej w Chrome
Teraz powinniśmy zaufać organizacji certyfikującej. Dodajmy jej plik pem
jako Authority
w ustawieniach przeglądarki. W pasku adresu wpisujemy:
chrome://settings/certificates
Zobaczymy:
Po kliknięciu import i wybraniu pliku myCA.pem
zaznaczamy jakim operacjom tej organizacji chcemy ufać:
Zaufanie organizacji certyfikującej w Firefox
W Firefox wchodzimy na adres about:preferences#privacy
i w zakładce "Certificates" do "View Certificates". Następnie wybieramy import i plik myCA.pem
od razu zaznaczamy organizację certyfikującą jako zaufaną
W przeciwieństwie do Chrome, te ustawienia są niezależne od systemu operacyjnego.
Zaufanie organizacji certyfikującej na Mac OS w Chrome
Na komputerach z Mac OS
nie możemy zmienić ustawień bezpośrednio w chome. Zamiast tego otwieramy finder. Znajdujemy w nim plik myCA.pem
i klikamy go dwa razy.
po potwierdzeniu hasłem powinniśmy zobaczyć w programie "Pęk Kluczy" (Keychain) naszą organizację w zakładce "Certificates"
Teraz musimy oznaczyć ten certyfikat jako zaufany wybierając opcję "Always Trust".
Konfiguracja Nginx jako proxy
Kolejny raz zmieniamy ustawienia nginx
. Tym razem przełączamy się na wygenerowany certyfikat i jego klucz.
server {
listen 443 ssl;
server_name local.dev;
ssl_certificate ssl/local.dev.crt;
ssl_certificate_key ssl/local.dev.key;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
}
}
Nie możemy zapomnieć o przeładowaniu serwera:
sudo systemctl reload nginx.service
Na Mac OS
nie ma systemctl
i używamy brew
sudo brew services restart nginx
sudo pkill nginx
sudo nginx
Po wejściu na stronę:
https://local.dev/
możemy cieszyć się widokiem kłódki przy adresie lokalnej strony:
- na Chrome
- oraz na Firefox
W konsoli nie zobaczymy jednak poprawnego wyniku:
http https://local.dev
http: error: SSLError: HTTPSConnectionPool(host='local.dev', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1123)'))) while doing a GET request to URL: https://local.dev/
Błąd mówi nam, że nie udało się zweryfikować lokalnego wystawcy certyfikatu. Aby request z konsoli zadziałał musimy wskazać certyfikat organizacji weryfikującej jako argument flagi --verify
.
http --verify /etc/nginx/ssl/myCA.pem https://local.dev
lub na Mac OS
http --verify /usr/local/etc/nginx/ssl/myCA.pem https://local.dev
Zastosowania lokalnego certyfikatu SSL
Pokazaliśmy jak skonfigurować połączenie po https na lokalnym komputerze, co jest szczególnie przydatne w developmencie aplikacji webowych. Zwykle można rozwijać swoje projekty lokalnie z użyciem http
.
Czasami https
jest wymagany przez takie mechanizmy jak:
- ustawienia Secure lub SameSite dla Cookie
- ustawienia dostępu dla kamery lub mikrofonu w przeglądarce
- niektóre adresy webhooks zewnętrznych API
Zalety i wady Caddy
Auth0 w swojej dokumentacji rekomenduje wykorzystanie programu caddy
.
Jego instalacja to
yay -S caddy
lub
brew install caddy
Wyłączymy teraz nasz serwer nginx
.
sudo pkill nginx
uruchamiany caddy
poleceniem
caddy reverse-proxy --from localhost:443 --to localhost:8000
I mamy następujący efekt:
- Na chrome działa nam kłódka na stronie
https://localhost
2. Na Firefox https://localhost
nie działa
3. Z poziomu linii komend (httpie) też nie działa
4. Z drugiej strony curl działa curl https://localhost
.
Czyli "caddy" to metoda na bardzo szybkie konfigurowanie lokalnego ssl ale z ograniczeniami. Ich dokumentacja wygląda obiecująco, ale można się spodziewać, że napotykając na błędy będziemy mieli znacznie mniejsze szanse na support od community, niż w przypadku samodzielnej konfiguracji zgodnie z krokami przedstawionymi w tym wpisie. Jeśli zaczniemy od Caddy bez rozumienia jak skonfigurować ssl samodzielnie, to szansa, że spotkane błędy zatrzymają nas na długi czas znacznie wzrośnie.
Wartościowe linki pogłębiające temat SSL
Przygotowując ten wpis korzystałem z wielu zewnętrznych źródeł. Najbardziej wartościowe jakie znalazłem są podlinkowane poniżej.