El objetivo de esta práctica es aprender a utilizar archivos .htaccess en un servidor Apache para configurar:
- Autenticación
- Gestión de errores
- Redirecciones
- Reescritura de URLs
- Cabeceras de seguridad
- Control de indexación en buscadores
- Ocultación y restricción de carpetas
- Optimización mediante caché y compresión
Se realizará de forma práctica para observar los efectos directamente.
Requisitos
- Apache instalado y funcionando
- Permisos para editar archivos en
/var/www/html/o carpeta equivalente
Parte 1: Activación de .htaccess en Apache
- Editar el VirtualHost y localizar:
<Directory /var/www/html>
AllowOverride None
Require all granted
</Directory>
- Cambiar
NoneporAll:
AllowOverride All
- Reiniciar Apache:
sudo systemctl restart apache2
- Crear un archivo
/var/www/html/.htaccesscon este contenido:
AddDefaultCharset UTF-8
Si no hay errores, el .htaccess está funcionando.
Para editar el VirtualHost típico en Ubuntu/Debian:
- Abrir el archivo de sitio por defecto:
sudo nano /etc/apache2/sites-available/000-default.conf
- Buscar un bloque
<VirtualHost *:80>que contiene algo como:
<VirtualHost *:80>
DocumentRoot /var/www/html
...
<Directory /var/www/html>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
- Aquí vive la línea “AllowOverride” que decide si
.htaccesses un ciudadano con derechos o un fantasma ignorado. CambiarAllowOverride NoneporAllowOverride All. - Guardar, cerrar y reiniciar Apache:
sudo systemctl restart apache2
Parte 2: Autenticación Básica con .htaccess
- Crear una carpeta
/var/www/html/admin/con unindex.html. - Crear
/var/www/html/.htpasswdgenerando un usuario:
htpasswd -c /var/www/html/.htpasswd admin
- Crear
/var/www/html/admin/.htaccess:
AuthType Basic
AuthName "Zona restringida"
AuthUserFile /var/www/html/.htpasswd
Require valid-user
Entrar a http://servidor/admin y probar el acceso.
htpasswd
htpasswd es una pequeña herramienta de Apache que fabrica archivos de credenciales para la autenticación HTTP básica. Básica significa sin florituras: usuario/contraseña codificados en Base64 y enviados en cabecera; útil para poner una puerta rápida delante de una carpeta, una página de administración o un endpoint casero.
Funciona generando un fichero (normalmente .htpasswd) con uno o varios usuarios y sus contraseñas hash. Este fichero luego lo consumes desde .htaccess o desde la configuración del VirtualHost.
- Abres la terminal en tu servidor.
- Ejecutas:
htpasswd -c /ruta/al/fichero/.htpasswd nombreusuario
El -c crea el fichero. Si ya existe y quieres añadir otro usuario, lo omites:
htpasswd /ruta/al/fichero/.htpasswd otro_usuario
Al ejecutar, te pedirá contraseña y te la meterá en el fichero con hash. El algoritmo por defecto suele ser bcrypt/MD5 APR según versión; lo importante es que nunca guarda contraseñas en claro.
Un .htpasswd típico se ve así:
juan:$apr1$9tDRSv/.v8BxP1kF/Np7A.
maria:$apr1$OQy4dRxr$2aCdBORded8HNoo.t2GxU.
Luego en .htaccess pones la compuerta:
AuthType Basic
AuthName "Zona restringida"
AuthUserFile /ruta/al/fichero/.htpasswd
Require valid-user
El navegador, al entrar en esa carpeta, lanzará un cuadro de login del siglo pasado (que sigue cumpliendo su función). En HTTPS está bien para control de acceso simple; en HTTP es como mandar postales sin sobre.
Parte 3: Gestión de Errores Personalizados
- Crear
404.htmly403.htmlen la raíz. - Añadir en
/var/www/html/.htaccess:
ErrorDocument 404 /404.html
ErrorDocument 403 /403.html
- Probar introduciendo una URL inexistente.
Parte 4: Cabeceras de Seguridad
Añadir en /var/www/html/.htaccess:
Header set X-Frame-Options "DENY"
Header set X-Content-Type-Options "nosniff"
Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "no-referrer-when-downgrade"
Header set Permissions-Policy "geolocation=()"
Explicación breve:
X-Frame-Options: DENYEsta prohíbe que tu página se cargue dentro de un<iframe>de otra página. ¿Para qué sirve? Para evitar el clickjacking, un truco donde otra web maliciosa mete la tuya en un iframe invisible y coloca botones encima, de manera que el usuario cree estar haciendo una cosa y está pulsando otra. ConDENY, el navegador dice “no me meto en iframes de nadie” y así se acaba el truco de magia.X-Content-Type-Options: nosniffLos navegadores suelen ser listillos: si reciben un archivo con un tipo incorrecto, intentan adivinarlo (“sniffing MIME”). Eso abre una puerta curiosa: si subes un.pngque en realidad es JavaScript con una cabecera errónea, algunos navegadores podrían intentar ejecutarlo. Connosniffel navegador promete no “oler” el tipo y usar sólo lo declarado. Evita ciertos vectores de XSS y descarga ejecutable camuflada.X-XSS-Protection: 1; mode=blockEsto es un modo antiguo del filtro anti-XSS integrado en algunos navegadores. Le indica al navegador que si detecta un ataque de Cross-Site Scripting reflejado, bloquee la carga de la página en vez de intentar sanearla. Hoy en día está un poco de capa caída (Chrome lo retiró en favor de Content-Security-Policy) pero en entornos legacy todavía es un salvavidas aceptable.Referrer-Policy: no-referrer-when-downgradeCada vez que pulsas un enlace, el navegador suele enviar una cabeceraReferer(sin la segunda “r”, por herencia histórica) indicando desde qué página vienes. Esa miguita de pan puede revelar urls privadas, parámetros de sesión o rutas internas.no-referrer-when-downgradeordena no enviar elReferercuando pasas de HTTPS a HTTP (es decir, no “degradar” seguridad). Existen políticas más estrictas (strict-origin,no-referrer, etc.), pero esta ya reduce filtraciones triviales.Permissions-Policy: geolocation=()Aquí entramos en una política moderna que controla APIs del navegador. Es como darle a tu web un panel de permisos: cámara, micrófono, geolocalización, sensores…geolocation=()significa “no concedo geolocalización a nadie, ni siquiera a mí”. Puedes ser más granular, por ejemplogeolocation=(self)permitiría sólo a tu propio dominio. Eso reduce el “exceso de curiosidad” del navegador y de scripts de terceros.- El resumen conceptual es que no protegen tu código del mal, sino que recortan la superficie de comportamiento del navegador, reduciendo trucos clásicos: iframes invisibles, tipos MIME engañosos, ejecución de scripts inesperados y filtración de metadatos. Es parecido a darle menos herramientas a un niño travieso: no podrá arreglar un coche, pero tampoco desmontar la casa.
Más allá de estas cabeceras existen otras piezas más finas como Content-Security-Policy (CSP), que es una gramática entera para decir “qué scripts, imágenes, estilos y conexiones están permitidos”. CSP convierte el navegador en una especie de sandbox configurable, y ahí es donde el mundo del XSS moderno se vuelve deporte olímpico.
Parte 5: Redirecciones HTTP
- Redirección permanente (301):
Redirect 301 /viejo.html /nuevo.html
- Temporal (302):
Redirect 302 /promo /promo-2026
Recomendación: usar 301 solo cuando sea definitivo.
Cuando haces una redirección 302 (Found / Moved Temporarily) estás diciendo dos cosas:
— Primero: “el recurso sigue existiendo en la URL original, pero por ahora estoy sirviendo desde otra URL”.
— Segundo: “no actualices tus mapas, no caches esto como definitivo”.
Consecuencias prácticas:
- Los navegadores no la memorizan como algo fijo.
No guardan en disco que la URL ha cambiado. Si mañana quitas la redirección, el navegador volverá a pedir la original sin pelearse contigo. Es como poner un cartel de “pasen por la puerta lateral mientras pintamos la principal”. - Los buscadores no transfieren ‘peso’ SEO.
A diferencia de un 301, Google y compañía no asumen que la URL nueva es la buena. Mantienen la original en sus índices. Es la forma de decirles: “no reorganices tu mapa, solo estoy haciendo obras”. - No cambia los enlaces de terceros.
Si un usuario sigue un enlace desde otra web, la redirección lo mueve, pero esa web no actualizará su enlace, porque no hay garantía de permanencia. - Sirve para pruebas y mantenimiento.
Si estás desplegando un nuevo diseño, probando un AB testing o moviendo tráfico durante un rato, una 302 te da una especie de reversibilidad instantánea.
En contraste, un 301 (Moved Permanently) es como una mudanza registrada en el ayuntamiento: los navegadores pueden cachearla, los buscadores actualizan índices y pasan “autoridad” a la nueva URL, y los cambios tardan más en revertirse porque todos asumen que era definitivo.
Lo temporal no cambia la cartografía del navegador ni del buscador, solo redirige el tráfico en tiempo real. Es útil cuando estás probando algo, cuando tienes dudas o cuando quieres mantener la puerta original como referencia verdadera. Luego, cuando estés seguro de la mudanza, ya haces el 301 y los mapas del mundo cambian de sitio.
Parte 6: Reescritura de URLs con mod_rewrite
La reescritura de URLs permite transformar una URL solicitada por el navegador en otra diferente de forma interna, sin que el usuario lo note. Se utiliza para crear URLs más limpias, eliminar extensiones (.php, .html) o implementar un patrón MVC (Front Controller).
6.1 Activación del módulo
Activar el módulo responsable de la reescritura:
sudo a2enmod rewrite
sudo systemctl restart apache2
Sin este módulo, las reglas de reescritura no funcionarán.
6.2 Requisito en el VirtualHost
Dentro del VirtualHost debe estar permitido el uso de .htaccess. En el bloque <Directory> debe existir:
AllowOverride All
Si estuviera en None, Apache ignorará las reglas del .htaccess.
6.3 Ejemplo simple: eliminar extensión
Crear un archivo hola.html en el DocumentRoot (/var/www/html/).
Crear o editar el .htaccess en el mismo directorio con:
RewriteEngine On
RewriteRule ^hola$ hola.html [L]
RewriteEngine On→ activa el motor de reescrituraRewriteRule ^hola$ hola.html→ si se solicita/hola, entregarhola.html[L]→ indica que esta es la última regla si coincide
Prueba
Acceder en el navegador:
http://servidor/hola
Aunque el archivo real sea hola.html, el navegador verá una URL sin extensión.
6.4 Ejemplo avanzado: patrón MVC (Front Controller)
Este patrón es común en frameworks y sitios dinámicos. La idea es que todas las peticiones que no sean archivos ni carpetas reales se envían a index.php.
6.4.1 Código
En el .htaccess del DocumentRoot:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php?route=$1 [L,QSA]
6.4.2 Explicación de las condiciones
RewriteCond %{REQUEST_FILENAME} !-f
→ si lo solicitado no es un archivo físicoRewriteCond %{REQUEST_FILENAME} !-d
→ si lo solicitado no es un directorio
6.4.3 Explicación de la regla
^(.+)$→ coincide con cualquier ruta solicitadaindex.php?route=$1→ envía la ruta al scriptindex.phpmediante el parámetroroute[L,QSA]:L→ última regla si coincideQSA→ preserva parámetros existentes (?id=3etc.)
6.5 Demostración práctica
Crear un archivo index.php con:
<?php
echo "Ruta solicitada: " . ($_GET['route'] ?? 'ninguna');
?>
Acceder desde el navegador a distintas rutas:
http://servidor/productos
http://servidor/usuarios/editar/7
http://servidor/pepito
Salida esperada:
Ruta solicitada: productos
Ruta solicitada: usuarios/editar/7
Ruta solicitada: pepito
Esto confirma que index.php captura todas las rutas y puede decidir qué controlador o vista cargar.
6.6 Caso especial: archivos reales
Si se accede a:
/style.css
/logo.png
/index.php
o cualquier recurso físico, NO pasa por la reescritura porque las condiciones lo impiden:
RewriteCond %{REQUEST_FILENAME} !-f → archivo físico
RewriteCond %{REQUEST_FILENAME} !-d → directorio físico
De esta forma no se rompe el funcionamiento del sitio.
6.7 Uso habitual en aplicaciones web
Este sistema se utiliza para:
- URLs amigables para SEO
- Frameworks PHP (Laravel, Symfony, CodeIgniter)
- Paneles de administración
- APIs REST
- Front Controllers (único punto de entrada)
En lugar de:
index.php?route=usuarios/listar
se obtiene:
/usuarios/listar
más limpio y apto para buscadores.
La reescritura de URLs no cambia el archivo real que se ejecuta, solo traduce internamente la ruta.
El usuario ve una URL limpia y el servidor recibe una ruta estructurada para procesar.
Parte 7: Control del Listado de Directorios
Para evitar que se muestren archivos:
Options -Indexes
Crear una carpeta sin index.html y comprobar el resultado.
Parte 8: Forzar HTTPS
Añadir:
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Requiere certificado SSL configurado.
Parte 9: Cacheo y Compresión
Cache de archivos (si mod_expires activado):
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 30 days"
ExpiresByType text/css "access plus 7 days"
ExpiresByType application/javascript "access plus 7 days"
</IfModule>
Compresión (si mod_deflate activado):
SetOutputFilter DEFLATE
Cuando decíamos “cache de archivos (si mod_expires activado)” nos referíamos a una función del servidor web Apache diseñada para decirle al navegador cuánto tiempo puede guardar ciertos archivos sin volver a descargarlos.
En castellano llano:
La caché es la costumbre del navegador de guardar copias locales de cosas que ya descargó (como imágenes, CSS o JavaScript) para no pedirlas de nuevo al servidor cada vez que visitas la página. Si ya tienes el logo en tu disco, ¿para qué volver a bajarlo?
El módulo mod_expires es una parte de Apache que permite controlar cuánto dura esa copia local, poniendo un “fecha de caducidad” en las respuestas.
Si lo activas y configurás reglas como:
ExpiresByType image/png "access plus 30 days"
Estás diciendo: “los archivos PNG pueden vivir 30 días en la caché del navegador”.
¿Resultado práctico? La primera visita carga todo desde el servidor, las siguientes cargan desde disco; la web vuela y el servidor descansa.
Esencialmente, es un acuerdo temporal entre servidor y navegador, parecido a:
– Servidor: “Este archivo apenas cambia, guárdalo un mes”.
– Navegador: “Perfecto, no te molesto hasta que pase ese mes”.
En el ecosistema web, la caché es una de las razones por las que las páginas parecen “instantáneas” después de la primera visita, y una forma elegante de reducir gasto de CPU, ancho de banda y tiempo de espera. Sin caché, cada visita sería como entrar por primera vez en un supermercado cada día: todo el mundo preguntando todo el rato dónde están los pasillos.
Parte 10: Interacción con Buscadores (SEO y Indexación)
robots.txt
Crear en raíz del sitio:
User-agent: *
Disallow: /admin/
Disallow: /backup/
Nota: los bots maliciosos pueden ignorarlo.
Evitar indexación con HTTP (X-Robots-Tag)
En .htaccess:
Header set X-Robots-Tag "noindex, nofollow"
Usos habituales:
- PDFs
- Paneles internos
- Carpetas privadas
Bloquear bots por User-Agent
Ejemplo:
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} googlebot [NC]
RewriteRule ^ - [R=403]
Parte 11: Ocultación y Restricción de Carpetas
Impedir acceso web a una carpeta:
Dentro de la carpeta crear .htaccess:
Require all denied
O por archivo:
<FilesMatch "\.(sql|bak|zip|tar)$">
Require all denied
</FilesMatch>
Restringir por IP:
Require ip 192.168.1.0/24
Evitar ejecución de PHP en carpetas de subida:
<FilesMatch "\.(php|phtml)$">
Require all denied
</FilesMatch>
Muy útil en uploads/ o storage/.
Con .htaccess es posible:
- Restringir acceso
- Personalizar errores
- Crear URLs amigables
- Controlar bot y rastreadores
- Proteger información sensible
- Forzar HTTPS
- Mejorar SEO
- Mejorar rendimiento
Advertencia para producción:
.htaccessse evalúa en cada petición- Incrementa carga del servidor
- En grandes entornos se recomienda configurar desde VirtualHost








![[Reto] - Infraestructura virtualizada con Ubuntu Server 9dc81aff-d57d-45b1-83e9-c70955561713](https://laaventuradeaprender.com/wp-content/uploads/2026/03/9dc81aff-d57d-45b1-83e9-c70955561713-150x150.png)