Preskoči na sadržaj

Konfiguracija HTTPS-a u web poslužitelju Apache HTTP Server

Modul mod_ssl implementira podršku za SSLv3 i TLSv1.x u Apacheju (SSLv2 više nije podržan jer se smatra nesigurnim) koji se koriste kod ostvarivanja HTTPS veza. Za izvođenje kriptografskih algoritama koristi se OpenSSL.

Hint

Više detalja o SSL-u i TLS-u u Apacheju moguće je pronaći u službenoj dokumentaciji u dijelu Apache SSL/TLS Encryption

Stvaranje i postavljanje certifikata i privatnog ključa

Stvorimo samopotpisani certifikat server.crt koji traje 30 dana i njegov pripadni tajni ključ server.key (takva imena datoteka će nam trebati kasnije):

$ openssl req -x509 -nodes -days 30 -newkey rsa:4096 -keyout server.key -out server.crt
Generating a RSA private key
................................................................................................................................................................++++
......................................................................................................................................................................................................++++
writing new private key to 'server.key'
-----
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]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

Dodajmo dvije nove naredbe COPY u Dockerfile kojima ćemo kopirati certifikat i tajni ključ u direktorij /usr/local/apache2/conf unutar slike:

FROM httpd:2.4
COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
COPY ./www /var/www
COPY server.crt /usr/local/apache2/conf
COPY server.key /usr/local/apache2/conf

U datoteci my-httpd.conf odkomentirajmo linije:

#...
#LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
#...
#LoadModule ssl_module modules/mod_ssl.so
#...
#Include conf/extra/httpd-ssl.conf
#...

Modul mod_ssl za svoj rad zahtijeva modul mod_socache_shmcb pa prva od tri linije učitava taj modul. Druga linija učitava mod_ssl, a treća njegovu konfiguraciju u kojoj se nalaze naredbe kao što je Listen 443 koja uključuje HTTPS na vratima 443. Konfiguraciju ćemo nešto kasnije uređivati.

Izgradimo sliku i pokrenimo kontejner:

$ docker build -t "my-httpd:2.4-4" .
Sending build context to Docker daemon  32.26kB
Step 1/5 : FROM httpd:2.4
---> b2c2ab6dcf2e
Step 2/5 : COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
---> 765440898954
Step 3/5 : COPY ./www /var/www
---> a0c0158083fb
Step 4/5 : COPY server.key /usr/local/apache2/conf
---> 6ffd3334243b
Step 5/5 : COPY server.crt /usr/local/apache2/conf
---> d5b1f004015f
Successfully built d5b1f004015f
Successfully tagged my-httpd:2.4-4
$ docker run my-httpd:2.4-4
[Sun May 10 17:02:53.475776 2020] [ssl:warn] [pid 1:tid 140698297431168] AH01906: www.example.com:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 17:02:53.476093 2020] [ssl:warn] [pid 1:tid 140698297431168] AH01909: www.example.com:443:0 server certificate does NOT include an ID which matches the server name
[Sun May 10 17:02:53.478932 2020] [ssl:warn] [pid 1:tid 140698297431168] AH01906: www.example.com:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 17:02:53.478939 2020] [ssl:warn] [pid 1:tid 140698297431168] AH01909: www.example.com:443:0 server certificate does NOT include an ID which matches the server name
[Sun May 10 17:02:53.480043 2020] [mpm_event:notice] [pid 1:tid 140698297431168] AH00489: Apache/2.4.43 (Unix) OpenSSL/1.1.1d configured -- resuming normal operations
[Sun May 10 17:02:53.480070 2020] [core:notice] [pid 1:tid 140698297431168] AH00094: Command line: 'httpd -D FOREGROUND'

Ukoliko se kod izdavanja certifikata pod Common Name unese apache-primjer.rm.miletic.net, upozorenja server certificate does NOT include an ID which matches the server name ne bi trebalo biti. Ovo drugo upozorenje, server certificate is a CA certificate (BasicConstraints: CA == TRUE !?), Apache daje zato što je certifikat samopotpisan. Zbog toga kod testiranja naredba curl treba parametar -k i imamo:

$ curl -k https://172.17.0.2/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
</body></html>

Konfiguracijske naredbe HTTPS poslužitelja

