Vai al contenuto principale

Proteggere una directory con .htaccess: Basic Auth, APR1-MD5 e bcrypt

Di CaptainDNS
Pubblicato il 12 agosto 2026

Schema del dialogo HTTP Basic Auth tra un browser e un server Apache o nginx protetto da password
TL;DR
  • Basic Auth HTTP protegge una directory tramite un file .htpasswd (Apache) o auth_basic_user_file (nginx), in pochi minuti di configurazione.
  • La password non viene mai memorizzata in chiaro: è sottoposta a hash, in APR1-MD5 (storico, compatibile ovunque) o in bcrypt (consigliato, costo di calcolo regolabile).
  • Senza HTTPS, Basic Auth viaggia in chiaro: da attivare solo dietro TLS.
  • Il file .htpasswd deve risiedere fuori dalla root web, mai in una directory scaricabile.
  • Generate i vostri hash senza riga di comando con i generatori APR1-MD5 e bcrypt di CaptainDNS.

Un server di preproduzione che finisce indicizzato da Google. Una dashboard di monitoraggio accessibile a chiunque indovini l'URL. Una documentazione tecnica pubblicata per errore in chiaro sul web. Tre scenari ordinari, e una sola soluzione di primo intervento in tutti e tre i casi: Basic Auth.

Basic Auth esiste dagli albori del web e non è quasi mai cambiata da allora. Protegge una directory o un file con una semplice coppia utente/password, senza database, senza sessioni, senza dipendenze applicative. Su Apache bastano un file .htaccess e un file .htpasswd. Su nginx, due direttive, auth_basic e auth_basic_user_file. Cinque minuti di configurazione, e l'accesso è chiuso.

Questa guida illustra la configurazione concreta su Apache e su nginx, con le trappole che fanno perdere un'ora: AllowOverride configurato male, percorso del file dimenticato, servizio mai ricaricato. Spiega anche cosa accade realmente in un file .htpasswd. Perché Apache ha inventato il proprio formato di hash, APR1-MD5. Perché bcrypt lo ha ampiamente sostituito. E come scegliere tra i due con cognizione di causa, anziché per abitudine o per copia-incolla da un tutorial del 2015.

Questo contenuto è rivolto ad amministratori di sistema, DevOps e sviluppatori backend che gestiscono il proprio hosting e vogliono chiudere un accesso senza aspettare l'implementazione di un SSO o di una VPN.

Perché proteggere una directory con password su un server web?

Basic Auth chiude l'accesso HTTP a una directory in pochi minuti, senza codice applicativo né database da gestire.

I casi d'uso ricorrono di continuo: un ambiente di preproduzione su un sottodominio del tipo preprod.captaindns.com, un back-office interno senza autenticazione propria, una documentazione tecnica che non ha nulla a che fare in pubblico, uno strumento di monitoraggio esposto per errore durante un deploy fatto di fretta. In tutti questi casi, nessuno ha il tempo né la necessità di scrivere un sistema di account completo. Uno strato HTTP è più che sufficiente.

La trappola classica: credere che una directory non linkata dal sito resti invisibile. Un robots.txt che esclude /preprod/ impedisce solo la scansione dei motori che rispettano lo standard, non l'accesso diretto. Uno scanner automatico, un link condiviso per errore in un ticket, una riga di log che trapela da qualche parte, e l'URL circola. Esistono strumenti dedicati a scandagliare sistematicamente i percorsi più comuni (staging, dev, admin, backup) su interi blocchi IP. Niente di paranoico: è rumore di fondo permanente su Internet.

Una rapida ispezione dei log di accesso di un server esposto su Internet basta a convincersene. Richieste verso /admin/, /wp-admin/, /.env, /backup.zip o /phpinfo.php arrivano in continuazione, minuto dopo minuto, molto prima che un essere umano abbia avuto il tempo di trovare l'URL con altri mezzi. Queste scansioni non prendono di mira nessuno in particolare: spazzolano interi blocchi IP alla ricerca di percorsi noti per essere mal protetti. Una directory di preproduzione dal nome prevedibile, /staging/ o /preprod/, finisce prima o poi in questi log.

Basic Auth HTTP non è un'autenticazione applicativa completa. Nessun logout pulito: il browser conserva le credenziali in memoria finché non viene chiuso. Nessuna limitazione del numero di tentativi. Nessuna sessione con scadenza, nessuna gestione fine dei ruoli. È un lucchetto alla porta, non un controllo d'accesso sofisticato. Per un accesso che richiede ruoli differenziati o una tracciabilità fine, serve uno strato applicativo dedicato. Per chiudere rapidamente un accesso sensibile in attesa di meglio, o in aggiunta a una protezione già attiva, Basic Auth fa molto bene il suo lavoro.

