Ir al contenido
Correo Panel de control Elegir plan

Archivo .htaccess en el hosting: ejemplos de reglas probadas

Archivo .htaccess en el hosting: ejemplos de reglas probadas

Un archivo .htaccess contiene reglas del servidor web Apache para un solo directorio de tu sitio: redirecciones, archivos bloqueados, protección con contraseña, caché y compresión. Solo funciona cuando Apache procesa realmente la petición, y nginx lo ignora por completo. A continuación tienes reglas que probamos en un banco de pruebas con nginx delante de Apache (una configuración habitual de los paneles de control): cada una con código listo para usar, la respuesta real de curl -I y la trampa que puede hacer que falle sin avisar.

Cómo lo probamos

El 10 de noviembre de 2026, en un banco de pruebas: contenedor Ubuntu 24.04, nginx 1.24.0 delante de Apache 2.4.58 (mpm_event, AllowOverride All), PHP-FPM 8.3.6, MariaDB 10.11, WordPress 6.4.3 con enlaces permanentes amigables. La configuración de nginx reproduce una plantilla habitual de los paneles de hosting: nginx sirve por sí mismo los archivos estáticos de una lista de extensiones y pasa todo lo demás a Apache. También probamos nginx sin Apache y un proxy que se comunica con Apache por HTTP simple. Cada regla se verificó con curl y la velocidad se midió con ab. Estos resultados describen el banco de pruebas, no los servidores de RichHost.

Dónde funciona .htaccess y dónde se ignora

Apache busca un .htaccess en cada directorio del camino hasta el archivo solicitado y lo vuelve a leer en cada petición. Por eso los cambios se aplican al instante, sin reiniciar, y por eso un solo error de sintaxis tumba todo el sitio: en el banco, una errata devolvía un error 500 Internal Server Error en todas las URL. El archivo está en la raíz del sitio (normalmente la carpeta public_html del dominio), y los gestores de archivos suelen ocultarlo porque su nombre empieza por un punto.

La trampa principal: nginx no lee .htaccess. En un servidor solo con nginx nuestras reglas de bloqueo no hicieron nada, y el banco sirvió tanto .env como una copia wp-config.php.bak con la contraseña de la base de datos. Con nginx más Apache es más sutil. Esta plantilla deja que nginx sirva directamente los archivos con extensiones como jpg, png, webp, css, js, txt, pdf, zip, html, htm y unas cuantas decenas más. Esas peticiones nunca llegan a Apache, así que para ellas .htaccess no existe. En el banco esto produjo cuatro sorpresas:

  • una redirección desde un old-page.html existente no hacía nada, nginx devolvía el archivo antiguo con 200;
  • una contraseña en un directorio protegía index.php, pero report.html en el mismo directorio se abría sin contraseña;
  • una regla de bloqueo para *.zip devolvía 403 desde Apache, pero el archivo comprimido se descargaba sin problema a través de nginx;
  • las reglas de caché para imágenes no se aplicaban, porque nginx pone él mismo Cache-Control: max-age=315360000.

De ahí sale una prueba rápida: si la respuesta de un archivo muestra max-age=315360000 y una fecha Expires en 2037, lo sirvió nginx y tus reglas de .htaccess nunca lo vieron.

Cómo comprobar cualquier regla

Guarda una copia antes de editar (cp .htaccess .htaccess.bak) y, después de cada cambio, revisa las cabeceras de respuesta en lugar del navegador, que guarda en caché las redirecciones 301:

curl -I http://example.com/
curl -IL http://www.example.com/page/

La opción -L sigue toda la cadena de redirecciones. Si de repente todas las páginas devuelven 500, restaura la copia y abre el registro de errores del dominio: Apache escribe ahí el motivo exacto, por ejemplo Invalid command 'Order', perhaps misspelled or defined by a module not included in the server configuration.

Redirecciones

De HTTP a HTTPS y el bucle detrás de un proxy

La regla clásica de la mayoría de las guías:

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

Por HTTP funciona: HTTP/1.1 301, Location: https://example.com/hello.php. En esta plantilla, nginx también reenvía las peticiones HTTPS a Apache por HTTPS, así que Apache ve una conexión segura y responde 200. Pero si un proxy termina el TLS y habla con Apache por HTTP simple (Cloudflare en modo Flexible, un balanceador de carga), Apache considera insegura cada petición y redirige a la misma URL. En el banco, curl se detuvo con Maximum (50) redirects followed, y un navegador mostraría ERR_TOO_MANY_REDIRECTS. La solución es comprobar la cabecera en la que el proxy transmite el esquema original.

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

