Lección 4.4.5 · 60 min

Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos

Una clave de API con permiso de retirada, sin restricción de IP y escrita en un fichero del repositorio es una cuenta abierta. Esta lección recorre el camino de una credencial, desde dónde se guarda hasta cómo se revoca, con claves de juguete que no se pueden confundir con reales: lectura desde ficheros de credenciales, objetos que no se dejan imprimir, logs tachados, un escáner de secretos, un exchange simulado con permisos y lista de IP, y una rotación sin peticiones fallidas.

Fig. — Lo que consigue un atacante desde su máquina con cada clave filtrada, en el exchange simulado
Lecciones del nivel 4
  1. 4.1 · Teoría y optimización de carteras

    1. 4.1.1 Markowitz: frontera eficiente y restricciones reales
    2. 4.1.2 Estimación de covarianzas: shrinkage, modelos factoriales y denoising
    3. 4.1.3 Black-Litterman: del equilibrio del mercado a las visiones
    4. 4.1.4 Risk parity, mínima varianza, HRP y HERC
    5. 4.1.5 Optimización con CVaR, drawdown, rotación y costes
    6. 4.1.6 Modelos factoriales: CAPM, Fama-French, PCA y atribución
    7. 4.1.7 Rebalanceo y carteras de estrategias
    8. 4.1.8 Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt
  2. 4.2 · Gestión del riesgo y tamaño de posición

    1. 4.2.1 Riesgo por operación, stops y riesgo de ruina
    2. 4.2.2 Criterio de Kelly y volatility targeting: cuánto arriesgar
    3. 4.2.3 VaR, Expected Shortfall y backtesting del VaR
    4. 4.2.4 Stress testing y riesgos de correlación, liquidez, gap y contraparte
    5. 4.2.5 Control del drawdown y kill switch
    6. 4.2.6 Riesgo de derivados: griegas de cartera, margen y liquidaciones
  3. 4.3 · Microestructura y ejecución

    1. 4.3.1 Implementation shortfall, VWAP, TWAP y Almgren-Chriss
    2. 4.3.2 Order flow imbalance, señales de microestructura y latencia
  4. 4.4 · Brokers y APIs

    1. 4.4.1 Elegir bróker desde España o la UE: regulación, activos, costes y API
    2. 4.4.2 Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes
    3. 4.4.3 Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20
    4. 4.4.4 APIs de exchanges cripto: ccxt, Binance, Bybit, Kraken y Coinbase
    5. 4.4.5 Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos
  5. 4.5 · Sistema de trading en vivo

    1. 4.5.1 Arquitectura de un sistema en vivo: de los datos a la reconciliación
    2. 4.5.2 Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones
    3. 4.5.3 Paper trading, shadow trading y seguimiento del slippage
    4. 4.5.4 Programación de tareas y alertas: cron, APScheduler, Prefect, Telegram, Slack y email
    5. 4.5.5 Tolerancia a fallos: idempotencia, estado persistente y reconciliación al arrancar
    6. 4.5.6 NautilusTrader o LEAN en real: del backtest a producción con el mismo código

Qué se protege y de quién

Las lecciones 4.4.3 y 4.4.4 usaron claves falsas contra simuladores. En real, cada bróker da una credencial distinta y cada una abre una puerta distinta:

Bróker o exchangeCredencialQué permite si se filtra
Exchanges cripto (Binance, Bybit, Kraken, Coinbase)Clave y secreto (o clave privada Ed25519, RSA o ECDSA)Lo que digan sus permisos: leer, operar, retirar
AlpacaClave y secreto, distintos para papel y realLeer y operar en la cuenta; según Alpaca, las claves de la Trading API no pueden ordenar transferencias bancarias
OANDAToken personalAcceso a todas las subcuentas de la cuenta; OANDA no lo guarda, así que si se pierde se revoca y se genera otro
Interactive BrokersUsuario, contraseña y segundo factor en TWS o IB GatewayLa sesión de la API la abre el programa local; la API tiene modo de solo lectura y lista de IP de confianza
MetaTrader 5Número de cuenta, contraseña y servidorLa cuenta entera desde cualquier terminal

Las fuentes de cada fila están en la sección de permisos. Lo que hay que proteger no es solo la clave: también el token de un JWT mientras vale (lección 4.4.4), una URL firmada mientras está en la ventana de tiempo y el fichero de sesión de un terminal.

De quién se protege, en orden de frecuencia real: de uno mismo (un print, una captura de pantalla, un notebook guardado con la salida, un commit), de los registros (logs que se copian a otros sistemas o a un servicio externo), de la propia máquina (otros procesos y usuarios, copias de seguridad) y, por último, de un atacante que entra en el servidor o en una dependencia. Los tres primeros no necesitan a nadie malintencionado para que la clave acabe donde no debe.

Gestor o ficheroProceso del botFirma y cabeceraBróker o exchangeLogsProcesos hijosRepo y notebooks
Fuente Mermaid
flowchart TB
  G[Gestor o fichero] --> P[Proceso del bot]
  P --> F[Firma y cabecera]
  F --> X[Bróker o exchange]
  P -.-> L[Logs]
  P -.-> H[Procesos hijos]
  P -.-> R[Repo y notebooks]

Las flechas continuas son el camino que debe seguir la credencial; las discontinuas, las fugas que esta lección cierra una por una.

Claves de juguete

Las celdas usan claves que no se pueden confundir con reales: empiezan por FALSA_ y el resto sale del SHA-256 del nombre. Ni siquiera esas se imprimen: para identificar una clave sin revelarla basta su huella, los primeros caracteres del SHA-256 de su valor, que es lo que se puede escribir en un log o en un informe.

IN [01]python
import hashlib

def falsa(nombre, largo=40):
    """Clave de juguete: prefijo FALSA_ y relleno determinista."""
    return "FALSA_" + hashlib.sha256(nombre.encode()).hexdigest()[
        : largo - 6]

def huella(valor):
    """Identifica una clave sin revelarla: 8 hex de su SHA-256."""
    return hashlib.sha256(valor.encode()).hexdigest()[:8]

CLAVES = {n: falsa(n) for n in ["ALPACA_KEY_ID", "ALPACA_SECRET",
                                "BINANCE_SECRET", "OANDA_TOKEN"]}
for n, v in CLAVES.items():
    print(f"{n:<15} {len(v)} caracteres  huella {huella(v)}")