Le alternative esistono, e sono spesso più robuste sulla carta. Una VPN filtra l'accesso di rete prima ancora che una qualsiasi richiesta HTTP raggiunga il server. Un SSO aziendale, Okta, Google Workspace o Azure AD, centralizza gli account e il logging delle connessioni. Un reverse proxy con OAuth2, oauth2-proxy o Authelia per esempio, aggiunge una vera sessione applicativa con scadenza e logout pulito. Ma queste soluzioni richiedono un'infrastruttura a sé stante: un server VPN da mantenere, un identity provider da integrare, un proxy supplementare da deployare e monitorare. Basic Auth si configura in pochi minuti con ciò che già gira sul server, senza dipendenze esterne né account da provisioning altrove. Non la più elegante sulla carta. Ma chiude davvero un accesso prima della fine della giornata.

Basic Auth e Digest Auth: come funziona l'autenticazione HTTP?

Il protocollo HTTP include da sempre un meccanismo di autenticazione basato su header, senza cookie né sessione applicativa.

Lo scambio avviene in due tempi. Il browser richiede una risorsa protetta, il server risponde 401 Unauthorized con un header WWW-Authenticate che specifica lo schema atteso e il nome dell'area protetta, il realm. Il browser mostra allora la sua finestra di dialogo nativa, l'utente inserisce le credenziali, e il browser ripete la richiesta con un header Authorization. Fintanto che la sessione del browser resta aperta, questo header viene reinviato automaticamente a ogni richiesta verso lo stesso realm, senza nuova digitazione.

Basic Auth: il meccanismo standard (RFC 7617)

Basic Auth codifica l'identificativo e la password in base64, senza alcuna crittografia. Il formato dell'header Authorization è Basic seguito da base64(utente:password).

Punto da tenere subito a mente: base64 non è crittografia. È una semplice codifica reversibile, decodificabile da chiunque con un solo comando.

$ echo -n "admin:password" | base64
YWRtaW46cGFzc3dvcmQ=

La RFC 7617 lo precisa senza mezzi termini: lo schema Basic non fornisce alcuna protezione di riservatezza per le credenziali trasmesse, e il suo utilizzo su una connessione non cifrata le espone a chiunque intercetti il traffico. È per questo che Basic Auth non dovrebbe mai girare su HTTP nudo.

Questo dialogo si osserva direttamente con curl. Una prima richiesta senza credenziali riceve il rifiuto:

$ curl -i https://preprod.captaindns.com/
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Area riservata"

Con le credenziali, la richiesta passa:

$ curl -i -u admin:password https://preprod.captaindns.com/
HTTP/1.1 200 OK

L'opzione -u di curl costruisce autonomamente l'header Authorization codificato in base64. È esattamente ciò che fa un browser dietro le quinte dopo l'inserimento nella sua finestra di dialogo.

Digest Auth: perché è quasi scomparso

Digest Auth esegue l'hash della password lato client prima dell'invio, anziché trasmetterla in chiaro codificata.

Il calcolo combina l'identificativo, il realm, la password e un nonce, un valore casuale monouso fornito dal server, tramite una funzione di hash: storicamente MD5, con SHA-256 disponibile dalla RFC 7616. In teoria, questo approccio protegge le credenziali anche senza HTTPS, poiché la password stessa non transita mai sulla rete.

Lo scambio si basa su una risposta server nettamente più ricca di Basic Auth:

WWW-Authenticate: Digest realm="Area riservata",
    qop="auth", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
    opaque="5ccc069c403ebaf9f0171e9517f40e41"

Il client calcola allora HA1 = MD5(utente:realm:password), HA2 = MD5(metodo:URI), poi una risposta finale MD5(HA1:nonce:nc:cnonce:qop:HA2). Questa complessità di calcolo, ripetuta a ogni richiesta, è precisamente ciò che ha spinto la maggior parte delle implementazioni a preferire Basic Auth dietro TLS piuttosto che Digest Auth senza TLS.

In pratica, Digest Auth è quasi scomparso. La sua implementazione è più complessa lato client: gestione del nonce, del contatore di richieste nc, del cnonce. Il supporto resta disomogeneo sui casi avanzati, e crea problemi dietro certi proxy e load balancer che non si aspettano questo tipo di dialogo. Soprattutto, la diffusione capillare di HTTPS ha reso il suo principale vantaggio obsoleto. Proteggere una password in transito non ha più grande utilità quando l'intera connessione è già cifrata end-to-end. Apache (mod_auth_digest) e nginx tramite moduli di terze parti lo supportano ancora, ma quasi nessuno lo utilizza più. Il resto di questa guida si concentra su Basic Auth, lo standard di fatto dietro HTTPS.

Breve storia dell'hashing delle password sul web

Il formato di hash utilizzato da .htpasswd ha una storia che risale agli albori di Unix, ben prima dell'esistenza del web.

crypt() Unix e il limite degli 8 caratteri DES

La funzione crypt() di Unix, progettata alla fine degli anni '70 da Robert Morris per la settima edizione di Unix, cifra una password con una variante del DES (Data Encryption Standard) ripetuta 25 volte. Un salt di 12 bit, ovvero 4.096 valori possibili, codificato su 2 caratteri, impedisce il riutilizzo diretto di una tabella precalcolata tra sistemi diversi.

Problema: DES lavora su blocchi da 56 bit utili, il che limita la password considerata a 8 caratteri. Il nono carattere e i successivi vengono ignorati, puramente e semplicemente. Una password di 20 caratteri si comporta esattamente come i suoi primi 8.

