Ir al contenido principal

Proteger un directorio con .htaccess: Basic Auth, APR1-MD5 y bcrypt

Por CaptainDNS
Publicado el 12 de agosto de 2026

Diagrama del diálogo HTTP Basic Auth entre un navegador y un servidor Apache o nginx protegido por contraseña
TL;DR
  • Basic Auth HTTP protege un directorio mediante un archivo .htpasswd (Apache) o auth_basic_user_file (nginx), en pocos minutos de configuración.
  • La contraseña nunca se almacena en texto plano: se guarda con hash, en APR1-MD5 (histórico, compatible en todas partes) o en bcrypt (recomendado, coste de cálculo ajustable).
  • Sin HTTPS, Basic Auth viaja en claro: debe activarse únicamente detrás de TLS.
  • El archivo .htpasswd debe residir fuera de la raíz web, nunca en un directorio descargable.
  • Genere sus hash sin línea de comandos con los generadores APR1-MD5 y bcrypt de CaptainDNS.

Un servidor de preproducción olvidado e indexado por Google. Un panel de monitorización accesible para cualquiera que adivine la URL. Una documentación técnica publicada por error en abierto en la web. Tres escenarios cotidianos, y una única solución de primeros auxilios en los tres casos: Basic Auth.

Basic Auth existe desde los inicios de la web y apenas ha cambiado desde entonces. Protege un directorio o un archivo con un simple par usuario/contraseña, sin base de datos, sin sesión, sin dependencia de la aplicación. En Apache, basta con un archivo .htaccess y un archivo .htpasswd. En nginx, dos directivas: auth_basic y auth_basic_user_file. Cinco minutos de configuración y el acceso queda cerrado.

Esta guía detalla la configuración concreta en Apache y en nginx, con las trampas que hacen perder una hora: AllowOverride mal configurado, ruta del archivo olvidada, servicio nunca recargado. También explica qué ocurre realmente dentro de un archivo .htpasswd. Por qué Apache inventó su propio formato de hash, APR1-MD5. Por qué bcrypt lo ha reemplazado ampliamente. Y cómo elegir entre ambos con conocimiento de causa, en lugar de por costumbre o copiando y pegando un tutorial de 2015.

Este contenido está dirigido a administradores de sistemas, DevOps y desarrolladores backend que gestionan su propio hosting y quieren cerrar un acceso sin esperar a implantar un SSO o una VPN.

¿Por qué proteger un directorio con contraseña en un servidor web?

Basic Auth cierra el acceso HTTP a un directorio en pocos minutos, sin código de aplicación ni base de datos que gestionar.

Los casos de uso se repiten constantemente: un entorno de preproducción en un subdominio del tipo preprod.captaindns.com, un back-office interno sin autenticación propia, una documentación técnica que no debería ser pública, una herramienta de monitorización expuesta por error durante un despliegue urgente. En todos estos casos, nadie tiene tiempo ni necesidad de programar un sistema de cuentas completo. Una capa HTTP es más que suficiente.

La trampa clásica: creer que un directorio no enlazado desde el sitio permanece invisible. Un robots.txt que excluya /preprod/ solo impide el rastreo de los motores que respetan el estándar, no el acceso directo. Un escáner automatizado, un enlace compartido por error en un ticket, una entrada de log que se filtra por alguna parte, y la URL circula. Existen herramientas enteras para escanear sistemáticamente las rutas comunes (staging, dev, admin, backup) en rangos IP completos. Nada de paranoia: es ruido de fondo permanente en Internet.

Basta con una inspección rápida de los logs de acceso de un servidor expuesto a Internet para convencerse. Las peticiones hacia /admin/, /wp-admin/, /.env, /backup.zip o /phpinfo.php llegan continuamente, minuto tras minuto, mucho antes de que un humano haya podido encontrar la URL por otra vía. Estos escaneos no apuntan a nadie en particular: barren rangos IP enteros en busca de rutas conocidas por estar mal protegidas. Un directorio de preproducción con un nombre predecible, /staging/ o /preprod/, acaba tarde o temprano en esos logs.

Basic Auth HTTP no es una autenticación de aplicación completa. No hay cierre de sesión real: el navegador conserva las credenciales en memoria mientras no se cierre. No hay limitación del número de intentos. No hay sesión con expiración, ni gestión detallada de roles. Es un cerrojo en la puerta, no un control de acceso elaborado. Para un acceso que exige roles diferenciados o trazabilidad detallada, hace falta una capa de aplicación específica. Para cerrar rápido un acceso sensible mientras se espera algo mejor, o como complemento de una protección ya existente, Basic Auth cumple perfectamente su función.

