Ir al contenido
OneKitly

Generar correctamente una clave secreta de aplicación

Publicado el 12/8/2026 · 14 min de lectura · Herramientas de texto e idioma

Daniel Okonkwo

Daniel OkonkwoDesarrollador front-end y redactor de Tecnología en OneKitly

Rendimiento web · Formatos de archivo

Verificado con 4 fuentes

Ver perfil
En resumen

Tres preguntas lo zanjan. Una: ¿qué fuente aleatoria llama? Un generador sólido llama a la aleatoriedad criptográfica de la plataforma —crypto.getRandomValues en un navegador, secrets en Python, random_bytes en PHP—. Uno dudoso llama a Math.random, documentado como inadecuado para seguridad: V8 lo implementa con xorshift128+, y un puñado de salidas consecutivas basta para reconstruir el estado interno y predecir el resto. Esta herramienta pasa: su randStr llama a rngInt, que rellena un Uint32Array con crypto.getRandomValues; la rama Math.random solo se ejecuta si falta crypto, cosa que ningún navegador hace. Dos: ¿cuánta entropía lleva la salida? Multiplica la longitud por el logaritmo en base dos del tamaño del alfabeto. El conjunto Django es aquí exactamente los cincuenta caracteres que usa el get_random_secret_key de Django —minúsculas, cifras y !@#$%^&*(-_=+)—, así que una clave de 50 caracteres lleva 50 × log2(50) = 282 bits, frente a los aproximadamente 128 bits que suelen tomarse como suelo. La opción alfanumérica da 297 bits. La parte de WordPress genera las ocho constantes de wp-config con 64 caracteres de un conjunto de 92, que es byte a byte wp_generate_password con sus dos indicadores de caracteres especiales: 418 bits cada una. Tres: ¿a dónde va la clave después? Ninguna de las dos respuestas sirve una vez pegada en una conversación, un ticket o un commit: un secreto compartido es un secreto gastado, y el único arreglo es generar otro y rotarlo.

Cómo distinguir un generador de claves secretas sólido de uno dudoso, con este como ejemplo trabajado: qué fuente aleatoria llama, cómo calcular la entropía por tu cuenta y qué se rompe de verdad el día que rotas la clave.

Primera pregunta: qué fuente aleatoria

Cada lenguaje tiene dos generadores de números aleatorios y no son intercambiables. Uno es rápido, determinista a partir de una semilla y pensado para barajar una lista de reproducción o desfasar un reintento. El otro es más lento, bebe de la reserva de entropía del sistema operativo y sirve para todo lo que un atacante querría adivinar. En JavaScript son Math.random y crypto.getRandomValues; en Python, random y secrets; en PHP, mt_rand y random_bytes. Usar el primero donde toca el segundo es la manera más común de equivocar un generador de claves.

Lo que descalifica a Math.random no es que su salida parezca regular —no lo parece—. Es que el generador es una pequeña máquina determinista cuyo estado se puede recuperar. V8, el motor de Chrome y de Node, usa xorshift128+, un estado de 128 bits avanzado con desplazamientos y o-exclusivos. A partir de una racha corta de salidas consecutivas, ese estado se reconstruye, y en cuanto se reconstruye se conocen todos los valores futuros y se pueden reproducir todos los pasados. El motor tampoco tiene obligación de sembrarlo de forma impredecible, y los navegadores lo han sembrado históricamente a partir de cosas que un atacante puede observar o influir.

Esta herramienta llama al correcto. Leyendo el código desde el botón hacia dentro: el manejador de generación construye una cadena carácter a carácter con randStr, randStr elige cada índice con rngInt, y rngInt reserva un Uint32Array de un elemento y lo rellena con crypto.getRandomValues. Existe un repliegue a Math.random, pero está detrás de una comprobación de la existencia de crypto y de crypto.getRandomValues, y getRandomValues está disponible en todo contexto de navegación, incluidos los no seguros, a diferencia de crypto.subtle. En la práctica, ese repliegue es código inalcanzable.

Segunda pregunta: cuántos bits, y cómo contarlos por tu cuenta

