VPS dla początkujących cz. 3: reverse proxy oraz SSL

Napisano dnia 23.07.2021 r. o godzinie 7:00
Autor: Piotr Sperka

Wstęp

Cześć, witaj w trzeciej części serii pod tytułem VPS dla początkujących. Poprzednim razem zainstalowaliśmy Dockera, i postawiliśmy na nim dwie usługi – WordPressa oraz phpMyAdmin. Zauważyliśmy również problemy – brak obsługi subdomen oraz HTTPS. I to właśnie są zagadnienia, którymi dziś się zajmiemy. W całym poniższym opisie założyłem, że operujesz z poziomu konta roota.

Nginx jako reverse proxy

Na początek podsumuję w skrócie co już mamy i do czego dążymy. Obecnie architektura uruchomionych aplikacji na serwerze wygląda mniej więcej tak:

VPS bez serwera proxy

VPS bez reverse proxy, obecna konfiguracja

Jak wiadomo, dany port sieciowy maszyny może być zajęty tylko przez jedną aplikację. W związku z tym w obecnej konfiguracji każda aplikacja/usługa musi być wystawiona na osobnym porcie. Niesie to za sobą szereg problemów. Dodatkowo chcielibyśmy mieć możliwość ustawiania stosownych subdomen, przekierowań, a także wprowadzić obsługę SSL (HTTPS). Wszystkie te problemy można rozwiązać stosując reverse proxy. W tej roli wykorzystamy serwer Nginx, który uruchomimy również w formie kontenera na Dockerze. Architektura, do której obecnie dążymy, będzie wyglądała następująco:

VPS z serwerem proxy

VPS z reverse proxy, docelowa konfiguracja

W tej konfiguracji jedynie kontener z serwerem Nginx będzie wystawiony na świat na portach 80 i 443 (HTTP i HTTPS), natomiast cała reszta komunikacji będzie zamknięta wewnątrz Dockera.

Instalacja Nginx

Instalację standardowo zaczynamy od stworzenia katalogu pod docker-compose i samego pliku konfiguracyjnego. Tworzymy katalog /docker/nginx-proxy i plik docker-compose.yml:

version: '3.5'

services:
  nginx:
    container_name: reverse_proxy
    image: nginx:latest
    ports:
      - 80:80
      - 443:443
    volumes:
      - /docker/nginx-proxy/nginx.conf:/etc/nginx/nginx.conf
      - /docker/nginx-proxy/proxy.conf:/etc/nginx/includes/proxy.conf
      - /docker/nginx-proxy/cache/:/etc/nginx/cache
      - /etc/letsencrypt/:/etc/letsencrypt/
    networks:
      - reverse_proxy
networks:
  reverse_proxy:
    name: reverse_proxy
    external: false

Jak widzisz w powyższej konfiguracji, wykorzystaliśmy serwer Nginx w najnowszej dostępnej wersji z Docker Huba. Stworzyliśmy sieć reverse_proxy, do której dostęp będą miały wszystkie usługi wystawiane poprzez reverse proxy. Dodatkowo zamontowaliśmy kilka wolumenów:

  • nginx.conf – plik konfiguracyjny Nginx.
  • proxy.conf – plik konfiguracyjny, który posłuży w pliku nginx.conf; opisuje zachowanie proxy.
  • katalog cache – zawiera pliki cache serwera Nginx.
  • katalog /etc/letsencrypt – będzie zawierał certyfikaty wygenerowane przez Let’s Encrypt.

Teraz przechodzimy to stworzenia plików konfiguracyjnych. Plik /docker/nginx-proxy/proxy.conf prezentuje się następująco:

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-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_intercept_errors on;

Natomiast plik /docker/nginx-proxy/nginx.conf następująco:

http {
  error_log /etc/nginx/error_log.log warn;
  client_max_body_size 200m;

  proxy_cache_path /etc/nginx/cache keys_zone=one:500m max_size=1000m;

#  server {
#    listen 80 default_server;
#    server_name _;
#    return 301 https://$host$request_uri;
#  }

  server {
    server_name twoja-domena.pl;
    listen 80;
    return 301 http://blog.twoja-domena.pl;
  }

  server {
    server_name blog.twoja-domena.pl;

    location / {
      include /etc/nginx/includes/proxy.conf;
      proxy_pass http://wordpress:80;
    }

    listen 80;
  }

  server {
    server_name pma.twoja-domena.pl;

    location / {
      include /etc/nginx/includes/proxy.conf;
      proxy_pass http://phpmyadmin:80;
    }

    listen 80;
  }
}

W powyższym pliku założyłem, że korzystasz z domeny twoja-domena.pl. Pierwszy blok server odpowiada za przekierowanie całego ruchu z HTTP na HTTPS. Celowo jest on tymczasowo zakomentowany, żeby umożliwić pierwsze testy bez certyfikatu. W przypadku uruchamiania tego typu usług, najlepiej jest stosować metodę małych kroczków. Drugi blok server zapewnia automatyczne przekierowanie z adresu twoja-domena.pl na blog.twoja-domena.pl. Jeśli takie przekierowanie Cię nie interesuje, również możesz go usunąć. Trzeci i czwarty blok server odpowiadają odpowiednio za przekierowanie na kontenery WordPressa oraz phpMyAdmin. Oczywiście, musisz pamiętać o dodaniu odpowiednich rekordów CNAME do konfiguracji DNS swojej domeny. W naszym hipotetycznym przypadku będą to dwa wpisy:

blog.twoja-domena.pl. CNAME twoja-domena.pl.
pma.twoja-domena.pl.  CNAME twoja-domena.pl.

Teraz możemy testowo uruchomić Nginxa znanym już poleceniem docker-compose up -d. Serwer uruchomi się, jednak co najważniejsze, zostanie stworzona sieć reverse_proxy. Możesz zweryfikować to poleceniem docker network ls. Musimy teraz do niej podłączyć wszystkie kontenery, które mają się ze sobą komunikować. Zaczynamy od modyfikacji pliku docker-compose.yml zawierającego konfigurację WordPressa:

version: '3.1'

services:

  wordpress:
    image: wordpress:latest
    restart: always
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: haslodobazy
      WORDPRESS_DB_NAME: wordpress-db
    volumes:
      - /docker/wordpress/html:/var/www/html
    expose:
      - 80
    networks:
      - reverse_proxy

  db:
    image: mariadb:latest
    restart: always
    environment:
      MYSQL_DATABASE: wordpress-db
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: haslodobazy
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - /docker/wordpress/mysql:/var/lib/mysql
    networks:
      - reverse_proxy
networks:
  reverse_proxy:
    external: true

Na czerwono zaznaczyłem fragmenty, które uległy zmianie. Przede wszystkim usunęliśmy tag port, zastępując go tagiem expose. Tag expose, w przeciwieństwie do tagu port, nie wystawi podanego portu na zewnątrz maszyny VPS, a jedynie umożliwi komunikację innym kontenerom na tym porcie. Znany już tag networks podłącza kontenery do sieci reverse_proxy. Teraz zajmijmy się drobnymi zmianami w pliku docker-compose.yml dla phpMyAdmin:

version: '3.1'

services:
  phpmyadmin:
    image: phpmyadmin
    restart: always
    environment:
      - PMA_ARBITRARY=1
    networks:
      - reverse_proxy

networks:
  reverse_proxy:
    external: true

No to jesteśmy prawie gotowi. Poleceniami

docker-compose down
docker-compose up -d

wykonanymi w katalogach /docker/phpmyadmin i /docker/wordpress tworzymy na nowo kontenery phpMyAdmin oraz WordPressa. Dla pewności restartujemy Nginx poleceniem docker-compose restart wykonanym w /docker/nginx-proxy. Pozostało tylko otworzyć w firewallu porty 80 i 443 poleceniami:

ufw allow 80/tcp
ufw allow 443/tcp