Existen alternativas, a menudo más robustas sobre el papel. Una VPN filtra el acceso a nivel de red antes de que ninguna petición HTTP llegue al servidor. Un SSO corporativo, Okta, Google Workspace o Azure AD, centraliza las cuentas y el registro de conexiones. Un proxy inverso con OAuth2, como oauth2-proxy o Authelia, añade una verdadera sesión de aplicación con expiración y cierre de sesión real. Pero estas soluciones requieren una infraestructura completa: un servidor VPN que mantener, un proveedor de identidad que integrar, un proxy adicional que desplegar y supervisar. Basic Auth se configura en pocos minutos con lo que ya está corriendo en el servidor, sin dependencia externa ni cuentas que aprovisionar en otro sitio. No es la más elegante sobre el papel. Pero cierra realmente un acceso antes del final del día.

Basic Auth y Digest Auth: ¿cómo funciona la autenticación HTTP?

El protocolo HTTP incluye desde siempre un mecanismo de autenticación basado en cabeceras, sin cookie ni sesión de aplicación.

El intercambio se produce en dos tiempos. El navegador solicita un recurso protegido, el servidor responde 401 Unauthorized con una cabecera WWW-Authenticate que indica el esquema esperado y el nombre del ámbito protegido, el realm. El navegador muestra entonces su diálogo nativo, el usuario introduce sus credenciales, y el navegador reenvía la petición con una cabecera Authorization. Mientras la sesión del navegador permanezca abierta, esta cabecera se reenvía automáticamente en cada petición al mismo realm, sin necesidad de volver a introducir las credenciales.

Basic Auth: el mecanismo estándar (RFC 7617)

Basic Auth codifica el usuario y la contraseña en base64, sin ningún tipo de cifrado. El formato de la cabecera Authorization es Basic seguido de base64(usuario:contraseña).

Punto a retener de inmediato: base64 no es cifrado. Es una simple codificación reversible, decodificable por cualquiera con un solo comando.

$ echo -n "admin:contraseña" | base64
YWRtaW46Y29udHJhc2XDsWE=

La RFC 7617 lo indica sin rodeos: el esquema Basic no proporciona ninguna protección de confidencialidad para las credenciales transmitidas, y su uso sobre una conexión no cifrada las expone a cualquiera que intercepte el tráfico. Por eso Basic Auth nunca debería funcionar sobre HTTP plano.

Este diálogo se observa directamente con curl. Una primera petición sin credenciales recibe el rechazo:

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

Con las credenciales, la petición pasa:

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

La opción -u de curl construye por sí misma la cabecera Authorization codificada en base64. Es exactamente lo que hace un navegador entre bastidores tras la introducción de credenciales en su diálogo.

Digest Auth: por qué prácticamente ha desaparecido

Digest Auth aplica un hash a la contraseña en el lado del cliente antes del envío, en lugar de transmitirla codificada en claro.

El cálculo combina el usuario, el realm, la contraseña y un nonce (un valor aleatorio de un solo uso proporcionado por el servidor) mediante una función de hash: históricamente MD5, con SHA-256 disponible desde la RFC 7616. En teoría, este enfoque protege las credenciales incluso sin HTTPS, ya que la contraseña en sí nunca viaja por la red.

El intercambio se apoya en una respuesta del servidor mucho más rica que Basic Auth:

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

El cliente calcula entonces HA1 = MD5(usuario:realm:contraseña), HA2 = MD5(método:URI), y luego una respuesta final MD5(HA1:nonce:nc:cnonce:qop:HA2). Esta complejidad de cálculo, repetida en cada petición, es precisamente lo que ha llevado a la mayoría de las implementaciones a preferir Basic Auth detrás de TLS antes que Digest Auth sin TLS.

En la práctica, Digest Auth casi ha desaparecido. Su implementación es más compleja en el lado del cliente: gestión del nonce, del contador de peticiones nc, del cnonce. El soporte sigue siendo desigual en los casos avanzados y presenta problemas detrás de ciertos proxies y balanceadores de carga que no esperan este tipo de diálogo. Sobre todo, la generalización de HTTPS ha vuelto caduca su principal ventaja. Proteger una contraseña en tránsito ya no tiene mucho sentido cuando toda la conexión está cifrada de extremo a extremo. Apache (mod_auth_digest) y nginx mediante módulos de terceros todavía lo soportan, pero casi nadie lo utiliza ya. El resto de esta guía se centra en Basic Auth, el estándar de facto detrás de HTTPS.

Breve historia del hash de contraseñas en la web

El formato de hash utilizado por .htpasswd tiene una historia que se remonta a los inicios de Unix, mucho antes de que existiera la web.

crypt() de Unix y el límite de los 8 caracteres de DES

La función crypt() de Unix, diseñada a finales de los años 1970 por Robert Morris para la séptima edición de Unix, cifra una contraseña con una variante del DES (Data Encryption Standard) repetida 25 veces. Una sal de 12 bits (4.096 valores posibles), codificada en 2 caracteres, impide la reutilización directa de una tabla precalculada entre sistemas.

Problema: DES trabaja sobre bloques de 56 bits útiles, lo que limita la contraseña procesada a 8 caracteres. El noveno carácter y los siguientes se ignoran, simple y llanamente. Una contraseña de 20 caracteres se comporta exactamente como sus 8 primeros.