La aritmética cabe en una línea. Si cada carácter se extrae de forma independiente y uniforme de un alfabeto de n caracteres, cada carácter lleva log2(n) bits, y una clave de longitud L lleva L × log2(n). Eso es todo. No hay corrección por «lleva símbolos» ni por «parece complicada»: el tamaño del alfabeto y la longitud son las únicas entradas, y por eso una clave larga en minúsculas gana a una corta llena de puntuación.

Aplícala al ajuste por defecto. El conjunto Django lo forman las veintiséis minúsculas, las diez cifras y los catorce caracteres !@#$%^&*(-_=+): cincuenta en total, sin duplicados, cosa que importa porque un carácter repetido encogería en silencio el alfabeto efectivo. log2(50) vale 5,644 bits por carácter, así que la clave de 50 caracteres por defecto lleva 282,2 bits. Pasar a la opción alfanumérica sube el alfabeto a sesenta y dos y la clave a 297,7 bits; la opción completa lo sube a setenta y seis y 312,4 bits. Las tres son enormes al lado de los 128 bits que se citan como suficientes: veintitrés caracteres del conjunto Django ya los superarían.

La parte de WordPress se mide igual. Emite las ocho constantes de wp-config —las cuatro claves y las cuatro sales— de 64 caracteres cada una, sacadas de un conjunto de 92: las sesenta y dos letras y cifras, después !@#$%^&*() y por último -_ []{}<>~`+=,.;:/?|. Es carácter por carácter lo que produce el propio wp_generate_password de WordPress con sus dos argumentos de caracteres especiales, y 64 × log2(92) da 417,5 bits por constante. Dos detalles de ese conjunto merecen atención: contiene un espacio, cosa legal y algo sorprendente, y no contiene ni comilla simple ni barra invertida, y por eso el valor generado cae en define('AUTH_KEY', '…'); sin escape alguno.

El sesgo del módulo, dicho con honestidad

Hay un defecto en la implementación y conviene enunciarlo con precisión, porque una formulación vaga resultaría más alarmante que el defecto. Para convertir un número aleatorio de 32 bits en un índice dentro de un alfabeto de 50 caracteres, el código toma el resto: índice igual a valor módulo 50. Dos elevado a treinta y dos es 4 294 967 296, y eso no se divide entre cincuenta. El resto es cuarenta y seis, así que cuarenta y seis de los cincuenta caracteres pueden salir de 85 899 346 de los valores posibles y los otros cuatro de 85 899 345.

Los caracteres favorecidos son, pues, más probables que los demás en una proporción de una parte entre 85 899 345, unos 1,2 por cien millones. Para el conjunto WordPress de 92 caracteres el resto es doce y la proporción unos 2,1 por cien millones. Un generador que corrigiera esto usaría muestreo por rechazo —volver a sacar cada vez que el valor cae en la cola irregular— y ganaría una diferencia que ninguna medición sobre una clave de 50 caracteres podría detectar. Es una nota al pie, no una advertencia.

El sesgo desaparece por completo cuando el tamaño del alfabeto es una potencia de dos, porque entonces la división es exacta: un alfabeto de 64 caracteres o de 256 valores no tiene ninguno. Esa es la razón elegante por la que buena parte del material criptográfico se expresa en hexadecimal o en base64: la aritmética es exacta y no queda nada que discutir.

Para qué sirve la clave, lo que decide qué rompe rotarla

La documentación de Django enumera exactamente qué depende de SECRET_KEY: cada sesión, salvo que uses el backend de sesión por caché y hayas sustituido el hash de autenticación de sesión por defecto; cada mensaje guardado en una cookie; cada token de restablecimiento de contraseña; y cada uso del framework de firma que no aporte su propia clave. La consecuencia está escrita en la misma página: cuando una clave deja de ser SECRET_KEY y no figura en SECRET_KEY_FALLBACKS, todo eso queda invalidado de golpe.

En la práctica eso significa que todo el mundo queda desconectado y que cada enlace de restablecimiento que ya esté en un buzón deja de funcionar: exactamente lo que quieres tras una filtración y exactamente lo que no quieres un martes por la tarde sin motivo. SECRET_KEY_FALLBACKS es la manera documentada de tener las dos cosas: pon la clave nueva en SECRET_KEY, conserva la vieja en la lista de repliegue el tiempo suficiente para que caduquen las sesiones y los enlaces pendientes, y luego retírala. Django es explícito en que las contraseñas de los usuarios no derivan de la clave y no se ven afectadas en ningún caso.