Alla fine degli anni '70, 25 iterazioni di DES rappresentavano un costo di calcolo reale. Negli anni '90, la potenza dei processori consumer era stata moltiplicata di diversi ordini di grandezza. Questa protezione era già superata di fronte a un attacco a forza bruta condotto da un semplice PC.

Verificare questo comportamento richiede un solo comando, ancora disponibile tramite openssl:

$ openssl passwd -crypt -salt ab passwordMoltoLunga
abXXXXXXXXXXXX

Sostituire passwordMoltoLunga con una qualsiasi stringa di oltre 8 caratteri produce esattamente lo stesso hash finché i primi 8 caratteri restano identici. La prova più semplice che un algoritmo progettato nel 1979 non ha più posto in un file .htpasswd creato oggi.

Perché Apache ha creato APR1-MD5 con la versione 1.3?

Il problema di Apache non era tanto la debolezza di crypt() quanto la sua portabilità. L'implementazione di crypt() differisce da un sistema all'altro: la glibc Linux, i BSD e Windows non producono gli stessi hash per gli stessi input. Un file .htpasswd generato su una macchina Linux poteva diventare illeggibile una volta deployato su un server BSD.

La soluzione arrivò da FreeBSD, dove Poul-Henning Kamp aveva progettato nel 1994 un formato di hash basato su MD5, indipendente dall'implementazione di crypt() del sistema ospite. Apache ha ripreso questo principio per il suo comando htpasswd, sotto il nome apr1, disponibile tramite l'opzione -m da Apache 1.3 nel 1998. Risultato: lo stesso hash, prodotto e verificato in modo identico qualunque sia l'OS del server.

APR1-MD5 applica 1.000 iterazioni di MD5 sulla password e un salt di 8 caratteri, con una codifica finale sull'alfabeto base64 proprio del formato crypt. Rispetto a un MD5 nudo, una singola iterazione, il guadagno è reale: mille volte più calcolo per testare una password. Ma MD5 resta una funzione veloce, progettata in origine per l'integrità dei dati e non per resistere a tentativi massivi. Una GPU moderna calcola milioni di iterazioni MD5 al secondo. Di conseguenza, APR1-MD5 resta vulnerabile agli attacchi a dizionario su password deboli o medie.

L'arrivo di bcrypt, Provos e Mazières, 1999

bcrypt nasce un anno dopo l'adozione di APR1-MD5 da parte di Apache, con un cambio di approccio radicale: anziché fissare un numero di iterazioni, tanto vale renderlo regolabile nel tempo.

Niels Provos e David Mazières pubblicano nel 1999 «A Future-Adaptable Password Scheme» alla conferenza USENIX. La loro constatazione: qualsiasi funzione di hash a costo fisso finisce per essere raggiunta dall'hardware, prima o poi. La loro risposta, bcrypt, deriva dalla fase di inizializzazione della chiave del cifrario Blowfish, progettato da Bruce Schneier nel 1993, volutamente costosa in memoria e in calcolo. Il numero di round si regola tramite un fattore di costo, espresso come potenza di due: ogni incremento raddoppia il lavoro richiesto all'attaccante come al server.

Questa concezione cambia le carte in tavola. Un algoritmo a iterazioni fisse deve essere sostituito quando diventa troppo veloce da calcolare per l'hardware dell'epoca. bcrypt, invece, si adegua: si aumenta il fattore di costo, e l'hash segue il ritmo del progresso hardware senza cambiare algoritmo né rompere la compatibilità degli hash già memorizzati.

htpasswd -B produce bcrypt da Apache 2.4, rilasciato nel 2012, ed è diventato lo standard de facto per .htpasswd come per la memorizzazione di password in generale. Sui progetti recenti, Argon2, vincitore della Password Hashing Competition del 2015, gli fa ormai concorrenza, in particolare per la sua resistenza rafforzata ai circuiti dedicati (ASIC). Argon2 resta fuori dal perimetro di .htpasswd: né Apache né nginx lo supportano nativamente per l'autenticazione HTTP.

L'articolo originale del 1999 raccomandava un fattore di costo predefinito di 6, giudicato ragionevole per l'hardware dell'epoca. Il valore predefinito di htpasswd -B è oggi 10, e salire a 12 per un accesso sensibile resta comune. È esattamente il meccanismo che Provos e Mazières avevano anticipato: il numero in sé non ha alcuna importanza, conta solo la possibilità di aumentarlo senza cambiare algoritmo né rompere gli hash già memorizzati.

Configurare la protezione con password con Apache (.htaccess e .htpasswd)

Su Apache, bastano due file: .htpasswd per gli account, .htaccess o un blocco <Directory> per la dichiarazione.

Creare il file .htpasswd

Il comando htpasswd, fornito dal pacchetto apache2-utils su Debian/Ubuntu o httpd-tools su RHEL/CentOS, gestisce la creazione e l'aggiornamento del file.

# Creare il file e aggiungere un primo account, in bcrypt
htpasswd -c -B /var/www/secrets/.htpasswd admin

