Un archivo .htaccess contiene directivas de Apache que se aplican al directorio donde está y a todos sus subdirectorios. Apache lo lee en cada petición, así que los cambios tienen efecto al instante, sin recargar el servidor. Es la forma habitual de configurar redirecciones y reglas de reescritura en hostings compartidos y en aplicaciones como WordPress. En este tutorial activarás .htaccess en Apache sobre Ubuntu 24.04 y lo usarás para las tareas más comunes: redirecciones, HTTPS obligatorio, URL limpias, protección con contraseña y cabeceras de caché y seguridad.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, y un usuario no root con privilegios sudo.
  • Apache instalado (sudo apt install apache2) con un sitio cuyo DocumentRoot sea /var/www/your_domain. Sustituye your_domain por tu dominio en toda la guía.
  • Para las reglas de HTTPS, un certificado TLS ya instalado en el sitio, por ejemplo de Let's Encrypt.

Paso 1: Permitir .htaccess en el sitio

Apache solo lee .htaccess en los directorios donde AllowOverride lo permite. En Ubuntu, /etc/apache2/apache2.conf define AllowOverride None para /var/www/, así que por defecto los archivos .htaccess se ignoran sin ningún aviso.

Abre el VirtualHost de tu sitio:

sudo nano /etc/apache2/sites-available/your_domain.conf

Añade un bloque <Directory> para el DocumentRoot dentro de <VirtualHost *:80> (y dentro de <VirtualHost *:443> si tienes un archivo aparte para HTTPS, por ejemplo your_domain-le-ssl.conf creado por Certbot):

<VirtualHost *:80>
    ServerName your_domain
    ServerAlias www.your_domain
    DocumentRoot /var/www/your_domain

    <Directory /var/www/your_domain>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

AllowOverride All permite cualquier directiva válida en .htaccess. Si quieres restringirlo, indica solo las categorías que usa esta guía: AllowOverride FileInfo AuthConfig Indexes Options. FileInfo cubre reescrituras, redirecciones, cabeceras y páginas de error; AuthConfig, la autenticación y Require; Indexes, las directivas de caché de mod_expires, y Options, la directiva Options.

Paso 2: Activar los módulos necesarios

Las reglas de esta guía usan mod_rewrite para reescrituras, mod_headers para cabeceras y mod_expires para la caché del navegador. En Ubuntu vienen instalados pero no habilitados:

sudo a2enmod rewrite headers expires

Valida la configuración y reinicia Apache para cargar los módulos:

sudo apache2ctl configtest
sudo systemctl restart apache2
Syntax OK

Comprueba que están cargados:

sudo apache2ctl -M | grep -E 'rewrite|headers|expires'
 expires_module (shared)
 headers_module (shared)
 rewrite_module (shared)

Paso 3: Comprobar que Apache lee .htaccess

Crea el archivo en la raíz del sitio con una línea deliberadamente inválida:

echo "EstaDirectivaNoExiste" | sudo tee /var/www/your_domain/.htaccess

Pide la página:

curl -sI http://your_domain | head -n 1
HTTP/1.1 500 Internal Server Error

Un error 500 confirma que Apache lee el archivo. El log de errores muestra la causa:

sudo tail -n 1 /var/log/apache2/error.log
[core:alert] ... /var/www/your_domain/.htaccess: Invalid command 'EstaDirectivaNoExiste', perhaps misspelled or defined by a module not included in the server configuration

Si la respuesta es 200 OK, AllowOverride no se está aplicando: revisa el paso 1 y que editaste el VirtualHost correcto (sudo apache2ctl -S lista los sitios activos). Esta misma técnica te servirá para depurar: cualquier error de sintaxis en .htaccess produce un 500 y deja el motivo en el log.

Vacía el archivo para empezar a añadir reglas reales:

sudo truncate -s 0 /var/www/your_domain/.htaccess

Todas las reglas siguientes van en /var/www/your_domain/.htaccess. Ábrelo con:

sudo nano /var/www/your_domain/.htaccess

Paso 4: Forzar HTTPS y un único nombre de dominio

Conviene que el sitio responda en una sola dirección canónica, por ejemplo https://your_domain sin www. Añade al principio del archivo:

RewriteEngine On

# Redirigir www a la versión sin www, ya por HTTPS
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [L,R=301]

# Redirigir HTTP a HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

