StackPractices
intermediate Por Mathias Paulenko

Gestionar Secretos de Aplicaciones de Forma Segura

Cómo almacenar, rotar e inyectar API keys, contraseñas de base de datos y certificados sin hardcodearlos en código fuente o archivos de entorno.

Temas: devops

Visión general

Los secretos — API keys, contraseñas de base de datos, certificados TLS, claves de encriptación — son las joyas de la corona de cualquier aplicación. Hardcodearlos en código fuente los commitea a control de versiones para siempre, expuestos a cualquiera con acceso al repositorio. Almacenarlos en archivos .env de texto plano en servidores los deja legibles por cualquier proceso corriendo como el mismo usuario.

La gestión segura de secretos significa almacenarlos en vaults dedicados con encriptación at rest, control de acceso, audit logging y rotación automática. Las aplicaciones obtienen secretos en runtime a través de llamadas API autenticadas, nunca persistiéndolos a disco. Aqui se explica como secret managers nativos de cloud (AWS, GCP, Azure), HashiCorp Vault y Kubernetes Secrets.

Cuándo usarlo

Usa esta receta cuando:

  • Migrando de archivos .env de desarrollo a almacenamiento de secretos de producción. Consulta Environment Variables para patrones de configuración local.
  • Rotando credenciales comprometidas o cumpliendo con requerimientos de auditoría de seguridad. Consulta JWT Authentication para estrategias de rotación de tokens.
  • Compartiendo secretos entre microservicios, pipelines CI/CD y miembros del equipo. Consulta Docker Basics para inyección de secretos en contenedores.
  • Gestionando certificados TLS, claves SSH o strings de conexión a base de datos. Consulta Parse Config Files para referencias de secretos por configuración.
  • Auditando quién accedió a qué secreto y cuándo. Consulta Structured Logging para auditoría de logs.

Solución

AWS Secrets Manager (Python)

import boto3
import json

client = boto3.client('secretsmanager')

def get_secret(secret_name):
    response = client.get_secret_value(SecretId=secret_name)
    return json.loads(response['SecretString'])

db_creds = get_secret('prod/db/postgres')
conn = psycopg2.connect(
    host=db_creds['host'],
    user=db_creds['username'],
    password=db_creds['password'],
)

HashiCorp Vault (Go)

import "github.com/hashicorp/vault/api"

client, _ := api.NewClient(api.DefaultConfig())
client.SetToken("s.xxx")

secret, _ := client.KVv2("secret").Get(context.Background(), "database/creds")
username := secret.Data["username"].(string)
password := secret.Data["password"].(string)

Kubernetes Secrets

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:
  username: admin
  password: "{{ .Values.dbPassword }}"
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: app
        env:
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: username

Explicación

  • Encriptación at rest: Los secretos se encriptan antes de escribirse a disco.
  • Secretos en vivo: Un rol de PostgreSQL podría ser válido por 1 hora y luego revocarse automáticamente, minimizando el blast radius si se filtran.
  • Control de acceso: Políticas IAM, políticas de Vault y RBAC de Kubernetes restringen qué servicios o usuarios pueden leer qué secretos. Nunca otorgues acceso de lectura a todos los secretos.
  • Audit logging: cada lectura, escritura y rotación de secreto se loguea. Reenvía estos logs a herramientas SIEM para detección de anomalías.

Variantes

HerramientaPlataformaSecretos en vivoAuto-rotaciónMejor para
AWS Secrets ManagerAWSWorkloads nativos AWS
HashiCorp VaultMultiMulti-cloud, on-prem
Azure Key VaultAzureParcialEcosistemas Azure
GCP Secret ManagerGCPNoNoWorkloads nativos GCP
Kubernetes SecretsK8sNoNoInyección in-cluster