# Aggiungere un secondo account al file esistente (senza -c, altrimenti il file viene sovrascritto)
htpasswd -B /var/www/secrets/.htpasswd editor

L'opzione -c crea il file. Da usare una sola volta: rilanciarla cancella gli account già presenti. -B forza il formato bcrypt. Senza di essa, htpasswd ricade su APR1-MD5 come impostazione predefinita, oppure accetta -d per il vecchio crypt() DES, -s per SHA-1. Questi ultimi due formati non hanno più posto in un file creato oggi.

Niente accesso SSH, o apache2-utils assente dal server? Un generatore online produce la stessa riga, direttamente nel browser, senza installare nulla.

Su Apache, il modulo corrispondente, mod_auth_basic, con mod_authn_file per la lettura del file, deve essere attivato:

sudo a2enmod auth_basic authn_file
sudo systemctl reload apache2

Su una distribuzione dove questi moduli non sono caricati per impostazione predefinita, la configurazione seguente fallisce silenziosamente o restituisce un errore 500 Internal Server Error, visibile solo in error.log.

Dichiarare la protezione: AuthType, AuthName, AuthUserFile, Require valid-user

Due posizioni possibili per queste direttive: il file .htaccess della directory da proteggere, o un blocco <Directory> nella configurazione del VirtualHost.

In .htaccess, alla root della directory da proteggere:

AuthType Basic
AuthName "Area riservata"
AuthUserFile /var/www/secrets/.htpasswd
Require valid-user

Lo stesso risultato, nel VirtualHost, in blocco <Directory>:

<VirtualHost *:443>
    ServerName preprod.captaindns.com
    DocumentRoot /var/www/html/preprod

    <Directory "/var/www/html/preprod">
        AuthType Basic
        AuthName "Area riservata"
        AuthUserFile /var/www/secrets/.htpasswd
        Require valid-user
    </Directory>
</VirtualHost>

Le due sintassi sono funzionalmente equivalenti, ma non in termini di prestazioni. Un .htaccess viene riletto da Apache a ogni richiesta, per ogni directory genitore del percorso richiesto. Il blocco <Directory>, invece, viene caricato una sola volta all'avvio del server. Su un sito ad alto traffico, il blocco <Directory> evita una lettura disco ripetuta a ogni richiesta.

Require valid-user autorizza qualsiasi account presente nel file .htpasswd, qualunque sia il suo nome. Per restringere l'accesso ad account specifici, Require user admin editor sostituisce la riga.

La trappola AllowOverride AuthConfig

Un .htaccess con direttive di autenticazione perfettamente corrette, ma che non produce alcun effetto. È il sintomo numero uno segnalato sui forum di hosting e su StackOverflow.

La causa: la direttiva AllowOverride, definita a livello di VirtualHost o di configurazione globale, controlla quali categorie di direttive un .htaccess ha il diritto di modificare. Se vale None, Apache ignora puramente e semplicemente il file .htaccess. Senza errore, senza avviso visibile nei log applicativi. Solo una directory che resta aperta.

<Directory "/var/www/html">
    AllowOverride AuthConfig
</Directory>

AuthConfig autorizza le direttive legate all'autenticazione (AuthType, AuthName, AuthUserFile, Require). All autorizza tutto, il che funziona ma apre più del necessario. Dopo la modifica, basta un reload:

sudo apachectl configtest && sudo systemctl reload apache2

configtest verifica la sintassi prima di ricaricare. Un semplice test evita di rompere un server in produzione per un errore di battitura nel file di configurazione.

Verificare che la protezione funzioni

Dopo il reload, una richiesta curl conferma la messa in opera prima di testare nel browser:

curl -i https://preprod.captaindns.com/preprod/
# Deve restituire 401 Unauthorized

curl -i -u admin:password https://preprod.captaindns.com/preprod/
# Deve restituire 200 OK

Se il primo comando restituisce direttamente 200, due cause probabili: AllowOverride che ignora il .htaccess, o il blocco <Directory> puntato sul percorso sbagliato. Il file di log degli errori, /var/log/apache2/error.log su Debian/Ubuntu, conferma la lettura o meno del file AuthUserFile a ogni tentativo.

Configurare la protezione con password con nginx (auth_basic)

nginx protegge una directory con due direttive in un blocco location, senza alcun file .htaccess.

auth_basic e auth_basic_user_file

Il modulo ngx_http_auth_basic_module è compilato di default in nginx. Un blocco location è sufficiente:

server {
    listen 443 ssl;
    server_name preprod.captaindns.com;

    location /preprod/ {
        auth_basic           "Area riservata";
        auth_basic_user_file /var/www/secrets/.htpasswd;
    }
}

auth_basic definisce il testo del realm, mostrato nella finestra di dialogo del browser, oppure off per disattivare l'autenticazione su un sottopercorso ereditato da un location padre. auth_basic_user_file punta allo stesso formato di file utilizzato da Apache.

