Guías de seguridad

Mantener el .env fuera de la web pública en hosting compartido

Haz que .env, .git y vendor sean inalcanzables en hosting compartido: la app fuera de la raíz web y solo public/ expuesto. Primeros auxilios con .htaccess.

Publicado 2026-06-07 Actualizado 2026-10-06 11 min de lectura
En esta página

Para: cualquiera que ejecute apps tipo Laravel (donde solo public/ debería estar expuesto) en hosting compartido con Apache/.htaccess. Destilado de un incidente real (→ .env expuesto al mundo) — para asegurar tu propio despliegue.

La visión de este sitio: esto no es 'el error de una persona'

La cultura temprana del hosting compartido ponía todo directamente bajo public_html. Laravel (que espera que solo se exponga public/) fluyó hacia eso, y un diseño peligroso se convirtió en el estándar de facto — incluso la propia documentación de los proveedores pone los proyectos dentro de public_html. Así que la solución es proceso, no vigilancia: cambia tu valor por defecto de despliegue.

Acierta con la ubicación (diagrama)

✗ Peligroso (cuerpo en la raíz pública)

public_html/
├─ .env   ← ¡legible!
├─ .git/  vendor/
└─ public/

✓ Seguro (cuerpo fuera, solo public)

app-laravel/   ← fuera de la raíz web
├─ .env  .git/  ← inalcanzables
└─ public/ → requerido desde public_html
Cuerpo fuera del docroot, exponer solo public/. Eso por sí solo hace inalcanzable el .env.

Qué solución encaja con tu entorno

"Mantener el .env fuera de la web" se hace de forma distinta según el tipo de servidor y tus permisos. Busca primero tu fila y después sigue los pasos de abajo.

EntornoSolución permanentePrimeros auxiliosCuidado con
Hosting compartido (con SSH)Saca el cuerpo de la app de la carpeta pública y deja allí solo un index.php pequeño (Paso 2)Denegar en .htaccessConvertir la propia carpeta pública en un symlink devuelve 500 en algunos entornos
Hosting compartido (solo FTP)Vuelve a subir el cuerpo de la app un nivel por encima de la carpeta pública (public_html, etc.) con tu cliente FTPDenegar en .htaccessLa mayoría de los hostings compartidos permiten escribir por encima de la carpeta pública; si el tuyo no, pregunta al soporte
El panel de control permite elegir la carpeta públicaApunta la carpeta pública directamente a app-laravel/publicNo hace falta (pero compruébalo)Tras el cambio, borra cualquier copia de la app que quede en la carpeta pública antigua
VPS (Apache)Apunta el DocumentRoot del VirtualHost a public/Reglas de denegación en la configuración del servidorCon AllowOverride None (el valor por defecto en Apache 2.4), el .htaccess no se lee
VPS (Nginx)Apunta root a public/ y deniega los archivos que empiezan por puntoReglas de denegación en la configuración del servidorNginx no lee el .htaccess

El objetivo es el mismo en todos los casos: que no haya .env, .git, vendor/ ni storage/ dentro de la carpeta pública. La documentación oficial de Laravel también advierte que mover index.php a la raíz del proyecto y servir desde ahí expone muchos archivos de configuración sensibles a internet.

Paso 1: Primeros auxilios (rápido, reversible)

Añade un bloque de denegación al principio del .htaccess de la raíz web (un ejemplo de configuración defensiva). Las páginas normales no se ven afectadas.

# === SECURITY BLOCK ===
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
 
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
    Require all denied
</FilesMatch>
 
<FilesMatch "(?i)^(phpinfo|info)\.php$">
    Require all denied
</FilesMatch>
 
# if Laravel internals are visible at /<app>/storage/... etc.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===

Los primeros auxilios no son el objetivo

Una lista negra siempre deja algo fuera: composer.lock o cualquier nombre de archivo nuevo. Trátalo como ganar tiempo hasta el Paso 2.

Paso 2: Reestructurar (la solución permanente)

Mueve el cuerpo de la app fuera de la raíz web y deja un punto de entrada diminuto que solo lo cargue.

# 1. Move the body outside the web root (same filesystem → mv is instant, atomic)
mv ~/example/public_html/app  ~/example/app-laravel
 
# 2. Put only a minimal entry in the web root
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
 
# 3. Copy Laravel public/.htaccess, symlink static assets
# 4. Clear config cache
rm -f ~/example/app-laravel/bootstrap/cache/{config,services,packages,routes-v7}.php

Una lección de operar este sitio: bootstrap-redirect en lugar de symlink