OUT [01]Claves de juguete y su huella
ALPACA_KEY_ID   40 caracteres  huella eed46cb8
ALPACA_SECRET   40 caracteres  huella e9633bee
BINANCE_SECRET  40 caracteres  huella 45d99b76
OANDA_TOKEN     40 caracteres  huella fcdf7418

Dónde vive cada credencial

La lección 0.2.4 enseñó a leer variables de entorno y ficheros .env y a mostrar la configuración sin los secretos. Es el mínimo, y para un ordenador personal suele bastar. En un servidor que opera solo, hay que ir más lejos, porque las variables de entorno tienen dos problemas que su propia documentación reconoce:

  • Se heredan y se exponen. La página de systemd sobre la ejecución de servicios dice que las variables de entorno «no son adecuadas para pasar secretos» a un servicio: se exponen a clientes sin privilegios por D-Bus y se propagan a todo el árbol de procesos. Recomienda sus credenciales (LoadCredential=), que se entregan como ficheros en un directorio cuya ruta está en $CREDENTIALS_DIRECTORY y que no se propagan a los hijos. La guía de secretos de Docker Compose dice lo mismo con otras palabras («las variables de entorno suelen estar disponibles para todos los procesos» y pueden acabar en los logs) y monta cada secreto como un fichero en /run/secrets/<nombre>1Nota 1 · systemd.exec (sección de credenciales, LoadCredential= y $CREDENTIALS_DIRECTORY), System and Service Credentials y Docker, Manage secrets securely in Docker Compose. Consultadas el 22/10/2026..
  • Se pueden leer desde fuera del proceso. En Linux, /proc/<pid>/environ contiene el entorno inicial de un proceso; lo puede leer quien pase la comprobación de acceso de ptrace, que en la práctica es el mismo usuario o root2Nota 2 · Linux man-pages, proc_pid_environ(5). Consultada el 22/10/2026..

La guía de gestión de secretos de OWASP resume la recomendación: las variables de entorno «no se recomiendan salvo que los otros métodos no sean posibles»3Nota 3 · OWASP, Secrets Management Cheat Sheet y Logging Cheat Sheet (sección «Data to exclude»). Consultadas el 22/10/2026.. Una función que lee una credencial con ese orden de preferencia, y que se niega a leer un fichero que pueden leer otros usuarios:

IN [02]python
import os
import stat
import tempfile
from pathlib import Path

class CredencialAusente(LookupError):
    pass

def leer_credencial(nombre):
    """Fichero en $CREDENTIALS_DIRECTORY (systemd) o /run/secrets
    (Docker); si no, variable de entorno; si no, error."""
    for d in filter(None, [os.environ.get("CREDENTIALS_DIRECTORY"),
                           "/run/secrets"]):
        p = Path(d) / nombre
        if p.is_file():
            modo = p.stat().st_mode
            if modo & (stat.S_IRWXG | stat.S_IRWXO):
                raise PermissionError(
                    f"{nombre}: permisos {stat.filemode(modo)}; solo "
                    "debe poder leerlo su dueño (chmod 600)")
            return p.read_text().strip(), "fichero"
    if nombre in os.environ:
        return os.environ[nombre], "entorno"
    raise CredencialAusente(f"falta {nombre}; sin valor por defecto")

credenciales = Path(tempfile.mkdtemp(prefix="cred-"))
os.environ["CREDENTIALS_DIRECTORY"] = str(credenciales)
fichero = credenciales / "ALPACA_SECRET"
fichero.write_text(CLAVES["ALPACA_SECRET"] + "\n")
fichero.chmod(0o644)                       # legible por todos
os.environ["ALPACA_KEY_ID"] = CLAVES["ALPACA_KEY_ID"]

for n in ["ALPACA_KEY_ID", "ALPACA_SECRET", "OANDA_TOKEN"]:
    try:
        v, origen = leer_credencial(n)
        print(f"{n:<14} {origen:<8} huella {huella(v)}")
    except (PermissionError, CredencialAusente) as e:
        print(f"{n:<14} {type(e).__name__}: {e}")
fichero.chmod(0o600)
v, origen = leer_credencial("ALPACA_SECRET")
print("tras chmod 600:", origen, huella(v))
OUT [02]Leer una credencial: fichero de credenciales, entorno o error
ALPACA_KEY_ID  entorno  huella eed46cb8
ALPACA_SECRET  PermissionError: ALPACA_SECRET: permisos -rw-r--r--; solo debe poder leerlo su dueño (chmod 600)
OANDA_TOKEN    CredencialAusente: falta OANDA_TOKEN; sin valor por defecto
tras chmod 600: fichero e9633bee

Tres comportamientos deliberados. La clave de Alpaca llega del entorno porque no hay fichero. El secreto está en un fichero que pueden leer todos (-rw-r--r--) y la función se niega a usarlo hasta que los permisos son de solo dueño: es el mismo control que hace ssh con las claves privadas. Y el token de OANDA no existe: la función lanza un error en lugar de devolver un valor por defecto, porque un secreto por defecto es un secreto en el código.

Con systemd, el servicio declara la credencial y el programa la lee del directorio, sin variables de entorno:

IN [03]ini
# /etc/systemd/system/bot.service (fragmento)
[Service]
User=bot
LoadCredential=ALPACA_SECRET:/etc/bot/credenciales/alpaca_secret
ExecStart=/opt/bot/.venv/bin/python -m bot

Llaveros del sistema y gestores de secretos

Para un ordenador de escritorio, el llavero del sistema operativo guarda la credencial cifrada con la sesión del usuario. La librería keyring (versión 25.7.0, licencia MIT) usa el Llavero de macOS, el almacén de credenciales de Windows y Secret Service o KWallet en Linux; en un servidor sin escritorio necesita levantar un servicio de llavero y desbloquearlo, lo que le quita gran parte de la ventaja4Nota 4 · keyring en PyPI y su README (sección sobre Linux sin escritorio); python-dotenv en PyPI. Consultadas el 22/10/2026.. python-dotenv (1.2.4, BSD-3-Clause) carga un .env en os.environ sin sobrescribir las variables que ya existen, la misma precedencia que el lector propio de la lección 0.2.4. Ninguna de las dos está en el entorno del curso:

IN [04]llavero_y_dotenv.pypython
import keyring                                 # uv add keyring
from dotenv import load_dotenv                 # uv add python-dotenv