Questo file .htpasswd è direttamente compatibile tra i due server. Una coppia utente/password generata lato Apache funziona tale e quale su nginx, e viceversa. nginx legge il formato $apr1$, tramite la propria implementazione embedded di MD5 crypt, da molto tempo, e il formato bcrypt $2y$ da nginx 1.0.3, sui sistemi la cui crypt_r() di sistema supporta bcrypt, il che copre le distribuzioni Linux correnti dotate di libxcrypt.

nginx non legge .htaccess: una differenza architetturale da comprendere

nginx non cerca mai file .htaccess. L'intera configurazione risiede in nginx.conf e nei file inclusi, caricati una sola volta all'avvio del servizio.

Questa scelta architetturale, rivendicata fin dagli inizi di nginx, spiega parte della sua reputazione di prestazioni. Apache, con AllowOverride attivo, verifica l'esistenza di un .htaccess in ogni directory genitore del percorso richiesto, a ogni richiesta. nginx non deve fare nulla di simile: tutta la configurazione è già caricata in memoria, pronta all'uso, ancor prima della prima richiesta.

Conseguenza pratica: copiare e incollare una configurazione .htaccess Apache in una directory servita da nginx non produce rigorosamente nulla. Il file viene ignorato in silenzio, senza messaggio di errore. Le direttive equivalenti devono essere riscritte nel blocco server o location corrispondente, poi applicate con un reload:

sudo nginx -t && sudo systemctl reload nginx

Combinare con una restrizione IP

nginx accetta allow e deny in aggiunta a auth_basic, nello stesso blocco location:

location /preprod/ {
    allow 203.0.113.0/24;
    deny  all;

    auth_basic           "Area riservata";
    auth_basic_user_file /var/www/secrets/.htpasswd;
}

I due strati si combinano. Un IP fuori dall'intervallo autorizzato riceve un 403 prima ancora che nginx richieda le credenziali. Un IP autorizzato deve comunque autenticarsi successivamente. Questa doppia barriera protegge dal furto della sola password: un attaccante che ottiene le credenziali ma non si trova sulla rete giusta resta bloccato a monte.

Basic Auth dietro un reverse proxy: Traefik e Caddy

Il formato .htpasswd non si limita ad Apache e nginx. I reverse proxy moderni lo riprendono tale e quale, con un vincolo notevole sul formato di hash accettato a seconda del proxy.

Traefik legge direttamente un file .htpasswd tramite il suo middleware basicAuth, dichiarato in label Docker o in configurazione statica:

labels:
  - "traefik.http.middlewares.preprod-auth.basicauth.usersfile=/etc/traefik/.htpasswd"
  - "traefik.http.routers.preprod.middlewares=preprod-auth"

Il file referenziato è lo stesso .htpasswd generato per Apache o nginx: APR1-MD5 e bcrypt funzionano entrambi, senza conversione.

Caddy, invece, restringe la scelta al minimo indispensabile. La sua direttiva basic_auth accetta solo il formato bcrypt, prodotto dal proprio comando caddy hash-password o, in modo equivalente, da htpasswd -B:

caddy hash-password --plaintext passwordMoltoLunga

Un file .htpasswd ereditato in APR1-MD5 fallisce silenziosamente dietro Caddy: impossibile autenticarsi finché gli account non sono stati rigenerati in bcrypt. Questo vincolo conferma ciò che la storia del formato lasciava già intuire: bcrypt è oggi l'unico formato che attraversa tutti i server e reverse proxy web correnti, senza eccezioni.

Schema del dialogo HTTP Basic Auth: richiesta iniziale, risposta 401 con WWW-Authenticate, richiesta ripetuta con l'header Authorization, risposta 200

APR1-MD5 vs bcrypt: quale algoritmo scegliere per .htpasswd?

Per un file .htpasswd creato oggi, bcrypt è la scelta predefinita. APR1-MD5 si giustifica solo per un vincolo di compatibilità preciso.

Funzionamento di APR1-MD5

Il calcolo concatena due impronte MD5 intermedie, poi 1.000 iterazioni, prima di una codifica specifica.

Dapprima, due impronte iniziali vengono calcolate: una sulla concatenazione password + $apr1$ + salt, l'altra su password + salt + password. In seguito, frammenti della seconda impronta vengono reiniettati nella prima secondo uno schema che dipende dalla lunghezza della password. Arriva poi il ciclo di 1.000 iterazioni: a ogni giro, password, salt e impronta del giro precedente si ricombinano in un ordine che varia secondo la parità del numero del giro. Il risultato finale, 16 byte, viene riletto in un ordine interlacciato e poi codificato sull'alfabeto base64 proprio di crypt (./0-9A-Za-z), per produrre i 22 caratteri dell'impronta.

Sono queste 1.000 iterazioni a distinguere APR1-MD5 da un MD5 nudo: moltiplicano per mille il costo di un tentativo. Mille è irrisorio di fronte a una GPU moderna, capace di testare diversi miliardi di combinazioni MD5 al secondo.

Funzionamento di bcrypt

bcrypt deriva la fase di configurazione della chiave, o key setup, del cifrario Blowfish, volutamente costosa da calcolare.