A finales de los años 1970, 25 iteraciones de DES representaban un coste de cálculo real. En los años 1990, la potencia de los procesadores de consumo se había multiplicado por varios órdenes de magnitud. Esta protección ya estaba superada frente a un ataque por fuerza bruta lanzado desde un simple PC.

Verificar este comportamiento solo requiere un comando, todavía disponible mediante openssl:

$ openssl passwd -crypt -salt ab contraseñaMuyLarga
abXXXXXXXXXXXX

Sustituir contraseñaMuyLarga por cualquier cadena de más de 8 caracteres produce exactamente el mismo hash mientras los 8 primeros caracteres permanezcan idénticos. La prueba más simple de que un algoritmo diseñado en 1979 ya no tiene cabida en un archivo .htpasswd creado hoy.

¿Por qué Apache creó APR1-MD5 con la versión 1.3?

El problema de Apache no era tanto la debilidad de crypt() como su portabilidad. La implementación de crypt() difiere de un sistema a otro: la glibc de Linux, los BSD y Windows no producen los mismos hash para las mismas entradas. Un archivo .htpasswd generado en una máquina Linux podía volverse ilegible una vez desplegado en un servidor BSD.

La solución vino de FreeBSD, donde Poul-Henning Kamp había diseñado en 1994 un formato de hash basado en MD5, independiente de la implementación crypt() del sistema anfitrión. Apache retomó este principio para su comando htpasswd, bajo el nombre apr1, disponible mediante la opción -m desde Apache 1.3 en 1998. Resultado: el mismo hash, producido y verificado de forma idéntica independientemente del sistema operativo del servidor.

APR1-MD5 aplica 1.000 iteraciones de MD5 sobre la contraseña y una sal de 8 caracteres, con una codificación final sobre el alfabeto base64 propio del formato crypt. Frente a un MD5 simple, una sola iteración, la ganancia es real: mil veces más cálculo para probar una contraseña. Pero MD5 sigue siendo una función rápida, diseñada originalmente para la integridad de datos y no para resistir intentos masivos. Una GPU moderna calcula millones de iteraciones MD5 por segundo. Por eso, APR1-MD5 sigue siendo vulnerable a ataques por diccionario sobre contraseñas débiles o medianas.

La llegada de bcrypt, Provos y Mazières, 1999

bcrypt nace un año después de la adopción de APR1-MD5 por Apache, con un cambio de enfoque radical: en lugar de fijar un número de iteraciones, mejor hacerlo ajustable con el tiempo.

Niels Provos y David Mazières publican en 1999 «A Future-Adaptable Password Scheme» en la conferencia USENIX. Su constatación: toda función de hash de coste fijo acaba siendo alcanzada por el hardware, tarde o temprano. Su respuesta, bcrypt, deriva de la fase de inicialización de clave del cifrado Blowfish, diseñado por Bruce Schneier en 1993, deliberadamente costoso en memoria y cálculo. El número de rondas se ajusta mediante un factor de coste, expresado como potencia de dos: cada incremento duplica el trabajo exigido tanto al atacante como al servidor.

Este diseño cambia el panorama. Un algoritmo de iteraciones fijas debe ser reemplazado cuando se vuelve demasiado rápido de calcular para el hardware de la época. bcrypt, en cambio, se ajusta: se aumenta el factor de coste y el hash sigue el ritmo del progreso del hardware sin cambiar de algoritmo ni romper la compatibilidad de los hash ya almacenados.

htpasswd -B produce bcrypt desde Apache 2.4, lanzado en 2012, y se ha convertido en el estándar de facto para .htpasswd y para el almacenamiento de contraseñas en general. En proyectos recientes, Argon2, ganador de la Password Hashing Competition de 2015, le hace la competencia, especialmente por su resistencia reforzada a circuitos dedicados (ASIC). Argon2 queda fuera del alcance de .htpasswd: ni Apache ni nginx lo soportan de forma nativa para la autenticación HTTP.

El artículo original de 1999 recomendaba un factor de coste por defecto de 6, considerado razonable para el hardware de la época. El valor por defecto de htpasswd -B es hoy 10, y subir a 12 para un acceso sensible sigue siendo habitual. Es exactamente el mecanismo que Provos y Mazières habían anticipado: la cifra en sí no tiene ninguna importancia, solo cuenta la posibilidad de aumentarla sin cambiar de algoritmo ni romper los hash ya almacenados.

Configurar la protección por contraseña con Apache (.htaccess y .htpasswd)

En Apache, dos archivos bastan: .htpasswd para las cuentas, .htaccess o un bloque <Directory> para la declaración.

Crear el archivo .htpasswd

El comando htpasswd, proporcionado por el paquete apache2-utils en Debian/Ubuntu o httpd-tools en RHEL/CentOS, gestiona la creación y actualización del archivo.

# Crear el archivo y añadir una primera cuenta, en bcrypt
htpasswd -c -B /var/www/secrets/.htpasswd admin

