Variables de Entorno: La regla de oro que todo desarrollador debe conocer (nunca más credenciales en GitHub)

Tutorial universal para PHP, Node.js, Python, React, Next.js y más. Protege tus credenciales de una vez por todas

Esta es la regla de oro que todo desarrollador debe conocer: nunca metas credenciales en el código. Suena obvio, pero es el error que más brechas de seguridad causa. Y lo peor es que no lo haces maliciosamente — lo haces sin darte cuenta cada vez que aceptas el código que te genera una IA.

Te sientas, abres ChatGPT o Cursor, y le dices: "Hazme un login con Stripe" o "Conecta una base de datos MySQL". La IA te devuelve algo como esto:

// Lo que te devuelve la IA
const dbPassword = "miPasswordDeProduccion123!";
const stripeKey = "sk_live_ABC123xyz_real";
const jwtSecret = "miSuperSecretoJWT";

// Y luego conecta así
await stripe.paymentIntents.create({
    amount: 5000,
    apiKey: stripeKey
});

Lo copias. Lo pegas en tu proyecto. Ejecutas npm start o php server.php. Funciona. El login conecta con Stripe. La base de datos responde. Todo verde.

Pero entonces alguien te dice: "Súbelo a GitHub para compartirlo" o "Despliegalo en Vercel". Haces git push. Y aquí es donde empieza el problema.

Lo que pasa cuando subes ese código

Bots automatizados escanean GitHub 24 horas al día, 7 días a la semana. No son personas revisando tu código — son máquinas que buscan patrones como sk_live_, password=, api_key. En el momento exacto en que tu commit llega a GitHub, tu clave ya está en una lista.

En 2025 se detectaron 29 millones de secretos hardcodeados en repositorios públicos de GitHub — un 34% más que el año anterior. Y lo peor: el 64% de esas credenciales filtradas hace años siguen siendo válidas. Eso significa que alguien puede estar usando tu clave de producción ahora mismo, y no te enteras.

29M Secretos detectados en GitHub en 2025 — Fuente: GitGuardian State of Secrets Sprawl 2026
64% De credenciales filtradas en 2022 siguen activas — Fuente: GitGuardian

El coste medio de una brecha por credenciales robadas ronda los 4.8 millones de dólares. Y la detección suele tardar casi un año — para cuando te enteras, el daño ya está hecho.

Por qué la IA genera código así

Las IAs priorizan que el código funcione y sea fácil de entender. Para no complicar el ejemplo con configuración extra, meten los valores directamente. Es útil para aprender, pero es un error de seguridad grave si lo subes a un repositorio. El código de ejemplo no es código de producción.

La solución: variables de entorno

Son valores de configuración que viven fuera de tu código. En vez de escribir la contraseña directamente en el archivo, le dices a tu aplicación que la busque en un lugar externo — el entorno del sistema operativo o un archivo .env que nunca se sube a Git.

La diferencia es esta:

# ❌ Hardcodeado — si esto llega a GitHub, tus credenciales son públicas
spring.datasource.password=miPassword123!

# ✅ Variable de entorno — el archivo no tiene ningún secreto
spring.datasource.password={DB_PASSWORD}

El segundo archivo se puede subir a GitHub sin ningún problema. Solo dice dónde buscar el valor. El valor real está en tu máquina, en un archivo .env que Git no toca.

Implementación por tecnología

PHP

En PHP usamos la librería vlucas/phpdotenv para cargar variables de entorno:

Instalación

# Con Composer
composer require vlucas/phpdotenv

Crear archivo .env

# .env - NUNCA subir a Git
DB_HOST=localhost
DB_USERNAME=mi_usuario
DB_PASSWORD=mi_password_super_segura
DB_DATABASE=mi_base_datos

STRIPE_API_KEY=sk_test_mi_clave_de_stripe
SENDGRID_API_KEY=SG.mi_clave_de_sendgrid

Cargar en tu aplicación

<?php
require_once 'vendor/autoload.php';

// Cargar variables de entorno
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

// ✅ Usar variables de entorno
$host = $_ENV['DB_HOST'];
$username = $_ENV['DB_USERNAME'];
$password = $_ENV['DB_PASSWORD'];

// Conexión segura a la base de datos
$pdo = new PDO(
    "mysql:host=$host;dbname=" . $_ENV['DB_DATABASE'],
    $username,
    $password
);
?>

Node.js / Express

Node.js usa el paquete dotenv para manejar variables de entorno:

Instalación

# Con npm
npm install dotenv

# Con yarn
yarn add dotenv

Configuración en tu app

// app.js o server.js
require('dotenv').config();

const express = require('express');
const app = express();

// ✅ Variables de entorno disponibles
const PORT = process.env.PORT || 3000;
const STRIPE_KEY = process.env.STRIPE_API_KEY;
const JWT_SECRET = process.env.JWT_SECRET;

app.listen(PORT, () => {
    console.log(`Servidor corriendo en puerto ${PORT}`);
});

Python / Django / Flask

Python usa python-dotenv para cargar variables de entorno:

Instalación