Questa fase, denominata EksBlowfish (Expensive Key Schedule Blowfish), mescola password e salt nelle sottochiavi di Blowfish attraverso un numero di round pari a 2 elevato al fattore di costo scelto. A differenza di MD5, questo passaggio sollecita accessi alla memoria non sequenziali che limitano fortemente la parallelizzazione su GPU o circuito dedicato: ogni unità di calcolo deve accedere a una tabella di grandi dimensioni in modo imprevedibile, uno schema che l'hardware massivamente parallelo gestisce male. Il salt, 128 bit, è integrato direttamente nell'hash finale, a differenza di APR1-MD5 che lo memorizza in un campo separato.

Con il fattore di costo 10 adottato per impostazione predefinita in htpasswd -B e nei generatori CaptainDNS, il calcolo si aggira intorno ai 60 ms su un server generico. Passare a 12 lo porta a circa 250 ms. Questo tempo viene pagato a ogni richiesta autenticata, non solo alla creazione dell'account: un fattore troppo elevato si sente immediatamente all'uso.

Tabella comparativa estesa

FormatoPrefissoSaltCosto di calcoloResistenza GPU/ASICCompatibilità serverIntroduzioneVerdetto
crypt() DESnessuno12 bit (2 car.)25 round DES, password troncata a 8 car.NullaSolo storicoFine anni '70Obsoleto
SHA-1 {SHA}Nessuno1 iterazioneNullaApache, nginxAnni '90Da evitare
APR1-MD5$apr1$8 car.1.000 iterazioni MD5DeboleApache, nginx, TraefikApache 1.3, 1998Compatibilità massima
bcrypt$2y$128 bit2^N round, regolabileElevataApache 2.4+, nginx recente, Traefik, Caddy1999 (USENIX)Consigliato

Il divario tra le ultime due righe non è solo una questione di generazione. APR1-MD5 ha un costo fissato una volta per tutte nel 1998. bcrypt si ricalibra: passare un fattore di costo da 10 a 12 oggi riproduce, per un attaccante, una difficoltà relativa comparabile a quella dell'articolo del 1999, mentre l'hardware è stato moltiplicato nel frattempo.

Verificare un hash offline

I due formati si controllano con le utility di sistema, senza generatore online:

# APR1-MD5, imponendo il salt per confrontare
openssl passwd -apr1 -salt Xq7nD2mR passwordMoltoLunga

# bcrypt, costo 12
htpasswd -nbB -C 12 admin passwordMoltoLunga

Per bcrypt, il confronto diretto di due hash non funziona mai: il salt cambia a ogni calcolo, anche a password e costo identici. L'unica verifica valida riesegue il calcolo con il salt già presente nell'hash memorizzato, cosa che fa nativamente la funzione crypt() del sistema al momento dell'autenticazione.

Grafico comparativo del tempo di calcolo per algoritmo di hash .htpasswd, scala logaritmica, APR1-MD5 contro bcrypt ai fattori di costo 8, 10, 12 e 14

Permessi dei file: completare la protezione con password

Basic Auth protegge l'accesso HTTP a una directory. Non protegge più nulla se il file .htpasswd stesso è mal posizionato o mal protetto lato filesystem.

Posizione del file .htpasswd, fuori da DocumentRoot

Il file .htpasswd non deve mai trovarsi in una directory servita direttamente dal server web.

/var/www/html/          <- DocumentRoot, servito via HTTP
/var/www/secrets/       <- fuori DocumentRoot, mai servito
    .htpasswd

Un file .htpasswd piazzato per errore in /var/www/html/.htpasswd diventa, salvo blocco esplicito dei file che iniziano con un punto, scaricabile con una semplice richiesta HTTP. Un attaccante recupera allora tutti gli hash del file e li attacca offline, con tutta calma, senza che il fattore di costo del server lo rallenti minimamente. Il calcolo avviene da lui, non da voi.

Permessi consigliati (chmod, chown, umask)

Due livelli di permessi contano: il file .htpasswd stesso, e la directory protetta.

Per .htpasswd, puntare a 640 (lettura/scrittura per il proprietario, lettura per il gruppo, niente per gli altri), con un proprietario coerente con l'utente del server web, www-data su Debian/Ubuntu, apache su RHEL, o il suo gruppo:

chown root:www-data /var/www/secrets/.htpasswd
chmod 640 /var/www/secrets/.htpasswd

644 resta una trappola classica in troubleshooting rapido: questa modalità rende il file leggibile da tutti gli utenti del sistema, non solo dal processo Apache o nginx. Su un server condiviso o multi-utente, qualsiasi account locale può allora leggere gli hash. Lo stesso riflesso si applica alla directory protetta: 750 è più che sufficiente, proprietario e gruppo soltanto, niente per gli altri. 777 in troubleshooting, si vede ancora troppo spesso, e non ha mai risolto nulla in modo duraturo.

Una parola su umask, spesso dimenticato: se il file .htpasswd viene ricreato da uno script di deploy anziché a mano con htpasswd, il valore di umask del processo che lo scrive determina i suoi permessi predefiniti. Un umask troppo permissivo, 022 o ancora meno restrittivo, può ricreare un file leggibile da tutti a ogni deploy, silenziosamente, sovrascrivendo il chmod 640 impostato manualmente la volta precedente. Verificare i permessi dopo ogni deploy automatizzato evita la regressione.

