diff --git a/cicd/nginx/.gitignore b/cicd/nginx/.gitignore new file mode 100644 index 0000000..16ab72f --- /dev/null +++ b/cicd/nginx/.gitignore @@ -0,0 +1,2 @@ +# Dati Let's Encrypt gestiti da certbot: contengono chiavi private, NON committare. +certbot/ diff --git a/cicd/nginx/README.md b/cicd/nginx/README.md new file mode 100644 index 0000000..2dd519c --- /dev/null +++ b/cicd/nginx/README.md @@ -0,0 +1,62 @@ +# Reverse proxy nginx + Let's Encrypt + +Reverse proxy nginx davanti ad Apache per `ifattidinapoli.it`. I certificati SSL +sono gestiti automaticamente da Let's Encrypt tramite certbot (validazione +HTTP-01 via webroot), con **un unico certificato multi-dominio (SAN)** che copre +tutti gli host serviti da questo nginx. + +## Domini coperti dal certificato + +- `www.ifattidinapoli.it` (primario / CN) +- `ifattidinapoli.it` +- `preproduction.ifattidinapoli.it` +- `test.ifattidinapoli.it` + +La lista si definisce nell'array `domains` di [init-letsencrypt.sh](init-letsencrypt.sh). +Il primo dominio e' il primario e da' il nome alla cartella +`/etc/letsencrypt/live//` referenziata in [nginx.conf](nginx.conf). + +## Primo setup + +Prerequisiti: +- La rete docker esterna `nginx-network` deve esistere (`docker network create nginx-network`). +- Le porte 80 e 443 dell'host devono essere pubblicamente raggiungibili per + tutti i domini (la validazione HTTP-01 passa dalla porta 80). +- I record DNS di tutti i domini devono puntare a questo server. + +```bash +# 1. (consigliato) prova prima in staging: metti staging=1 in init-letsencrypt.sh +./init-letsencrypt.sh + +# 2. verifica che tutto funzioni, poi rimetti staging=0 e rilancia +./init-letsencrypt.sh +``` + +Lo script: +1. scarica i parametri TLS raccomandati, +2. crea un certificato dummy self-signed cosi' nginx puo' partire, +3. avvia nginx, +4. cancella il dummy e richiede il certificato reale a Let's Encrypt, +5. ricarica nginx. + +## Rinnovo + +Il rinnovo e' automatico. [renew.sh](renew.sh) lancia `certbot renew` (che +rinnova solo se manca meno di 30 giorni alla scadenza) e ricarica nginx. + +Schedularlo in crontab sull'host (`crontab -e`): + +``` +0 3 * * * cd /percorso/di/cicd/nginx && ./renew.sh > /tmp/renew.log 2>&1 || logger -t CRON_RENEW -f /tmp/renew.log +``` + +Se lo script fallisce, l'output finisce in syslog; altrimenti nessun output. + +## Note sulla migrazione da Sectigo + +- I vecchi certificati Sectigo sono in `ssl/`. Il mount `./ssl:/etc/ssl:ro` in + [docker-compose.yml](docker-compose.yml) e i file in `ssl/` possono essere + rimossi una volta verificato che Let's Encrypt funziona. +- La cartella `certbot/` (chiavi private incluse) e' in `.gitignore` e **non va + committata**. Le vecchie chiavi Sectigo sono invece presenti nella history git: + valutare con il fornitore se revocarle. diff --git a/cicd/nginx/docker-compose.yml b/cicd/nginx/docker-compose.yml index d825b43..4fcc037 100644 --- a/cicd/nginx/docker-compose.yml +++ b/cicd/nginx/docker-compose.yml @@ -11,12 +11,19 @@ services: volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./ssl:/etc/ssl:ro + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot logging: driver: "json-file" options: max-file: 4 max-size: 100m compress: "true" + certbot: + image: certbot/certbot + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot diff --git a/cicd/nginx/init-letsencrypt.sh b/cicd/nginx/init-letsencrypt.sh new file mode 100755 index 0000000..9d4d03f --- /dev/null +++ b/cicd/nginx/init-letsencrypt.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Bootstrap del certificato Let's Encrypt unico (multi-dominio / SAN) per il +# reverse proxy di ifattidinapoli.it. +# +# A differenza del progetto Gogs-Reverse (un certificato separato per dominio), +# qui si richiede UN SOLO certificato che copre tutti i domini serviti da questo +# nginx. Il primo dominio dell'array e' il primario (CN) e da' il nome alla +# cartella /etc/letsencrypt/live// referenziata in nginx.conf. +# +# Lanciarlo una volta sola, al primo setup. Per i rinnovi usare renew.sh. + +set -e + +if ! [ -x "$(command -v docker-compose)" ]; then + echo 'Error: docker-compose is not installed.' >&2 + exit 1 +fi + +# Domini coperti dal certificato. Il PRIMO e' il primario (CN) e da' il nome +# alla cartella live/. Se lo cambi, aggiorna anche i path in nginx.conf. +domains=( + "www.ifattidinapoli.it" + "ifattidinapoli.it" + "preproduction.ifattidinapoli.it" + "test.ifattidinapoli.it" +) + +primary_domain="${domains[0]}" + +rsa_key_size=4096 +data_path="./certbot" +email="riccardo.didato@gmail.com" +# Metti a 1 per usare l'ambiente di STAGING di Let's Encrypt (test senza +# consumare i rate limit). Passato il test, rimetti a 0 e rilancia. +staging=0 + +# Costruisce la sequenza di argomenti -d per certbot +domain_args="" +for domain in "${domains[@]}"; do + domain_args="$domain_args -d $domain" +done + +# --------------------------------------------------------------------------- +# FASE 0: parametri TLS raccomandati (una tantum) +# --------------------------------------------------------------------------- +if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then + echo "### Downloading recommended TLS parameters ..." + mkdir -p "$data_path/conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" + echo +fi + +# --------------------------------------------------------------------------- +# FASE 1: certificato dummy self-signed +# nginx non parte se i path ssl_certificate non esistono. Creiamo un cert +# temporaneo cosi' nginx si avvia e puo' servire la validazione ACME su :80. +# --------------------------------------------------------------------------- +if [ -d "$data_path/conf/live/$primary_domain" ]; then + echo "### Existing certificate found for $primary_domain. Skipping dummy generation." +else + echo "### Creating dummy certificate for $primary_domain ..." + path="/etc/letsencrypt/live/$primary_domain" + mkdir -p "$data_path/conf/live/$primary_domain" + docker-compose run --rm --entrypoint "\ + openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1 \ + -keyout '$path/privkey.pem' \ + -out '$path/fullchain.pem' \ + -subj '/CN=localhost'" certbot + echo +fi + +echo "### Starting nginx ..." +docker-compose up --force-recreate -d nginx +echo + +# --------------------------------------------------------------------------- +# FASE 2: richiesta del certificato REALE +# --------------------------------------------------------------------------- +echo "### Deleting dummy certificate ..." +docker-compose run --rm --entrypoint "\ + rm -Rf /etc/letsencrypt/live/$primary_domain && \ + rm -Rf /etc/letsencrypt/archive/$primary_domain && \ + rm -Rf /etc/letsencrypt/renewal/$primary_domain.conf" certbot +echo + +case "$email" in + "") email_arg="--register-unsafely-without-email" ;; + *) email_arg="--email $email" ;; +esac + +if [ "$staging" != "0" ]; then staging_arg="--staging"; fi + +echo "### Requesting Let's Encrypt certificate for: ${domains[*]} ..." +docker-compose run --rm --entrypoint "\ + certbot certonly --webroot -w /var/www/certbot \ + $staging_arg \ + $email_arg \ + $domain_args \ + --rsa-key-size $rsa_key_size \ + --agree-tos \ + --force-renewal" certbot +echo + +echo "### Reloading nginx ..." +docker-compose exec nginx nginx -s reload diff --git a/cicd/nginx/nginx.conf b/cicd/nginx/nginx.conf index cadc0f3..134947b 100644 --- a/cicd/nginx/nginx.conf +++ b/cicd/nginx/nginx.conf @@ -5,34 +5,53 @@ events { worker_connections 1024; } http { client_max_body_size 5M; + # Path del certificato Let's Encrypt unico multi-dominio (SAN). + # Il nome della cartella live/ corrisponde al primo -d passato a certbot + # in init-letsencrypt.sh (dominio primario / CN). + # ssl_certificate /etc/letsencrypt/live/www.ifattidinapoli.it/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/www.ifattidinapoli.it/privkey.pem; + server { listen 80; server_name ifattidinapoli.it; - return 301 https://www.ifattidinapoli.it$request_uri; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://www.ifattidinapoli.it$request_uri; + } } - + server { listen 443 ssl; server_name ifattidinapoli.it; - ssl_certificate /etc/ssl/2025/www_ifattidinapoli_it.chained.crt; - ssl_certificate_key /etc/ssl/2025/www_ifattidinapoli_it.key; + ssl_certificate /etc/letsencrypt/live/www.ifattidinapoli.it/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/www.ifattidinapoli.it/privkey.pem; return 301 https://www.ifattidinapoli.it$request_uri; } server { - listen 80; server_name www.ifattidinapoli.it preproduction.ifattidinapoli.it; - return 301 https://$host$request_uri; - } - - server { + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + + server { listen 443 ssl; server_name www.ifattidinapoli.it preproduction.ifattidinapoli.it default_server; - ssl_certificate /etc/ssl/2025/www_ifattidinapoli_it.chained.crt; - ssl_certificate_key /etc/ssl/2025/www_ifattidinapoli_it.key; + ssl_certificate /etc/letsencrypt/live/www.ifattidinapoli.it/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/www.ifattidinapoli.it/privkey.pem; location / { proxy_pass http://fdn2.production.webserver/; @@ -42,10 +61,26 @@ http { } - server { + server { listen 80; server_name test.ifattidinapoli.it; + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name test.ifattidinapoli.it; + + ssl_certificate /etc/letsencrypt/live/www.ifattidinapoli.it/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/www.ifattidinapoli.it/privkey.pem; + location / { proxy_pass http://fdn2.test.webserver/; proxy_redirect off; diff --git a/cicd/nginx/renew.sh b/cicd/nginx/renew.sh new file mode 100755 index 0000000..a273ddb --- /dev/null +++ b/cicd/nginx/renew.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Rinnova il certificato SSL e ricarica nginx. +# Schedulato via crontab sull'host (vedi README). certbot renew rinnova solo se +# il certificato scade entro 30 giorni, quindi e' sicuro lanciarlo ogni giorno. + +/usr/local/bin/docker-compose run --rm --entrypoint "certbot renew" certbot +/usr/local/bin/docker-compose exec -T nginx nginx -s reload