1 18/05/2026 10 min

Certificato SSL su GitLab: il punto non è solo “installarlo”

Su GitLab il certificato TLS non serve solo a “mettere il lucchetto”. Se l’hostname non coincide con quello usato dagli utenti, se c’è un reverse proxy davanti, o se il rinnovo automatico non è stato pensato bene, il risultato è un accesso instabile: redirect errati, cookie non validi, clone HTTPS che fallisce, webhook che smettono di chiamare i servizi esterni e, nei casi peggiori, una UI che sembra funzionare ma rompe le integrazioni. La procedura corretta parte sempre dal nome pubblico del servizio e finisce con una verifica del percorso completo: browser, API, git via HTTPS e rinnovo del certificato.

Qui assumo il caso più comune: GitLab Omnibus su Linux, esposto direttamente su Internet oppure dietro un reverse proxy. Se stai usando una distribuzione containerizzata, GitLab Helm, oppure una terminazione TLS fatta da CDN/WAF, i passaggi cambiano nei dettagli ma la logica resta la stessa: un solo punto deve presentare il certificato corretto per il nome DNS pubblico, e GitLab deve conoscere quell’URL senza ambiguità.

Prima decisione: TLS terminato su GitLab o sul proxy

La scelta architetturale va fatta prima di copiare file in giro. Se GitLab riceve direttamente il traffico HTTPS, il certificato va configurato nel file `gitlab.rb` e gestito dal componente embedded NGINX. Se invece hai un reverse proxy esterno, il certificato va installato lì e GitLab va istruito a lavorare dietro `https://` senza tentare di rigenerare o servire un certificato proprio.

Regola pratica: un solo layer deve terminare TLS. Duplicare la terminazione “per sicurezza” crea quasi sempre problemi di redirect, header `X-Forwarded-Proto` incoerenti o mismatch tra URL pubblica e URL interna.

Installazione diretta su GitLab Omnibus

Se GitLab espone direttamente HTTPS, la configurazione ruota attorno a tre elementi: il nome esterno, il certificato e la chiave privata. In Omnibus i file standard sono `gitlab.rb` e, dopo la riconfigurazione, la directory `gitlab-ctl` gestisce i servizi interni.

Prima di modificare, fai un backup del file di configurazione. È un cambio reversibile e ti evita di perdere un setup già funzionante.

1. Verifica il nome pubblico che gli utenti usano davvero:

gitlab.example.com

2. Copia certificato e chiave in una posizione coerente, con permessi stretti. Un percorso tipico è:

/etc/gitlab/ssl/gitlab.example.com.crt
/etc/gitlab/ssl/gitlab.example.com.key

3. Imposta ownership e permessi. La chiave privata non deve essere leggibile da utenti non necessari.

sudo mkdir -p /etc/gitlab/ssl
sudo cp fullchain.pem /etc/gitlab/ssl/gitlab.example.com.crt
sudo cp privkey.pem /etc/gitlab/ssl/gitlab.example.com.key
sudo chown root:root /etc/gitlab/ssl/gitlab.example.com.*
sudo chmod 600 /etc/gitlab/ssl/gitlab.example.com.key
sudo chmod 644 /etc/gitlab/ssl/gitlab.example.com.crt

4. Modifica `gitlab.rb` e imposta l’URL esterno con HTTPS:

external_url 'https://gitlab.example.com'
nginx['redirect_http_to_https'] = true
nginx['ssl_certificate'] = "/etc/gitlab/ssl/gitlab.example.com.crt"
nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/gitlab.example.com.key"

5. Applica la configurazione:

sudo gitlab-ctl reconfigure

6. Controlla che NGINX interno sia partito senza errori e che il certificato sia stato caricato:

sudo gitlab-ctl status
sudo gitlab-ctl tail nginx

Se `gitlab-ctl reconfigure` fallisce, il primo posto da guardare è il log dell’embedded NGINX e la sintassi dei path nel file `gitlab.rb`. Il classico errore è indicare una chiave in un percorso diverso da quello effettivamente montato oppure usare un certificato senza catena completa quando il client si aspetta il full chain.

Catena completa: il dettaglio che evita metà dei ticket