keyring.set_password("bot-trading", "binance", "<se pega una vez>")
secreto = keyring.get_password("bot-trading", "binance")

load_dotenv(".env", override=False)            # el entorno manda

En la nube, cada proveedor tiene su gestor de secretos: guarda el valor cifrado, registra quién lo lee, da permisos por identidad de la máquina (sin claves para leer las claves) y puede rotar el secreto. La llamada para leer uno es parecida en todos5Nota 5 · AWS Secrets Manager con Python, Google Cloud Secret Manager: acceder a una versión, Azure Key Vault Secrets para Python y hvac: KV v2. Consultadas el 22/10/2026.:

IN [05]gestores_en_la_nube.pypython
# AWS Secrets Manager (boto3)
import boto3
sm = boto3.client("secretsmanager")
secreto = sm.get_secret_value(SecretId="bot/binance")["SecretString"]

# Google Cloud Secret Manager
from google.cloud import secretmanager
cliente = secretmanager.SecretManagerServiceClient()
nombre = "projects/mi-proyecto/secrets/binance/versions/latest"
secreto = cliente.access_secret_version(
    request={"name": nombre}).payload.data.decode("UTF-8")

# Azure Key Vault
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
kv = SecretClient(vault_url="https://mi-boveda.vault.azure.net/",
                  credential=DefaultAzureCredential())
secreto = kv.get_secret("binance").value

# HashiCorp Vault (hvac), motor KV versión 2
import hvac
secreto = hvac.Client().secrets.kv.v2.read_secret_version(
    path="bot/binance")["data"]["data"]["secret"]
OpciónCifrado en reposoAuditoría de lecturasRotaciónCoste y riesgo propio
Variable de entorno o .envNoNoManualSe hereda y se filtra con facilidad
Fichero de credenciales (systemd, Docker)Opcional (LoadCredentialEncrypted= en systemd)NoManualDepende de los permisos del fichero
Llavero del sistema (keyring)Sí, con la sesión del usuarioNoManualIncómodo en servidores sin escritorio
Gestor en la nube o VaultSíSíSí, programableOtra dependencia: si no responde, el bot no arranca; coste por lectura

La documentación de AWS recomienda guardar en caché el valor leído: un bot que pide el secreto en cada orden multiplica el coste y añade un punto de fallo a cada petición. La caché, a su vez, es la que hace que una rotación tarde en llegar a todas las réplicas, como se verá en la simulación de rotación.

Que no se escape

Un tipo que no se deja imprimir

La mayoría de las fugas son un valor que se imprime sin querer: en un print de depuración, en el repr de un diccionario de configuración, en un f-string de un mensaje de error. Un tipo propio cuyo repr es la huella corta todas esas vías a la vez, y obliga a escribir .revelar() en el único sitio donde hace falta el valor: al firmar.

IN [06]python
import json
import pickle

class Secreto:
    __slots__ = ("nombre", "_valor")

    def __init__(self, nombre, valor):
        self.nombre, self._valor = nombre, valor

    def revelar(self):
        return self._valor

    def __repr__(self):
        return f"<Secreto {self.nombre} {huella(self._valor)}>"

    __str__ = __repr__

    def __reduce__(self):
        raise TypeError("un Secreto no se serializa")

s = Secreto("BINANCE_SECRET", CLAVES["BINANCE_SECRET"])
config = {"exchange": "binance", "secret": s}
print(s)
print(f"firmando con {s}")
print(config)
for nombre, f in [("json", json.dumps), ("pickle", pickle.dumps)]:
    try:
        f(config)
    except TypeError as e:
        print(f"{nombre}: {e}")
OUT [06]Un tipo Secreto
<Secreto BINANCE_SECRET 45d99b76>
firmando con <Secreto BINANCE_SECRET 45d99b76>
{'exchange': 'binance', 'secret': <Secreto BINANCE_SECRET 45d99b76>}
json: Object of type Secreto is not JSON serializable
pickle: un Secreto no se serializa

El secreto aparece como su huella en print, en el f-string y dentro del diccionario. json.dumps no sabe serializarlo y pickle lo tiene prohibido: la configuración no se puede volcar a un fichero o a una caché con el secreto dentro por accidente. No es un cifrado: el valor sigue en la memoria del proceso y revelar() lo devuelve. Protege de los errores, no de alguien que ya está dentro del proceso.

Logs tachados

La guía de logging de OWASP lista lo que no debe registrarse tal cual: identificadores de sesión, tokens de acceso, contraseñas, cadenas de conexión y claves de cifrado, entre otros3. Un sistema de trading registra peticiones HTTP para poder depurarlas, y una petición firmada lleva la clave en una cabecera y la firma en la URL. La firma no es la clave, pero mientras está dentro de la ventana de tiempo sirve para repetir la petición.

Un logging.Filter puede modificar el registro, pero no ve el texto de la traza de una excepción, que se añade al formatear. Por eso el tachado se hace en un Formatter, sobre el texto final: los valores conocidos se cambian por su huella y los patrones (firmas, cabeceras de clave, tokens de portador), por una marca. Para que la salida de la lección no dependa de rutas de ficheros, la traza se reduce a su última línea:

IN [07]python
import logging
import re
import sys

class Tachador(logging.Formatter):
    PATRONES = [
        (re.compile(r"(signature=)[0-9a-fA-F]+"), r"\1[TACHADO]"),
        (re.compile(r"(Bearer )[\w.~+/=-]+"), r"\1[TACHADO]"),
        (re.compile(r"('X-MBX-APIKEY': ')[^']+"), r"\1[TACHADO]"),
    ]

    def __init__(self, fmt, secretos):
        super().__init__(fmt)
        self.secretos = [x.revelar() for x in secretos]

    def formatException(self, ei):
        return "  " + super().formatException(ei).splitlines()[-1]

    def format(self, record):
        texto = super().format(record)
        for v in self.secretos:
            texto = texto.replace(v, f"[TACHADO {huella(v)}]")
        for patron, cambio in self.PATRONES:
            texto = patron.sub(cambio, texto)
        return texto

log = logging.getLogger("bot.http")
log.setLevel(logging.DEBUG)
log.propagate = False
salida = logging.StreamHandler(sys.stdout)
salida.setFormatter(Tachador("%(levelname)s %(message)s", [s]))
log.addHandler(salida)