Cómo funciona:

  • RewriteCond define una condición que debe cumplirse para aplicar la RewriteRule que la sigue. %1 es lo capturado entre paréntesis en la condición, es decir, el dominio sin www..
  • ^ en RewriteRule coincide con cualquier ruta; %{REQUEST_URI} conserva la ruta y la consulta original.
  • R=301 genera una redirección permanente y L detiene el procesamiento de las reglas siguientes.

Si el sitio está detrás de un balanceador o un proxy que termina TLS, Apache recibe siempre HTTP y la condición %{HTTPS} off crea un bucle de redirecciones. En ese caso usa la cabecera que envía el proxy:

RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

Guarda el archivo y comprueba las redirecciones:

curl -sI http://www.your_domain/contacto | grep -E '^(HTTP|Location)'
HTTP/1.1 301 Moved Permanently
Location: https://your_domain/contacto

Paso 5: Redirigir páginas movidas

Para redirecciones fijas de una URL a otra no necesitas mod_rewrite: la directiva Redirect de mod_alias, habilitado por defecto, es más simple. Añade al archivo:

Redirect 301 /pagina-antigua.html /pagina-nueva/
Redirect 301 /blog/ https://blog.your_domain/

La segunda línea redirige también todo lo que cuelga de /blog/, conservando el resto de la ruta: /blog/post-1 va a https://blog.your_domain/post-1. Para patrones, usa RedirectMatch con una expresión regular:

RedirectMatch 301 ^/productos/([0-9]+)\.html$ /tienda/producto/$1

Compruébalo:

curl -sI https://your_domain/pagina-antigua.html | grep -E '^(HTTP|Location)'
HTTP/1.1 301 Moved Permanently
Location: https://your_domain/pagina-nueva/

Paso 6: Crear URL limpias

Estas reglas permiten acceder a acerca.html como /acerca, sin extensión, y dirigen al index.php de la aplicación todas las rutas que no correspondan a un archivo o directorio real (el patrón de controlador frontal que usan WordPress, Laravel y la mayoría de frameworks PHP). Añádelas después de las reglas del paso 4:

# Servir /acerca desde acerca.html si existe
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^(.+)$ $1.html [L]

# Enviar el resto de rutas inexistentes a index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

-f comprueba que existe un archivo y -d un directorio. Sin la bandera R, la reescritura es interna: el navegador sigue viendo /acerca en la barra de direcciones. Usa solo el segundo bloque si tu aplicación es PHP, o solo el primero si es un sitio HTML estático.

Crea una página de prueba y compruébala:

echo "<h1>Acerca de</h1>" | sudo tee /var/www/your_domain/acerca.html
curl -s https://your_domain/acerca
<h1>Acerca de</h1>

Paso 7: Proteger un directorio con contraseña

La autenticación básica de Apache es útil para proteger un área de administración o un entorno de pruebas. Instala htpasswd, incluido en el paquete apache2-utils:

sudo apt install apache2-utils

Crea el archivo de contraseñas fuera del DocumentRoot, para que nunca se pueda descargar desde la web. La opción -c crea el archivo y solo debe usarse con el primer usuario; te pedirá la contraseña:

sudo htpasswd -c /etc/apache2/.htpasswd your_user

Apache debe poder leerlo, pero nadie más:

sudo chown root:www-data /etc/apache2/.htpasswd
sudo chmod 640 /etc/apache2/.htpasswd

Crea un .htaccess en el directorio que quieras proteger, por ejemplo /var/www/your_domain/admin:

sudo mkdir -p /var/www/your_domain/admin
sudo nano /var/www/your_domain/admin/.htaccess
AuthType Basic
AuthName "Zona restringida"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user

Comprueba que sin credenciales se deniega el acceso y con ellas se permite:

curl -sI https://your_domain/admin/ | head -n 1
curl -sI -u your_user https://your_domain/admin/ | head -n 1
HTTP/1.1 401 Unauthorized
HTTP/1.1 200 OK

El segundo comando pide la contraseña. Si el directorio está vacío, verás 403 o 404 en lugar de 200, lo que igualmente indica que la autenticación se ha superado. La autenticación básica envía las credenciales codificadas pero sin cifrar, así que úsala solo sobre HTTPS.

Paso 8: Bloquear el acceso a archivos sensibles