Jeśli wszystko wykonałeś poprawnie, wejście pod adres http://pma.twoja-domena.pl zaskutkuje wyświetleniem strony phpMyAdmin z logowaniem do bazy danych. Nie bez przyczyny zacząłem od phpMyAdmin. Żeby WordPress zaczął działać w pełni poprawnie, musisz zmodyfikować w bazie dwa rekordy w tabeli wp-options, tak, by wskazywały na twoja-domena.pl. Interesujące Cię rekordy zaznaczyłem na poniższym obrazku:

Modyfikacja rekordów domenty w ustawieniach WordPressa

Modyfikacja rekordów domenty w ustawieniach WordPressa

Po tej zmianie swobodnie możesz wypróbować działanie strony http://blog.twoja-domena.pl. Jeśli wszystko działa, możemy przejść do wygenerowania certyfikatu SSL przy pomocy Let’s Encrypt i Cloudflare.

SSL z LetsEncrypt i Cloudflare

Let’s Encrypt umożliwia darmowe wygenerowanie certyfikatów SSL dla Twojej domeny. Szerzej na ten temat można poczytać na stronie Let’s Encrypt i w wielu innych miejscach. Można generować zarówno certyfikaty dla każdej subdomeny osobno, jak i certyfikat wildcard, działający dla domeny i wszystkich jej subdomen. Taki certyfikat będzie dla nas wygodniejszy z punktu widzenia administrowania. Jest jednak jeden haczyk, a w zasadzie to dwa.

Do udowodnienia, że jesteśmy właścicielami danej domeny potrzeba ukończyć tak zwane wyzwanie. Więcej na ten temat można poczytać tutaj. Aby wygenerować certyfikat wildcard, nie możemy użyć najprostszego wyzwania HTTP-01. Wykorzystamy więc wyzwanie DNS-01. Polega ono na tym, że na czas generowania certyfikatu trzeba dodać odpowiednie wpisy w DNS, do którego podpięta jest nasza domena. Żeby działało to dobrze, potrzebny jest dostawca DNS u którego zmiany szybko się propagują. Nie chcemy czekać wielu godzin, aż zmiany staną się dostępne. Dodatkowo najlepiej nie robić tego ręcznie. Dlaczego? Z powodu wygody. Certyfikaty Let’s Encrypt ważne są tylko 3 miesiące, więc dobrze jest ten proces zautomatyzować. I tutaj wchodzi program jakim jest Certbot. Po pierwsze umożliwia on wygenerowanie certyfikatów jednym poleceniem, a po drugie współpracuje z wieloma różnymi dostawcami DNS w celu pełnej automatyzacji procesu. Pełną listę wspieranych dostawców można znaleźć w dokumentacji, albo na przykład tutaj. Dla nas najważniejszy jest fakt, że znajduje się wśród nich Cloudflare.

Zaczynamy od wygenerowania tokenu API Cloudflare, który umożliwi programowi Certbot dodanie odpowiedniego wpisu i ukończenie wyzwania DNS-01. Po zalogowaniu się, przechodzimy do My Account i API Tokens. Generujemy token z uprawnieniem Zone.DNS dla wszystkich stref. Zapisujemy token, gdyż po wyjściu ze strony nie da się już ponownie go wyświetlić. Wracamy teraz do naszej maszyny i tworzymy plik, w którym zapiszemy token. W moim wypadku był to plik /root/.secrets/certbot/cloudflare.ini. Jego zawartość to jedna linia:

dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567

Oczywiście, ciąg znaków podmieniamy na swój wygenerowany token. Certbota nie instalujemy z repozytorium oprogramowania. W systemie Debian 10 dostępna wersja jest wręcz antyczna. Instalujemy za to Python 3:

apt install python3 python3-venv

Następnie tworzymy venv dla Certbota, instalujemy go wraz z pluginem dla Cloudflare i dodajemy dowiązanie symboliczne, który spowoduje, że program znajdzie się w ścieżce dostępnej w PATH.