# Añadir una segunda cuenta al archivo existente (sin -c, o el archivo se sobrescribe)
htpasswd -B /var/www/secrets/.htpasswd editor

La opción -c crea el archivo. Solo debe usarse una vez: volver a lanzarla borra las cuentas ya existentes. -B fuerza el formato bcrypt. Sin ella, htpasswd usa APR1-MD5 por defecto, o acepta -d para el antiguo crypt() DES, -s para SHA-1. Estos dos últimos formatos ya no tienen cabida en un archivo creado hoy.

¿Sin acceso SSH o sin apache2-utils en el servidor? Un generador en línea produce la misma línea, directamente en el navegador, sin instalar nada.

En Apache, el módulo correspondiente, mod_auth_basic, junto con mod_authn_file para la lectura del archivo, debe estar activado:

sudo a2enmod auth_basic authn_file
sudo systemctl reload apache2

En una distribución donde estos módulos no estén cargados por defecto, la siguiente configuración falla silenciosamente o devuelve un error 500 Internal Server Error, visible únicamente en error.log.

Declarar la protección: AuthType, AuthName, AuthUserFile, Require valid-user

Dos ubicaciones posibles para estas directivas: el archivo .htaccess del directorio a proteger, o un bloque <Directory> en la configuración del VirtualHost.

En .htaccess, en la raíz del directorio a proteger:

AuthType Basic
AuthName "Zona reservada"
AuthUserFile /var/www/secrets/.htpasswd
Require valid-user

El mismo resultado, en el VirtualHost, mediante bloque <Directory>:

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

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

Las dos sintaxis son funcionalmente equivalentes, pero no en rendimiento. Un .htaccess es releído por Apache en cada petición, para cada directorio padre de la ruta solicitada. El bloque <Directory>, en cambio, se carga una sola vez al iniciar el servidor. En un sitio con mucho tráfico, el bloque <Directory> evita una lectura de disco repetida en cada petición.

Require valid-user autoriza a cualquier cuenta presente en el archivo .htpasswd, sin importar su nombre. Para restringir el acceso a cuentas concretas, Require user admin editor reemplaza la línea.

La trampa AllowOverride AuthConfig

Un .htaccess con directivas de autenticación perfectamente correctas, pero que no produce ningún efecto. Es el síntoma número uno reportado en los foros de hosting y en StackOverflow.

La causa: la directiva AllowOverride, definida a nivel de VirtualHost o de la configuración global, controla qué categorías de directivas puede modificar un .htaccess. Si vale None, Apache ignora pura y simplemente el archivo .htaccess. Sin error, sin advertencia visible en los logs de aplicación. Solo un directorio que sigue abierto.

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

AuthConfig autoriza las directivas relacionadas con la autenticación (AuthType, AuthName, AuthUserFile, Require). All autoriza todo, lo cual funciona pero abre más de lo necesario. Después de la modificación, basta con recargar:

sudo apachectl configtest && sudo systemctl reload apache2

configtest verifica la sintaxis antes de recargar. Una simple comprobación evita romper un servidor en producción por un error tipográfico en el archivo de configuración.

Verificar que la protección funciona

Después de recargar, una petición curl confirma la puesta en marcha antes de probar en el navegador:

curl -i https://preprod.captaindns.com/preprod/
# Debe devolver 401 Unauthorized

curl -i -u admin:contraseña https://preprod.captaindns.com/preprod/
# Debe devolver 200 OK

Si el primer comando devuelve directamente 200, dos causas probables: AllowOverride que ignora el .htaccess, o el bloque <Directory> mal dirigido a la ruta incorrecta. El archivo de registro de errores, /var/log/apache2/error.log en Debian/Ubuntu, confirma la lectura o no del archivo AuthUserFile en cada intento.

Configurar la protección por contraseña con nginx (auth_basic)

nginx protege un directorio con dos directivas en un bloque location, sin ningún archivo .htaccess.

auth_basic y auth_basic_user_file

El módulo ngx_http_auth_basic_module viene compilado por defecto en nginx. Basta con un bloque location:

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

    location /preprod/ {
        auth_basic           "Zona reservada";
        auth_basic_user_file /var/www/secrets/.htpasswd;
    }
}

auth_basic define el texto del realm, mostrado en el diálogo del navegador, u off para desactivar la autenticación en una subruta heredada de un location padre. auth_basic_user_file apunta al mismo formato de archivo que el utilizado por Apache.

Este archivo .htpasswd es directamente compatible entre ambos servidores. Un par usuario/contraseña generado en Apache funciona tal cual en nginx, y viceversa. nginx lee el formato $apr1$ mediante su propia implementación embebida de MD5 crypt desde hace tiempo, y el formato bcrypt $2y$ desde nginx 1.0.3, en sistemas cuya crypt_r() soporte bcrypt, lo que cubre las distribuciones Linux habituales equipadas con libxcrypt.

nginx no lee .htaccess: una diferencia de arquitectura que hay que entender