Molti problemi “SSL non valido” non dipendono dal certificato leaf ma dalla catena incompleta. Con GitLab, soprattutto se gli utenti accedono da browser moderni, conviene usare un file che includa la full chain del certificato. Se usi Let’s Encrypt, il file `fullchain.pem` è di solito quello corretto per il certificato pubblico. Se usi una CA commerciale, verifica che il bundle contenga gli intermedi richiesti.

Controllo rapido lato server:

openssl s_client -connect gitlab.example.com:443 -servername gitlab.example.com -showcerts

Nel risultato devi vedere il certificato del server e, idealmente, la catena completa. Se il browser segnala “issuer unknown” o “unable to get local issuer certificate”, il problema è quasi sempre qui.

GitLab dietro reverse proxy: il certificato sta davanti, non dentro GitLab

Se davanti a GitLab c’è NGINX, Apache, HAProxy, Traefik o un bilanciatore gestito, il certificato va caricato sul proxy e GitLab deve sapere di essere dietro HTTPS. In questo scenario GitLab può restare in HTTP sul loopback o sulla rete privata, ma l’URL pubblico deve essere coerente con il dominio usato dagli utenti.

Configurazione tipica lato GitLab Omnibus dietro proxy:

external_url 'https://gitlab.example.com'
nginx['listen_https'] = false
nginx['listen_port'] = 80
nginx['listen_address'] = '127.0.0.1'
letsencrypt['enable'] = false

Su un proxy NGINX esterno, il blocco server deve presentare il certificato e inoltrare gli header corretti:

server {
    listen 443 ssl http2;
    server_name gitlab.example.com;

    ssl_certificate     /etc/ssl/certs/gitlab.example.com.fullchain.pem;
    ssl_certificate_key /etc/ssl/private/gitlab.example.com.key;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Ssl on;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Il punto critico non è solo la porta, ma la coerenza tra `Host`, `X-Forwarded-Proto` e l’`external_url` impostato in GitLab. Se uno di questi tre elementi non coincide, puoi ottenere redirect infiniti o link interni generati in HTTP invece che in HTTPS.

Let’s Encrypt su GitLab: quando ha senso e quando no

GitLab Omnibus supporta Let’s Encrypt in modo nativo, ma non è la scelta ideale in ogni contesto. Ha senso se GitLab è direttamente esposto, il DNS punta correttamente al server e la porta 80 è raggiungibile per la challenge HTTP-01. Non ha senso se hai un proxy che già gestisce i certificati, se la porta 80 è bloccata, o se vuoi centralizzare i rinnovi sul layer di front-end.

Abilitazione base in `gitlab.rb`:

external_url 'https://gitlab.example.com'
letsencrypt['enable'] = true
letsencrypt['contact_emails'] = ['admin@example.com']
letsencrypt['auto_renew'] = true

Dopo `gitlab-ctl reconfigure`, controlla che il certificato sia stato emesso e che il rinnovo sia schedulato. In caso di fallimento, i motivi più frequenti sono: DNS non risolto verso l’host corretto, porta 80 filtrata, oppure un proxy davanti che intercetta la challenge prima di GitLab.

Verifica utile:

sudo gitlab-ctl renew-le-certs --dry-run

Se il dry-run fallisce, non forzare il rinnovo a caso: prima correggi raggiungibilità e routing, poi riprova. La challenge ACME è molto più sensibile ai dettagli di rete di quanto sembri quando si guarda solo la GUI.

Rinnovo automatico: il certificato funziona oggi, ma domani?

Il vero costo operativo non è installare il certificato, ma evitare l’ennesimo ticket tra 60 o 90 giorni. Se gestisci manualmente i file, devi avere un processo di rinnovo con una scadenza monitorata, un reload del servizio e un controllo post-rinnovo. Se usi Let’s Encrypt nativo o un cron esterno, il rinnovo va verificato con anticipo, non il giorno della scadenza.

Controllo della scadenza dal server:

openssl x509 -in /etc/gitlab/ssl/gitlab.example.com.crt -noout -dates

Se il certificato è gestito dal proxy, punta al file corretto del proxy, non a quello interno di GitLab. Il controllo va fatto sul punto che termina TLS, non “dove pensi che sia”.

Un refresh tipico per NGINX esterno dopo rinnovo:

sudo nginx -t && sudo systemctl reload nginx

Su GitLab Omnibus invece il reload passa dal controllo di configurazione e dalla riconfigurazione del servizio, non da un semplice reload del demone NGINX di sistema, perché spesso NGINX è gestito internamente da Omnibus.

Verifiche funzionali: non fermarti al lucchetto del browser

Il browser verde non basta. GitLab usa HTTPS per la UI, ma anche per clone, API, webhook e notifiche. Dopo l’installazione del certificato, fai almeno questi test:

1. Risposta HTTP e redirect corretti:

curl -I http://gitlab.example.com
curl -I https://gitlab.example.com

Atteso: il primo deve redirigere a HTTPS, il secondo deve rispondere con `200`, `302` o un altro codice coerente con la pagina iniziale, ma senza errori TLS.

2. Verifica del certificato presentato:

curl -vI https://gitlab.example.com

Atteso: nessun errore di handshake, CN/SAN coerenti con il dominio, catena valida.

3. Clone via HTTPS:

git ls-remote https://gitlab.example.com/gruppo/progetto.git

Atteso: prompt credenziali o risposta corretta dal repository, senza errori di certificato o redirect verso URL sbagliati.

4. Login e callback applicative: controlla eventuali integrazioni con OAuth, SSO o webhook. Se il certificato è valido ma l’`external_url` resta in HTTP, molte integrazioni continueranno a puntare al protocollo sbagliato.

Errori tipici che fanno perdere tempo

Un errore classico è installare il certificato giusto ma con nome DNS sbagliato. Se il certificato è emesso per `gitlab.example.com` e gli utenti entrano da `git.example.com`, il browser segnalerà mismatch. In GitLab il nome pubblico deve essere quello che appare nel certificato, nei redirect e nei link generati dall’applicazione.

Altro errore frequente: lasciare attivo un redirect HTTP a livello di proxy e un altro dentro GitLab, con il risultato di creare catene di redirect inutili o loop. Se il proxy termina TLS, GitLab non deve “reinventare” il redirect. Se GitLab termina TLS, il proxy deve limitarsi a inoltrare correttamente.

Infine, attenzione ai permessi della chiave privata. Se il file è troppo aperto, il servizio può rifiutarlo o stai esponendo materiale sensibile inutilmente. La chiave va trattata come segreto operativo: accesso minimo, backup controllato, nessuna copia sparsa in home directory o repo di configurazione.

Procedura consigliata in pratica

Se vuoi una sequenza pulita e poco ambigua, questa è quella che uso in ambienti reali con GitLab Omnibus esposto direttamente:

  1. Conferma il dominio pubblico e il record DNS.
  2. Prepara certificato e chiave con full chain.
  3. Salva un backup di `gitlab.rb`.
  4. Imposta `external_url` su HTTPS.
  5. Configura i path del certificato nel file di GitLab.
  6. Esegui `gitlab-ctl reconfigure`.
  7. Controlla log e stato servizi.
  8. Verifica browser, `curl` e `git ls-remote`.
  9. Abilita monitoraggio scadenza certificato.

Se invece sei dietro proxy, la sequenza cambia così: certificato sul proxy, header corretti, GitLab consapevole dell’URL HTTPS pubblico, test di redirect e integrazioni. Non mescolare i due modelli nella stessa installazione senza una ragione precisa.

Checklist finale prima di andare in produzione

Prima del cutover, verifica almeno questi punti:

  • Il certificato corrisponde al nome DNS pubblico.
  • La full chain è presente e valida.
  • `external_url` usa `https://`.
  • Il redirect da HTTP a HTTPS funziona una sola volta.
  • Clone Git via HTTPS funziona senza warning.
  • Il rinnovo automatico è testato o documentato.
  • I log non mostrano errori di handshake o permessi sui file TLS.

Se uno di questi punti manca, non è un dettaglio cosmetico: è un difetto operativo che prima o poi si presenta come ticket di accesso, integrazione rotta o certificato scaduto. La differenza tra una installazione pulita e una che genera supporto continuo sta quasi sempre nella disciplina del naming, della chain e del rinnovo.

Assunzione: procedura riferita a GitLab Omnibus su Linux con certificato pubblico gestito manualmente o tramite Let’s Encrypt; se usi proxy, container o Helm, adatta il punto di terminazione TLS e i path di configurazione.