Proxy Apache per servizio Docker con SSL

Proxy Apache per servizio Docker con SSL

Photo by Denny Müller on Unsplash

Alla ricerca della documentazione perduta

Oggigiorno se si deve configurare un web proxy per esporre un servizio gestito tramite Docker, la maggior parte della documentazione e delle guide online fa riferimento ad Nginx, il poco materiale che si trova per Apache invece è obsoleto o semplicemente non funziona. Ma perché scrivere una guida per un proxy Apache per Docker? Semplice: ci sono ancora situazioni in cui non è possibile sostituire Nginx ad Apache e quindi c’è la necessità un proxy Apache che punta ad un docker con a sua volta il web server Apache.

La difficoltà sta nel configurare tutto correttamente, soprattutto per quanto riguarda il certificato SSL per la connessione HTTPS.

Questa guida quindi sarà divisa in due parti, una prima parte con la configurazione del proxy, installato direttamente nel server, ed una seconda parte con la configurazione dei servizi Docker.

Gli esempi di questo articolo sono stati scritti in un server Debian Linux, ma sono facilmente adattabili a qualunque altra distribuzione.

Parte 1: come configurare il proxy Apache

Generazione certificato SSL

Per la generazione del certificato SSL ci affidiamo a Certbot / Let’s Encrypt. In questo articolo non spiegherò come installare Certbot, la guida ufficiale è più che sufficiente.

Per creare il certificato basta avere il DNS configurato ed eseguire questo comando che non ha tanto bisogno di spiegazioni:

certbot certonly -d example.com --apache -m "info@example.com" --agree-tos --no-eff-email

Moduli Apache

Per il corretto funzionamento di Apache come proxy server per gestire connessioni HTTPS è necessaria l’attivazione dei seguenti moduli:

  • http2
  • proxy
  • proxy_http
  • ssl
  • socache_shmcb

Si possono attivare con il seguente comando:

a2enmod http2 proxy proxy_http ssl socache_shmcb

Configurazione Virtual Host

Per comodità la configurazione del virtual host preferisco effettuarla in due file: uno per la versione http ed uno per la versione https, con il primo che farà un redirect al secondo.

Configurazione Virtual Host sulla porta 80 (http)

Il virtual host per la porta 80 semplicemente reindirizza alla versione HTTPS.

<VirtualHost *:80>
  ServerName example.com
  ServerAlias www.example.com

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

  ErrorLog ${APACHE_LOG_DIR}/example.com.error.log
  CustomLog ${APACHE_LOG_DIR}/example.com.access.log combined
</VirtualHost>

Configurazione Virtual Host sulla porta 443 (https)

Il virtual host per la porta 443 invece ha due sezioni da commentare.

La prima riguarda il proxy vero e proprio: attenzione a mettere il nome di dominio corretto nelle direttive ProxyPass e ProxyPassReverse. In queste due direttive va messa la porta HTTPS in cui Docker sarà in ascolto.

La seconda sezione è quella relativa ad SSL dove in sostanza viene semplicemente caricata la configurazione di Let’s Encrypt ed indicati i percorsi dei file di certificato.

<IfModule mod_ssl.c>
<VirtualHost *:443>
  ServerName example.com
  ServerAlias www.example.com

  ErrorLog ${APACHE_LOG_DIR}/example.com.error.log
  CustomLog ${APACHE_LOG_DIR}/example.com.access.log combined

  ProxyRequests on
  ProxyPreserveHost On
  ProxyPass / https://www.example.com:8080/
  ProxyPassReverse / https://www.example.com:8080/

  SSLProxyEngine On
  SSLProxyVerify none
  SSLProxyCheckPeerCN off
  SSLProxyCheckPeerName off
  SSLProxyCheckPeerExpire off

  Include /etc/letsencrypt/options-ssl-apache.conf
  SSLCertificateFile /etc/letsencrypt/live/www.example.com/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem

</VirtualHost>
</IfModule>

Parte 2: come configurare Docker

Servizio Docker

Per la gestione di Docker normalmente mi trovo bene con Docker Compose, ecco quindi la configurazione del servizio Apache in tandem con PHP-FPM.

Per il container PHP-FPM utilizzo devilbox/php-fpm che funziona molto bene, mentre per Apache l’immagine “httpd:alpine” è valida nella maggior parte dei casi.

Ecco un esempio di file docker-compose.yml.

version: '3'

services:

  #
  # PHP-FPM
  #
  www_example_com:
    image: devilbox/php-fpm:7.4-mods
    container_name: www_example_php
    restart: unless-stopped
    networks:
      - www_example_com
    volumes:
      - ./data/www:/srv/www/www.example.com/
      - ./conf/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf
      - ./conf/php/conf.d/docker-php-ext-opcache.ini:/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
      - ./logs/php-fpm:/var/log/php-fpm
    user: "${UID}:${GID}"
    environment:
      TIMEZONE: "Europe/Rome"
      ENABLE_MAIL: 1
      DEBUG_ENTRYPOINT: 0

  #
  # Apache
  #
  www_example_apache:
    image: httpd:alpine
    container_name: www_example_apache
    restart: unless-stopped
    networks:
      - www_example_net
    ports:
      - 8081:443
    volumes:
      - ./data/www:/srv/www/www.example.com/
      - ./conf/apache2/httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ./conf/apache2/www.example.com.conf:/usr/local/apache2/conf/extra/httpd-vhosts.conf
      - /etc/letsencrypt/live/www.example.com/fullchain.pem:/usr/local/apache2/conf/fullchain.pem
      - /etc/letsencrypt/live/www.example.com/privkey.pem:/usr/local/apache2/conf/privkey.pem
      - ./logs/apache2:/usr/local/apache2/logs
    depends_on:
      - www_example_php