nginx nunca busca un archivo .htaccess. Toda la configuración reside en nginx.conf y los archivos incluidos, cargados una sola vez al iniciar el servicio.

Esta decisión de arquitectura, adoptada desde los inicios de nginx, explica parte de su reputación de rendimiento. Apache, con AllowOverride activo, verifica la existencia de un .htaccess en cada directorio padre de la ruta solicitada, en cada petición. nginx no tiene nada similar que hacer: toda la configuración ya está cargada en memoria, lista para usar, antes incluso de la primera petición.

Consecuencia práctica: copiar y pegar una configuración .htaccess de Apache en un directorio servido por nginx no hace absolutamente nada. El archivo se ignora en silencio, sin mensaje de error. Las directivas equivalentes deben reescribirse en el bloque server o location correspondiente, y luego aplicarse mediante una recarga:

sudo nginx -t && sudo systemctl reload nginx

Combinar con una restricción IP

nginx acepta allow y deny como complemento de auth_basic, en el mismo bloque location:

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

    auth_basic           "Zona reservada";
    auth_basic_user_file /var/www/secrets/.htpasswd;
}

Las dos capas se combinan. Una IP fuera del rango autorizado recibe un 403 antes incluso de que nginx solicite las credenciales. Una IP autorizada debe aun así autenticarse después. Esta doble barrera protege incluso si alguien roba la contraseña: un atacante que obtenga las credenciales pero no esté en la red correcta se queda bloqueado de antemano.

Basic Auth detrás de un proxy inverso: Traefik y Caddy

El formato .htpasswd no se limita a Apache y nginx. Los proxies inversos modernos lo retoman tal cual, con una restricción notable en cuanto al formato de hash aceptado según el proxy.

Traefik lee directamente un archivo .htpasswd mediante su middleware basicAuth, declarado como etiqueta Docker o en configuración estática:

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

El archivo referenciado es el mismo .htpasswd generado para Apache o nginx: tanto APR1-MD5 como bcrypt funcionan, sin conversión.

Caddy, por su parte, restringe la elección al mínimo estricto. Su directiva basic_auth solo acepta el formato bcrypt, producido por su propio comando caddy hash-password o, de forma equivalente, por htpasswd -B:

caddy hash-password --plaintext contraseñaMuyLarga

Un archivo .htpasswd heredado en APR1-MD5 falla silenciosamente detrás de Caddy: imposible autenticarse mientras las cuentas no se hayan regenerado en bcrypt. Esta restricción confirma lo que la historia del formato ya dejaba entrever: bcrypt es hoy el único formato que atraviesa todos los servidores y proxies inversos web habituales, sin excepción.

Diagrama del diálogo HTTP Basic Auth: petición inicial, respuesta 401 con WWW-Authenticate, petición reenviada con la cabecera Authorization, respuesta 200

APR1-MD5 vs bcrypt: ¿qué algoritmo elegir para .htpasswd?

Para un archivo .htpasswd creado hoy, bcrypt es la opción por defecto. APR1-MD5 solo se justifica por una restricción de compatibilidad concreta.

Funcionamiento de APR1-MD5

El cálculo encadena dos huellas MD5 intermedias, luego 1.000 iteraciones, antes de una codificación específica.

Primero, se calculan dos huellas iniciales: una sobre la concatenación contraseña + $apr1$ + sal, y otra sobre contraseña + sal + contraseña. A continuación, fragmentos de la segunda huella se reinyectan en la primera según un patrón que depende de la longitud de la contraseña. Luego viene el bucle de 1.000 iteraciones: en cada vuelta, contraseña, sal y huella de la vuelta anterior se recombinan en un orden que varía según la paridad del número de vuelta. El resultado final, 16 bytes, se relee en un orden entrelazado y luego se codifica sobre el alfabeto base64 propio de crypt (./0-9A-Za-z), para producir los 22 caracteres de la huella.

Son estas 1.000 iteraciones las que distinguen APR1-MD5 de un MD5 simple: multiplican por mil el coste de un intento. Mil, algo irrisorio frente a una GPU moderna, capaz de probar varios miles de millones de combinaciones MD5 por segundo.

Funcionamiento de bcrypt

bcrypt deriva la fase de configuración de clave, o key setup, del cifrado Blowfish, deliberadamente costosa de calcular.

Esta fase, llamada EksBlowfish (Expensive Key Schedule Blowfish), mezcla contraseña y sal en las subclaves de Blowfish a través de un número de rondas igual a 2 elevado al factor de coste elegido. A diferencia de MD5, esta etapa requiere accesos a memoria no secuenciales que limitan fuertemente la paralelización en GPU o circuito dedicado: cada unidad de cálculo debe acceder a una tabla de gran tamaño de forma impredecible, un esquema que el hardware masivamente paralelo gestiona mal. La sal, de 128 bits, se integra directamente en el hash final, a diferencia de APR1-MD5 que la almacena en un campo separado.

