Section author: Vedran Miletić

Konfiguracija virtualnih domaćina u web poslužitelju Apache HTTP Server

Konfiguracijska naredba <VirtualHost> (dokumentacija) omogućuje Apacheju da poslužuje više različitih web sjedišta koja se nalaze na više različitih domena putem jedne IP adrese i vrata.

Primjer primjene virtualnih domaćina

Ilustracije radi, promotrimo web sjedišta www.math.uniri.hr, www.phy.uniri.hr i www.biotech.uniri.hr.

$ curl -v -I http://www.phy.uniri.hr/hr/
*   Trying 193.198.209.33:80...
* Connected to www.phy.uniri.hr (193.198.209.33) port 80 (#0)
> HEAD /hr/ HTTP/1.1
> Host: www.phy.uniri.hr
> User-Agent: curl/7.70.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
(...)

$ curl -v -I http://www.math.uniri.hr/hr/
*   Trying 193.198.209.33:80...
* Connected to www.math.uniri.hr (193.198.209.33) port 80 (#0)
> HEAD /hr/ HTTP/1.1
> Host: www.math.uniri.hr
> User-Agent: curl/7.70.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
(...)

$ curl -v -I http://www.biotech.uniri.hr/hr/
*   Trying 193.198.209.33:80...
* Connected to www.biotech.uniri.hr (193.198.209.33) port 80 (#0)
> HEAD /hr/ HTTP/1.1
> Host: www.biotech.uniri.hr
> User-Agent: curl/7.70.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
(...)

Uočimo da se ova tri web sjedišta nalaze na istoj IP adresi (193.198.209.33) i istim vratima (80), ali poslužitelj zna točno koji sadržaj mora isporučiti u odgovoru zahvaljujući vrijednosti polja Host: u svakom od HTTP zahtjeva. Vrijednost tog polja se može postaviti u cURL-u kod slanja HTTP zahtjeva:

$ curl --header "Host: www.math.uniri.hr" http://www.biotech.uniri.hr/hr/
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="hr-hr" lang="hr-hr" >
<head>
  <base href="http://www.math.uniri.hr/hr/" />
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <meta name="generator" content="Joomla! - Open Source Content Management" />
  <title>Odjel za matematiku - Vijesti</title>
(...)

Server Name Indication

Ovaj pristup 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 HTTP

Recimo da imamo dvije domene, prometej.rm.miletic.net i epimetej.rm.miletic.net te da želimo na njih postaviti dva različita web sjedišta. Stvorimo sadržaj tih web sjedišta:

$ mkdir -p www/{epimetej,prometej}/html
$ echo '<html><body><h1>Epimetej</h1></body></html>' > www/epimetej/html/index.html
$ echo '<html><body><h1>Prometej</h1></body></html>' > www/prometej/html/index.html

Apache koji koristimo već dolazi s konfiguracijskom datotekom za virtualne domaćine koju možemo prilagoditi i uključiti. Dohvatimo tu datoteku:

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

Uredimo tu datoteku; uočimo da su nam konfiguracijske naredbe ServerAdmin, ServerName i DocumentRoot već poznate, samo se nalaze unutar bloka naredbi <VirtualHost>:

# Virtual Hosts
#
# Required modules: mod_log_config

# If you want to maintain multiple domains/hostnames on your
# machine you can setup VirtualHost containers for them. Most configurations
# use only name-based virtual hosts so the server doesn't need to worry about
# IP addresses. This is indicated by the asterisks in the directives below.
#
# Please see the documentation at
# <URL:http://httpd.apache.org/docs/2.4/vhosts/>
# for further details before you try to setup virtual hosts.
#
(...)
<VirtualHost *:80>
    ServerAdmin webmaster@dummy-host.example.com
    DocumentRoot "/usr/local/apache2/docs/dummy-host.example.com"
    ServerName dummy-host.example.com
    ServerAlias www.dummy-host.example.com
    ErrorLog "logs/dummy-host.example.com-error_log"
    CustomLog "logs/dummy-host.example.com-access_log" common
</VirtualHost>

<VirtualHost *:80>
    ServerAdmin webmaster@dummy-host2.example.com
    DocumentRoot "/usr/local/apache2/docs/dummy-host2.example.com"
    ServerName dummy-host2.example.com
    ErrorLog "logs/dummy-host2.example.com-error_log"
    CustomLog "logs/dummy-host2.example.com-access_log" common
</VirtualHost>

Maknemo li konfiguracijsku naredbu ServerAdmin, za taj virtualni domaćin koristit će se ona vrijednost koju smo već ranije postavili u datoteci my-httpd.conf, što nam odgovara. Naredbu ServerAlias ne trebamo jer imamo samo jednu domenu po virtualnom domaćinu. Također, logging nam ovdje nije bitan pa ćemo i te dvije naredbe maknuti, čime smo eliminirali i potrebu za modulom mod_log_config koja se navodi u zaglavlju konfiguracijske datoteke u komentarima. Naposlijetku, moramo dodati dozvolu pristupa (Require all granted) za svaki od direktorija koji se koristi kao DocumentRoot. Konfiguracija (zasad samo za HTTP vrata 80) je oblika:

<VirtualHost *:80>
    DocumentRoot "/var/www/epimetej/html"
    <Directory "/var/www/epimetej/html">
        Require all granted
    </Directory>
    ServerName epimetej.rm.miletic.net
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot "/var/www/prometej/html"
    <Directory "/var/www/prometej/html">
        Require all granted
    </Directory>
    ServerName prometej.rm.miletic.net
</VirtualHost>

Kad se koriste virtualni domaćini, tada prvi navedeni u konfiguracijskoj datoteci postaje zadani virtualni domaćin i koristi se kod odgovora na zahtjeve kod kojih vrijednost u polju Host: ne odgovara ni jednoj vrijednosti konfiguracijske naredbe ServerName. Zato je dobra praksa eksplicitno prvo navesti zadani virtualni domaćin:

<VirtualHost *:80>
    DocumentRoot "/var/www/html"
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot "/var/www/epimetej/html"
    <Directory "/var/www/epimetej/html">
        Require all granted
    </Directory>
    ServerName epimetej.rm.miletic.net
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot "/var/www/prometej/html"
    <Directory "/var/www/prometej/html">
        Require all granted
    </Directory>
    ServerName prometej.rm.miletic.net
</VirtualHost>

U datoteci my-httpd.conf odkomentirajmo liniju koja uključuje konfiguracijsku datoteku koju smo upravo uredili:

(...)
# Virtual hosts
#Include conf/extra/httpd-vhosts.conf
(...)

Promijenimo Dockerfile da uključuje novu datoteku, dodajmo još jednu naredbu COPY:

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

Izgradimo sliku i pokrenimo Docker kontejner:

sudo docker build -t "my-httpd:2.4-5" .
Sending build context to Docker daemon  54.78kB
Step 1/7 : FROM httpd:2.4
---> b2c2ab6dcf2e
Step 2/7 : COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
---> e475515ada38
Step 3/7 : COPY ./www /var/www
---> c764360e0255
Step 4/7 : COPY server.crt /usr/local/apache2/conf
---> fc1bfa23df69
Step 5/7 : COPY server.key /usr/local/apache2/conf
---> b1b86d1c6b3d
Step 6/7 : COPY ./my-httpd-ssl.conf /usr/local/apache2/conf/extra/httpd-ssl.conf
---> 9c09539265cd
Step 7/7 : COPY ./my-httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf
---> 7839a9247066
Successfully built 7839a9247066
Successfully tagged my-httpd:2.4-5

$ sudo docker run my-httpd:2.4-5
[Sun May 10 22:09:41.234807 2020] [ssl:warn] [pid 1:tid 139624574960768] AH01906: www.example.com:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 22:09:41.235132 2020] [ssl:warn] [pid 1:tid 139624574960768] AH01909: www.example.com:443:0 server certificate does NOT include an ID which matches the server name
[Sun May 10 22:09:41.238436 2020] [ssl:warn] [pid 1:tid 139624574960768] AH01906: www.example.com:443:0 server certificate is a CA certificate (BasicConstraints: CA == TRUE !?)
[Sun May 10 22:09:41.238443 2020] [ssl:warn] [pid 1:tid 139624574960768] AH01909: www.example.com:443:0 server certificate does NOT include an ID which matches the server name
[Sun May 10 22:09:41.239474 2020] [mpm_event:notice] [pid 1:tid 139624574960768] AH00489: Apache/2.4.43 (Unix) OpenSSL/1.1.1d configured -- resuming normal operations
[Sun May 10 22:09:41.239504 2020] [core:notice] [pid 1:tid 139624574960768] AH00094: Command line: 'httpd -D FOREGROUND'

Uvjerimo se da nam virtualni domaćini rade:

$ curl --header "Host: prometej.rm.miletic.net" http://172.17.0.2/
<html><body><h1>Prometej</h1></body></html>
$ curl --header "Host: epimetej.rm.miletic.net" http://172.17.0.2/
<html><body><h1>Epimetej</h1></body></html>

Uvjerimo se da nam zadani virtualni domaćin hvata sve zahtjeve koji ne pašu na ova dva iznad:

$ curl http://172.17.0.2/
<html><body><h1>Radi!</h1></body></html>
$ curl --header "Host: atlas.rm.miletic.net" http://172.17.0.2/
<html><body><h1>Radi!</h1></body></html>

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:

$ sudo 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

$ sudo 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>