WordPress no ofrece ningún periodo de gracia equivalente, y su propia documentación lo dice en una frase: puedes cambiar las claves en cualquier momento para invalidar todas las cookies existentes, y eso significa que cada usuario tendrá que volver a entrar. No hay lista de repliegue; el cambio es inmediato y total. La misma página señala que las cuatro claves son obligatorias y las cuatro sales solo recomendadas, porque WordPress genera él mismo las sales si no se define ninguna: una razón más para pegar las ocho y no la mitad.

Dónde se quema igualmente una clave bien generada

El generador es la parte fácil. Casi todos los incidentes reales empiezan después. Una clave acaba en un archivo de configuración que se commitea, en una captura pegada en un ticket, en un mensaje a un compañero, en un registro de compilación, en una variable de entorno que imprime un endpoint de depuración, o en una capa de imagen de contenedor que sobrevive a la rama de la que salió. Ninguno de esos casos es un fallo criptográfico; todos acaban con la utilidad de la clave igual de rotundamente.

El caso del control de versiones merece frase propia, porque el instinto es erróneo. Borrar la línea y commitear el borrado no quita la clave: sigue en el historial, sigue en cada clon que alguien haya hecho y sigue en cada fork y cada espejo del repositorio. Reescribir el historial solo ayuda si además invalidas cada copia ya descargada, cosa que no puedes. La única respuesta sólida es dar la clave por pública y rotarla.

Generar en el navegador está muy bien, y mejor de lo que suena. La clave la produce la página que ya tienes abierta, a partir de la aleatoriedad del propio sistema operativo, y solo existe en esa pestaña hasta que la copias. No se envía nada a ninguna parte, porque nada necesita enviarse: sacar cincuenta índices y unir cincuenta caracteres cabe en unas pocas líneas, y un servidor solo añadiría un lugar, no inspeccionable, donde la clave habría existido.

Todas las opciones de los dos generadores, con la entropía calculada como longitud × log2(alfabeto)
AjusteAlfabetoEntropía
Django por defecto, 50 caracteres50 — minúsculas, cifras, !@#$%^&*(-_=+)282,2 bits — byte a byte el get_random_secret_key de Django
Django, alfanumérico, 50 caracteres62 — letras de ambas cajas y cifras297,7 bits — seguro en una variable de shell, nada que entrecomillar
Django, conjunto completo, 50 caracteres76 — el conjunto alfanumérico más los catorce símbolos312,4 bits — 30 bits más que el ajuste por defecto, sin ganancia práctica
Django, 100 caracteres50 — el mismo conjunto, el doble de longitud564,4 bits — el ajuste existe, la necesidad no
WordPress conjunto completo, 64 caracteres, ocho constantes92 — igual a wp_generate_password con sus dos indicadores417,5 bits cada una; contiene un espacio, ni comilla simple ni barra invertida
WordPress solo alfanumérico, 64 caracteres62 — letras y cifras381,1 bits — 36 bits menos, y nada que escapar en ningún sitio
Cualquier generador que llame a Math.randomLo que digaEfectivamente nula — el estado se recupera a partir de unas pocas salidas
Generador de clave secreta DjangoGenera una SECRET_KEY de Django criptográficamente segura, con longitud y conjunto de caracteres a elegir.Probar la herramienta

Preguntas frecuentes

