Obsah

Reverzní HTTP proxy server

K čemu je to dobré? Potřebu jsem začal mít ve chvíli, kdy jsem začal více pracovat s dockerem. Najednou jsem měl několik služeb, které běží přes http a na různých portech. Přistupovat k takovým službám přes porty je nepohodlné, pro běžné uživatele prakticky nepoužitelné a ještě navíc nezabezpečené. Protože služby běží defaultně skrze HTTP.

I když je v názvu HTTP proxy server, tak je jednoduché ho změnit na HTTPS server.

Reverzní proxy lze rozjet minimálně na 2 serverech. Apache nebo nginx. Server bude naslouchat na klasických portech 80 a 443 a přesměruje komunikaci na localhost:9000 (Portainer) nebo na localhost:1880 (Node RED). Uživatel porty neuvidí, bude přistupovat k serveru klasicky.

Apache

Nejprve jsem potřeboval zapnout potřebné moduly:

sudo a2enmod proxy proxy_http proxy_balancer lbmethod_byrequests rewrite headers
sudo systemctl restart apache2

U Apache se upraví konfigurační soubor /etc/apache2/sites-available/000-default.conf.

<VirtualHost *:80>
    ProxyPreserveHost On

    ProxyPass / http://127.0.0.1:1880/
    ProxyPassReverse / http://127.0.0.1:1880/
</VirtualHost>

Tady pozor na lomítka na konci za portem. Bez nich jsem měl problémy. Návod jsem čerpat odsud: How To Use Apache as a Reverse-Proxy with mod_proxy on Ubuntu 20.04. Další návod je zde: https://www.howtogeek.com/devops/how-to-set-up-a-reverse-proxy-with-apache/.

Po úpravě konfiguračního souboru je potřeba reloadnout konfiguraci nebo restartovat apache.

sudo systemctl reload apache2

zapnutí HTTPS

Pro zapnutí https je potřeba zapnout další modul:

sudo a2enmod ssl
sudo systemctl restart apache2

Konfigurační soubor /etc/apache2/sites-enabled/default-ssl.conf pak může vypadat takto:

 <VirtualHost *:443>
 
        SSLEngine On
 
        # Set the path to SSL certificate
        # Usage: SSLCertificateFile /path/to/cert.pem
        SSLCertificateFile /etc/apache2/ssl/file.pem
 
 
        # Servers to proxy the connection, or;
        # List of application servers:
        # Usage:
        # ProxyPass / http://[IP Addr.]:[port]/
        # ProxyPassReverse / http://[IP Addr.]:[port]/
        # Example: 
        ProxyPass / http://127.0.0.1:1880/
        ProxyPassReverse / http://127.0.0.1:1880/
 
        # Or, balance the load:
        # ProxyPass / balancer://balancer_cluster_name
 
    </VirtualHost>

problém - Lost Connection..

Problém byl, že u Node REDu apache nefungoval jak má. Po chvíli Node RED vypisoval hlášení: „Lost connection..“. Hledal jsem v diskusích a nenašel jsem uspokojivé řešení, které by fungovalo. Nakonec jsem na podnět v diskusích přešel na nginx - ten byl skutečně řešením.

Při testování umělé inteligence chatGPT jsem vznesl dotaz a našla mi řešení pro apache, které se zdá být funkční. Problém je opravdu s web sockety a přidal jsem do konfiguračního souboru toto:

RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*)           ws://localhost:1880/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*)           http://localhost:1880/$1 [P,L]

reverzní proxy pro více služeb

S touto konfigurací jsem se natrápil nejvíce. Chtěl jsem, aby jednotlivé služby běžely na těchto url:

Pozor - je důležité na konci dát vždycky lomítko - jak v URL, tak konfiguraci, jinak jsem se setkal s problémy a dlouho trvalo, než jsem na toto přišel. Konfigurační soubor apache pak bude obsahovat tyto řádky (počítám s websockety pro nodered):

RewriteEngine on

RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/nodered/(.*) ws://localhost:1880/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule ^/nodered/(.*) http://localhost:1880/$1 [P,L]

ProxyPass /nodered/ http://localhost:1880/
ProxyPassReverse /nodered/ http://localhost:1880/

ProxyPass /portainer/ http://localhost:9000/
ProxyPassReverse /portainer/ http://localhost:9000/

úpravy pro Nextcloudpi

Nejprve jsem do konfiguračního souboru Apache zadal tyto řádky:

RewriteRule ^/nextcloud/(.*) ws://localhost:8880/$1 [P,L]  
RewriteRule ^/nextcloud/(.*) http://localhost:8880/$1 [P,L]

Poté je podle návodu k nextcloudu nutné editovat soubor config.php a nastavit v něm reverzní proxy. V mém případě jsem musel zadat něco takového:

'trusted_proxies' =>
array (
  11 => '127.0.0.1',
  12 => '::1',
  13 => 'e906aaab10d6',
  14 => '172.27.0.3',
  15 => '192.168.1.20'
),
'overwriteprotocol' => 'https',
'overwritehost' => 'mojedomena.cz',
'overwritewebroot'  => '/nextcloud',
'overwritecondaddr' => '^192\.168\.1\.20$'

Bohužel jsem zjistil, že konfigurace nebyla dostačující. Webové rozhraní fungovalo, ale když jsem se snažil použít klienta na automatickou synchronizaci, tak klient měl problém s https protokolem. Prozatím jsem hledání řešení vzdal, protože jsem ani po několika hodinách nebyl úspěšný.

PiHole

Pro konfiguraci Pi Hole bylo nutné do Apache zadat tyto řádky:

ProxyPass /pihole/ http://localhost:8089/admin/       
ProxyPassReverse /pihole/ http://localhost:8089/admin/

Stejně služba nefungovala jak má, protože se mi nevypisovala uložená data. Pak jsem narazil na tuto diskusi s řešením: https://www.reddit.com/r/Actualfixes/comments/ulzrbj/fix_pihole_behind_proxy_error_invalid_json/

Řešení popisuje, že je potřeba přizpůsobit nastavení lighttpd v docker kontejneru. V docker konterjneru přidat do souboru /etc/lightttpd/external.conf tento řádek (nastavit tam IP adresu serveru):

setenv.add-environment = ( "VIRTUAL_HOST" => "<your-domain>" )

Řešení bude funkční pouze do chvíle aktualizaci kontejneru. Proto jsem do docker compose souboru přidal tento řádek:

volumes:
  - /mojecesta/external.conf:/etc/lighttpd/external.conf

Nejprve ale bylo potřeba na disku vytvořit soubor /mojecesta/external.conf jinak se mi nepodařilo kontejner spustit.

Pak jsem ale našel lepší řešení v dokumentaci docker Pi Hole kontejneru. Výsledná konfigurace pro docker-compose pak vypadá takto:

pihole:
  container_name: pihole
  image: pihole/pihole:latest
  ports:
  - "8089:80/tcp"
  - "53:53/tcp"
  - "53:53/udp"
  - "67:67/udp"
  environment:
  - TZ=Etc/UTC
  - WEBPASSWORD=supertajneheslo
  - INTERFACE=eth0
  - VIRTUAL_HOST=192.168.1.20
  volumes:
  - ./volumes/pihole/etc-pihole:/etc/pihole
  - ./volumes/pihole/etc-dnsmasq.d:/etc/dnsmasq.d
    #- ./volumes/pihole/external.conf:/etc/lighttpd/external.conf
  dns:
  - 127.0.0.1
  - 1.1.1.1
  cap_add:
  - NET_ADMIN
  restart: unless-stopped

V závěru jsem objevil ještě jeden problém. Pi Hole po přihlášení přesměrovává na IP_ADRESA_SERVER/admin . Takže po přihlášení do Pi Hole jsem přesměrován mimo reverzní proxy. Nakonec jsem to vyřešil tak, že jsem Pi Hole spustil na IP_ADRESA_SERVER/admin namísto /pihole. Funkční konfigurace Apache pak vypadá takto:

RewriteRule ^(.*)/pihole/$ $1/admin/ [R]
ProxyPass /admin/ http://localhost:8089/admin/
ProxyPassReverse /admin/ http://localhost:8089/admin/

Setkal jsem se s problémem, když jsem pihole nemohl nastartovat, protože port 53 byl obsazený již nějakou jinou službou. V mém případě to byl connect manager. Pomohla editace souboru /etc/connman.prefs (v distribuci OSMC je soubor tady /etc/osmc/prefs.d/connman) a nastavit:

dnsproxy=no

A pak restartovat connection managera:

systemctl restart connman

Další tipy k samotnému Pi Hole:

A tady odkaz na servery s blocklisty:

Redmine

Potřeboval jsem nasadit Redmine pomocí Dockeru. Databáze MySQL běží na hostitelském počítači také v Dockeru, ale naslouchá pouze na `127.0.0.1`. Aby se Redmine mohl připojit k databázi, použil jsem nástroj `socat`, který přeposílá porty. Apache běží také jako kontejner a slouží jako reverzní proxy.

Konfigurace Redmine kontejneru

Vytvořil jsem následující `docker-compose.yml`:

redmine:                                                                                  
    container_name: redmine                                                               
    image: redmine                                                                        
    restart: always                                                                       
    ports:                                                                                
      - '127.0.0.1:3000:3000'                                                             
    environment:                                                                                                                           
      REDMINE_DB_MYSQL: 172.17.0.1                                                        
      REDMINE_DB_PORT: 33060                                                              
      REDMINE_DB_USERNAME: db-user                                         
      REDMINE_DB_PASSWORD: db-password                                               
      REDMINE_DB_DATABASE: db-name                                             
      REDMINE_SECRET_KEY_BASE: secret-key                                        
    volumes:                                                                              
      - './volumes/redmine/configuration.yml:/usr/src/redmine/config/configuration.yml' 

Redmine se připojuje na IP `172.17.0.1`, což je adresa hostitelského systému z pohledu Dockeru. Jelikož databáze naslouchá pouze na `127.0.0.1`, bylo nutné vytvořit TCP forward pomocí `socat`.

Socat pro přesměrování TCP portu

Použil jsem službu `systemd`, která při startu systému spustí `socat`:

/etc/systemd/system/mysql-forward-docker.service
 
[Unit]
Description=MySQL TCP forwarder for Docker
After=network-online.target docker.service
Wants=network-online.target docker.service
 
[Service]
ExecStartPre=/bin/bash -c 'until /usr/bin/socat -T1 -u TCP-LISTEN:0,bind=172.17.0.1 - > /dev/null & PID=$!; sleep 1; kill $PID; do echo "Waiting for 172.17.0.1 to become bindable..."; sleep 2; done'
ExecStart=/usr/bin/socat TCP-LISTEN:33060,bind=172.17.0.1,fork TCP:127.0.0.1:3306
Restart=on-failure
RestartSec=3
 
[Install]
WantedBy=multi-user.target

Tento skript vytvoří forward: všechny požadavky na `172.17.0.1:33060` jsou přeposílány na `127.0.0.1:3306`. Tímto způsobem se Redmine kontejner dostane k databázi, která běží na hostiteli.

* `socat` slouží jako jednoduchý TCP forwarder. * Využívá `fork`, takže zvládne více spojení najednou. * Před spuštěním hlavního příkazu čeká, než bude možné IP `172.17.0.1` bindnout.

MySQL uživatel a přístup

Při vytváření uživatele v MySQL jsem musel explicitně uvést IP adresu `127.0.0.1`. Pokud bych použil `localhost`, spojení by nefungovalo.

Například:

CREATE USER 'db-user'@'127.0.0.1' IDENTIFIED BY 'db-password';
GRANT ALL PRIVILEGES ON db-name.* TO 'db-user'@'127.0.0.1';

Nastavení firewallu