Ultimo promemoria utile: Basic Auth protegge l'accesso HTTP, non l'accesso filesystem. Un chmod sbagliato bypassa tutto lo strato applicativo, in silenzio, senza che nulla nei log HTTP lo segnali.

Buone pratiche e trappole da evitare

Basic Auth configurata male dà una falsa impressione di sicurezza. Alcune regole semplici evitano la maggior parte dei punti ciechi.

HTTPS non è negoziabile. La RFC 7617 lo precisa: lo schema Basic non cifra nulla. Senza TLS, la password codificata in base64 si legge in chiaro da chiunque intercetti il traffico, su un Wi-Fi pubblico, un proxy compromesso o un semplice strumento di cattura pacchetti. Attivare Basic Auth su HTTP nudo equivale ad appendere la password a un cartello.

Non riutilizzare mai una password applicativa o personale per un account .htpasswd. Questi file sono spesso meno sorvegliati dei sistemi di autenticazione principali, e una password condivisa tra due sistemi moltiplica la superficie d'attacco in caso di fuga di uno dei due.

Combinare con una restrizione IP quando il pubblico di riferimento è noto in anticipo. Su Apache, Require ip 203.0.113.0/24 in aggiunta a Require valid-user, tramite RequireAny. Su nginx, allow e deny dichiarati prima della direttiva auth_basic. Due strati valgono più di uno per un accesso veramente sensibile.

Un account condiviso da un intero team non traccia nulla. Se tre persone si connettono con lo stesso identificativo admin, impossibile sapere chi ha fatto cosa in caso di incidente. Un account per persona costa trenta secondi di configurazione in più, e cambia tutto il giorno in cui bisogna capire cosa è successo.

Il realm, il testo dichiarato in AuthName o auth_basic, non è solo un dettaglio cosmetico. Una dicitura vaga come «Area riservata» evita di rivelare la natura del servizio protetto a chi si imbatte nel prompt senza essere invitato. Chiamare il realm «Pannello admin Odoo» o «Grafana interno» fornisce gratuitamente un'informazione a chiunque testi URL a caso.

Cambiare una password in .htpasswd non basta a disconnettere un browser già autenticato. La credenziale resta memorizzata lato client finché il browser non riceve un nuovo 401, e Basic Auth non ha alcun meccanismo di logout per provocarlo. Due metodi forzano comunque un nuovo prompt senza intervento lato client: cambiare il realm, il testo dichiarato in AuthName o auth_basic, crea un'area distinta agli occhi del browser, che non reinvia più la vecchia credenziale e richiede la digitazione. Altrimenti, chiudere il browser o aprire l'URL in una finestra di navigazione privata resta l'unico modo affidabile per svuotare una credenziale Basic Auth già memorizzata.

I log di accesso standard non registrano mai la password in chiaro: l'header Authorization non appare nel formato di log predefinito di Apache o di nginx. È un comportamento sicuro per impostazione predefinita, ma vale la pena verificarlo esplicitamente se un formato di log personalizzato è stato aggiunto da qualche parte nella configurazione. Un %i mal piazzato in un LogFormat Apache scriverebbe la coppia utente/password codificata in base64 in un file di log, potenzialmente meno protetto del file .htpasswd stesso.

Su un'infrastruttura con più ambienti, dev, staging, preproduzione, gestire i file .htpasswd a mano diventa presto un caos: un account dimenticato dopo l'uscita di un collega, una password che resta ancora in APR1-MD5 mentre il resto è passato a bcrypt. Un playbook Ansible o uno script di deploy che rigenera il file a partire da una fonte unica, ad esempio un file che elenca gli account autorizzati, evita la deriva tra ambienti e fornisce un punto unico dove revocare un accesso.

Basic Auth resta un lucchetto HTTP, non un sistema di autenticazione applicativo. Nessun logout pulito: chiudere il browser o cambiare URL resta l'unico modo per liberarsene lato client. Nessun blocco dopo più fallimenti, nessuna rotazione automatica delle password. Per un accesso destinato a durare e ad accogliere più utenti con diritti diversi, prevedere una migrazione verso uno strato applicativo dedicato il prima possibile.

Piano d'azione consigliato

  1. Scegliere il server interessato: Apache (.htaccess/.htpasswd) o nginx (auth_basic), secondo l'infrastruttura in essere.
  2. Generare un hash bcrypt per ogni account, o APR1-MD5 solo se un vincolo di compatibilità datato lo impone.
  3. Posizionare il file .htpasswd fuori dalla root web, con permessi 640.
  4. Verificare che il sito sia in HTTPS prima di attivare Basic Auth: mai su HTTP nudo.
  5. Testare con un account reale, poi documentare le credenziali in un gestore di password di team, non in un file di testo condiviso.

Generare i vostri hash .htpasswd senza riga di comando

Niente pacchetto apache2-utils installato, niente accesso SSH, o semplicemente voglia di fare più in fretta: i due generatori CaptainDNS producono direttamente la riga da incollare in .htpasswd.