# Con pip
pip install python-dotenv

# O añadir a requirements.txt
python-dotenv==1.0.0

Flask

# app.py
import os
from dotenv import load_dotenv
from flask import Flask

# Cargar variables de entorno
load_dotenv()

app = Flask(__name__)

# ✅ Usar variables de entorno
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
DATABASE_URL = os.getenv('DATABASE_URL')
STRIPE_KEY = os.getenv('STRIPE_API_KEY')

Django settings.py

# settings.py
import os
from dotenv import load_dotenv

load_dotenv()

# ✅ Configuración segura
SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG', 'False') == 'True'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('DB_NAME'),
        'USER': os.getenv('DB_USER'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST'),
    }
}

React (Frontend)

En React, las variables de entorno deben empezar con REACT_APP_ para ser accesibles en el frontend:

Archivo .env

# .env - Variables para React
REACT_APP_API_URL=https://api.miapp.com
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_mi_clave_publica
REACT_APP_GOOGLE_ANALYTICS_ID=GA-123456789

# ⚠️ Variables del servidor (NO accesibles en frontend)
STRIPE_SECRET_KEY=sk_test_mi_clave_secreta

Usar en componentes

// src/components/PaymentForm.js
import React from 'react';

function PaymentForm() {
    // ✅ Acceder a variables de entorno
    const apiUrl = process.env.REACT_APP_API_URL;
    const stripeKey = process.env.REACT_APP_STRIPE_PUBLIC_KEY;
    
    const handlePayment = async () => {
        const response = await fetch(`${apiUrl}/payment`);
        // Procesar pago...
    };
    
    return (
        <div>
            <h2>Pago Seguro</h2>
            {/* Formulario de pago */}
        </div>
    );
}

Next.js

Next.js maneja variables tanto del servidor como del cliente de forma inteligente:

Archivo .env.local

# .env.local - Configuración de Next.js

# Variables del servidor (seguras)
STRIPE_SECRET_KEY=sk_test_mi_clave_secreta
DATABASE_URL=postgresql://usuario:password@localhost/midb
JWT_SECRET=mi_jwt_super_secreto

# Variables del cliente (públicas - prefijo NEXT_PUBLIC_)
NEXT_PUBLIC_API_URL=https://api.miapp.com
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_mi_clave_publica
NEXT_PUBLIC_GA_ID=GA-123456789

En API routes (servidor)

// pages/api/payment.js
export default async function handler(req, res) {
    // ✅ Variables del servidor accesibles
    const stripeSecret = process.env.STRIPE_SECRET_KEY;
    const jwtSecret = process.env.JWT_SECRET;
    
    // Lógica de pago segura...
}

En componentes (cliente)

// components/Analytics.js
export default function Analytics() {
    // ✅ Solo variables públicas accesibles
    const gaId = process.env.NEXT_PUBLIC_GA_ID;
    const apiUrl = process.env.NEXT_PUBLIC_API_URL;
    
    // ❌ Esto sería undefined (variable no pública)
    // const secret = process.env.JWT_SECRET;
    
    return <div>Analytics configurado</div>;
}

Vite

Vite usa el prefijo VITE_ para variables accesibles en el frontend:

Archivo .env

# .env para Vite
VITE_API_URL=https://api.miapp.com
VITE_APP_TITLE=Mi App Increíble
VITE_STRIPE_PUBLIC_KEY=pk_test_mi_clave

# Variables del servidor (no accesibles desde frontend)
STRIPE_SECRET_KEY=sk_test_mi_clave_secreta

Usar en componentes

// src/main.js
console.log(import.meta.env.VITE_API_URL); // ✅ Funciona
console.log(import.meta.env.STRIPE_SECRET_KEY); // ❌ undefined

Control de versiones: El archivo sagrado .gitignore

Ojo, que aquí es donde la mayoría mete la pata. Tu archivo .env NUNCA debe llegar a Git. Jamás. Never. Nunca de los nuncas.

.gitignore universal

# CRÍTICO: Variables de entorno
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Node.js
node_modules/
npm-debug.log*

# PHP
vendor/
composer.lock

# Python
__pycache__/
*.pyc
venv/

# Logs con información sensible
*.log
logs/

# Archivos de configuración específicos
config.ini
settings.local.php

Archivo .env.example: La plantilla perfecta

Crea un archivo .env.example que SÍ debes subir a Git. Este archivo muestra la estructura sin exponer valores reales:

# .env.example - Copia este archivo como .env y completa con tus credenciales
# IMPORTANTE: NUNCA subas el archivo .env a Git

# Base de datos
DB_HOST=localhost
DB_USERNAME=tu_usuario_aqui
DB_PASSWORD=tu_password_aqui
DB_DATABASE=nombre_base_datos

# APIs externas
STRIPE_API_KEY=sk_test_tu_clave_aqui
STRIPE_PUBLIC_KEY=pk_test_tu_clave_publica_aqui
SENDGRID_API_KEY=SG.tu_clave_aqui

# JWT
JWT_SECRET=genera_un_secreto_random_muy_largo