Apache ya deniega en Ubuntu el acceso a los archivos que empiezan por .ht (como el propio .htaccess). Añade reglas para otros archivos que no deben descargarse nunca, como copias de seguridad, configuraciones o el directorio .git, y desactiva los listados de directorios. En el .htaccess de la raíz:

Options -Indexes

<FilesMatch "\.(env|ini|log|sql|bak|swp)$">
    Require all denied
</FilesMatch>

RewriteRule (^|/)\.git(/|$) - [R=404,L]
  • Options -Indexes hace que un directorio sin index devuelva 403 en lugar de listar su contenido.
  • FilesMatch con Require all denied devuelve 403 para las extensiones indicadas.
  • La RewriteRule responde 404 a cualquier ruta dentro de .git, como si no existiera, sin confirmar su presencia. Colócala justo después de RewriteEngine On, antes de las reglas del paso 6, para que se evalúe primero.

Compruébalo. La primera prueba solo devuelve 403 si existe un archivo .env en la raíz del sitio:

curl -sI https://your_domain/.env | head -n 1
curl -sI https://your_domain/.git/config | head -n 1
HTTP/1.1 403 Forbidden
HTTP/1.1 404 Not Found

Paso 9: Añadir cabeceras de caché y seguridad

mod_expires indica al navegador cuánto tiempo puede reutilizar cada tipo de archivo sin volver a pedirlo. Aplícalo a los recursos estáticos que llevan versión en el nombre o cambian poco:

<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/webp "access plus 1 month"
    ExpiresByType image/png "access plus 1 month"
    ExpiresByType image/jpeg "access plus 1 month"
    ExpiresByType image/svg+xml "access plus 1 month"
    ExpiresByType text/css "access plus 1 week"
    ExpiresByType text/javascript "access plus 1 week"
    ExpiresByType application/javascript "access plus 1 week"
    ExpiresByType font/woff2 "access plus 1 year"
</IfModule>

No asignes una caché larga al HTML, o los visitantes tardarán en ver los cambios del sitio. Añade también algunas cabeceras de seguridad con mod_headers:

<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

Comprueba las cabeceras de un archivo CSS de tu sitio (sustituye la ruta por una real):

curl -sI https://your_domain/css/style.css | grep -iE 'cache-control|expires|x-content-type'
Cache-Control: max-age=604800
Expires: Fri, 02 Oct 2026 10:00:00 GMT
X-Content-Type-Options: nosniff

Páginas de error personalizadas

Con ErrorDocument puedes sustituir las páginas de error genéricas de Apache por las tuyas. La ruta es relativa al DocumentRoot:

ErrorDocument 404 /errores/404.html
ErrorDocument 403 /errores/403.html

Crea los archivos en /var/www/your_domain/errores/ y comprueba que una URL inexistente devuelve tu página con el código 404.

Solución de problemas

  • Las reglas no hacen nada: Apache no lee el archivo. Repite la prueba del paso 3; si no obtienes un 500, revisa AllowOverride en el VirtualHost que realmente atiende el dominio.
  • 500 Internal Server Error tras editar el archivo: hay un error de sintaxis o se usa un módulo no habilitado. sudo tail /var/log/apache2/error.log indica la línea y la directiva.
  • Invalid command 'RewriteEngine' o 'Header' en el log: el módulo no está activo. Actívalo con sudo a2enmod rewrite o sudo a2enmod headers y reinicia Apache.
  • ERR_TOO_MANY_REDIRECTS: el sitio está detrás de un proxy o CDN que termina TLS y la regla %{HTTPS} off se cumple siempre. Usa la variante con X-Forwarded-Proto del paso 4.
  • options not allowed here en el log: AllowOverride no incluye la categoría de esa directiva (por ejemplo Options necesita AllowOverride Options o All).

Conclusión

Has habilitado .htaccess en Apache, has comprobado que el servidor lo lee y lo has usado para forzar HTTPS con un dominio canónico, redirigir páginas, crear URL limpias, proteger un directorio con contraseña, bloquear archivos sensibles y añadir cabeceras de caché y seguridad.

Como siguientes pasos puedes:

  • Mover las reglas estables al VirtualHost y volver a AllowOverride None si no necesitas que se cambien sin acceso root, para ganar rendimiento.
  • Activar la compresión de respuestas con mod_deflate o mod_brotli.
  • Añadir la cabecera Strict-Transport-Security una vez que todo el sitio funcione solo por HTTPS.