Vidimo da HTTPS poslužitelj odgovara na upit, ali ne sadržajem datoteke index.html koji smo se nadali dobiti. Na strani poslužitelja ispisuju se poruke o pogreškama:

[Sun May 10 17:02:56.688057 2020] [authz_core:error] [pid 9:tid 140698145773312] [client 172.17.0.1:54160] AH01630: client denied by server configuration: /usr/local/apache2/htdocs/
172.17.0.1 - - [10/May/2020:17:02:56 +0000] "GET / HTTP/1.1" 403 199

Morat ćemo konfigurirati DocumentRoot i za HTTPS poslužitelj. Dohvatimo konfiguracijsku datoteku:

$ docker run --rm httpd:2.4 cat /usr/local/apache2/conf/extra/httpd-ssl.conf > my-httpd-ssl.conf

Todo

Nedostaje kratak opis sadržaja datoteke.

Uredimo datoteku; uočimo da u njoj postoji niz konfiguracijskih naredbi koje počinju nizom slova SSL i određuju način rada SSL-a. Naredbom SSLCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES (dokumentacija) navode se dozvoljeni šifrarnici. Podsjetimo se da se kod SSL-a koriste četiri vrste šifrarnika:

  • algoritam za razmjenu ključeva: RSA, Diffie-Hellman, Elliptic Curve Diffie-Hellman, Secure Remote Password
  • algoritam za autentifikaciju: RSA, Diffie-Hellman, DSS, ECDSA, a specijalno može i nijedan
  • algoritam za šifrirane poruka: AES, DES, Triple-DES, RC4, RC2, IDEA, itd.
  • algoritam za računanje sažetka koda autentifikacije poruke: MD5, SHA ili SHA1, SHA256, SHA384

Prve dvije vrste algoritma se u nekim podjelama šifrirarnika navode zajedno pa onda kažemo da su tri različite vrste šifrarnika. U našem slučaju vrijednost HIGH:MEDIUM:!MD5:!RC4:!3DES ima značenje:

  • HIGH -- svi šifrarnici koji koriste trostruki DES ili bolji algoritam (npr. AES)
  • MEDIUM -- svi šifrarnici koji koriste 128-bitno ili bolje šifriranje
  • !MD5 -- ne MD5
  • !RC4 -- ne RC4
  • !3DES -- ne trostruki DES (bolji algoritmi od trostrukog DES-a su dozvoljeni)

Note

Za današnje standarde korištenje ovoliko širokog skupa šifrarnika (koji onda uključuje i neke slabe šifrarnike) ne bi bilo preporučeno. Popise preporučenih šifrarnika možete pročitati na Remy van Elstovom blogu u članku Strong SSL Security on Apache2 i na Mozillinom wikiju u članku Security/Server Side TLS. Pored toga, Mozilla je razvila generator konfiguracija SSL-a koji je vrlo jednostavan za korištenje, a podržava i Apache.

Naredbom openssl ciphers možemo provjeriti o kojim se točno šifrarnicima radi:

$ openssl ciphers -v 'HIGH:MEDIUM:!MD5:!RC4:!3DES'
TLS_AES_256_GCM_SHA384  TLSv1.3 Kx=any      Au=any  Enc=AESGCM(256) Mac=AEAD
TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any      Au=any  Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_AES_128_GCM_SHA256  TLSv1.3 Kx=any      Au=any  Enc=AESGCM(128) Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(256) Mac=AEAD
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(256) Mac=AEAD
DHE-DSS-AES256-GCM-SHA384 TLSv1.2 Kx=DH       Au=DSS  Enc=AESGCM(256) Mac=AEAD
DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH       Au=RSA  Enc=AESGCM(256) Mac=AEAD
ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH     Au=RSA  Enc=CHACHA20/POLY1305(256) Mac=AEAD
(...)
PSK-CAMELLIA128-SHA256  TLSv1 Kx=PSK      Au=PSK  Enc=Camellia(128) Mac=SHA256
DHE-RSA-SEED-SHA        SSLv3 Kx=DH       Au=RSA  Enc=SEED(128) Mac=SHA1
DHE-DSS-SEED-SHA        SSLv3 Kx=DH       Au=DSS  Enc=SEED(128) Mac=SHA1
ADH-SEED-SHA            SSLv3 Kx=DH       Au=None Enc=SEED(128) Mac=SHA1
SEED-SHA                SSLv3 Kx=RSA      Au=RSA  Enc=SEED(128) Mac=SHA1

