generating...Paste this
Pick your framework, hit regenerate a couple of times if it makes you feel better, and copy. The key comes from crypto.getRandomValues in your browser, which is backed by your operating system's random number generator. Nothing leaves the page. If you would rather not trust a web page at all (reasonable, I build these for a living and I still have opinions), every format here has an offline command further down.
If you only care about one framework, the dedicated pages go deeper: the Flask secret key generator, the Django secret key generator, the JWT secret generator, the API key generator and the encryption key generator for raw AES and Fernet keys.
What a secret key actually does
HTTP forgets you between requests, so frameworks hand your browser a little state to carry around: a session cookie, a CSRF token, a "remember me" cookie, a password reset link. The problem is that the browser belongs to the user, and the user can edit anything in it. If the cookie says user_id=42, what stops someone from changing it to user_id=1?
The secret key does. Most frameworks run the value through HMAC with the secret key and attach the result as a signature. When the value comes back, the server recomputes the HMAC. If a single byte changed, the signature no longer matches and the value is rejected. Without the key, nobody can produce a valid signature for a modified value.
Some frameworks go further and encrypt. Laravel encrypts its cookies (the session cookie included) with APP_KEY using AES-256-CBC plus a MAC. Auth.js uses AUTH_SECRET to encrypt session tokens and hash email verification tokens. Rails feeds secret_key_base into a key generator that derives the keys for both signed and encrypted cookies.
Which gives you the one sentence worth remembering: anyone holding your secret key can mint sessions your app will believe. For most apps that means logging in as any user, admins included. Django's settings documentation goes further and says a known SECRET_KEY "can lead to privilege escalation and remote code execution vulnerabilities." Treat it like your database password, because it is at least as valuable.
How long should a secret key be?
32 random bytes (256 bits) is the sensible floor. Most signing is HMAC-SHA256, and a key with 256 bits of entropy is as strong as that construction gets. The express-session docs say it directly: HMAC-SHA256 signs the session ID, so the secret "should contain at least 32 bytes of entropy." Going bigger costs nothing, which is why Rails uses 64 bytes.
The trap is confusing characters with bytes. A 32-byte key is 64 characters as hex, 44 as base64 with padding, and 43 as base64url without. A 64-character string you typed yourself is not 256 bits of anything. What matters is how many random choices went into it and how many options each choice had, which is why this page shows entropy in bits next to each key.
Here is what each format on this page works out to:
| Framework | Setting | Format | Entropy | Official command |
|---|---|---|---|---|
| Flask | SECRET_KEY | 32 bytes as 64 hex characters | 256 bits | python -c 'import secrets; print(secrets.token_hex())' |
| Django | SECRET_KEY | 50 characters from a-z 0-9 !@#$%^&*(-_=+) | about 282 bits | get_random_secret_key() |
| Express | secret option | 32 bytes as 64 hex characters | 256 bits | none, use crypto.randomBytes |
| Laravel | APP_KEY | base64: plus 32 bytes as base64 | 256 bits | php artisan key:generate |
| Auth.js | AUTH_SECRET | 32 bytes as base64 | 256 bits | npx auth secret |
| Rails | secret_key_base | 64 bytes as 128 hex characters | 512 bits | bin/rails secret |
| WordPress | 8 keys and salts | 64 characters each from a 92 character set | about 417 bits each | wp config shuffle-salts |
I pulled those formats from each project's source rather than from memory. Laravel's key:generate returns 'base64:'.base64_encode(Encrypter::generateKey(...)), and for the default AES-256-CBC cipher that is 32 random bytes. The Auth.js CLI fills 32 bytes with crypto.getRandomValues and prints them as base64. Rails' secret command is literally puts SecureRandom.hex(64). WordPress core's installer picks 64 characters per key with random_int from a-z A-Z 0-9 plus !@#$%^&*()-_ []{}<>~`+=,.;:/?|, and this page uses the same set.
Generate a secret key offline
Any of these produce a key as good as the one above. Run them on the machine that will use the key and it never touches a clipboard in a browser.
# Generic, 32 bytes
openssl rand -hex 32
openssl rand -base64 32
python3 -c "import secrets; print(secrets.token_hex(32))"
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Flask (token_hex defaults to 32 bytes)
python3 -c "import secrets; print(secrets.token_hex())"
# Django
python3 -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
# Laravel: writes APP_KEY into .env, or --show to just print it
php artisan key:generate
php artisan key:generate --show
# Auth.js: writes AUTH_SECRET into .env.local for Next.js
npx auth secret
# Rails
bin/rails secret
# WordPress: replace the salts in wp-config.php
wp config shuffle-salts
curl https://api.wordpress.org/secret-key/1.1/salt/
The WordPress salt service at api.wordpress.org/secret-key/1.1/salt/ returns eight ready-made define() lines. WordPress core itself only falls back to that service if the local random number generator fails, so generating them locally (here, or with WP-CLI) is exactly what a fresh install does anyway.
Where to put it (and where not to)
The key belongs in the environment, not in the code. The pattern is the same everywhere: a .env file in development that is listed in .gitignore, and in production an environment variable injected by your platform or pulled from a secrets manager like AWS Secrets Manager, Google Secret Manager, Azure Key Vault or HashiCorp Vault.
# .gitignore
.env
.env.local
.env.*.local
Read it in a way that fails loudly when it is missing. os.environ["SECRET_KEY"] crashes at startup, which is what you want. os.environ.get("SECRET_KEY", "dev") happily boots production with the word "dev" as your signing key, which is how people end up in incident reports.
A few specific places I have watched secret keys leak from:
- Git history. Deleting the file in a later commit does nothing. The old commit is still in every clone.
- Docker images. A
COPY .env .or anENV SECRET_KEY=...line bakes the key into a layer that anyone who can pull the image can read. Pass it at runtime instead. - Error pages and logs. Debug pages that dump settings, and startup logs that print config "to help debugging."
- Tutorials. The key from the blog post you copied is now the key of every app built from that blog post.
- Frontend bundles. In Next.js, anything prefixed
NEXT_PUBLIC_ships to the browser.AUTH_SECRETmust never get that prefix.
Also use one key per environment and one key per app. If staging and production share a key, a staging leak is a production leak.
Rotating a secret key without logging everyone out
Swap the key and everything signed with the old one stops validating. Sometimes that is exactly what you want: after a leak, logging everyone out is the point. For routine rotation, most frameworks let the old key keep verifying for a while.
Django has SECRET_KEY_FALLBACKS. Put the new key in SECRET_KEY, move the old one to the front of the fallbacks list, and remove it once old sessions and reset tokens have expired. Django signs with the current key and accepts any key in the list.
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
SECRET_KEY_FALLBACKS = [os.environ["DJANGO_SECRET_KEY_OLD"]]
Flask added SECRET_KEY_FALLBACKS in version 3.1. Flask's built-in cookie session honors it, but the Flask docs warn that extensions using SECRET_KEY may not support it yet, so check anything that signs its own tokens. Underneath, itsdangerous has accepted a list of keys for a while: signing uses the last key in the list, and verification tries every key.
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
app.config["SECRET_KEY_FALLBACKS"] = [os.environ["SECRET_KEY_OLD"]]
express-session takes an array. The first secret signs new cookies, and all of them are checked when verifying.
app.use(session({
secret: [process.env.SESSION_SECRET, process.env.SESSION_SECRET_OLD],
resave: false,
saveUninitialized: false,
}))
Laravel reads a comma separated APP_PREVIOUS_KEYS. It encrypts with APP_KEY and tries the previous keys when decryption with the current key fails. This matters more in Laravel than elsewhere, because anything you stored with Crypt::encryptString is unreadable once its key is gone.
APP_KEY="base64:NEW..."
APP_PREVIOUS_KEYS="base64:OLD..."
Auth.js accepts an array for the secret option. The newest secret goes first and is used for new sessions, and older ones still decrypt existing tokens.
Rails: changing secret_key_base expires active sessions, and the Rails security guide notes encrypted cookies, signed cookies and Active Storage files can be affected too. Rails has cookie rotators for changing cookie configuration gracefully, but plan a key change as "everyone logs in again" unless you have set those up and tested them.
WordPress has no fallback mechanism. New salts invalidate every login cookie immediately, and the comment in wp-config-sample.php says as much. After a compromise that is a feature: shuffle the salts and every session on the site, including the attacker's, is gone.
Common mistakes
- Shipping the default. Django's
startprojectwrites a key prefixed withdjango-insecure-precisely so you notice.python manage.py check --deployflags it. - Using a password as the key. "Summer2024!" is not a secret key. If a human chose it, an attacker's wordlist probably includes it, and signed cookies let them test guesses offline without ever touching your server.
- One key for everything. Your session secret, your JWT signing secret and your webhook secret should be different values. If one leaks, you rotate one thing.
- Treating signing as encryption. A Flask or Django signed cookie is readable by anyone. The signature stops edits, not eyes. Do not put anything confidential in it.
- Mangling it on the way in. Characters like
$,#and%mean something to shells, Docker Compose and systemd. Django keys contain all three. Quote with single quotes, and see the Django page for the details.
For the bigger picture on keeping secrets out of places they should not be, I wrote up my opinionated security best practices and a piece on the principle of least privilege.
Secret Key FAQ
What is a secret key used for?
In most web frameworks the secret key signs things the server hands to the browser, like session cookies, CSRF tokens and password reset links, so the server can tell later whether they were tampered with. In a few frameworks, like Laravel and Auth.js, it also encrypts them. Anyone who has the key can forge those values, which usually means logging in as any user.
How long should a secret key be?
Use at least 32 random bytes, which is 256 bits. As hex that is 64 characters and as base64 it is 44. Some frameworks want more or a specific format: Rails uses 64 bytes as 128 hex characters, Django uses 50 characters from a 50 character alphabet, and WordPress uses 64 characters per key.
Is it safe to generate a secret key on a website?
This page builds the key in your browser with crypto.getRandomValues and does not send it anywhere. That said, the most paranoid option is the framework's own command or openssl rand on the machine that will use the key. Both are listed on this page, and they produce the same format.
Can I use the same secret key for development and production?
No. Development keys end up in laptops, screenshots, CI logs and sometimes git. If production shares that key, every one of those places can forge production sessions. Generate a separate key per environment.
What happens if I change my secret key?
Everything signed or encrypted with the old key stops validating. Users get logged out, pending password reset links die, and in Laravel any data you encrypted with the old key can no longer be decrypted. Most frameworks let you keep the old key as a fallback for a while so rotation does not log everyone out at once.
I committed my secret key to git. What now?
Treat it as leaked. Generate a new key, deploy it, and then decide whether to keep the old one as a short-lived fallback. Deleting the commit does not help much, because clones, forks and CI caches keep the history. Rotation is the fix.
Where do I get WordPress salts?
WordPress offers a salt service at api.wordpress.org/secret-key/1.1/salt/ that returns eight define lines, and WP-CLI can replace them in place with wp config shuffle-salts. The WordPress option on this page produces the same eight constants, 64 characters each, from the same character set WordPress core uses.
Related tools: Django secret key generator, Flask secret key generator, JWT secret generator, API key generator, encryption key generator, and the strong random password generator.
-Sethers