Redmine běží v jiné Docker síti a má vlastní IP adresu. V mém případě to byla `172.19.0.2`. Aby se kontejner mohl připojit na port 33060, bylo třeba povolit tento provoz ve firewallu:

ufw allow from 172.19.0.2/16 to any port 33060

IP adresu kontejneru lze zjistit příkazem:

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' redmine

Tímto jsem zajistil funkční připojení Redmine k databázi běžící na hostitelském systému.

Nastavení Apache

Jelikož Redmine běží v Dockeru na portu `127.0.0.1:3000`, bylo potřeba nakonfigurovat reverzní proxy přes Apache. Apache běží také jako kontejner a naslouchá na portu 80 a 443.

Použil jsem následující konfiguraci virtuálních hostů v Apache:

<VirtualHost *:80>
    ServerName redmine.example.com
    ServerAlias redmine.example.com
 
    ServerAdmin info@example.com
    Redirect permanent "/" "https://redmine.example.com/"
</VirtualHost>
 
<IfModule mod_ssl.c>
    <VirtualHost _default_:443>
 
        ServerName redmine.example.com
        ServerAlias redmine.example.com
        ServerAdmin info@example.com
 
        Protocols h2 http/1.1
 
        # Reverse proxy konfigurace
        ProxyPreserveHost On
        ProxyPass / http://localhost:3000/
        ProxyPassReverse / http://localhost:3000/
 
        # SSL konfigurace
        SSLEngine on
 
        SSLCertificateFile      /var/lib/dehydrated/certs/example.com/cert.pem
        SSLCertificateKeyFile   /var/lib/dehydrated/certs/example.com/privkey.pem
        SSLCertificateChainFile /var/lib/dehydrated/certs/example.com/chain.pem
        SSLCACertificateFile    /var/lib/dehydrated/certs/example.com/fullchain.pem
 
    </VirtualHost>
</IfModule>

Tato konfigurace:

Tímto způsobem lze přistupovat k Redmine z prohlížeče pomocí domény `https://redmine.example.com/`.

Nginx

Přechod na nginx trochu zabolel. Ze začátku jsem dělal chybu, že za každým konfiguračním příkazem musí být středník. Rovnou ukazuji konfiguraci serveru, kdy přesměrovávám http na https a provozuji 2 služby. Node RED a portainer. Při konfiguraci mi pomohl zdroj Behind a reverse proxy.

Nejprve defaultní server s Node RED v souboru: /etc/nginx/sites-enabled/default:

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        # SSL configuration
        #
        listen 443 ssl default_server;
        #listen [::]:443 ssl default_server;

        ssl_certificate         /etc/ssl/server-cert.pem;
        ssl_certificate_key     /etc/ssl/server-key.pem;

        server_name garmin.cn;

        location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        #try_files $uri $uri/ =404;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;

        proxy_pass http://localhost:1880;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        }
}

Pak editaci služby portainer v souboru: /etc/nginx/sites-enabled/portainer.garmin.cn. Tady už řeším automatické přesměrování z HTTP na HTTPS.

server {
        listen 80;
        server_name portainer.garmin.cn;
        return 301 https://portainer.garmin.cn$request_uri;
}


server {


        listen 443 ssl;

        server_name portainer.garmin.cn;




        ssl_certificate /etc/ssl/server-cert.pem;
        ssl_certificate_key /etc/ssl/server-key.pem;

        # SSL configuration
        #
        # listen 443 ssl default_server;
        # listen [::]:443 ssl default_server;
        #
        # Note: You should disable gzip for SSL traffic.
        # See: https://bugs.debian.org/773332
        #
        # Read up on ssl_ciphers to ensure a secure configuration.
        # See: https://bugs.debian.org/765782
        #
        # Self signed certs generated by the ssl-cert package
        # Don't use them in a production server!
        #
        # include snippets/snakeoil.conf;

        #root /var/www/html;

        # Add index.php to the list if you are using PHP
        #index index.html index.htm index.nginx-debian.html;


        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $host;

                proxy_pass http://localhost:9000;

                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
        }

}