Con el factor de coste 10 adoptado por defecto en htpasswd -B y los generadores CaptainDNS, el cálculo ronda los 60 ms en un servidor genérico. Pasar a 12 lo lleva a unos 250 ms. Este tiempo se paga en cada petición autenticada, no solo al crear la cuenta: un factor demasiado elevado se nota inmediatamente en el uso.

Tabla comparativa extendida

FormatoPrefijoSalCoste de cálculoResistencia GPU/ASICCompatibilidad servidoresIntroducciónVeredicto
crypt() DESninguno12 bits (2 car.)25 rondas DES, contraseña truncada a 8 car.NulaSolo históricoFinales años 1970Obsoleto
SHA-1 {SHA}Ninguna1 iteraciónNulaApache, nginxAños 1990A evitar
APR1-MD5$apr1$8 car.1.000 iteraciones MD5BajaApache, nginx, TraefikApache 1.3, 1998Compatibilidad máxima
bcrypt$2y$128 bits2^N rondas, ajustableAltaApache 2.4+, nginx reciente, Traefik, Caddy1999 (USENIX)Recomendado

La diferencia entre las dos últimas filas no es solo una cuestión de generación. APR1-MD5 tiene un coste fijado de una vez por todas en 1998. bcrypt se recalibra: pasar un factor de coste de 10 a 12 hoy reproduce, para un atacante, una dificultad relativa comparable a la del artículo de 1999, a pesar de que el hardware se ha multiplicado entretanto.

Verificar un hash fuera de línea

Ambos formatos se comprueban con las utilidades del sistema, sin generador en línea:

# APR1-MD5, imponiendo la sal para comparar
openssl passwd -apr1 -salt Xq7nD2mR contraseñaMuyLarga

# bcrypt, coste 12
htpasswd -nbB -C 12 admin contraseñaMuyLarga

Para bcrypt, la comparación directa de dos hash nunca funciona: la sal cambia en cada cálculo, incluso con contraseña y coste idénticos. La única verificación válida repite el cálculo con la sal ya presente en el hash almacenado, lo que hace de forma nativa la función crypt() del sistema en el momento de la autenticación.

Gráfico comparativo del tiempo de cálculo por algoritmo de hash .htpasswd, escala logarítmica, APR1-MD5 frente a bcrypt con factores de coste 8, 10, 12 y 14

Permisos de archivos: completar la protección por contraseña

Basic Auth protege el acceso HTTP a un directorio. No protege nada en absoluto si el propio archivo .htpasswd está mal ubicado o mal protegido a nivel del sistema de archivos.

Ubicación del archivo .htpasswd, fuera del DocumentRoot

El archivo .htpasswd nunca debe encontrarse en un directorio servido directamente por el servidor web.

/var/www/html/          <- DocumentRoot, servido por HTTP
/var/www/secrets/       <- fuera del DocumentRoot, nunca servido
    .htpasswd

Un archivo .htpasswd colocado por error en /var/www/html/.htpasswd se vuelve, salvo bloqueo explícito de los archivos que empiezan por punto, descargable con una simple petición HTTP. Un atacante obtiene entonces todos los hash del archivo y los ataca fuera de línea, tranquilamente, sin que el factor de coste del servidor lo ralentice lo más mínimo. El cálculo se ejecuta en la máquina del atacante, no en la del servidor.

Permisos recomendados (chmod, chown, umask)

Dos niveles de permisos importan: el archivo .htpasswd en sí, y el directorio protegido.

Para .htpasswd, apuntar a 640 (lectura/escritura para el propietario, lectura para el grupo, nada para otros), con un propietario coherente con el usuario del servidor web, www-data en Debian/Ubuntu, apache en RHEL, o su grupo:

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

644 sigue siendo una trampa clásica en reparaciones rápidas: este modo hace el archivo legible por todos los usuarios del sistema, no solo por el proceso Apache o nginx. En un servidor compartido o multiusuario, cualquier cuenta local puede entonces leer los hash. El mismo reflejo se aplica al directorio protegido: 750 es más que suficiente, solo propietario y grupo, nada para otros. 777 en reparaciones rápidas, todavía se ve con demasiada frecuencia, y nunca ha solucionado nada de forma duradera.

Una nota sobre umask, a menudo olvidada: si el archivo .htpasswd se recrea mediante un script de despliegue en lugar de manualmente con htpasswd, el valor de umask del proceso que lo escribe determina sus permisos por defecto. Un umask demasiado permisivo, 022 o incluso menos restrictivo, puede recrear un archivo legible por todos en cada despliegue, silenciosamente, sobrescribiendo el chmod 640 aplicado manualmente la vez anterior. Verificar los permisos después de cada despliegue automatizado evita la regresión.

Último recordatorio útil: Basic Auth protege el acceso HTTP, no el acceso al sistema de archivos. Un chmod incorrecto elude toda la capa de aplicación, en silencio, sin que nada en los logs HTTP lo señale.

Buenas prácticas y trampas a evitar

Basic Auth mal configurada da una falsa sensación de seguridad. Unas pocas reglas simples evitan la mayoría de los puntos ciegos.