Con esta regla, las tres configuraciones del banco respondieron bien: HTTP recibió 301 y las dos variantes HTTPS, 200. La cadena de consulta se conserva automáticamente: ?utm=1&b=2 llegó intacta a la URL HTTPS. En muchos paneles también puedes activar la redirección a HTTPS en los ajustes SSL del dominio; entonces se encarga nginx y no necesitas ninguna regla en .htaccess.

www y sin www en una sola redirección

RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [L,R=301]
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

El orden importa. De esta forma, http://www.example.com/sample-page/ llegó a https://example.com/sample-page/ con una sola redirección. Con la regla HTTPS primero, la cadena creció a dos saltos. En sentido contrario, de sin www a www:

RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

301 para una sola página

Redirect 301 /old-page.html https://example.com/new-page.html
Redirect 301 /old-post/ https://example.com/hello-world/

Ambas líneas devolvieron 301 a la nueva URL cuando estaban por encima del bloque de WordPress. La misma redirección escrita como RewriteRule debajo del bloque no se activó: WordPress capturó la URL y devolvió 404. Y no olvides los archivos estáticos: mientras old-page.html seguía en el disco, nginx lo servía con 200, y la redirección solo funcionó después de borrar el archivo. Tienes más patrones, incluidos traslados masivos y cambios de dominio, en nuestra guía sobre la redirección 301 en .htaccess y Nginx.

Barra final

WordPress no necesita nada más: redirigió /sample-page a /sample-page/ por sí solo. La popular regla para “quitar la barra final”, combinada con esos enlaces permanentes, creó un bucle infinito: .htaccess quitaba la barra y WordPress la volvía a añadir. Para sitios sin WordPress cuyas URL deben terminar en barra:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !/$
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1/ [R=301,L]

En el banco, /catalog/shoes?color=red pasó a /catalog/shoes/?color=red, mientras que los archivos reales como /hello.php no se tocaron.

El bloque predeterminado de WordPress

WordPress 6.4 escribe este bloque por sí mismo cuando guardas los ajustes de enlaces permanentes. Sin él, /hello-world/ devolvía 404 desde Apache en el banco; con él, 200.

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

Coloca todas tus reglas por encima de # BEGIN WordPress: WordPress reescribe todo lo que hay entre los marcadores, y una regla debajo del bloque nunca ve una petición que no corresponda a un archivo real.

Seguridad: bloqueo de archivos, listas de IP permitidas, contraseñas

Bloquear el acceso a archivos

<FilesMatch "^(\.env|wp-config\.php|xmlrpc\.php)$">
  Require all denied
</FilesMatch>
<FilesMatch "\.(bak|old|orig|sql|zip|tar|gz|log)$">
  Require all denied
</FilesMatch>

Sin estas reglas, Apache servía .env con la contraseña de la base de datos, y wp-config.php.bak se descargaba como un archivo normal con la línea DB_PASSWORD incluida. El propio wp-config.php devolvía 200 con una página en blanco: PHP lo ejecuta y no imprime nada, así que el peligro real son las copias con otra extensión. Con las reglas, todas esas URL devolvieron 403, también xmlrpc.php, incluidas las peticiones POST. Los archivos estáticos vuelven a ser la excepción: backup.zip se seguía descargando a través de nginx, así que no guardes archivos comprimidos ni volcados de la base de datos en public_html.

La sintaxis antigua de Apache 2.2 (Order Allow,Deny y Deny from all) solo funciona con mod_access_compat. Cuando desactivamos ese módulo, el mismo archivo provocó un error 500 en todas las páginas del sitio, así que usa Require all denied.

Acceso solo desde tu IP

<Files wp-login.php>
  Require ip 203.0.113.10
</Files>

Detrás de nginx, Apache ve la dirección del proxy. Sin mod_remoteip, una regla con la IP del cliente bloqueó a todo el mundo en el banco, mientras que Require ip 127.0.0.1 dejó pasar a todos. Con mod_remoteip activado sobre la cabecera X-Real-IP (una configuración habitual), la regla funcionó exactamente: la dirección permitida recibió 200, otra 403, y una cabecera X-Real-IP falsificada no sirvió de nada, porque nginx la sobrescribe.