firma = hashlib.sha256(b"una firma cualquiera").hexdigest()
log.debug("POST /api/v3/order?symbol=XSNUSDT&timestamp=1760449080805"
          "&signature=%s cabeceras %s", firma,
          {"X-MBX-APIKEY": falsa("BINANCE_KEY")})
log.info("websocket: Authorization: Bearer %s", falsa("JWT", 60))
try:
    raise ValueError(f"secreto con formato raro: {s.revelar()}")
except ValueError:
    log.exception("fallo al firmar")
OUT [07]Un formateador que tacha secretos
DEBUG POST /api/v3/order?symbol=XSNUSDT&timestamp=1760449080805&signature=[TACHADO] cabeceras {'X-MBX-APIKEY': '[TACHADO]'}
INFO websocket: Authorization: Bearer [TACHADO]
ERROR fallo al firmar
  ValueError: secreto con formato raro: [TACHADO 45d99b76]

La URL conserva todo lo útil para depurar (ruta, símbolo, hora) y pierde la firma; la cabecera, la clave; el token, su valor. La última línea es el caso que se escapa a casi todos: un mensaje de excepción construido con el valor del secreto (aquí a propósito; en la vida real, una librería que incluye la petición en su error). El formateador lo sustituye por la huella. Los patrones son una red para lo que no se conoce de antemano; lo que sí se conoce (los valores cargados al arrancar) se tacha por valor, que no depende de acertar con una expresión regular.

Lo que hereda un proceso hijo

IN [08]python
import subprocess

os.environ["PPT_FALSA_SECRET"] = falsa("PPT_FALSA_SECRET")
hijo = [sys.executable, "-c",
        "import os; print('PPT_FALSA_SECRET' in os.environ)"]
r = subprocess.run(hijo, capture_output=True, text=True)
print("hijo con el entorno heredado:", r.stdout.strip())
limpio = {k: v for k, v in os.environ.items()
          if not k.startswith("PPT_") and "SECRET" not in k
          and k not in ("ALPACA_KEY_ID", "CREDENTIALS_DIRECTORY")}
r = subprocess.run(hijo, capture_output=True, text=True, env=limpio)
print("hijo con entorno filtrado:   ", r.stdout.strip())
OUT [08]El entorno del padre llega al hijo
hijo con el entorno heredado: True
hijo con entorno filtrado:    False

El hijo ve la variable con el secreto; con env= filtrado, no. Un bot lanza hijos más a menudo de lo que parece: un informe en otro proceso, un git para registrar la versión, una herramienta de compresión de logs. Cada uno recibe las claves si viven en el entorno, y cualquiera de ellos puede escribirlas en un volcado o un mensaje de error. Con ficheros de credenciales el problema no existe: el hijo solo lee lo que se le pasa.

Detectar fugas antes del commit

Si una clave llega al repositorio, borrarla en el commit siguiente no sirve: sigue en el historial, en los clones y en las copias que hicieran otros. Hay que detectarla antes del commit. Las herramientas reales combinan patrones de proveedores conocidos con medidas de lo aleatorio que parece un texto; la versión mínima de la segunda es la entropía de Shannon por carácter, H=−∑ipilog⁡2piH = -\sum_i p_i \log_2 p_i, con pip_i la frecuencia de cada carácter. Un texto en hexadecimal aleatorio se acerca a 4 bits por carácter; una palabra o un identificador legible queda por debajo de 3,5.

La celda crea un repositorio de juguete con las fugas típicas (una clave escrita en config.py, una cabecera con un token guardada en la salida de un notebook, un .env que sí está en .gitignore) y un fichero de tests con un identificador de orden de Alpaca:

IN [09]python
repo = Path(tempfile.mkdtemp(prefix="repo-"))
(repo / ".gitignore").write_text(".env\n__pycache__/\n")
(repo / ".env").write_text(
    f"BINANCE_SECRET={CLAVES['BINANCE_SECRET']}\n")
(repo / "config.py").write_text(
    'SIMBOLO = "XSNUSDT"\n'
    'ESTRATEGIA = "momentum_veinte_sesiones"\n'
    f'API_SECRET = "{falsa("API_SECRET")}"   # solo para probar\n')
(repo / "analisis.ipynb").write_text(json.dumps({"cells": [{
    "outputs": [{"text": [
        f"Authorization: Bearer {falsa('OANDA_TOKEN', 48)}\n"]}]}]}))
(repo / "test_ordenes.py").write_text(
    'RELLENO = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"\n'
    'ID_ORDEN = "6f1c2a9e-4b7d-4c1a-9e55-000000039595"\n')
(repo / "README.md").write_text(
    "Estrategia de rebalanceo_diario_al_cierre con ETF.\n")
print(sorted(p.name for p in repo.iterdir()))
OUT [09]Un repositorio con fugas
['.env', '.gitignore', 'README.md', 'analisis.ipynb', 'config.py', 'test_ordenes.py']
IN [10]python
import collections
import fnmatch
import math

TOKEN = re.compile(r"[A-Za-z0-9_+/=-]{20,}")

def entropia(texto):
    n = len(texto)
    return -sum(c / n * math.log2(c / n)
                for c in collections.Counter(texto).values())

def escanear(repo, umbral=3.5, permitidos=()):
    ignorados = (repo / ".gitignore").read_text().split()
    hallazgos, todos = [], []
    for p in sorted(repo.rglob("*")):
        if not p.is_file() or p.name == ".gitignore" or any(
                fnmatch.fnmatch(p.name, g.rstrip("/"))
                for g in ignorados):
            continue
        for i, linea in enumerate(p.read_text().splitlines(), 1):
            for t in TOKEN.findall(linea):
                h = entropia(t)
                todos.append((p.name, len(t), round(h, 2)))
                if h >= umbral and not any(
                        re.fullmatch(x, t) for x in permitidos):
                    hallazgos.append((f"{p.name}:{i}", len(t),
                                      round(h, 2), huella(t)))
    return hallazgos, todos

hallazgos, todos = escanear(repo)
for h in hallazgos:
    print("posible secreto en {} ({} caracteres, H = {}) huella {}"
          .format(*h))
print("textos largos revisados:", len(todos))
OUT [10]Un escáner por entropía
posible secreto en analisis.ipynb:1 (48 caracteres, H = 4.17) huella 3ed2901d
posible secreto en config.py:3 (40 caracteres, H = 4.01) huella bc1df40e
posible secreto en test_ordenes.py:2 (36 caracteres, H = 3.68) huella 0d85cfe0
textos largos revisados: 6