Note

Skup algoritama za šifriranje čije korištenje se preporuča kod rada sa TLS/SSL certifikatima mijenja se iz godine u godinu kako se pronalaze sigurnosni propusti u njima i kako procesna moć računala raste pa je dobro kod postavljanja TLS-a/SSL-a provjeriti aktualne najbolje prakse, primjerice one koje navodi Qualys SLL Labs, autor SSL Server Testa i SSL Client Testa.

Naredba SSLProtocol all -SSLv3 (dokumentacija) definira dozvoljene verzije TLS-a i SSL-a. Vrijednost all -SSLv3 uključuje TLSv1, TLSv1.1, TLSv1.2, TLSv1.3, a isključuje SSLv3.

Naredbe SSLCertificateFile "/usr/local/apache2/conf/server.crt" i SSLCertificateKeyFile "/usr/local/apache2/conf/server.key" navode certifikat i pripadni tajni ključ koji se koriste (respektivno).

Osim konfiguracijskih naredbi koje počinju nizom slova SSL, unutar bloka <VirtualHost></VirtualHost> vidimo i već poznate naredbe DocumentRoot, ServerName i ServerAdmin:

<VirtualHost _default_:443>

#   General setup for the virtual host
DocumentRoot "/usr/local/apache2/htdocs"
ServerName www.example.com:443
ServerAdmin you@example.com
#...
</VirtualHost>

Detaljnije se načinom rada virtualnih domaćina (naredba <VirtualHost>, dokumentacija) bavimo drugdje. Postavimo DocumentRoot "/var/www/html" i ServerName apache-primjer.rm.miletic.net:443, a ServerAdmin na vlastitu e-mail adresu. Dodajmo u Dockerfile kopiranje izmijenjene datoteke na odgovarajuće mjesto:

FROM httpd:2.4
COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
COPY ./www /var/www
COPY server.crt /usr/local/apache2/conf
COPY server.key /usr/local/apache2/conf
COPY ./my-httpd-ssl.conf /usr/local/apache2/conf/extra/httpd-ssl.conf

Izgradimo sliku i pokrenimo Docker kontejner:

$ docker build -t "my-httpd:2.4-5" .
Sending build context to Docker daemon  72.19kB
Step 1/6 : FROM httpd:2.4
---> b2c2ab6dcf2e
Step 2/6 : COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
---> Using cache
---> 765440898954
Step 3/6 : COPY ./www /var/www
---> Using cache
---> a0c0158083fb
Step 4/6 : COPY server.crt /usr/local/apache2/conf
---> baafa15fa976
Step 5/6 : COPY server.key /usr/local/apache2/conf
---> eebe8db06676
Step 6/6 : COPY ./my-httpd-ssl.conf /usr/local/apache2/conf/extra/httpd-ssl.conf
---> 0514261bb6fe
Successfully built 0514261bb6fe
Successfully tagged my-httpd:2.4-5
$ docker run my-httpd:2.4-5
[Sun May 10 18:57:41.346009 2020] [ssl:warn] [pid 1:tid 139678405715072] AH01906: www.example.com:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 18:57:41.346345 2020] [ssl:warn] [pid 1:tid 139678405715072] AH01909: www.example.com:443:0 server certificate does NOT include an ID which matches the server name
[Sun May 10 18:57:41.349298 2020] [ssl:warn] [pid 1:tid 139678405715072] AH01906: www.example.com:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 18:57:41.349305 2020] [ssl:warn] [pid 1:tid 139678405715072] AH01909: www.example.com:443:0 server certificate does NOT include an ID which matches the server name
[Sun May 10 18:57:41.350403 2020] [mpm_event:notice] [pid 1:tid 139678405715072] AH00489: Apache/2.4.43 (Unix) OpenSSL/1.1.1d configured -- resuming normal operations
[Sun May 10 18:57:41.350429 2020] [core:notice] [pid 1:tid 139678405715072] AH00094: Command line: 'httpd -D FOREGROUND'

Uvjerimo se da sada radi ispravno:

$ curl -k https://172.17.0.2/
<html><body><h1>Radi!</h1></body></html>

Hint

U praksi je ponekad moguće automatizirati čitav ovdje opisani postupak. Naime, Apache od verzije 2.4.30 nadalje sadrži mod_md koji dohvaća certifikate s besplatnog i otvorenog autoriteta certifikata Let's Encrypt za domene navedene pod konfiguracijskom naredbom MDomain. Nažalost, taj postupak ovdje ne možemo koristiti jer Let's Encrypt zahtijeva da imamo registrirane domene na internetu kako bi mogao izdati certifikat za njih. Pored toga, čak i kad možemo automatizirati postavljanje certifikata, dobro je znati kako čitav postupak izgleda kako bismo se mogli snaći u situacijama kad neki dio tog automatiziranog postupka zakaže.

Server Name Indication

Ranije prikazani način dohvaćanja domaćina kojeg želimo navođenjem njegovog imena u zaglavlju Host neće raditi kad se koristi HTTPS:

$ curl --header "Host: www.math.uniri.hr" https://www.biotech.uniri.hr/hr/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.10 (Debian) Server at www.math.uniri.hr Port 443</address>
</body></html>

Razlog je da više HTTPS poslužitelja na jednoj IP adresi zahtijeva Server Name Indication (kraće SNI, dokumentiran u RFC-u 6066: Transport Layer Security (TLS) Extensions: Extension Definitions u odjeljku 3. Server Name Indication). Za tu svrhu cURL ima parametre --resolve i --connect-to, od kojih prvi koristimo u nastavku.

Konfiguracija virtualnih domaćina za HTTPS

Želimo li virtualne domaćine korisiti u kombinaciji s HTTPS-om, dodat ćemo u my-httpd-vhosts.conf blokove za virtualne domaćine na HTTPS vratima 443:

<VirtualHost *:443>
    DocumentRoot "/var/www/html"
    SSLCertificateFile "/usr/local/apache2/conf/server.crt"
    SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"
</VirtualHost>

<VirtualHost *:443>
    DocumentRoot "/var/www/epimetej/html"
    <Directory "/var/www/epimetej/html">
        Require all granted
    </Directory>
    ServerName epimetej.rm.miletic.net
    SSLCertificateFile "/usr/local/apache2/conf/epimetej.crt"
    SSLCertificateKeyFile "/usr/local/apache2/conf/epimetej.key"
</VirtualHost>

<VirtualHost *:443>
    DocumentRoot "/var/www/prometej/html"
    <Directory "/var/www/prometej/html">
        Require all granted
    </Directory>
    ServerName prometej.rm.miletic.net
    SSLCertificateFile "/usr/local/apache2/conf/prometej.crt"
    SSLCertificateKeyFile "/usr/local/apache2/conf/prometej.key"
</VirtualHost>

Vidimo da opet imamo zadani blok i onda po jedan blok za svako web sjedište. Ovi blokovi imaju dodatne konfiguracijske naredbe SSLCertificateFile i SSLCertificateKeyFile. Certifikate i privatne ključeve ćemo kao i ranije generirati OpenSSL-om i paziti da pod Common Name navedemo imena domena:

$ openssl req -x509 -nodes -days 30 -newkey rsa:4096 -keyout epimetej.key -out epimetej.crt
(...)
Common Name (e.g. server FQDN or YOUR name) []:epimetej.rm.miletic.net
(...)

$ openssl req -x509 -nodes -days 30 -newkey rsa:4096 -keyout prometej.key -out prometej.crt
(...)
Common Name (e.g. server FQDN or YOUR name) []:prometej.rm.miletic.net
(...)

Dockerfile ćemo dodati 4 nove naredbe COPY i sada je oblika:

FROM httpd:2.4
COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
COPY ./www /var/www
COPY server.crt /usr/local/apache2/conf
COPY server.key /usr/local/apache2/conf
COPY ./my-httpd-ssl.conf /usr/local/apache2/conf/extra/httpd-ssl.conf
COPY ./my-httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf
COPY epimetej.crt /usr/local/apache2/conf
COPY epimetej.key /usr/local/apache2/conf
COPY prometej.crt /usr/local/apache2/conf
COPY prometej.key /usr/local/apache2/conf