python3 -m venv /opt/certbot/
/opt/certbot/bin/pip install --upgrade pip
/opt/certbot/bin/pip install certbot
/opt/certbot/bin/pip install certbot-dns-cloudflare
ln -s /opt/certbot/bin/certbot /usr/bin/certbot

Ostatnim krokiem jest wygenerowanie certyfikatu wildcard. Warto odnotować, że certyfikat ten musi dotyczyć zarówno twoja-domena.pl, jak i *.twoja-domena.pl. Wildcard *.twoja-domena.pl nie obejmuje domeny bez subdomeny!

certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini -d twoja-domena.pl -d *.twoja-domena.pl

Jeśli wszystko przeszło poprawnie, właśnie staliśmy się posiadaczami certyfikatu SSL dla naszej domeny. Pozostało teraz zmodyfikować plik nginx.conf oraz zrestartować reverse proxy poleceniem docker-compose restart. W tym momencie przechodząc pod dowolną ze skonfigurowanych subdomen, powinieneś zostać przekierowany pod HTTPS, a przeglądarka nie powinna zgłosić żadnych zastrzeżeń co do certyfikatu. Zmodyfikowany plik nginx.conf wygląda następująco:

http {
  error_log /etc/nginx/error_log.log warn;
  client_max_body_size 200m;

  proxy_cache_path /etc/nginx/cache keys_zone=one:500m max_size=1000m;

  server {
    listen 80 default_server;
    server_name _;
    return 301 https://$host$request_uri;
  }

  server {
    server_name twoja-domena.pl;
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/twoja-domena.pl/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/twoja-domena.pl/privkey.pem;
    return 301 https://blog.twoja-domena.pl;
  }

  server {
    server_name blog.twoja-domena.pl;

    location / {
      include /etc/nginx/includes/proxy.conf;
      proxy_pass http://wordpress:80;
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/twoja-domena.pl/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/twoja-domena.pl/privkey.pem;
  }

  server {
    server_name pma.twoja-domena.pl;

    location / {
      include /etc/nginx/includes/proxy.conf;
      proxy_pass http://phpmyadmin:80;
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/twoja-domena.pl/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/twoja-domena.pl/privkey.pem;
  }
}

Porządki

Na koniec warto poczynić trochę porządków. Po pierwsze warto zamknąć w firewallu porty 8080 i 8081, skoro już ich nie używamy. Dodane reguły wraz z numerami porządkowymi można wyświetlić poleceniem ufw status numbered, natomiast poleceniem ufw delete numer_reguły usuwamy daną regułę. Warto po każdym usunięciu na nowo wyświetlić listę, gdyż numeracja przesuwa się 🙂 Inną drogą jest wywołanie poleceń:

ufw delete 8080/tcp
ufw delete 8081/tcp

Obie drogi są równoważne, wybierz tą, która jest dla Ciebie wygodniejsza. Ostatnią sprawą jest usunięcie z Dockera nieużywanych już elementów, na przykład sieci. Zrobisz to poleceniem docker system prune.

Podsumowanie

Podsumowując, dzisiaj udało nam się skonfigurować reverse proxy i pozyskać certyfikaty SSL. Dzięki temu mamy wygodną obsługę subdomen, jak i działające HTTPS z ważnymi certyfikatami. Jeśli jesteś dociekliwym czytelnikiem lub czytelniczką, zauważyłeś może dwie rzeczy. Po pierwsze nic nie wspomniałem o automatycznym odświeżeniu certyfikatów. Drugą sprawą jest konieczność restartu serwera proxy (i każdej innej usługi wykorzystującej certyfikaty) po ich odświeżeniu. I to właśnie są niektóre z tematów, które poruszymy w jednym z następnych odcinków. A już za tydzień zajmiemy się rozszerzeniem naszego katalogu usług o Giteę, Nextcloud oraz Dokuwiki.

O pozbyciu się pewnych niedogodności związanych z odświeżaniem certyfikatu SSL przeczytasz w tym wpisie.

Jak zwykle – zapraszam do kontaktu i do następnego 😉