Protección con contraseña mediante .htpasswd

htpasswd -c -B /home/user/web/example.com/private/.htpasswd admin
AuthType Basic
AuthName "Restricted area"
AuthUserFile /home/user/web/example.com/private/.htpasswd
Require valid-user

Guarda el archivo de contraseñas fuera de public_html y usa la ruta completa. Apache respondió 401 Unauthorized con WWW-Authenticate: Basic realm="Restricted area", y 200 con la contraseña correcta. Pero a través de nginx, report.html en el directorio protegido se abría sin contraseña. Si el directorio contiene archivos estáticos, la contraseña tiene que comprobarla nginx: algunos paneles ofrecen para ello una protección con contraseña en los ajustes del dominio, que escribe la regla en la configuración de nginx. En un hosting compartido, pídeselo al soporte de tu proveedor.

Caché y compresión

<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType image/webp "access plus 1 year"
  ExpiresByType image/png "access plus 1 year"
  ExpiresByType text/css "access plus 1 month"
  ExpiresByType text/javascript "access plus 1 month"
  ExpiresByType application/javascript "access plus 1 month"
</IfModule>
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/plain text/css text/javascript application/javascript application/json image/svg+xml
</IfModule>

Apache añadió Cache-Control: max-age=31536000 al PNG y 30 días a jquery.min.js. Ojo con el tipo de JavaScript: en Ubuntu 24.04 los archivos .js se sirven como text/javascript, así que una regla solo con application/javascript no los cubre. La compresión marcó una clara diferencia: jquery.min.js pasó de 87.553 a 30.372 bytes, el CSS de bloques de WordPress de 110.147 a 14.472 y el HTML de la página de inicio de 83.776 a 14.533. Con nginx delante, ambos bloques solo afectan a lo que sirve Apache, es decir, al HTML: las imágenes, el CSS y el JS los cachea y comprime nginx.

Páginas de error personalizadas

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

Usa una ruta, no una URL completa: con ErrorDocument 404 https://example.com/404.html Apache respondió 302 Found en lugar de 404, de modo que los buscadores ven una redirección en vez de un error. Para los archivos .php inexistentes no se usó la página, porque el propio PHP-FPM devuelve “File not found.”. En WordPress, el tema genera las páginas 404, así que ahí ErrorDocument 404 sobra.

Ajustes de PHP: php_value o .user.ini

La línea php_value upload_max_filesize 64M en .htaccess solo funciona con mod_php. Con PHP-FPM provocó un error 500 en todas las páginas con Invalid command 'php_value'. Envolverla en <IfModule mod_php.c> elimina el error, pero el valor no cambia. El lugar correcto es un archivo .user.ini en la raíz del sitio.

upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M

PHP no tomó los nuevos valores enseguida: user_ini.cache_ttl vale 300 segundos por defecto, y tras una edición el banco siguió mostrando el valor anterior durante otros 206 segundos. .user.ini funciona igual en un servidor solo con nginx. Si subir el límite de memoria no ayuda, busca la causa en el código; consulta nuestra guía sobre el error Allowed memory size exhausted.

¿.htaccess hace más lento un sitio?

Comparamos Apache con AllowOverride None y AllowOverride All usando ab (16 peticiones simultáneas, cinco rondas alternas, mediana). El archivo .htaccess tenía 37 líneas: www y HTTPS, una redirección, dos bloques de bloqueo, caché, compresión, ErrorDocument y el bloque de WordPress.

Configuración de Apache Archivo estático, pet/s Script PHP mínimo, pet/s
AllowOverride None 26.301 7.417
Las mismas 37 líneas en la configuración del servidor 24.267 7.614
AllowOverride All con un .htaccess de 37 líneas 14.817 6.080

Permitir .htaccess casi no cuesta nada por sí solo: en una ronda aparte, Apache con AllowOverride All pero sin ningún archivo de ese tipo sirvió 24.268 peticiones por segundo frente a 24.356 con None. Lo que cuesta es el análisis: Apache lee y analiza .htaccess de nuevo en cada petición, por lo que el rendimiento con archivos estáticos cayó en torno al 44 %, mientras que las mismas reglas en la configuración del servidor costaron alrededor del 8 %. Son unos 26 microsegundos por petición en un banco de 2 vCPU, mientras que generar una página de WordPress tardaba unos 85 milisegundos, así que en páginas dinámicas la diferencia desaparece. Durante las mediciones se ejecutaban otras pruebas en el contenedor, por lo que las cifras absolutas tienen ruido; compara solo las proporciones.