HTTPS no es negociable. La RFC 7617 lo indica: el esquema Basic no cifra nada. Sin TLS, la contraseña codificada en base64 se lee en claro por cualquiera que intercepte el tráfico, en una Wi-Fi pública, un proxy comprometido o una simple herramienta de captura de paquetes. Activar Basic Auth sobre HTTP plano equivale a mostrar la contraseña en un cartel.

No reutilizar nunca una contraseña de aplicación o personal para una cuenta .htpasswd. Estos archivos suelen estar menos supervisados que los sistemas de autenticación principales, y una contraseña compartida entre dos sistemas multiplica la superficie de ataque en caso de fuga de uno de los dos.

Combinar con una restricción IP cuando el público destinatario se conoce de antemano. En Apache, Require ip 203.0.113.0/24 como complemento de Require valid-user, mediante RequireAny. En nginx, allow y deny declarados antes de la directiva auth_basic. Dos capas valen más que una para un acceso realmente sensible.

Una cuenta compartida por todo un equipo no deja trazabilidad. Si tres personas se conectan con el mismo usuario admin, imposible saber quién hizo qué en caso de incidente. Una cuenta por persona cuesta treinta segundos más de configuración y lo cambia todo el día que haya que entender qué ha pasado.

El realm, el texto declarado en AuthName o auth_basic, no es un simple detalle cosmético. Un nombre vago como «Zona reservada» evita revelar la naturaleza del servicio protegido a quien se tope con el diálogo sin haber sido invitado. Llamar al realm «Panel de administración Odoo» o «Grafana interno» regala información a cualquiera que pruebe URLs al azar.

Cambiar una contraseña en .htpasswd no basta para desconectar a un navegador ya autenticado. La credencial permanece memorizada en el lado del cliente mientras el navegador no reciba un nuevo 401, y Basic Auth no tiene ningún mecanismo de cierre de sesión para provocarlo. Dos métodos fuerzan aun así un nuevo diálogo sin intervención del cliente: cambiar el realm (el texto declarado en AuthName o auth_basic) crea un ámbito distinto a ojos del navegador, que deja de enviar la credencial antigua y vuelve a pedir la contraseña. Si no, cerrar el navegador o abrir la URL en una ventana de navegación privada sigue siendo la única forma fiable de eliminar unas credenciales Basic Auth ya memorizadas.

Los logs de acceso estándar nunca registran la contraseña en claro: la cabecera Authorization no aparece en el formato de log por defecto de Apache o nginx. Es un comportamiento seguro por defecto, pero vale la pena verificarlo explícitamente si se ha añadido un formato de log personalizado en algún lugar de la configuración. Un %i mal colocado en un LogFormat de Apache escribiría el par usuario/contraseña codificado en base64 en un archivo de log, potencialmente menos protegido que el propio archivo .htpasswd.

En una infraestructura con varios entornos (dev, staging, preproducción), gestionar los archivos .htpasswd a mano se convierte rápidamente en un caos: una cuenta olvidada tras la marcha de un compañero, una contraseña que sigue en APR1-MD5 mientras el resto ya ha pasado a bcrypt. Un playbook de Ansible o un script de despliegue que regenere el archivo a partir de una fuente única (por ejemplo, un archivo con la lista de cuentas autorizadas) evita la deriva entre entornos y proporciona un punto único donde retirar un acceso.

Basic Auth sigue siendo un cerrojo HTTP, no un sistema de autenticación de aplicación. No hay cierre de sesión real: cerrar el navegador o cambiar de URL sigue siendo la única forma de deshacerse de ella en el lado del cliente. No hay bloqueo tras varios fallos, ni rotación automática de contraseñas. Para un acceso que vaya a perdurar y a acoger a varios usuarios con distintos derechos, hay que planificar una migración a una capa de aplicación específica en cuanto sea posible.

🎯 Plan de acción recomendado

  1. Elegir el servidor implicado: Apache (.htaccess/.htpasswd) o nginx (auth_basic), según la infraestructura existente.
  2. Generar un hash bcrypt para cada cuenta, o APR1-MD5 solo si una restricción de compatibilidad antigua lo impone.
  3. Ubicar el archivo .htpasswd fuera de la raíz web, con permisos 640.
  4. Verificar que el sitio está en HTTPS antes de activar Basic Auth: nunca sobre HTTP plano.
  5. Probar con una cuenta real y luego documentar las credenciales en un gestor de contraseñas de equipo, no en un archivo de texto compartido.

Genere sus hash .htpasswd sin línea de comandos

Sin el paquete apache2-utils instalado, sin acceso SSH, o simplemente con ganas de ir más rápido: los dos generadores CaptainDNS producen directamente la línea que hay que pegar en .htpasswd.

Genere su hash .htpasswd

FAQ

¿Qué es un archivo .htaccess?

Un archivo de configuración leído por Apache en cada directorio donde se encuentre, salvo que una directiva AllowOverride None lo haya desactivado a nivel del servidor. Declara reglas locales, entre ellas la autenticación, sin modificar la configuración global del VirtualHost. nginx nunca lo lee: toda su configuración reside en nginx.conf.