networks:
  www_example_net:

Entrambi i servizi montano la directory ./data/www che contiene il sito e dei file di configurazione. Eccoli qui di seguito, uno per uno:

File di configurazione dei servizi Docker

Configurazione pool FPM (www.conf)

Qui va segnalata la porta 9000 che poi andrà usata nella configurazione di Apache. Ho anche attivato il log degli errori di FPM sempre molto utile.

[www]

user = www-data
group = www-data

listen = 9000

pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3


catch_workers_output = yes
php_admin_flag[log_errors] = on
php_admin_flag[display_errors] = off
php_admin_value[error_reporting] = E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_DEPRECATED
php_admin_value[error_log] = /var/log/php-fpm/php-fpm.error.log

Configurazione PHP Opcache (docker-php-ext-opcache.ini)

Per ottimizzare le prestazioni di PHP utilizzo opcache. Attenzione che si tratta di impostazioni un po' aggressive e se si modificano i file PHP potrebbe essere necessario il riavvio del servizio.

[opcache]
# Carica la libreria Opcache
zend_extension=opcache

# Attivo opcache
opcache.enable=1

; https://www.scalingphpbook.com/blog/2014/02/14/best-zend-opcache-settings.html
; 0 means it will check on every request
; 0 is irrelevant if opcache.validate_timestamps=0 which is desirable in production
opcache.revalidate_freq=0

; in produzione si mette 0, in -dev si mette 1, quindi si inibisce il parametro precedente
; attenzione però, quando si fanno modifiche ai file PHP bisogna riavviare fpm
opcache.validate_timestamps=0

; dipende dal numero di file PHP
; Quindi con il comando seguente vedo circa quanti file PHP ci sono:
; find . -type f -print | grep php | wc -l
opcache.max_accelerated_files=20000

; si può ottimizzare con opcache_get_status() per vedere quanta memoria si sta usando
opcache.memory_consumption=192

opcache.max_wasted_percentage=10
opcache.interned_strings_buffer=16
opcache.fast_shutdown=1

Configurazione web server Apache (httpd.conf)

Qui ho preso la configurazione di default dell’immagine docker httpd ed ho semplicemente decommentato le righe riguardanti dei moduli da attivare. Ecco quali sono:

LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule request_module modules/mod_request.so
LoadModule include_module modules/mod_include.so
LoadModule filter_module modules/mod_filter.so
LoadModule deflate_module modules/mod_deflate.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule expires_module modules/mod_expires.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule http2_module modules/mod_http2.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so

Il resto del file è invariato.

Configurazione Virtual Host dentro Docker (www.example.com.conf)

Nella documentazione ufficiale dell’immagine docker httpd si suggerisce di usare il file di configurazione specifico per SSL, però non si addice benissimo alla situazione con virtual host, quindi preferisco avere la configurazione di SSL direttamente nel file di configurazione del virtual host.

Attenzione: ricordarsi di mettere la porta corretta per FPM ed i percorsi dei certificati di Let’s Encrypt che sono stati montati tramite Docker Compose.

Listen 443
SSLCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
SSLProxyCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
SSLHonorCipherOrder on
SSLProtocol all -SSLv3
SSLProxyProtocol all -SSLv3
SSLPassPhraseDialog  builtin
SSLSessionCache        "shmcb:/usr/local/apache2/logs/ssl_scache(512000)"
SSLSessionCacheTimeout  300

<VirtualHost *:443>
  ServerName example.com
  ServerAlias www.example.com
  DocumentRoot "/srv/www/www.example.com"
  ErrorLog "logs/www_example_error.log"
  CustomLog "logs/ww_example_access.log" combined
  ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://www_example_php:9000/srv/www/www.example.com/$1

  SSLEngine on
  SSLCertificateFile "/usr/local/apache2/conf/fullchain.pem"
  SSLCertificateKeyFile "/usr/local/apache2/conf/privkey.pem"

  <Directory "/srv/www/www.example.com">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
    DirectoryIndex index.php
  </Directory>
</VirtualHost>

Conclusione

Abbiamo quindi visto tutta la configurazione per far funzionare un proxy Apache con supporto ad SSL. Chiaramente si tratta id una soluzione semplice, adatta per un singolo sito. Sicuramente si può implementare qualcosa di più evoluto. Però questa è la configurazione di base, la più semplice che ho trovato e la più facilmente replicabile e si può usare come punto di partenza per progetti più complessi.

Per concludere ricordo che se si deve gestire un server in produzione conviene però convertire il tutto in un sistema di gestione tramite strumenti tipo Ansible in modo da rendere più robusto e flessibile il sistema. Magari quando avrò un po' di tempo potrei scrivere qualche altro appunto.

Leonardo Finetti

Leonardo Finetti
Si occupa di informatica dalla metà degli anni novanta principalmente in ambito web con tecnologie Open Source. Esperto di Drupal e di SEO offre consulenze in tali ambiti e nel tempo libero si diletta scrivendo articoli di informatica ed anche di design, ergonomia, usabilità e sicurezza.

Se ti piace questo sito puoi usare il link di affiliazione Amazon cliccando qui.