Izgradit ćemo sliku i pokrenut Docker kontejner:

$ docker build -t "my-httpd:2.4-6" .
Sending build context to Docker daemon   72.7kB
Step 1/11 : FROM httpd:2.4
---> b2c2ab6dcf2e
Step 2/11 : COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
---> Using cache
---> e475515ada38
Step 3/11 : COPY ./www /var/www
---> Using cache
---> c764360e0255
Step 4/11 : COPY server.crt /usr/local/apache2/conf
---> Using cache
---> fc1bfa23df69
Step 5/11 : COPY server.key /usr/local/apache2/conf
---> Using cache
---> b1b86d1c6b3d
Step 6/11 : COPY ./my-httpd-ssl.conf /usr/local/apache2/conf/extra/httpd-ssl.conf
---> Using cache
---> 9c09539265cd
Step 7/11 : COPY ./my-httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf
---> 34ae4d97114f
Step 8/11 : COPY epimetej.crt /usr/local/apache2/conf
---> 96fd7f0e9a88
Step 9/11 : COPY epimetej.key /usr/local/apache2/conf
---> 5a63ce1a7686
Step 10/11 : COPY prometej.crt /usr/local/apache2/conf
---> eb5005e4dbfe
Step 11/11 : COPY prometej.key /usr/local/apache2/conf
---> 3b4c03a5e682
Successfully built 3b4c03a5e682

$ docker run my-httpd:2.4-6
[Sun May 10 22:49:09.438791 2020] [ssl:warn] [pid 1:tid 140677310489728] AH01906: www.example.com:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 22:49:09.439105 2020] [ssl:warn] [pid 1:tid 140677310489728] AH01909: www.example.com:443:0 server certificate does NOT include an ID which matches the server name
[Sun May 10 22:49:09.439521 2020] [ssl:warn] [pid 1:tid 140677310489728] AH01906: prometej.rm.miletic.net:80:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 22:49:09.439933 2020] [ssl:warn] [pid 1:tid 140677310489728] AH01906: epimetej.rm.miletic.net:80:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 22:49:09.443353 2020] [ssl:warn] [pid 1:tid 140677310489728] AH01906: www.example.com:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 22:49:09.443360 2020] [ssl:warn] [pid 1:tid 140677310489728] AH01909: www.example.com:443:0 server certificate does NOT include an ID which matches the server name
[Sun May 10 22:49:09.443707 2020] [ssl:warn] [pid 1:tid 140677310489728] AH01906: prometej.rm.miletic.net:80:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 22:49:09.444046 2020] [ssl:warn] [pid 1:tid 140677310489728] AH01906: epimetej.rm.miletic.net:80:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 22:49:09.445125 2020] [mpm_event:notice] [pid 1:tid 140677310489728] AH00489: Apache/2.4.43 (Unix) OpenSSL/1.1.1d configured -- resuming normal operations
[Sun May 10 22:49:09.445153 2020] [core:notice] [pid 1:tid 140677310489728] AH00094: Command line: 'httpd -D FOREGROUND'

Sad možemo cURL-om isprobati da virtualni domaćini rade ispravno kad se koristi HTTPS (zbog SNI-ja nije dovoljno koristiti --header Host: ... pa koristimo --resolve):

$ curl -k --resolve prometej.rm.miletic.net:443:172.17.0.2 https://prometej.rm.miletic.net/
<html><body><h1>Prometej</h1></body></html>
$ curl -k --resolve epimetej.rm.miletic.net:443:172.17.0.2 https://epimetej.rm.miletic.net/
<html><body><h1>Epimetej</h1></body></html>

Mi ovdje cURL-u parametrom --resolve kažemo da preskoči DNS pretragu i zatraži URL-ove https://prometej.rm.miletic.net/ i https://epimetej.rm.miletic.net/ na adresi 172.17.0.2 i vratima 443. Uvjerimo se da ostali zahtjevi završavaju na zadanom virtualnom domaćinu:

$ curl -k --resolve atlas.rm.miletic.net:443:172.17.0.2 https://atlas.rm.miletic.net/
<html><body><h1>Radi!</h1></body></html>
$ curl -k https://172.17.0.2/
<html><body><h1>Radi!</h1></body></html>

Author: Vedran Miletić