Genera il tuo hash .htpasswd

FAQ

Che cos'è un file .htaccess?

Un file di configurazione letto da Apache in ogni directory in cui si trova, a meno che una direttiva AllowOverride None non lo abbia disattivato a livello di server. Dichiara regole locali, tra cui l'autenticazione, senza toccare la configurazione globale del VirtualHost. nginx non lo legge mai: tutta la sua configurazione risiede in nginx.conf.

Dove posizionare il file .htpasswd sul server?

Fuori dalla root web (DocumentRoot per Apache, root per nginx), ad esempio in /var/www/secrets/. Un file .htpasswd accessibile via HTTP può essere scaricato, e i suoi hash attaccati offline, senza che il fattore di costo di bcrypt rallenti minimamente l'attaccante.

.htpasswd è ancora sicuro oggi?

Sì, a condizione di usare bcrypt (htpasswd -B) anziché APR1-MD5 o il vecchio crypt() DES, e di posizionare il file fuori dalla root web con permessi restrittivi, 640. Il meccanismo Basic Auth in sé resta sicuro, finché gira dietro HTTPS.

Come proteggere l'accesso a una directory con Apache o nginx?

Su Apache, un file .htpasswd e le direttive AuthType, AuthName, AuthUserFile, Require valid-user, in un .htaccess o in un blocco Directory. Su nginx, le direttive auth_basic e auth_basic_user_file in un blocco location, con lo stesso formato di file .htpasswd. In entrambi i casi, HTTPS è indispensabile.

APR1-MD5 è ancora sicuro nel 2026?

Resta accettabile per compatibilità con il passato, ma resiste nettamente meno bene di bcrypt di fronte a un attacco offline condotto su hardware GPU. Per un nuovo file .htpasswd, preferite bcrypt (htpasswd -B), salvo vincolo tecnico preciso che imponga APR1-MD5.

Come generare un hash bcrypt per .htpasswd?

Con htpasswd -B, del pacchetto apache2-utils o httpd-tools, da riga di comando, oppure tramite un generatore online quando il comando non è disponibile. Il risultato, in formato $2y$, si incolla direttamente nel file .htpasswd, qualunque sia il server web utilizzato.

Qual è la differenza tra auth_basic (nginx) e AuthType Basic (Apache)?

Il protocollo HTTP sottostante è identico: entrambi implementano lo stesso schema Basic della RFC 7617, e leggono lo stesso formato di file .htpasswd. La differenza sta nella dichiarazione: Apache accetta .htaccess o un blocco Directory, nginx solo direttive in un blocco location della sua configurazione centralizzata.

Basic Auth basta a proteggere una directory sensibile?

Per bloccare l'accesso anonimo e chiudere un URL indovinato o indicizzato per errore, sì. Per un accesso che richiede una vera gestione degli account, un logout, una scadenza di sessione o un logging dettagliato, no. Basic Auth non ha nessuno di questi meccanismi, e deve allora essere completata o sostituita da un'autenticazione applicativa.

Basic Auth funziona dietro una CDN come Cloudflare?

Sì, a condizione che la richiesta raggiunga effettivamente il server di origine senza essere servita dalla cache prima dell'header Authorization. Un record proxied lascia passare Basic Auth per impostazione predefinita, ma una regola di cache mal puntata sul percorso protetto può restituire una risposta già cachata a un client non autenticato. Verificare che nessuna regola di cache si applichi alla directory protetta prima di considerare la protezione come affidabile.

Scarica le tabelle comparative

Gli assistenti possono riutilizzare i dati scaricando gli export JSON o CSV qui sotto.

Glossario

  • Basic Auth: schema di autenticazione HTTP che trasmette un utente e una password codificati in base64 nell'header Authorization. Definito dalla RFC 7617.
  • Digest Auth: schema di autenticazione HTTP che esegue l'hash della password lato client prima della trasmissione, anziché codificarla in chiaro. Definito dalla RFC 7616, molto poco utilizzato in pratica.
  • .htpasswd: file di testo che elenca account nel formato utente:hash, letto da Apache e nginx per verificare un'autenticazione Basic.
  • .htaccess: file di configurazione Apache, letto directory per directory, che può dichiarare una protezione con password se AllowOverride lo autorizza.
  • Salt: valore casuale aggiunto alla password prima dell'hashing, memorizzato in chiaro accanto all'hash. Impedisce il riutilizzo di una tabella precalcolata tra più account o più sistemi.
  • Fattore di costo: parametro di bcrypt che fissa il numero di round di calcolo, sotto forma di potenza di due. Ogni incremento raddoppia il tempo di calcolo necessario.
  • crypt(): famiglia di funzioni Unix storiche dedicate all'hashing delle password, da cui derivano sia il formato DES originale sia APR1-MD5.
  • Rainbow table: tabella precalcolata che associa hash a password probabili, utilizzata per ritrovare rapidamente una password a partire dal suo hash non salato.
  • AllowOverride: direttiva Apache che definisce quali categorie di direttive un file .htaccess ha il diritto di modificare. AllowOverride None disattiva completamente la lettura del .htaccess.

Fonti

Articoli simili