Hacer del propio public_html un symlink a public/ puede devolver 500 en el docroot de un subdominio registrado en algunos entornos (no se sigue). Mantener el docroot como un directorio real y hacer require por ruta absoluta desde un pequeño index.php (la forma "bootstrap-redirect") es más estable en hosting compartido con alcance restringido. Además, opcache puede memorizar una carga fallida y seguir devolviendo 500 — el último recurso es cambiar el nombre del archivo de entrada (entry.php).

Paso 3: Rota las claves (asume que fueron vistas)

Si hubo cualquier ventana de exposición, trata las claves del .env como ya vistas y reemplázalas. Orden: secretos de API externa / OAuth → claves de cifrado → correo → BD. Ver el glosario del .env. Reemitir un secreto de OAuth puede invalidar los refresh_token, así que ten lista primero una vía de reautorización.

Paso 4: Autocomprobación (hazlo un hábito)

Por último, confirma que nada esté de verdad expuesto en tu propio sitio (solo contra dominios que poseas).

# A 200 with a body means it's exposed. 403/404 is okay for now.
curl -sI https://your-domain/.env | head -1
curl -sI https://your-domain/.git/config | head -1

Comprobar esto en cada despliegue detecta pronto los errores de ubicación.

Cómo comprobar tu propia configuración (más a fondo)

Además de las dos líneas del Paso 4, estas comprobaciones detectan más de lo que suele pasarse por alto. Ejecútalas solo contra tu propio servidor y tus propios dominios.

1

Mira el contenido, no solo el código de estado

Algunas apps responden a cualquier URL desconocida con la página de inicio y un 200, así que el código de estado por sí solo no dice nada. Muestra solo el principio, por ejemplo curl -s https://your-domain/.env | head -c 120, y mira si aparecen líneas de configuración como APP_KEY= o DB_PASSWORD=. En .git/HEAD, una línea que empieza por ref: refs/heads/ significa que está expuesto.

2

Comprueba todas las apps, no solo una

Repite la comprobación para cada app en un subdominio o subdirectorio, no solo el dominio principal. Los errores de ubicación suelen copiarse al añadir apps, así que arreglar una no significa que las demás estén arregladas.

3

Busca en la carpeta pública del servidor

Con SSH, busca directamente los archivos peligrosos, por ejemplo find ~/public_html -maxdepth 4 \( -name ".env*" -o -name ".git" -o -name "*.sql" -o -name "*.zip" \). Que no salga nada es el resultado que quieres. Si solo tienes FTP, activa "mostrar archivos ocultos" en tu cliente FTP y revisa el listado.

4

Confirma que el .htaccess surte efecto

Pon en la carpeta pública un archivo de prueba inofensivo y sin secretos (por ejemplo check.bak con solo test dentro) y ábrelo por URL. Deberías recibir un 403. Si se abre, el .htaccess no se está leyendo, así que revisa el tipo de servidor (Nginx, por ejemplo) o su configuración. Borra el archivo de prueba al terminar.

5

Asegúrate de que el modo debug está desactivado

En el .env, APP_ENV debe ser production y APP_DEBUG debe ser false. La documentación de Laravel dice que APP_DEBUG=true en producción conlleva el riesgo de exponer valores de configuración sensibles a tus usuarios. Con SSH, php artisan about --only=environment muestra lo mismo.

Errores frecuentes y cómo corregirlos

  • Borrar el .env pero dejar el .git: un .git expuesto puede permitir reconstruir commits antiguos, incluido un .env o claves antiguas. Saca también .git de la carpeta pública y reemite cualquier secreto que se haya subido alguna vez en un commit.
  • Dejar copias como .env.bak o .env.old en la carpeta pública: una copia con valores reales es tan sensible como el .env. Sácala o bórrala. Deja solo valores de ejemplo en .env.example.
  • Subir todo el proyecto como ZIP y dejarlo tras descomprimirlo: el ZIP contiene el .env. Bórralo justo después de descomprimir.
  • Ejecutar producción con APP_DEBUG=true: las páginas de error pueden revelar valores de configuración. Ponlo siempre en false en producción.
  • Creer que estás a salvo porque escribiste el .htaccess: las configuraciones con Nginx o con AllowOverride None nunca lo leen. Usa la comprobación con el archivo de prueba de arriba para confirmarlo.
  • Cambiar el APP_KEY de golpe: según la documentación de Laravel, cambiar la clave cierra la sesión de todos los usuarios y hace que los datos cifrados con la clave antigua ya no se puedan descifrar. En Laravel 11 y posteriores, pon la clave antigua en APP_PREVIOUS_KEYS antes de cambiarla.