El escáner encuentra la clave de config.py y el token del notebook, que es una fuga especialmente traicionera: el código del notebook no tiene ninguna clave, pero su salida guardada sí. No mira el .env, porque está en .gitignore y no se va a subir. Marca además el identificador de orden de Alpaca, un UUID: tiene entropía alta y no es un secreto. Ese falso positivo se excluye con un patrón permitido (ejercicio 2), no subiendo el umbral para todo. El relleno de equis no se marca: entropía cero.

Fig. — Entropía por carácter de los textos largos del repositorio de juguete y umbral del escáner

Para un repositorio real hay herramientas mantenidas que hacen esto y más: Gitleaks (licencia MIT; versión 8.30.1) funciona como gancho previo al commit o como acción de GitHub y trae reglas para muchos proveedores, entre ellas tokens de Coinbase y de Kraken pero no de Binance, Bybit ni Alpaca; detect-secrets (Apache-2.0) trabaja con una línea base de hallazgos aceptados6Nota 6 · Gitleaks, detect-secrets, GitHub, patrones de secret scanning admitidos y push protection. Consultadas el 22/10/2026.. GitHub bloquea por defecto los envíos de secretos que reconoce a repositorios públicos (push protection), pero su lista de patrones no incluye claves de Binance, Bybit, Kraken, Coinbase, Alpaca ni OANDA: para esas claves, solo lo genérico. El escáner propio de esta lección, o una regla propia en Gitleaks, cubre el hueco.

IN [11]yaml
# .pre-commit-config.yaml: Gitleaks antes de cada commit
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.30.1
    hooks:
      - id: gitleaks

Permisos mínimos

Cada bróker o exchange ofrece controles distintos sobre lo que puede hacer una credencial7Nota 7 · Binance, How to Create API Keys on Binance y permisos de la clave; Bybit, crear una clave de subcuenta e información de la clave; Kraken, API keys y permisos; Coinbase, autenticación y permisos por endpoint; Alpaca, Authentication y buenas prácticas de seguridad de claves; Interactive Brokers, configuración inicial («Read Only» activado por defecto), configuración de la API y conexiones remotas; OANDA, Authentication. Consultadas el 22/10/2026.:

Bróker o exchangePermisos de la claveRestricción por IPOtros
BinanceLectura, operar en spot y margen, retirada (y otros)Sí; obligatoria para activar la retiradaSegún su FAQ, una clave HMAC sin restricción de IP solo puede tener permiso de lectura
BybitSolo lectura o lectura y escritura, por producto; retirada solo en la cuenta principalSíUna clave sin IP asociada deja de valer a los 90 días
KrakenConsultar fondos, órdenes y operaciones; crear, modificar y cancelar órdenes; depósito; retirada; websocketSíCaducidad de la clave y ventana de nonce configurables
Coinbase (CDP)view, trade y transferLista de IP recomendada, no obligatoriaPortafolio al que se limita la clave
AlpacaSin permisos por clave; claves separadas de papel y realNo documentadaLas claves de la Trading API no pueden ordenar transferencias bancarias
Interactive BrokersModo de solo lectura de la API (activado por defecto)IP de confianza en TWS o IB GatewayOpción de aceptar solo conexiones locales
OANDAToken sin permisos por funciónNo documentadaRevocar y generar desde la gestión de acceso

La guía de claves de Kraken lo resume con dos consejos: una clave por propósito y la lista de IP, que «evita casi todo el mal uso de una clave aunque se filtre»; para operar en spot de forma algorítmica bastan consultar fondos, consultar órdenes, crear y cancelar órdenes, y una clave que solo envía órdenes no necesita la retirada.

Aplicado a un sistema con tres procesos (un recolector de datos y saldos, el motor de órdenes y un informe diario), el diseño son tres claves, ninguna con retirada y todas atadas a la IP del servidor. La retirada, si hace falta, la hace una persona desde la web, con su segundo factor. El simulador de la celda siguiente aplica las dos reglas que comparten todos los exchanges que las tienen (la IP se comprueba antes que el permiso) y prueba qué consigue cada clave desde el servidor y desde la máquina de un atacante. Las IP son de los rangos reservados para documentación (RFC 5737):

IN [12]python
import pandas as pd

IP_SERVIDOR, IP_ATACANTE = "198.51.100.7", "203.0.113.50"
NECESITA = {"saldo": "lectura", "orden": "operar",
            "retirada": "retirar"}

class ExchangeSim:
    def __init__(self):
        self.claves = {}

    def alta(self, kid, permisos, ips=None):
        self.claves[kid] = {"permisos": set(permisos), "ips": ips,
                            "activa": True}

    def atender(self, kid, ip, accion):
        c = self.claves.get(kid)
        if c is None or not c["activa"]:
            return "401 clave"
        if c["ips"] is not None and ip not in c["ips"]:
            return "401 IP"
        if NECESITA[accion] not in c["permisos"]:
            return "403"
        return "200"

ex = ExchangeSim()
ex.alta("datos", {"lectura"}, ips={IP_SERVIDOR})
ex.alta("motor", {"lectura", "operar"}, ips={IP_SERVIDOR})
ex.alta("motor sin IP", {"lectura", "operar"})
ex.alta("tutorial", {"lectura", "operar", "retirar"})
filas = {}
for kid in ex.claves:
    for origen, ip in [("servidor", IP_SERVIDOR),
                       ("atacante", IP_ATACANTE)]:
        filas[(kid, origen)] = {a: ex.atender(kid, ip, a)
                                for a in NECESITA}
pd.DataFrame(filas).T
OUT [12]Un exchange con permisos y lista de IP
                        saldo   orden retirada
datos        servidor     200     403      403
             atacante  401 IP  401 IP   401 IP
motor        servidor     200     200      403
             atacante  401 IP  401 IP   401 IP
motor sin IP servidor     200     200      403
             atacante     200     200      403
tutorial     servidor     200     200      200
             atacante     200     200      200

Desde el servidor, cada clave hace lo que debe y nada más: la de datos no puede operar (403) y ninguna de las del sistema puede retirar. Desde la máquina del atacante, las claves con lista de IP no sirven para nada (401 por IP). La clave del motor sin IP es el caso intermedio: el atacante no puede llevarse los fondos, pero puede operar con ellos, y operar es suficiente para hacer daño (comprar un activo ilíquido a precios absurdos contra órdenes propias puestas en otra cuenta traslada el saldo sin retirarlo). La clave «de tutorial», con todos los permisos y sin IP, es la cuenta entera: es la que se crea en cinco minutos para probar un ejemplo y se olvida en un fichero. La figura de la cabecera resume la columna del atacante.