# URLs
API_URL=https://api.miapp.com
FRONTEND_URL=https://miapp.com

# Entorno
NODE_ENV=development
DEBUG=true

¿Ya subiste credenciales por error?

PROTOCOLO DE EMERGENCIA

  1. Cambia INMEDIATAMENTE todas las credenciales expuestas
  2. Revoca y genera nuevas API keys
  3. Usa git-secrets para escanear el historial
  4. En casos graves, considera recrear el repositorio
  5. NUNCA asumas que "eliminar el archivo" es suficiente

Deployment por plataforma

Vercel

Dashboard (método fácil)

CLI (método pro)

# Instalar Vercel CLI
npm i -g vercel

# Añadir variables
vercel env add DB_PASSWORD
vercel env add STRIPE_API_KEY

Netlify

Dashboard

netlify.toml

# netlify.toml
[build]
  command = "npm run build"
  
# Variables de entorno para contextos específicos
[context.production.environment]
  NODE_ENV = "production"
  
[context.deploy-preview.environment]
  NODE_ENV = "staging"

Heroku

CLI

# Configurar variables
heroku config:set DB_PASSWORD=mi_password --app mi-app
heroku config:set STRIPE_API_KEY=sk_live_123 --app mi-app

# Ver variables configuradas
heroku config --app mi-app

Dashboard

AWS (Parameter Store)

# AWS CLI para gestionar parámetros
aws ssm put-parameter \
    --name "/myapp/production/db_password" \
    --value "mi_password_super_segura" \
    --type "SecureString"

Docker

# docker-compose.yml - Versión moderna sin 'version'
services:
  web:
    build: .
    environment:
      - DB_PASSWORD=${DB_PASSWORD}
      - STRIPE_API_KEY=${STRIPE_API_KEY}
    env_file:
      - .env
      
  # Ejemplo con secrets para producción
  app:
    build: .
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

Casos específicos por API

Stripe

# Configuración Stripe
STRIPE_PUBLIC_KEY=pk_test_123456789
STRIPE_SECRET_KEY=sk_test_987654321
STRIPE_WEBHOOK_SECRET=whsec_ABC123

# En producción
STRIPE_PUBLIC_KEY=pk_live_123456789
STRIPE_SECRET_KEY=sk_live_987654321

SendGrid

# SendGrid para emails
SENDGRID_API_KEY=SG.TuClaveAqui123456789
SENDGRID_FROM_EMAIL=noreply@miapp.com
SENDGRID_FROM_NAME=Mi App Increíble

Google APIs

# Google Services
GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-tu_secreto_aqui
GOOGLE_ANALYTICS_ID=GA-123456789

# Service Account (JSON como string)
GOOGLE_SERVICE_ACCOUNT='{"type":"service_account","project_id":"tu-proyecto"...}'

JWT Secrets

# JWT para autenticación
JWT_SECRET=tu_secreto_super_largo_y_random_123ABC456DEF
JWT_EXPIRES_IN=7d
JWT_REFRESH_SECRET=otro_secreto_diferente_para_refresh_tokens

Mejores prácticas avanzadas

Rotación de credenciales

Calendario de rotación

  • API Keys críticas: Cada 3 meses
  • Passwords de base de datos: Cada 6 meses
  • JWT Secrets: Cada mes
  • Tokens de acceso: Según expire automáticamente

Diferentes entornos

# .env.development
NODE_ENV=development
DEBUG=true
DB_URL=postgresql://localhost:5432/myapp_dev
STRIPE_API_KEY=sk_test_123

# .env.production
NODE_ENV=production
DEBUG=false
DB_URL=postgresql://prod-server:5432/myapp_prod
STRIPE_API_KEY=sk_live_456

Gestión de permisos por equipo

Monitoring y alertas

# Configuración de monitoring
SENTRY_DSN=https://abc123@sentry.io/123456
LOG_LEVEL=warn
ALERTS_WEBHOOK=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

Lo mínimo que deberías tener configurado antes de subir cualquier proyecto: el .env en .gitignore y las variables en tu plataforma de deployment. Si no lo haces, es cuestión de tiempo.

Herramientas de verificación

# Escanear tu repositorio en busca de secretos
npx @gitguardian/gg-shield-cli scan

# git-secrets para prevenir commits con credenciales
git secrets --register-aws
git secrets --scan

Regla de oro

Si hay dudas, NO lo hagas. Es mejor ser paranoico con la seguridad que explicar una brecha de datos de millones de euros.

Ahora ya sabes cómo proteger tus credenciales. La próxima vez que trabajes en un proyecto nuevo, lo primero que harás será crear el archivo .env y añadirlo al .gitignore. Así de simple.

Si después de leer esto sigues con credenciales hardcoded en algún proyecto... ya estás avisado, tío.

Qué aprender ahora

Las variables de entorno son solo el principio. Hay muchos más conceptos que todo developer profesional debe conocer.

PHP y MySQL CRUD: Tu primera base de datos conectada

Aprende a conectar bases de datos de forma segura usando las variables de entorno que acabas de dominar