Un caso real: archivos .env expuestos recogidos a gran escala

En agosto de 2024, el equipo de investigación de una empresa de seguridad informó de una operación que recogía automáticamente archivos .env expuestos en servidores web, usaba las credenciales de la nube que contenían para copiar datos y después exigía un rescate. Según el informe, se apuntó a unos 110.000 dominios y se recogieron más de 90.000 variables de entorno, de las que unas 7.000 eran credenciales de servicios en la nube y unas 1.500 eran credenciales de cuentas de redes sociales.

El informe no atribuye la causa a una vulnerabilidad del lado de la nube, sino a errores de configuración de las organizaciones afectadas, que habían expuesto sus archivos .env por error. De ahí salen dos lecciones:

  • Los .env expuestos no se encuentran por casualidad; hay programas que los buscan a gran escala
  • Un solo .env puede extender el daño a otros servicios, como el almacenamiento en la nube o el correo

No des por hecho que nadie encontrará un sitio pequeño. Un caso anonimizado del mismo error de ubicación está en El .env expuesto al mundo entero.

Sigue leyendo

Fuentes

  • Documentación de Laravel, "Deployment" (dirigir las peticiones a public/index.php, no servir nunca desde la raíz del proyecto, APP_DEBUG=false en producción): laravel.com
  • Documentación de Laravel, "Configuration" (no subir el .env al control de versiones; Debug Mode): laravel.com
  • Documentación de Laravel, "Encryption" (rotación del APP_KEY y APP_PREVIOUS_KEYS): laravel.com
  • Apache HTTP Server 2.4, "core - AllowOverride" (el .htaccess no se lee cuando vale None; el valor por defecto es None): httpd.apache.org
  • Apache HTTP Server 2.4, ".htaccess files" (cuándo usarlos y precauciones): httpd.apache.org
  • OWASP, "WSTG-CONF-04 Review Old Backup and Unreferenced Files for Sensitive Information": owasp.org
  • Unit 42 (Palo Alto Networks), "Leaked Environment Variables Allow Large-Scale Extortion Operation in Cloud Environments" (15 de agosto de 2024): unit42.paloaltonetworks.com

FAQ

Q¿Qué es lo más rápido que puedo hacer ahora mismo?
A

Añade un bloque de denegación al .htaccess de tu raíz web para .env, .git, volcados SQL, credentials.json, etc. Es reversible y no afecta a las páginas normales — pero son primeros auxilios; la solución duradera es reestructurar.

Q¿Por qué no basta con el .htaccess solo?
A

Una lista negra necesita una regla nueva para cada nombre de archivo nuevo, así que siempre se escapa algo. Mueve el cuerpo de la app fuera de la raíz web y el .env simplemente no se puede alcanzar, sin reglas que mantener.

Q¿Por qué no hacer simplemente un symlink de public/ para exponerlo?
A

En algunos entornos, reapuntar el docroot de un subdominio registrado a otro árbol vía symlink no se sigue y devuelve 500. Según la experiencia de este sitio, mantener el docroot como un directorio real y hacer `require` por ruta absoluta desde un pequeño index.php (la forma 'bootstrap-redirect') es más estable.

Q¿Cómo compruebo si el .env de mi sitio se ve desde fuera?
A

Contra un dominio que sea tuyo, abre /.env y /.git/HEAD con curl o con el navegador y mira tanto el código de estado HTTP como el contenido. Un 403 o 404 está bien por ahora. Un 200 que muestra líneas de configuración como 'APP_KEY=' significa que está expuesto. Si tienes varias apps en subdominios o subdirectorios, comprueba cada una. Con SSH, busca también .env o .git en la carpeta pública del servidor.

Q¿Y si el servidor usa Nginx, o el .htaccess no surte efecto?
A

Nginx no lee el .htaccess. En Apache, el .htaccess se ignora cuando AllowOverride es None. En ese caso, apunta la raíz web (root o DocumentRoot) al public/ de Laravel en la configuración del servidor. El ejemplo de Nginx de la documentación oficial de Laravel incluye además una regla que deniega los archivos que empiezan por punto (salvo .well-known).

QSi mi .env estuvo expuesto, ¿basta con borrar el archivo?
A

No. Si alguien lo descargó mientras estaba expuesto, las claves que contiene siguen funcionando aunque borres el archivo. Reemite las claves de API, los secretos de OAuth, las contraseñas de la BD y del correo y el APP_KEY, y revisa la actividad de cada servicio en busca de operaciones que no reconozcas. Si también estuvo expuesto .git, trata como filtrado cualquier secreto que haya estado alguna vez en un commit antiguo.