El coste de estos controles hay que tenerlo en cuenta al diseñar. La lista de IP corta el sistema en seco si cambia la IP de salida del servidor (un proveedor que reasigna direcciones, una IP dinámica, un cambio de red): todas las peticiones dan 401 hasta que alguien actualiza la lista. Un servidor con IP fija y una alarma por 401 lo resuelven. Separar claves por proceso multiplica las que hay que guardar y rotar.

Rotar sin cortar el sistema

La guía de OWASP pide rotar los secretos con regularidad, para que una credencial robada sirva poco tiempo, y automatizarlo, porque a mano es fácil equivocarse3. El error típico es revocar la clave antigua en el mismo momento en que se crea la nueva: el sistema rara vez lee la clave en el mismo instante en todas partes. La simulación tiene tres réplicas del bot que leen la clave del gestor con distinta caché (0, 2 y 5 minutos) y hacen una petición por minuto. A los 20 minutos se crea y publica la clave nueva; en un plan, la antigua se revoca en ese mismo minuto; en el otro, a los 30, cuando el exchange confirma que nadie la usa desde hace cinco minutos.

clave nuevaréplicas la leenuso de la antiguasin usoCrearPublicarComprobarRevocar
Fuente Mermaid
stateDiagram-v2
  [*] --> Crear
  Crear --> Publicar: clave nueva
  Publicar --> Comprobar: réplicas la leen
  Comprobar --> Comprobar: uso de la antigua
  Comprobar --> Revocar: sin uso
  Revocar --> [*]
IN [13]python
def rotar(plan, minutos=40, cache=(0, 2, 5)):
    ex = ExchangeSim()
    ex.alta("k1", {"lectura", "operar"})
    publicadas = [("k1", -60)]            # (clave, minuto publicada)
    uso, registro = {}, []
    for m in range(minutos):
        for (t, accion, kid) in plan:
            if t == m and accion == "crear":
                ex.alta(kid, {"lectura", "operar"})
            elif t == m and accion == "publicar":
                publicadas.append((kid, m))
            elif t == m and accion == "revocar":
                ex.claves[kid]["activa"] = False
        for r, retraso in enumerate(cache):
            kid = [k for k, t in publicadas if t <= m - retraso][-1]
            res = ex.atender(kid, IP_SERVIDOR, "orden")
            uso[kid] = m
            registro.append((m, r, kid, res))
    return registro, uso

a_la_vez = [(20, "crear", "k2"), (20, "publicar", "k2"),
            (20, "revocar", "k1")]
con_solape = [(20, "crear", "k2"), (20, "publicar", "k2"),
              (30, "revocar", "k1")]
for nombre, plan in [("a la vez", a_la_vez),
                     ("con solape", con_solape)]:
    registro, uso = rotar(plan)
    fallos = [(m, r) for m, r, k, res in registro if res != "200"]
    print(f"{nombre:<11} peticiones fallidas: {len(fallos)}"
          f"  último uso de k1: minuto {uso['k1']}")
    if fallos:
        print("  (minuto, réplica):", fallos)
OUT [13]Rotación: revocar a la vez o con solape
a la vez    peticiones fallidas: 7  último uso de k1: minuto 24
  (minuto, réplica): [(20, 1), (20, 2), (21, 1), (21, 2), (22, 2), (23, 2), (24, 2)]
con solape  peticiones fallidas: 0  último uso de k1: minuto 24

Revocando a la vez fallan 7 peticiones: las de la réplica con 2 minutos de caché en los minutos 20 y 21 y las de la de 5 minutos del 20 al 24. Con solape no falla ninguna, y el dato que permite revocar con seguridad es el último uso de la clave antigua (minuto 24): muchos exchanges muestran en su panel cuándo se usó por última vez cada clave, y el procedimiento consiste en mirarlo antes de revocar. En un bot que opera, 7 peticiones fallidas pueden ser órdenes que no se envían o cancelaciones que no llegan: el coste de rotar mal no es un error en un log, es una posición sin controlar durante cinco minutos.

Fig. — Rotación revocando la clave antigua en el mismo minuto en que se publica la nueva: petición de cada réplica por minuto

Un inventario con fechas

Rotar con regularidad exige saber qué claves hay, dónde se usan y cuándo se crearon. Algunas caducan solas: Bybit invalida a los 90 días las claves sin IP asociada, y Kraken permite crear claves con fecha de caducidad. Un inventario mínimo, con una política de 90 días y avisos a 15:

IN [14]python
from datetime import date

HOY = date(2026, 10, 22)
inventario = pd.DataFrame([
    ("binance", "motor", date(2026, 9, 1), True, "lectura,operar"),
    ("binance", "datos", date(2026, 3, 2), True, "lectura"),
    ("bybit", "motor", date(2026, 7, 30), False, "lectura,operar"),
    ("kraken", "informe", date(2026, 8, 10), True, "lectura"),
    ("kraken", "pruebas", date(2025, 11, 5), False,
     "lectura,operar,retirar"),
], columns=["exchange", "uso", "creada", "con_ip", "permisos"])

def revisar(fila, politica=90, aviso=15):
    edad = (HOY - fila.creada).days
    notas = []
    if "retirar" in fila.permisos:
        notas.append("quitar retirada")
    if not fila.con_ip:
        notas.append("sin IP")
        if fila.exchange == "bybit" and edad >= 90 - aviso:
            notas.append(f"Bybit la invalida en {90 - edad} días")
    if edad >= politica:
        notas.append("rotar ya")
    elif edad >= politica - aviso:
        notas.append(f"rotar en {politica - edad} días")
    return "; ".join(notas) or "en regla"

for f in inventario.itertuples():
    print(f"{f.exchange:<8} {f.uso:<8} {(HOY - f.creada).days:>4} días"
          f"  {revisar(f)}")
OUT [14]Inventario de claves: qué toca rotar
binance  motor      51 días  en regla
binance  datos     234 días  rotar ya
bybit    motor      84 días  sin IP; Bybit la invalida en 6 días; rotar en 6 días
kraken   informe    73 días  en regla
kraken   pruebas   351 días  quitar retirada; sin IP; rotar ya

