Tema
Secretos y archivos .env
Contraseñas de la base de datos, claves de Stripe, tokens de la API de correo, el secreto de las sesiones... Todo proyecto real maneja secretos. Escribirlos directamente en el código es uno de los errores más caros: basta con subir el repositorio a GitHub para que bots automáticos los encuentren en minutos. En esta lección aprenderás a gestionarlos con variables de entorno y archivos .env.
Concepto
Piensa en las llaves de tu casa. No las grabas en la puerta ni se las das a todo el que te visita; las guardas aparte y das copias solo a quien las necesita. Si pierdes una, cambias la cerradura.
Con los secretos es igual:
- El código es público (o podría llegar a serlo): se comparte con compañeros, se sube a Git, se copia a servidores.
- Los secretos viven fuera del código, en variables de entorno que cada máquina (tu PC, el servidor de pruebas, producción) define por su cuenta.
Un archivo .env es simplemente una forma cómoda de definir esas variables en desarrollo:
env
# .env — NUNCA se sube a Git
APP_ENV=local
APP_DEBUG=true
DB_HOST=localhost
DB_NAME=tienda
DB_USER=tienda_app
DB_PASS=Xk9#mP2$vL7qR4nW
SMTP_HOST=smtp.mailtrap.io
SMTP_USER=abc123
SMTP_PASS=def456
SESSION_SECRET=8f3a9c1e7b2d4f6a0e5c8b1d3f7a9e2c4b6d8f0a1c3e5b7d9f1a3c5e7b9d1f3a
STRIPE_SECRET_KEY=sk_test_51H...Ejemplo: el error clásico
php
<?php
// ❌ Secretos en el código: al hacer git push quedan públicos para siempre
$pdo = new PDO('mysql:host=localhost;dbname=tienda', 'root', 'MiPassword123');
$stripeKey = 'sk_live_51HxYz...';js
// ❌ Y en el frontend es todavía peor: cualquiera lo ve con "Ver código fuente"
const OPENAI_KEY = 'sk-proj-abc123...';
fetch('https://api.openai.com/v1/chat/completions', {
headers: { Authorization: `Bearer ${OPENAI_KEY}` },
});Secretos en el frontend
Todo lo que está en el JavaScript del navegador es público, aunque esté minificado o en un .env de Vite. En Vite, las variables con prefijo VITE_ y en Next.js las NEXT_PUBLIC_ se incrustan en el código que descarga el usuario. Las claves secretas solo pueden vivir en el servidor; el frontend llama a tu backend y tu backend llama al servicio externo.
Ejemplo: secretos con .env en PHP
bash
composer require vlucas/phpdotenvphp
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
// Carga .env desde la carpeta del proyecto (fuera de public_html idealmente)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad(); // no falla si no existe .env (en producción pueden venir del servidor)
// ✅ Exige que existan las variables críticas: falla rápido y claro
$dotenv->required(['DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS'])->notEmpty();
$dotenv->required('APP_ENV')->allowedValues(['local', 'staging', 'production']);
// Configuración de errores según el entorno
if ($_ENV['APP_ENV'] === 'production') {
ini_set('display_errors', '0');
ini_set('log_errors', '1');
}
$pdo = new PDO(
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $_ENV['DB_HOST'], $_ENV['DB_NAME']),
$_ENV['DB_USER'],
$_ENV['DB_PASS'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false]
);txt
mi-proyecto/
├── .env ← secretos (NO en Git, NO accesible por web)
├── .env.example ← plantilla sin valores reales (SÍ en Git)
├── .gitignore
├── bootstrap.php
├── vendor/
└── public/ ← única carpeta pública (document root)
└── index.php ← require __DIR__ . '/../bootstrap.php';Si el hosting no te deja cambiar el document root
En algunos hostings compartidos todo está en public_html. Entonces bloquea el acceso al .env y a otras carpetas sensibles:
apache
# public_html/.htaccess
# Bloquea archivos que empiezan por punto (.env, .git, .htpasswd)
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Bloquea carpetas internas
RedirectMatch 404 ^/(vendor|storage|\.git)(/|$)
RedirectMatch 404 /(composer\.(json|lock)|package\.json)$Comprueba que funciona visitando https://tusitio.com/.env: debe dar 403 o 404, nunca mostrar el contenido.
Ejemplo: secretos con .env en Node
bash
# Node 20.6+ ya lee .env de forma nativa:
node --env-file=.env server.js
# O con el paquete clásico:
npm install dotenvjs
// Con dotenv (si no usas --env-file)
import 'dotenv/config';
import { z } from 'zod';
// ✅ Validamos el entorno al arrancar: si falta algo, la app no inicia
const esquema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().default(3000),
DB_HOST: z.string().min(1),
DB_USER: z.string().min(1),
DB_PASS: z.string().min(1),
DB_NAME: z.string().min(1),
SESSION_SECRET: z.string().min(32, 'SESSION_SECRET debe tener al menos 32 caracteres'),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
});
const resultado = esquema.safeParse(process.env);
if (!resultado.success) {
console.error('❌ Variables de entorno inválidas:', resultado.error.flatten().fieldErrors);
process.exit(1);
}
// Exportamos un objeto tipado y congelado
export const config = Object.freeze(resultado.data);js
import { config } from './config.js';
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: config.DB_HOST,
user: config.DB_USER,
password: config.DB_PASS,
database: config.DB_NAME,
});.gitignore y .env.example
txt
# Secretos
.env
.env.*
!.env.example
# Dependencias y compilados
vendor/
node_modules/
# Logs y subidas de usuarios
*.log
storage/logs/
uploads/env
# Copia este archivo como .env y rellena los valores
APP_ENV=local
DB_HOST=localhost
DB_NAME=
DB_USER=
DB_PASS=
SESSION_SECRET=
STRIPE_SECRET_KEY=Genera secretos aleatorios de verdad
Nada de secreto123. Genera valores largos y aleatorios:
bash
# Cualquiera de estas opciones
php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
openssl rand -hex 32¿Filtraste un secreto? Plan de emergencia
Si subiste una clave a GitHub (aunque el repositorio sea privado, aunque lo borres al minuto):
- Revoca/rota la clave de inmediato en el panel del servicio (Stripe, AWS, base de datos). Esto es lo único que realmente te protege.
- Revisa los registros del servicio en busca de uso no autorizado.
- Actualiza la nueva clave en tu
.envy en el servidor. - Elimínala del historial de Git (con
git filter-repoo BFG) si el repositorio será público. Borrarla en un commit nuevo no basta: sigue en el historial.
bash
# Comprueba si un .env llegó a estar en el historial
git log --all --full-history -- .env
# Si ya lo habías commiteado, deja de seguirlo (sin borrarlo de tu disco)
git rm --cached .env
git commit -m "Dejar de versionar .env"Error común
Pensar que "borrar el commit" soluciona el problema. Bots escanean GitHub en tiempo real y prueban las claves AWS, Stripe y de bases de datos en segundos. Asume que cualquier secreto que tocó un repositorio remoto está comprometido y rótalo.
Buenas prácticas de nivel profesional
- Un secreto distinto por entorno: desarrollo, staging y producción nunca comparten claves.
- Mínimo privilegio también en las claves: usa claves restringidas (Stripe permite claves de solo lectura o con permisos concretos; AWS con políticas IAM mínimas).
- Producción sin archivo .env cuando sea posible: variables definidas en el panel del hosting (Vercel, Railway, Forge, cPanel), en systemd o en el gestor de secretos del proveedor (AWS Secrets Manager, Doppler, 1Password, Infisical).
- Escaneo automático: activa Secret scanning y Push protection en GitHub, o usa
gitleaksen un hook pre-commit. - No registres secretos en logs: cuidado con
console.log(process.env)ovar_dump($_ENV). - Entrega segura al cliente: comparte credenciales con un gestor de contraseñas o enlaces de un solo uso, nunca por WhatsApp o correo en texto plano.
bash
# Escanear el repositorio en busca de secretos con gitleaks
gitleaks detect --source . -vEjercicios
- Refactoriza un
config.phpcon usuario y contraseña de la base de datos escritos en el código para que usevlucas/phpdotenv. Incluye.env.exampley.gitignore. - Tienes un frontend en React que llama a la API de OpenAI con la clave en el código. ¿Cómo lo reestructuras?
- Un compañero subió por error el
.envde producción a un repositorio público hace 2 horas. Escribe en orden las acciones a tomar.
Ver solución
- Estructura:
php
<?php
// config.php
require __DIR__ . '/vendor/autoload.php';
Dotenv\Dotenv::createImmutable(__DIR__)->load();
$pdo = new PDO(
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4",
$_ENV['DB_USER'],
$_ENV['DB_PASS'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);env
# .env.example
DB_HOST=localhost
DB_NAME=
DB_USER=
DB_PASS=txt
# .gitignore
.env
vendor/- El frontend llama a tu backend (
POST /api/chat), que tiene la clave en una variable de entorno, añade autenticación y límite de uso, y hace la llamada a OpenAI. La clave nunca llega al navegador.
js
// server.js (Express)
app.post('/api/chat', requiereLogin, limiteChat, async (req, res) => {
const r = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4o-mini', messages: req.body.messages }),
});
res.status(r.status).json(await r.json());
});- (1) Rotar todas las credenciales del archivo: base de datos, correo, pasarelas de pago, APP_KEY/secretos de sesión. (2) Revisar logs de acceso de la BD y de los servicios. (3) Hacer privado el repositorio o eliminar el archivo del historial con
git filter-repo. (4) Actualizar las nuevas credenciales en producción. (5) Añadir.enval.gitignorey activar push protection / gitleaks para que no se repita. (6) Informar al cliente si hubo acceso a datos personales.
Resumen
- Los secretos nunca van en el código ni en el frontend.
- Usa variables de entorno; en desarrollo, un archivo
.envcargado conphpdotenv,dotenvo--env-file. .enven.gitignore;.env.examplesin valores reales en el repositorio.- Valida las variables al arrancar para fallar rápido.
- Bloquea el acceso web a
.envy carpetas internas. - Si un secreto se filtra, rótalo inmediatamente: borrarlo de Git no basta.