¿Una clave generada en mi navegador es tan segura como una generada en un servidor?
Más segura, si acaso. crypto.getRandomValues bebe de la misma entropía del sistema operativo que usaría un servidor, así que la aleatoriedad es de la misma calidad. La diferencia es el trayecto de la clave: generada en la página, existe en una pestaña y no va a ninguna parte hasta que la copias, mientras que un generador en servidor significa que la clave existió en una máquina que no controlas, en un proceso que no puedes inspeccionar, quizá en una línea de registro que nunca verás. Recarga la página o cierra la pestaña y el valor desaparece. Lo único de lo que un navegador no puede protegerte es del portapapeles y de aquello en lo que lo pegues después.
¿Cuánto tiene que medir realmente una clave secreta?
Toma el valor por defecto del framework y deja de pensarlo. La función de Django devuelve 50 caracteres, que con su alfabeto de 50 son 282 bits; WordPress emite 64 caracteres de un alfabeto de 92, que son 418. El umbral que suele citarse para un secreto simétrico ronda los 128 bits, y 23 caracteres del alfabeto Django ya lo superan. La longitud extra no cuesta nada y no aporta nada, y los ajustes de 80 o 100 caracteres están por exhaustividad, no porque nadie los necesite. La longitud solo se vuelve la palanca equivocada cuando el alfabeto es diminuto: una clave de 20 caracteres hecha solo de cifras lleva 66 bits, menos de la mitad de lo que lleva una de 20 caracteres del conjunto Django.
¿El sesgo del módulo significa que no debería usar este generador?
No. El sesgo es real y vale 1,2 por cien millones sobre el alfabeto Django: cuarenta y seis de los cincuenta caracteres son alcanzables desde 85 899 346 de los cuatro mil millones de valores de 32 bits posibles, y los otros cuatro desde 85 899 345. Repartido en cincuenta extracciones independientes, mueve la entropía de una clave en una cantidad muy por debajo de cualquier unidad que alguien imprimiría. Para lo que el sesgo sirve de verdad es como marcador cuando lees el generador de otra persona: ver un módulo desnudo te dice que nadie pensó en el muestreo por rechazo, lo cual es una pista sobre el resto del código, y ver muestreo por rechazo te dice que alguien lo pensó. Ninguna de las dos cosas es razón para rechazar una clave que ya tiene 282 bits.
¿Qué se rompe en el momento en que cambio la clave?
En Django: cada sesión que no esté en el backend de caché, cada mensaje guardado en una cookie y cada token de restablecimiento de contraseña, incluidos los enlaces que ya están en los buzones. La documentación lo dice con claridad y da la mitigación: mueve la clave vieja a SECRET_KEY_FALLBACKS, déjala ahí el tiempo suficiente para que las sesiones y los enlaces pendientes caduquen solos, y después bórrala. Las contraseñas de los usuarios no derivan de la clave y quedan intactas. En WordPress no hay lista de repliegue: cambiar cualquiera de las ocho constantes invalida de inmediato todas las cookies existentes, y su documentación dice con todas las letras que todos los usuarios tendrán que volver a entrar. Programa un cambio de clave para una hora tranquila, salvo que respondas a una filtración, en cuyo caso hazlo ya y asume las desconexiones.
He commiteado mi clave en git. ¿Basta con borrar la línea?
No. Un commit que borra una línea deja intacto el commit anterior, así que la clave sigue en el historial y sigue en cada clon, fork y espejo que ya existe. Reescribir el historial la quita de tu copia y de quien descargue después, pero no de quien descargó antes; y en un repositorio público hay que suponer que eso incluye escáneres automáticos, que vigilan los commits nuevos precisamente para esto. Da la clave por publicada: genera otra, pon la vieja en la lista de repliegue si tu framework tiene una, y quítala cuando pase la ventana de transición. Después haz la filtración estructuralmente imposible guardando el valor en una variable de entorno o en un almacén de secretos y commiteando solo un archivo que nombre la variable.

Artículos que podrían interesarte

Todas las guías

Herramientas relacionadas

Todo lo que sigue describe lo que hacen hoy estas cuatro herramientas, comprobado ejecutando su propio código sobre las entradas exactas que aparecen en cada artículo, y no lo que una norma les obligue a hacer. Cuando una herramienta falla en un caso, queda escrito con claridad en vez de sortearlo, y no se ha cambiado nada para que un artículo se lea mejor. Dos consecuencias. Pasa cualquier transformación primero sobre una copia y compara los dos extremos: una herramienta de texto que borra algo no lo anuncia. Y da un secreto por compartido en cuanto sale de la página: pegarlo en una conversación, un ticket o un repositorio lo quema, por bien generado que estuviera.

Fuentes

¿Has detectado un error en este artículo?