¿Dónde colocar el archivo .htpasswd en el servidor?

Fuera de la raíz web (DocumentRoot para Apache, root para nginx), por ejemplo en /var/www/secrets/. Un archivo .htpasswd accesible por HTTP puede ser descargado, y sus hash atacados fuera de línea, sin que el factor de coste de bcrypt ralentice al atacante lo más mínimo.

¿Sigue siendo seguro .htpasswd hoy en día?

Sí, siempre que se use bcrypt (htpasswd -B) en lugar de APR1-MD5 o el antiguo crypt() DES, y que el archivo se coloque fuera de la raíz web con permisos restrictivos, 640. El mecanismo Basic Auth en sí sigue siendo seguro mientras funcione detrás de HTTPS.

¿Cómo proteger el acceso a un directorio con Apache o nginx?

En Apache, un archivo .htpasswd y las directivas AuthType, AuthName, AuthUserFile, Require valid-user, en un .htaccess o un bloque Directory. En nginx, las directivas auth_basic y auth_basic_user_file en un bloque location, con el mismo formato de archivo .htpasswd. En ambos casos, HTTPS es indispensable.

¿Sigue siendo seguro APR1-MD5 en 2026?

Sigue siendo aceptable por compatibilidad hacia atrás, pero resiste mucho peor que bcrypt frente a un ataque fuera de línea realizado con hardware GPU. Para un nuevo archivo .htpasswd, prefiera bcrypt (htpasswd -B), salvo restricción técnica concreta que imponga APR1-MD5.

¿Cómo generar un hash bcrypt para .htpasswd?

Con htpasswd -B, del paquete apache2-utils o httpd-tools, en línea de comandos, o mediante un generador en línea cuando el comando no esté disponible. El resultado, en formato $2y$, se pega directamente en el archivo .htpasswd, sin importar el servidor web utilizado.

¿Cuál es la diferencia entre auth_basic (nginx) y AuthType Basic (Apache)?

El protocolo HTTP subyacente es idéntico: ambos implementan el mismo esquema Basic de la RFC 7617 y leen el mismo formato de archivo .htpasswd. La diferencia está en la declaración: Apache acepta .htaccess o un bloque Directory, nginx solo directivas en un bloque location de su configuración centralizada.

¿Basta Basic Auth para proteger un directorio sensible?

Para bloquear el acceso anónimo y cerrar una URL adivinada o indexada por error, sí. Para un acceso que exija una verdadera gestión de cuentas, cierre de sesión, expiración de sesión o un registro detallado, no. Basic Auth no tiene ninguno de estos mecanismos, y debe entonces complementarse o reemplazarse por una autenticación de aplicación.

¿Funciona Basic Auth detrás de un CDN como Cloudflare?

Sí, siempre que la petición llegue realmente al servidor de origen sin ser servida desde la caché antes de la cabecera Authorization. Un registro en modo proxy deja pasar Basic Auth por defecto, pero una regla de caché mal dirigida sobre la ruta protegida puede devolver una respuesta ya cacheada a un cliente no autenticado. Verifique que ninguna regla de caché se aplique al directorio protegido antes de considerar la protección como fiable.

Descarga las tablas comparativas

Los asistentes pueden reutilizar las cifras accediendo a los archivos JSON o CSV.

📖 Glosario

  • Basic Auth: esquema de autenticación HTTP que transmite un usuario y una contraseña codificados en base64 en la cabecera Authorization. Definido por la RFC 7617.
  • Digest Auth: esquema de autenticación HTTP que aplica un hash a la contraseña en el lado del cliente antes de la transmisión, en lugar de codificarla en claro. Definido por la RFC 7616, muy poco utilizado en la práctica.
  • .htpasswd: archivo de texto que lista cuentas en formato usuario:hash, leído por Apache y nginx para verificar una autenticación Basic.
  • .htaccess: archivo de configuración de Apache, leído directorio por directorio, que puede declarar una protección por contraseña si AllowOverride lo autoriza.
  • Sal (salt): valor aleatorio añadido a la contraseña antes del hash, almacenado en claro junto al hash. Impide la reutilización de una tabla precalculada entre varias cuentas o varios sistemas.
  • Factor de coste: parámetro de bcrypt que fija el número de rondas de cálculo, expresado como potencia de dos. Cada incremento duplica el tiempo de cálculo necesario.
  • crypt(): familia de funciones Unix históricas dedicadas al hash de contraseñas, de la que derivan tanto el formato DES original como APR1-MD5.
  • Rainbow table: tabla precalculada que asocia hash con contraseñas probables, utilizada para recuperar rápidamente una contraseña a partir de su hash sin sal.
  • AllowOverride: directiva de Apache que define qué categorías de directivas puede modificar un archivo .htaccess. AllowOverride None desactiva por completo la lectura del .htaccess.

Fuentes

Artículos relacionados