El inventario no guarda ninguna clave: guarda dónde está cada una, para qué sirve y cuándo se creó. La clave de datos de Binance pasa de la política (234 días) aunque esté bien restringida; la del motor de Bybit, sin IP, caduca en 6 días por la regla del propio exchange, y el sistema se pararía si nadie la rota antes. La clave de pruebas de Kraken tiene de todo lo que no debe: casi un año, retirada y ninguna IP. Es el caso más común en la práctica: no la del sistema en producción, sino la que se creó para un experimento y nadie borró.

Si una clave se filtra

El orden importa, y el primer paso no admite espera:

Revocar la claveRevisar actividadClave nuevaBuscar la víaCerrar la vía
Fuente Mermaid
flowchart TB
  A[Revocar la clave] --> B[Revisar actividad]
  B --> C[Clave nueva]
  C --> D[Buscar la vía]
  D --> E[Cerrar la vía]
  1. Revocar en el bróker o exchange. No borrar el commit, no cambiar la variable de entorno: revocar. Mientras la clave sea válida, cualquiera que la tenga puede usarla, y una clave en un repositorio público puede copiarse en segundos.
  2. Revisar la actividad desde la última vez que se sabe que la clave estaba a salvo: órdenes, operaciones, retiradas, cambios de configuración, inicios de sesión. El registro propio del sistema (lección 4.5.5) y el del exchange tienen que cuadrar.
  3. Crear una clave nueva con los permisos mínimos y desplegarla. Si otras credenciales estaban en el mismo sitio (el mismo .env, el mismo servidor), se rotan todas.
  4. Encontrar la vía por la que salió: el commit, el log, la captura, la dependencia.
  5. Cerrarla: el escáner antes del commit, el tachado en los logs, el fichero de credenciales en lugar del entorno.

Ejercicios

  1. Amplía Tachador para que tache también la cabecera API-Sign de Kraken (base64) y cualquier JWT (tres partes en base64url separadas por puntos, la primera empieza por eyJ), aunque no vaya precedido de Bearer.
Solución
IN [15]python
class TachadorPlus(Tachador):
    PATRONES = Tachador.PATRONES + [
        (re.compile(r"('API-Sign': ')[A-Za-z0-9+/=]+"), r"\1[TACHADO]"),
        (re.compile(r"eyJ[\w-]+\.[\w-]+\.[\w-]+"), "[JWT TACHADO]"),
    ]

log2 = logging.getLogger("bot.http2")
log2.setLevel(logging.DEBUG)
log2.propagate = False
h2 = logging.StreamHandler(sys.stdout)
h2.setFormatter(TachadorPlus("%(levelname)s %(message)s", [s]))
log2.addHandler(h2)
import base64

cab = {"API-Key": "FALSA_kraken",
       "API-Sign": base64.b64encode(b"FALSA" * 13).decode()}
jwt = ".".join(["eyJhbGciOiJFUzI1NiJ9", "eyJzdWIiOiJmYWxzYSJ9",
                "ZmFsc2EtZmlybWE"])
log2.debug("POST /0/private/AddOrder cabeceras %s", cab)
log2.debug("suscripción user con jwt=%s", jwt)
OUT [15]Tachar firmas de Kraken y JWT sueltos
DEBUG POST /0/private/AddOrder cabeceras {'API-Key': 'FALSA_kraken', 'API-Sign': '[TACHADO]'}
DEBUG suscripción user con jwt=[JWT TACHADO]