Conclusión: no renuncies a .htaccess en un hosting compartido, pero elimina las reglas que duplican lo que ya hacen nginx o el panel de control (caché de estáticos, HTTPS, bloqueo de archivos ocultos).

Chuleta: tarea, regla, comprobación

Tarea Regla Cómo comprobarlo
De HTTP a HTTPS RewriteCond %{HTTPS} off más la condición X-Forwarded-Proto curl -I http://…: 301 a https; curl -IL https://…: 200, sin bucle
De www a sin www RewriteCond %{HTTP_HOST} ^www\.(.+)$ curl -IL http://www…: un único 301
Mover una página Redirect 301 /old/ https://…/new/ encima del bloque de WordPress 301 con el Location correcto; archivo antiguo borrado
Barra final Nada en WordPress, una regla !-f en otros casos curl -IL, sin bucle
Bloquear .env, copias de seguridad, xmlrpc.php FilesMatch con Require all denied 403; comprobar aparte los archivos comprimidos a través de nginx
Administración solo desde tu IP Require ip más mod_remoteip en el servidor 200 desde tu red, 403 con datos móviles
Contraseña en un directorio AuthType Basic, .htpasswd fuera de public_html 401 sin contraseña, también para .html y .pdf
Caché de archivos estáticos mod_expires Cache-Control en el curl -I del archivo
Compresión mod_deflate curl -I -H 'Accept-Encoding: gzip': Content-Encoding: gzip
Páginas de error ErrorDocument 404 /404.html Estado 404, no 302
Límites de PHP .user.ini en lugar de php_value phpinfo() o ini_get() pasados 5 minutos

Preguntas frecuentes

¿Por qué el sitio muestra un error 500 después de editar .htaccess?

Apache no pudo interpretar una línea: una errata, una directiva de un módulo desactivado (php_value, Order) o un carácter suelto. Restaura la copia; la línea exacta aparece en el registro de errores.

¿Necesito .htaccess en un servidor solo con nginx?

No, nginx no lo lee. Esas mismas tareas se resuelven en la configuración de nginx, que en un hosting compartido cambia el administrador o el panel de control.

¿Qué permisos debe tener .htaccess?

Basta con 644: el propietario escribe y el servidor web lee. 777 nunca hace falta. Si Apache no puede leer el archivo en absoluto, todo el sitio devuelve un error 403 Forbidden y el registro muestra pcfg_openfile: unable to check htaccess file.

¿Por qué curl muestra la redirección y el navegador abre la página antigua?

Los navegadores recuerdan las redirecciones 301. Prueba en una ventana privada o vacía la caché, y usa R=302 mientras depuras.

Los sitios en RichHost se gestionan desde el panel de control propio con asistente de IA, y un certificado gratuito de Let’s Encrypt se activa en los ajustes del dominio. Consulta los planes en la página de tarifas de hosting.

Más sobre este tema

Todos los artículos
Permisos de archivos: chmod 644, 755, 600 y por qué no usar 777
Guías y tutoriales de hosting07.05.2026

Permisos de archivos: chmod 644, 755, 600 y por qué no usar 777

Qué permisos poner a directorios, archivos y wp-config.php, qué cambia el usuario de PHP, por qué 777 es peligroso y cómo corregir…

SSL gratis con Let’s Encrypt: panel, Certbot, renovación y errores
Guías y tutoriales de hosting15.04.2026

SSL gratis con Let’s Encrypt: panel, Certbot, renovación y errores

SSL gratis de Let's Encrypt en el panel del hosting y con Certbot: probado con un servidor ACME de pruebas, con tabla…

Copia de seguridad de una web: regla 3-2-1 y prueba de restauración
Guías y tutoriales de hosting28.03.2026

Copia de seguridad de una web: regla 3-2-1 y prueba de restauración

Qué copiar, con qué y cada cuánto, por qué un volcado sin single-transaction pierde pedidos, qué compresor usar y cómo comprobar que…