Lo que funciona

  • Nunca commitees secretos a Git: gitignorepara archivos. envy hooks pre-commit (comogit-secretsotruffleHog`) para escanear commits accidentales.
  • Rota secretos regularmente: configura políticas de rotación automática (30-90 días) y rota inmediatamente si un secreto es expuesto o un empleado se va.
  • Usa acceso least-privilege: otorga a cada servicio exactamente los secretos que necesita. Un servidor web no necesita la clave de encriptación de backups.
  • Cachea secretos brevemente, no para siempre: obtén secretos al inicio y refréscalos periódicamente. No llames al secret manager en cada request.
  • Separa secretos por entorno: prod/db/password, staging/db/password y dev/db/password deberían ser valores diferentes en diferentes paths de vault.

Errores comunes

  • Almacenar secretos en variables de entorno en hosts compartidos: las variables de entorno son visibles por todos los procesos en la misma máquina.
  • Olvidar rotar después de brechas: cambiar la contraseña de aplicación no es suficiente. Rota API keys, certificados y secretos de sesión de forma comprehensiva.
  • Loguear secretos: nunca loguees el valor completo de un secreto. Si debes loguear acceso, loguea el nombre del secreto y timestamp, nunca la contraseña misma.
  • Usar Kubernetes Secrets sin encriptación de etcd: por default, los Kubernetes Secrets están base64-encoded, no encriptados. Habilita encriptación at rest de etcd.

Tips de Rendimiento

  1. Cachea secretos en memoria. Obtén una vez al inicio, refresca periódicamente:
import time

class SecretCache:
    def __init__(self, ttl=300):
        self._cache = {}
        self._ttl = ttl
        self._timestamps = {}

    def get(self, name, fetch_func):
        if name not in self._cache or time.time() - self._timestamps[name] > self._ttl:
            self._cache[name] = fetch_func(name)
            self._timestamps[name] = time.time()
        return self._cache[name]
  1. Usa lecturas bulk de secretos. Obtén todos los secretos de un servicio en una sola llamada API:
# AWS: almacenar todos los secretos del servicio como un único secreto JSON
response = client.get_secret_value(SecretId='prod/api/all-secrets')
all_secrets = json.loads(response['SecretString'])
# all_secrets = {'db_url': '...', 'stripe_key': '...', 'sendgrid_key': '...'}
  1. Usa connection pooling para Vault. Reutiliza conexiones HTTP a Vault:
client, _ := api.NewClient(api.DefaultConfig())
// Client reutiliza conexiones internamente
// Ajusta settings de transport para alto throughput:
transport := &http.Transport{
    MaxIdleConns:        10,
    IdleConnTimeout:     30 * time.Second,
}
  1. Pre-carga secretos en init del contenedor. Obtén secretos durante el startup del contenedor, no en el primer request:
# Init container de Kubernetes
initContainers:
- name: secret-loader
  image: secret-loader:latest
  command: ["/bin/sh", "-c"]
  args:
    - |
      vault kv get -field=password secret/db > /secrets/db_password
      vault kv get -field=apikey secret/api > /secrets/api_key
  volumeMounts:
  - name: secrets
    mountPath: /secrets
  1. Usa sidecar para rotación de secretos. Un sidecar puede observar cambios de secretos y enviar señales al contenedor principal:
# Vault Agent sidecar envía SIGHUP cuando los secretos cambian
annotations:
  vault.hashicorp.com/agent-inject: "true"
  vault.hashicorp.com/agent-inject-command-db-creds: "kill -HUP 1"

Preguntas frecuentes

¿Esta solución está lista para producción?

Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.

¿Cuáles son las características de rendimiento?

El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.

¿Cómo depuro problemas con este enfoque?

Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.

GCP Secret Manager (Node.js)
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');

const client = new SecretManagerServiceClient();

async function getSecret(name) {
  const [version] = await client.accessSecretVersion({
    name: `projects/my-project/secrets/${name}/versions/latest`,
  });
  return version.payload.data.toString('utf8');
}

// Uso
const apiKey = await getSecret('stripe-api-key');
Doppler para Sincronización de Secretos

Doppler sincroniza secretos desde un dashboard central a tu entorno de runtime:

# Instalar Doppler CLI
$ brew install dopplerhq/doppler/doppler

# Login y seleccionar proyecto
$ doppler login
$ doppler setup

# Ejecutar app con secretos inyectados
$ doppler run -- npm start

# Exportar secretos a .env para CI
$ doppler secrets download --no-file --format=env > .env
Vault Agent Sidecar Injector (Kubernetes)
# helm values para Vault Agent Injector
injector:
  enabled: true
  replicas: 1

# Anotación de Pod para inyectar secretos como archivos
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "api-server"
    vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/api"
    vault.hashicorp.com/agent-inject-template-db-creds: |
      {{- with secret "database/creds/api" -}}
      DB_USER={{ .Data.username }}
      DB_PASS={{ .Data.password }}
      {{- end }}
spec:
  template:
    spec:
      containers:
      - name: api
        env:
        - name: DB_USER_FILE
          value: /vault/secrets/db-creds
Rotación de Secretos con AWS Lambda
import boto3
import json
import psycopg2

def rotate_secret(event, context):
    client = boto3.client('secretsmanager')
    secret_arn = event['SecretId']
    token = event['ClientRequestToken']

    # Obtener secreto actual
    current = client.get_secret_value(SecretId=secret_arn, VersionStage='AWSCURRENT')
    creds = json.loads(current['SecretString'])

    # Generar nueva contraseña
    new_password = generate_secure_password()

    # Actualizar contraseña de base de datos
    conn = psycopg2.connect(
        host=creds['host'],
        user=creds['username'],
        password=creds['password'],
        dbname='postgres'
    )
    conn.autocommit = True
    cursor = conn.cursor()
    cursor.execute(f"ALTER USER {creds['username']} WITH PASSWORD '{new_password}'")
    cursor.close()
    conn.close()

    # Actualizar secreto en AWS
    new_secret = json.dumps({
        **creds,
        'password': new_password
    })
    client.put_secret_value(
        SecretId=secret_arn,
        SecretString=new_secret,
        VersionStage='AWSPENDING'
    )
    client.update_secret_version_stage(
        SecretId=secret_arn,
        VersionStage='AWSCURRENT',
        MoveToVersion=token,
        RemoveFromVersion=current['VersionId']
    )

def generate_secure_password(length=32):
    import secrets
    import string
    alphabet = string.ascii_letters + string.digits + '!@#$%^&*'
    return ''.join(secrets.choice(alphabet) for _ in range(length))
Patrón de Inyección de Variables de Entorno
import os
from functools import lru_cache

@lru_cache(maxsize=1)
def get_secrets():
    """Carga secretos una vez al inicio, cachea por el lifetime del proceso."""
    if os.environ.get('ENVIRONMENT') == 'production':
        # Obtener de AWS Secrets Manager
        import boto3, json
        client = boto3.client('secretsmanager')
        response = client.get_secret_value(SecretId='prod/app/secrets')
        return json.loads(response['SecretString'])
    else:
        # Dev: cargar de archivo .env
        from dotenv import load_dotenv
        load_dotenv()
        return dict(os.environ)

# Uso en la aplicación
secrets = get_secrets()
db_url = secrets.get('DATABASE_URL')
api_key = secrets.get('STRIPE_API_KEY')
Escaneo de Secretos en CI/CD
# .github/workflows/secret-scan.yml
name: Secret Scan
on: [push, pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Historial completo para escaneo

      - name: TruffleHog
        uses: trufflesecurity/trufflehog@main
        with:
          path: .
          extra_args: --only-verified

      - name: GitLeaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Credenciales Dinámicas de Base de Datos con Vault
# Configurar database secrets engine de Vault
$ vault secrets enable database

# Configurar conexión PostgreSQL
$ vault write database/config/my-postgresql \
    plugin_name=postgresql-database-plugin \
    connection_url="postgresql://{{username}}:{{password}}@db:5432/mydb?sslmode=disable" \
    allowed_roles="readonly"

# Crear un rol que genera credenciales válidas por 1 hora
$ vault write database/roles/readonly \
    db_name=my-postgresql \
    creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
        GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
    default_ttl="1h" \
    max_ttl="24h"

# Generar credenciales bajo demanda
$ vault read database/creds/readonly
# Key        Value
# lease_id   database/creds/readonly/abc123
# password   A1b2C3d4E5f6G7h8
# username   v-token-readonly-xyz123