Przejdź do treści
Lekcja 90 z 117

Kurs › Moduł 10 › Lekcja 90

Apache z HTTPS

⏱ ok. 12 min czytania· moduł 10: Usługi i serwery

Po tej lekcji: kursant włącza HTTPS w Apache, rozumie, co robi certbot --apache, ustawia przekierowanie z 80 na 443, sprawdza certyfikat i odnawianie, a na RHEL umie nadać plikom strony kontekst SELinux httpd_sys_content_t.

Wstęp

przeglądarka z adresem http://kawiarnia.test i czerwonym napisem „Niezabezpieczona”

Twoja strona działa. Ale przeglądarka obok adresu pisze „Niezabezpieczona”. Klient kawiarni widzi to i myśli: „aha, tu mi ukradną kartę”. Nawet jeśli na stronie jest tylko menu i zdjęcie sernika.

I przeglądarka ma rację. Wszystko, co leci po HTTP, leci otwartym tekstem. Hasło do panelu, formularz kontaktowy, ciasteczko z sesją. Każdy po drodze, w kawiarnianym Wi-Fi, u dostawcy internetu, może to przeczytać. HTTP to pocztówka: listonosz, sąsiad i pies sąsiada mogą przeczytać, co piszesz.

Dziś zamieniamy pocztówkę na list w zaklejonej kopercie. HTTPS, kłódka w przeglądarce, certyfikat za darmo i automatyczne odnawianie, żebyś nie musiał o tym pamiętać.

HTTPS w jednym zdaniu

O tym, jak działa szyfrowanie i certyfikaty, mówiliśmy w lekcji o certyfikatach. Szybkie przypomnienie: HTTPS to HTTP w zaklejonej kopercie TLS. Serwer pokazuje certyfikat, czyli dowód osobisty strony podpisany przez urząd, któremu ufa przeglądarka. Przeglądarka sprawdza dowód, uzgadnia z serwerem tajny klucz i dalej rozmawiają szyfrem. HTTPS działa na porcie 443, HTTP na 80.

W Apache za HTTPS odpowiada moduł mod_ssl.

[Ubuntu] Jest w pakiecie apache2, trzeba go tylko włączyć:

sudo a2enmod ssl
sudo systemctl restart apache2

[RHEL] Osobny pakiet:

sudo dnf install mod_ssl
sudo systemctl restart httpd

Na RHEL mod_ssl od razu dorzuca /etc/httpd/conf.d/ssl.conf z przykładową konfiguracją HTTPS. Na Ubuntu przykład czeka w sites-available/default-ssl.conf. I na obu: otwórz port 443 w zaporze.

sudo ufw allow 'Apache Full'                          # Ubuntu: 80 i 443
sudo firewall-cmd --permanent --add-service=https     # RHEL
sudo firewall-cmd --reload

Na Ubuntu w ports.conf siedzi to:

Listen 80
<IfModule ssl_module>
    Listen 443
</IfModule>

Czyli Apache otwiera okienko 443 dopiero wtedy, kiedy masz włączony moduł SSL. Ładne: nie płacisz za okienko, którego nie używasz.

Certbot, czyli darmowy urząd certyfikacji z kurierem

Skąd wziąć certyfikat? Kiedyś się go kupowało, wypełniało formularze i ręcznie wklejało pliki. Dziś jest Let's Encrypt, urząd certyfikacji, który wydaje certyfikaty za darmo, automatycznie. A certbot to kurier, który chodzi do tego urzędu w Twoim imieniu: zamawia certyfikat, udowadnia, że domena jest Twoja, odbiera certyfikat, wkłada go do Apache i pilnuje odnawiania.

Instalacja:

sudo apt install certbot python3-certbot-apache     # Ubuntu
sudo dnf install certbot python3-certbot-apache     # RHEL, z repozytorium EPEL

Na RHEL certbota nie ma w podstawowych repozytoriach, trzeba włączyć EPEL, czyli dodatkowe pakiety budowane przez społeczność Fedory.

Warunek, bez którego nic nie zadziała: prawdziwa domena z rekordem DNS wskazującym na Twój serwer i otwarty port 80 z internetu. Na kawiarnia.test z /etc/hosts certbot nie zadziała, bo urząd w internecie nie ma pojęcia, gdzie jest Twoje /etc/hosts. Na maszynie wirtualnej w domu bez publicznego adresu też nie. To ćwiczenie na serwer w chmurze z domeną.

A teraz cała magia, jedna komenda:

sudo certbot --apache -d kawiarnia.pl -d www.kawiarnia.pl