La firma de Kraken queda tachada por el patrón de su cabecera y el JWT por su forma, que es reconocible: la cabecera de un JWT es un JSON que empieza por {", y {" en base64url es siempre eyJ. Es un patrón con pocos falsos positivos, a diferencia de la entropía, y por eso las herramientas de escaneo lo usan.

  1. Añade al escáner una lista de patrones permitidos para que no marque UUID y una regla que marque cualquier línea con la cabecera de una clave privada PEM (-----BEGIN … PRIVATE KEY-----), tenga la entropía que tenga.
Solución
IN [16]python
UUID = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
PEM = re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")
(repo / "clave_ed25519.pem").write_text(
    "-----BEGIN PRIVATE KEY-----\nFALSA\n-----END PRIVATE KEY-----\n")

def escanear_v2(repo):
    hallazgos, _ = escanear(repo, permitidos=[UUID])
    for p in sorted(repo.glob("*.pem")) + sorted(repo.glob("*.py")):
        for i, linea in enumerate(p.read_text().splitlines(), 1):
            if PEM.search(linea):
                hallazgos.append((f"{p.name}:{i}", "clave privada PEM"))
    return hallazgos

for h in escanear_v2(repo):
    print(h[0], "->", h[1] if len(h) == 2 else f"H = {h[2]}")
OUT [16]Escáner con patrones permitidos y claves PEM
analisis.ipynb:1 -> H = 4.17
config.py:3 -> H = 4.01
clave_ed25519.pem:1 -> clave privada PEM

El UUID ya no aparece y la clave PEM se detecta por su cabecera, aunque su contenido (aquí, la palabra FALSA) no tenga entropía alta. Las claves privadas de Ed25519, RSA y ECDSA que piden Binance y Coinbase (lección 4.4.4) se guardan en ficheros PEM: son el secreto más sensible del sistema y deben estar fuera del repositorio y con permisos de solo dueño, igual que el fichero de credenciales de la primera sección.

  1. Diseña las credenciales del proyecto del nivel: una cartera multi-estrategia en paper trading con Alpaca o IBKR durante 4 a 8 semanas, en un servidor propio. Qué credenciales hay, dónde se guardan, qué permisos tienen y cómo se pasa a real sin reutilizar nada de papel.
Solución

En Alpaca, dos pares de claves distintos por diseño (papel y real), y la documentación dice que no se pueden cruzar. Durante el proyecto solo existe el de papel, en un fichero de credenciales del servicio (LoadCredential= de systemd o /run/secrets de Docker) con permisos 600, leído con una función como leer_credencial y envuelto en Secreto desde el arranque. El modo (papel o real) lo decide una variable no secreta del entorno, comprobada contra la URL como en la lección 4.4.3. Alpaca no tiene permisos por clave ni lista de IP documentados, así que la protección está en el servidor: un usuario propio para el bot, el fichero de credenciales, el escáner antes de cada commit y los logs tachados. En IBKR la credencial es el usuario de TWS o IB Gateway, que vive en ese programa y no en el bot; el bot se conecta por el puerto local, y se configuran el modo de solo lectura en el proceso de datos, las IP de confianza y solo conexiones locales. Para pasar a real: claves nuevas creadas ese día (nunca las de papel), un inventario con su fecha, una alarma por 401 y una rotación con solape planificada a los 90 días. Ninguna credencial del sistema tiene permiso de retirada.

Footnotes

  1. systemd.exec (sección de credenciales, LoadCredential= y $CREDENTIALS_DIRECTORY), System and Service Credentials y Docker, Manage secrets securely in Docker Compose. Consultadas el 22/10/2026. ↩

  2. Linux man-pages, proc_pid_environ(5). Consultada el 22/10/2026. ↩

  3. OWASP, Secrets Management Cheat Sheet y Logging Cheat Sheet (sección «Data to exclude»). Consultadas el 22/10/2026. ↩ ↩2 ↩3

  4. keyring en PyPI y su README (sección sobre Linux sin escritorio); python-dotenv en PyPI. Consultadas el 22/10/2026. ↩

  5. AWS Secrets Manager con Python, Google Cloud Secret Manager: acceder a una versión, Azure Key Vault Secrets para Python y hvac: KV v2. Consultadas el 22/10/2026. ↩

  6. Gitleaks, detect-secrets, GitHub, patrones de secret scanning admitidos y push protection. Consultadas el 22/10/2026. ↩

  7. Binance, How to Create API Keys on Binance y permisos de la clave; Bybit, crear una clave de subcuenta e información de la clave; Kraken, API keys y permisos; Coinbase, autenticación y permisos por endpoint; Alpaca, Authentication y buenas prácticas de seguridad de claves; Interactive Brokers, configuración inicial («Read Only» activado por defecto), configuración de la API y conexiones remotas; OANDA, Authentication. Consultadas el 22/10/2026. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. Un bot lee su clave de una variable de entorno y lanza un proceso hijo para generar un informe. ¿Qué ve el hijo?

    • ANada: el entorno es privado de cada proceso
    • BSolo las variables que empiezan por PATH
    • CTodo el entorno del padre, clave incluida, salvo que se le pase un entorno filtrado con env=
    • DSolo si el hijo es un programa de Python
    Ver respuesta

    Respuesta C. Todo el entorno del padre, clave incluida, salvo que se le pase un entorno filtrado con env=

    Un proceso hijo hereda el entorno del padre. En la lección, el hijo ve la variable con la clave; con un env filtrado, no. Por eso systemd y Docker recomiendan ficheros de credenciales en lugar de variables para los secretos.

  2. ¿Qué combinación de permisos es la adecuada para el motor de órdenes de un sistema en un exchange?

    • ALectura y operar, sin retirada, restringida a la IP del servidor
    • BTodos los permisos, para no tener problemas
    • CSolo retirada
    • DLectura y retirada, sin operar
    Ver respuesta

    Respuesta A. Lectura y operar, sin retirada, restringida a la IP del servidor

    El motor necesita ver saldos y órdenes y enviar órdenes; nunca retirar fondos. Restringida a la IP del servidor, una clave filtrada no sirve desde otra máquina. En el simulador de la lección, esa clave filtrada no consigue nada desde la IP del atacante.

  3. En la simulación de rotación, revocar la clave antigua en el mismo minuto en que se publica la nueva da 7 peticiones fallidas. ¿Por qué?

    • APorque la clave nueva está mal generada
    • BPorque dos réplicas del sistema tardan 2 y 5 minutos en leer la clave nueva y siguen usando la revocada
    • CPorque el exchange limita las peticiones
    • DPor azar
    Ver respuesta

    Respuesta B. Porque dos réplicas del sistema tardan 2 y 5 minutos en leer la clave nueva y siguen usando la revocada

    Las réplicas guardan la credencial en caché un tiempo. Con solape (las dos claves válidas hasta comprobar que nadie usa la antigua) no falla ninguna petición.

  4. Una clave se ha subido por error a un repositorio público. ¿Qué es lo primero?

    • ABorrar el commit y forzar el push
    • BCambiar el nombre de la variable de entorno
    • CEsperar a ver si alguien la usa
    • DRevocar la clave en el bróker o exchange; después, revisar la actividad, crear otra y limpiar el repositorio
    Ver respuesta

    Respuesta D. Revocar la clave en el bróker o exchange; después, revisar la actividad, crear otra y limpiar el repositorio

    Borrar el commit no deshace la exposición: la clave pudo copiarse en segundos y sigue en clones y cachés. La única medida que cierra la puerta es revocarla en el emisor.

  5. ¿Qué hace el escáner por entropía de la lección con el identificador de orden 6f1c2a9e-… de Alpaca?

    • ALo marca como posible secreto: un UUID tiene entropía alta, y hay que excluirlo con una lista de patrones permitidos
    • BLo ignora porque es corto
    • CLo borra del fichero
    • DLo cifra
    Ver respuesta

    Respuesta A. Lo marca como posible secreto: un UUID tiene entropía alta, y hay que excluirlo con una lista de patrones permitidos

    La entropía mide lo aleatorio que parece un texto, no si es secreto. Los escáneres reales combinan patrones conocidos, entropía y listas de excepciones; un falso positivo se excluye de forma explícita, no bajando el umbral para todo.

Ideas clave

  • Un secreto no tiene valor por defecto: si falta, el programa no arranca. Se lee de un fichero de credenciales (systemd, Docker) o del entorno, con permisos de solo dueño, y nunca se escribe en el código ni en el repositorio.
  • Las fugas más comunes no son ataques: un print, un log de una URL firmada, una excepción, un proceso hijo que hereda el entorno, una celda de notebook guardada con su salida. Un tipo Secreto que no se imprime y un formateador que tacha cortan casi todas.
  • Permisos mínimos por proceso: lectura para datos e informes, operar sin retirada para el motor, restringidas a la IP del servidor. La retirada no la necesita ningún programa del sistema.
  • Rotar es crear la nueva, desplegarla, comprobar que nadie usa la antigua y solo entonces revocarla; si se filtra, se revoca primero y se investiga después.
  • Cada control tiene un coste: la lista de IP corta el sistema si cambia la IP del servidor; la rotación, si se hace sin solape; el gestor de secretos es otra dependencia que puede fallar.

Recursos

Datos