Co się wtedy dzieje:

  1. Certbot znajduje VirtualHosta z ServerName kawiarnia.pl.
  2. Let's Encrypt mówi: „udowodnij, że to Twoja domena. Połóż pod adresem http://kawiarnia.pl/.well-known/acme-challenge/... taki plik”. Certbot na chwilę dokłada regułę do Apache, urząd przychodzi przez port 80 i sprawdza. Jak listonosz, który zostawia awizo i sprawdza, czy ktoś je podniósł spod tego adresu.
  3. Certyfikat ląduje w /etc/letsencrypt/live/kawiarnia.pl/: fullchain.pem to certyfikat razem z łańcuchem pośrednich urzędów, privkey.pem to klucz prywatny.
  4. Certbot tworzy nowy plik strony, na Ubuntu kawiarnia.pl-le-ssl.conf, z <VirtualHost *:443> i ścieżkami do certyfikatu, i włącza go.
  5. I dorzuca przekierowanie z 80 na 443. W tej wersji certbota przekierowanie jest domyślnie włączone przy --apache, a wyłączasz je flagą --no-redirect.

Przy pierwszym uruchomieniu zapyta jeszcze o adres email (powiadomienia o problemach) i zgodę na regulamin. Do automatów są flagi --email, --agree-tos i -n, czyli bez zadawania pytań.

Zrób to sam: HTTPS z własnym certyfikatem

Skoro na .test certbot nie zadziała, zróbmy w domu to, co certbot robi za nas. Najpierw certyfikat podpisany samemu sobie, tak zwany self-signed. To jak legitymacja, którą sam sobie wydrukowałeś. Szyfrowanie jest prawdziwe, ale nikt poza Tobą jej nie uzna.

sudo mkdir -p /etc/ssl/kawiarnia
sudo openssl req -x509 -newkey rsa:2048 -nodes -days 30 \
  -keyout /etc/ssl/kawiarnia/klucz.pem -out /etc/ssl/kawiarnia/cert.pem \
  -subj '/CN=kawiarnia.test' \
  -addext 'subjectAltName=DNS:kawiarnia.test,DNS:www.kawiarnia.test'
sudo chmod 600 /etc/ssl/kawiarnia/klucz.pem

Klucz prywatny 600, bo to on jest kluczem do koperty. Apache czyta go jako root przy starcie, więc pracownicy www-data nie muszą mieć do niego dostępu i nie powinni.

Teraz drugi VirtualHost, tym razem na porcie 443, plik kawiarnia.test-ssl.conf:

<VirtualHost *:443>
    ServerName kawiarnia.test
    ServerAlias www.kawiarnia.test
    DocumentRoot /var/www/kawiarnia.test/public

    SSLEngine on
    SSLCertificateFile    /etc/ssl/kawiarnia/cert.pem
    SSLCertificateKeyFile /etc/ssl/kawiarnia/klucz.pem

    <Directory /var/www/kawiarnia.test/public>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/kawiarnia_error.log
    CustomLog ${APACHE_LOG_DIR}/kawiarnia_access.log combined
</VirtualHost>

Trzy nowe linijki: SSLEngine on włącza szyfrowanie dla tego skrzydła, a dwie kolejne mówią, gdzie leży certyfikat i klucz. Przy certyfikacie z certbota byłyby to /etc/letsencrypt/live/kawiarnia.pl/fullchain.pem i privkey.pem.

sudo a2ensite kawiarnia.test-ssl
sudo apachectl configtest && sudo systemctl reload apache2

Test:

curl https://kawiarnia.test/
curl: (60) SSL certificate problem: self-signed certificate

I bardzo dobrze. curl zachowuje się jak przeglądarka: „tej legitymacji nikt nie podpisał, nie wierzę”. Mamy dwa wyjścia:

curl -k https://kawiarnia.test/                                  # nie sprawdzaj, tylko na testy!
curl --cacert /etc/ssl/kawiarnia/cert.pem https://kawiarnia.test/karta   # zaufaj temu jednemu
<h1>Kawiarnia: dzis espresso za 5 zl</h1>
<h1>Menu: espresso, latte, sernik</h1>

-k to wyłączenie sprawdzania, czyli „wpuszczam każdego, kto pokaże cokolwiek”. Do testów w laboratorium, nigdy w skryptach na produkcji. --cacert jest uczciwszy: „ufam dokładnie temu certyfikatowi”. I zauważ: .htaccess z przekierowaniem /karta działa też po HTTPS, bo to ten sam folder.

Sprawdzamy certyfikat tak, jak w lekcji o certyfikatach:

echo | openssl s_client -connect kawiarnia.test:443 -servername kawiarnia.test 2>/dev/null \
  | openssl x509 -noout -subject -dates
subject=CN = kawiarnia.test
notBefore=Nov  3 15:34:22 2026 GMT
notAfter=Dec  3 15:34:22 2026 GMT

30 dni, tak jak zamówiliśmy.

Przekierowanie z 80 na 443

Mamy dwie wersje strony: zwykłą i szyfrowaną. Ale ludzie dalej wpisują kawiarnia.test bez https:// i lądują na pocztówce. Więc na porcie 80 zostawiamy już tylko portiera, który mówi: „proszę, wejście od drugiej strony budynku”.

Najprostsza wersja, w VirtualHoście na porcie 80:

Redirect permanent / https://kawiarnia.test/

A tak robi to certbot. Zajrzałem do jego kodu źródłowego, dokładnie te linijki dopisuje do VirtualHosta na porcie 80:

RewriteEngine on
RewriteCond %{SERVER_NAME} =kawiarnia.test [OR]
RewriteCond %{SERVER_NAME} =www.kawiarnia.test
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]

Po ludzku: „jeśli gość przyszedł do kawiarni albo do www.kawiarni, to cokolwiek chce (^ pasuje do wszystkiego), odeślij go pod ten sam adres, tylko przez https, kodem 301”. %{REQUEST_URI} to ścieżka, o którą prosił, więc nikt nie ląduje na stronie głównej zamiast w menu. NE to „nie psuj znaków specjalnych w adresie”.

Wstawiłem to u siebie i sprawdziłem:

curl -sI 'http://kawiarnia.test/karta?stolik=5' | grep -E 'HTTP|Location'
curl -sLk http://kawiarnia.test/karta
HTTP/1.1 301 Moved Permanently
Location: https://kawiarnia.test/karta?stolik=5
<h1>Menu: espresso, latte, sernik</h1>

Ścieżka i parametry przeżyły przeprowadzkę. -L w curlu każe iść za przekierowaniem, jak przeglądarka.

Odnawianie

Certyfikat z Let's Encrypt jest dziś ważny 90 dni. I będzie krócej: Let's Encrypt ogłosił skracanie do 64 dni od lutego 2027 i do 45 dni od lutego 2028. Odnawianie ręczne odpada, bo kiedyś zapomnisz, a zapomina się zawsze w piątek wieczorem.

Dobra wiadomość: pamiętasz budziki z lekcji 87? Pakiet certbota sam instaluje budzik. Na Ubuntu to timer systemd:

[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true

Dwa razy dziennie, z losowym opóźnieniem do 12 godzin, odpala certbot -q renew. Certbot sprawdza wszystkie certyfikaty i odnawia tylko te, którym zostało mało czasu. Na systemach bez systemd ten sam pakiet ma zapasowy wpis w /etc/cron.d/certbot.

Sprawdzasz tak:

systemctl list-timers | grep certbot
sudo certbot certificates          # jakie mam certyfikaty i do kiedy
sudo certbot renew --dry-run       # próba generalna odnowienia

--dry-run przechodzi całą procedurę na serwerze testowym i niczego nie zapisuje. Rób to po każdej zmianie w konfiguracji Apache, bo odnawianie przez --apache potrzebuje działającego portu 80.

[RHEL] Certbot z EPEL przynosi timer certbot-renew.timer, ale domyślnie wyłączony. Bez tego jednego kroku certyfikat po prostu wygaśnie, a Ty dowiesz się o tym od klienta:

sudo systemctl enable --now certbot-renew.timer

SELinux: strażnik z listą etykiet

Na Ubuntu to koniec. Na Red Hacie jest jeszcze jeden strażnik. Pamiętasz, że uprawnienia to klucze do pokojów? SELinux to dodatkowy ochroniarz, który oprócz klucza sprawdza naklejkę na drzwiach. Apache ma przepustkę „mogę czytać tylko pokoje z naklejką httpd_sys_content_t”. Jak na drzwiach jest inna naklejka, ochroniarz nie wpuści, choćby klucz pasował idealnie.

⚠️ Ćwicz to na maszynie wirtualnej z RHEL, Rocky albo Almą. Nie wyłączaj SELinuxa „bo przeszkadza”. To jak wyjęcie baterii z czujnika dymu, bo piszczy przy smażeniu.

Naklejki podglądasz flagą -Z:

ls -Z /var/www/html/index.html

W wyniku zobaczysz typ httpd_sys_content_t. Wszystko, co powstaje w /var/www, dostaje tę naklejkę automatycznie, bo system ma regułę „w tym miejscu takie naklejki”.

A teraz piekarnia z poprzedniej lekcji, w /srv/piekarnia. Na RHEL dasz Require all granted, prawa 644, i dalej 403. Bo pliki w /srv nie mają naklejki dla Apache. Naprawa w dwóch krokach, dokładnie tak, jak w dokumentacji Red Hata:

sudo semanage fcontext -a -t httpd_sys_content_t "/srv/piekarnia(/.*)?"
sudo restorecon -Rv /srv/piekarnia/

Pierwsza komenda dopisuje regułę do spisu: „folder /srv/piekarnia i wszystko pod nim ma mieć naklejkę httpd_sys_content_t”. (/.*)? to wyrażenie regularne na „i wszystko w środku”. Druga komenda przechodzi po plikach i nakleja to, co mówi spis. Samo semanage niczego nie zmienia na plikach, samo chcon zmienia, ale nie przetrwa pełnego przeetykietowania systemu. Dlatego zawsze para: semanage fcontext i restorecon.

Najczęstsza pułapka na RHEL: robisz stronę w domowym folderze i przenosisz ją mv do /var/www/html. mv zabiera ze sobą starą naklejkę, user_home_t, i Apache dostaje 403. cp tworzy nowy plik, który dostaje naklejkę miejsca docelowego. Jak już przeniosłeś:

sudo restorecon -Rv /var/www/html/

Skąd wiedzieć, że to SELinux, a nie zwykłe prawa? Zajrzyj do dziennika audytu:

sudo ausearch -m AVC -ts recent

Wpis avc: denied { read } z comm="httpd" to ochroniarz, który kogoś zatrzymał. Do tego jeszcze dwie rzeczy, które się przydadzą:

  • folder, do którego strona ma zapisywać (na przykład przesłane zdjęcia), dostaje typ httpd_sys_rw_content_t;
  • jeśli Apache ma słuchać na nietypowym porcie, na przykład 8081, SELinux też musi o tym wiedzieć: sudo semanage port -a -t http_port_t -p tcp 8081.

Jak to działa pod spodem

Kiedy przeglądarka łączy się z portem 443, zanim padnie choć jedno słowo HTTP, odbywa się uścisk dłoni TLS. Przeglądarka już w pierwszej wiadomości podaje nazwę strony, do której idzie. To rozszerzenie nazywa się SNI. Dzięki niemu Apache wie, który certyfikat pokazać, zanim rozszyfruje nagłówek Host. Bez SNI na jednym adresie IP dałoby się postawić tylko jedną stronę HTTPS. To dlatego w openssl s_client dajemy -servername.

Dalej mod_ssl używa biblioteki OpenSSL do szyfrowania, a po rozszyfrowaniu żądanie idzie dokładnie tą samą drogą co zwykły HTTP: VirtualHost, DocumentRoot, <Directory>, .htaccess. Szyfrowanie to tylko koperta, list w środku jest ten sam.

Certbot w /etc/letsencrypt/live/ trzyma dowiązania do najnowszej wersji plików, a stare wersje leżą w /etc/letsencrypt/archive/. Dlatego Apache zawsze wskazuje na live/...: po odnowieniu dowiązanie pokazuje nowy plik, a certbot przeładowuje Apache.

Ściąga flag

KomendaFlagaCo robi
a2enmodsslwłącz mod_ssl (Ubuntu)
dnf installmod_sslzainstaluj mod_ssl (RHEL)
certbot--apache -d DOMENAzdobądź certyfikat i skonfiguruj Apache
certbot--redirect / --no-redirectz przekierowaniem 80→443 albo bez
certbot--test-cert (--staging)certyfikat testowy, bez limitów
certbot--hstsdodaj nagłówek HSTS
certbotcertificateslista certyfikatów i daty ważności
certbotrenew --dry-runpróba odnowienia
openssl req-x509 -nodes -days Ncertyfikat self-signed do testów
openssl x509-noout -datesdaty ważności certyfikatu
curl-knie sprawdzaj certyfikatu (tylko testy)
curl--cacert PLIKufaj temu certyfikatowi
curl-Lidź za przekierowaniami
ls-Zpokaż kontekst SELinux
semanage fcontext-a -t TYP "ŚCIEŻKA(/.*)?"dopisz regułę etykiet
restorecon-Rvnadaj etykiety zgodnie z regułami
ausearch-m AVC -ts recentostatnie odmowy SELinuxa

⚠ Typowy błąd

Piszesz pierwszy VirtualHost na 443, robisz configtest i:

AH00526: Syntax error on line 6 of /etc/apache2/sites-enabled/kawiarnia.test-ssl.conf:
Invalid command 'SSLEngine', perhaps misspelled or defined by a module not included in the server configuration

Stary znajomy z poprzedniej lekcji, tylko w innym przebraniu. Apache nie zna słowa SSLEngine, bo zna je dopiero mod_ssl, a ten jest wyłączony. Naprawa: [Ubuntu] sudo a2enmod ssl, [RHEL] sudo dnf install mod_ssl, potem configtest i restart.

Drugi klasyk: certbot kończy się błędem weryfikacji domeny. Prawie zawsze to jedna z trzech rzeczy: rekord DNS nie wskazuje jeszcze na serwer, port 80 jest zamknięty w zaporze (albo w panelu dostawcy chmury), albo na porcie 80 siedzi coś innego niż Apache. Sprawdzasz dig +short kawiarnia.pl, sudo ss -ltnp | grep ':80 ' i z zewnątrz curl -I http://kawiarnia.pl/. Port 80 musi zostać otwarty także po włączeniu HTTPS, bo przez niego idzie każde odnowienie.

✎ Zadanie

Postaw na jednym Apache dwie strony (dwa VirtualHosty) pod dwiema nazwami dopisanymi do /etc/hosts. Sprawdź apachectl configtest i curl każdej nazwy.

Pokaż rozwiązanie

Zadanie było takie: jeden Apache, dwie strony, dwie nazwy w /etc/hosts. Najpierw nazwy. Udajemy, że mamy domeny, a tak naprawdę mówimy komputerowi: „obie te nazwy to ty sam”.

echo '127.0.0.1 sklep.test blog.test' | sudo tee -a /etc/hosts
sudo mkdir -p /var/www/sklep /var/www/blog
echo '<h1>Sklep z kapciami</h1>' | sudo tee /var/www/sklep/index.html
echo '<h1>Blog o kapciach</h1>' | sudo tee /var/www/blog/index.html

Teraz dwa pliki VirtualHost. [Ubuntu] w /etc/apache2/sites-available/sklep.conf, [RHEL] w /etc/httpd/conf.d/sklep.conf. Drugi plik identyczny, tylko z blog.

<VirtualHost *:80>
    ServerName sklep.test
    DocumentRoot /var/www/sklep
    ErrorLog ${APACHE_LOG_DIR}/sklep_error.log
    CustomLog ${APACHE_LOG_DIR}/sklep_access.log combined
</VirtualHost>

[RHEL] Tam nie ma zmiennej APACHE_LOG_DIR, więc piszesz wprost logs/sklep_error.log, czyli /var/log/httpd/. I pamiętasz o SELinuksie z lekcji 90: pliki w /var/www/... mają mieć kontekst httpd_sys_content_t. Jak je tworzysz w /var/www, dostają go same.

sudo a2ensite sklep blog        # [Ubuntu]; na RHEL pliki w conf.d działają od razu
sudo apachectl configtest
sudo systemctl reload apache2   # [RHEL] httpd
curl http://sklep.test
curl http://blog.test
Syntax OK
<h1>Sklep z kapciami</h1>
<h1>Blog o kapciach</h1>

A teraz smaczek. Wpisz curl http://127.0.0.1. Dostaniesz… bloga. Czemu? Bo jak Apache nie rozpozna nazwy, daje pierwszego VirtualHosta z listy. A pliki czyta alfabetycznie. blog jest przed sklep. Sprawdza to apachectl -S:

*:80                   is a NameVirtualHost
         default server blog.test (/etc/apache2/sites-enabled/blog.conf:1)
         port 80 namevhost blog.test (/etc/apache2/sites-enabled/blog.conf:1)
         port 80 namevhost sklep.test (/etc/apache2/sites-enabled/sklep.conf:1)

Sklep z kapciami przegrał z blogiem przez kolejność w alfabecie. Jak w szkole przy odpytywaniu. Dlatego wielu adminów nazywa domyślną stronę 000-default. Żeby zawsze była pierwsza.

Co dalej

Apache to recepcjonista, który sam obsługuje gości. W następnej lekcji poznasz Nginx i nową rolę: odźwiernego, który sam nic nie gotuje, tylko przekazuje gości do kuchni za ścianą. Reverse proxy, rozkładanie ruchu na kilka serwerów i HAProxy.