A key is not a password
Both are secrets, and that is where the resemblance ends. A password is remembered and typed by a person, which is why it is short, why it is guessable, and why it has to be stored behind a deliberately slow hash like bcrypt or argon2. A key is never remembered and never typed — it is written once into a config file and read back by a machine. Nothing about it needs to be memorable, so nothing about it needs to be weak.
That changes what you generate. There is no reason to draw a key from a keyboard alphabet, and no reason to stop at a length somebody would be willing to type. You draw bytes, as many as the algorithm wants, and then you decide how to write them down.
openssl rand -base64 32 | tr '+/' '-_' | tr -d '=' → 43 characters carrying exactly 256 bits
That line is the reason most people arrive here. It draws 32 bytes, prints them as base64, swaps the two characters a URL would object to, and strips the padding. The tool above is the same three steps with the pieces named and each one turned into a control.
Where does a key like that go? AUTH_SECRET, a JWT signing secret, a session cookie key, a CSRF token, a webhook signing secret, an API key you issue, or the key for a symmetric cipher. Every one of them wants the same thing: a value nobody can predict, in a form the surrounding file will accept unchanged.
Reading the openssl one-liner
The pipeline changes with the settings above, and every stage of it earns its place. Here is what each one does.
| Stage | What it does |
|---|---|
| openssl rand -base64 32 | Draws 32 bytes from OpenSSL's cryptographic generator and prints them as base64. The count is bytes, not characters — this is where the entropy is decided, and nothing after it adds any. |
| tr '+/' '-_' | Swaps the two symbols standard base64 uses that a URL does not. + becomes - and / becomes _, which is the whole difference between base64 and base64url. |
| tr -d '=' | Removes the trailing padding. It carries no information — it exists so a strict decoder knows where a block ended — and it is one more character a URL percent-encodes as %3D. |
One stage is missing from most copies of that command, and above 48 bytes it matters: openssl breaks its base64 output every 64 characters. Ask for a longer key and a newline lands inside it, silently, with nothing in the output to say so. The command here adds the strip when the length needs it:
openssl rand -base64 64 | tr '+/' '-_' | tr -d '=\n'
The base32 line reaches for base32 from GNU coreutils rather than openssl, which cannot write that alphabet. A stock macOS does not ship it, and there is no portable alternative worth printing in its place, so the command says plainly what it needs.
The command and the key above come from different generators — OpenSSL's and your browser's — so running it will not reproduce what is on screen. What it reproduces is the shape: the same byte count, the same alphabet, the same padding.
Four ways to write the same bytes
An encoding decides which characters carry the key. It never decides how much entropy the key holds. The widths below are for the default 32 bytes, unpadded.
| Encoding | Alphabet | Characters for 32 bytes | Reach for it when |
|---|---|---|---|
| base64url | A–Z a–z 0–9 - _ | 43 | The value is going into a URL, a cookie, a path segment or a .env file. Nothing in the alphabet needs escaping anywhere. |
| base64 | A–Z a–z 0–9 + / | 43 | You are matching something that already emits standard base64 — an existing config, a library default, somebody's README. |
| hex | 0–9 a–f | 64 | You want the form every language reads without a library, or the destination accepts only 0-9a-f. |
| base32 | A–Z 2–7 | 52 | A TOTP secret, or anything else that has to survive being read aloud or typed back in. |
Notice that the widths differ and the entropy does not. 32 bytes are 256 bits as 43 base64url characters, as 52 base32 characters, and as 64 hex characters. A longer string is not a stronger key — it is the same key spelled less densely.
Padding is the = at the end of a base64 or base32 string, and it carries no information at all. Keep it when the consumer is a strict decoder; drop it when the value is going into a URL, where each = becomes %3D.
Changing the encoding, the padding or the output above respells the key already on screen. It does not draw a new one — that would make the four rows impossible to compare against one another. Only the byte count and the New key button draw again.
How many bytes
The byte count is the only setting that changes how hard the key is to guess, and it does so exactly: every byte from a cryptographic generator carries eight bits, so the entropy is a multiplication rather than the estimate a password generator has to make.
The right number is usually decided for you by whatever consumes the key. HMAC-SHA256 wants 32; AES-128 wants exactly 16 and will not take 17. Where nothing specifies one, 32 bytes is the answer that fits nearly everything.
| Bytes | Bits | Rating | Key length for |
|---|---|---|---|
| 16 | 128 | Strong | AES-128 |
| 24 | 192 | Strong | AES-192 |
| 32 | 256 | Very strong | AES-256, ChaCha20, HMAC-SHA256 |
| 48 | 384 | Very strong | HMAC-SHA384 |
| 64 | 512 | Very strong | HMAC-SHA512 |
The 3 ratings are bands over the bit count rather than opinions about your setup: below 128 bits the key becomes the weakest part of any modern construction, and at 256 an exhaustive search is bounded by physics rather than by budget.
The last column names only exact matches, deliberately. 40 bytes is not an AES-256 key with eight to spare — it is a value AES cannot accept at all. HMAC is the exception: it takes a key of any length, and what is named is the size RFC 2104 recommends, which is the hash's own output length.
The field accepts 8 to 512 bytes. The floor is low enough to reach the weak band on purpose, so the rating has something to say; the ceiling exists so the field has one, not because 512 bytes helps.
Every control on this page
Five settings, and only the first of them changes the key. The rest change how it is written down.
| Option | What it does | Reach for it when |
|---|---|---|
| Bytes of randomness | How many bytes are drawn. Entropy is exactly eight bits per byte. | Set it to whatever the algorithm consuming the key specifies, and to 32 when nothing does. |
| Encoding | Which alphabet spells the bytes: base64url, base64, hex or base32. | Match the file the key is going into. base64url is safe everywhere. |
| Keep padding | Keeps the trailing = characters on a base64 or base32 string. | Turn it on for a strict decoder; leave it off for a URL or a .env line. |
| Output | Wraps the key as a bare value, a .env assignment, or a shell export line. | Pick the one matching wherever you are about to paste it. |
| Variable name | The name on the left of the = in the .env and export outputs. | Set it to the variable the key is for — AUTH_SECRET, JWT_SECRET, whatever your config reads. |
Two controls go quiet when they cannot do anything. Keep padding is disabled under hex, which spends exactly two characters a byte and so never has a partial group to pad.
Variable name is disabled under the bare output, which is the key and nothing around it. In both cases the control is disabled and says why, rather than being collected and silently ignored.
A name a shell would reject — one starting with a digit, or an empty one — does not blank the key. The output falls back to the bare form and the field says what is wrong with the name, because withholding the value you came for is a worse answer than withholding the wrapper around it.
The .env line is deliberately unquoted and the export line is deliberately double-quoted. Dotenv readers are split on whether quotes are part of the value; a shell is not, and an unquoted value there is at the mercy of whatever the encoding happened to draw.
The defaults are 32 bytes as unpadded base64url, bare — the form that goes into AUTH_SECRET without anything having to be escaped, quoted or trimmed.
Where the key is drawn
In your browser tab, by <code>crypto.getRandomValues</code>, and nowhere else. The page you loaded contains no key at all: the server renders a placeholder, and the first value appears once the tab has drawn it.
That is deliberate rather than incidental. A key generated on a server travels back inside an HTTP response body, and from there into access logs, into a TLS-terminating proxy, and into anything that buffered the response on the way. None of those are places a secret should be recoverable from.
Nothing about the key goes into the URL either. A link carries your settings — ?bytes=64&encoding=hex — and never the value, because a URL lands in browser history, in the referrer header, and in every proxy log between here and wherever you go next.
The honest caveat: a key that has been displayed on a screen is in a browser tab, in that tab's memory, and on your clipboard the moment you copy it. That is a different threat model from generating it on the machine it will run on. For anything guarding real users, the command above run locally is the stronger habit — and it is printed here for exactly that reason.
The same tool over MCP is a further step away again. A key returned to an assistant travels through that conversation and is retained by whatever is holding it, so the MCP description says so and returns the shell command alongside the key.
Living with a generated key
Rotate on a schedule, and on suspicion. Rotating a signing key invalidates every token signed with the old one, which is the point — but it is also a logout for everybody holding one, so choose the moment rather than discovering it.
Never commit one. A key in git history stays in git history through every clone and every fork, and rewriting the history does not retract the copies. If one lands there, rotate it; do not try to erase it.
One key, one job. A single secret shared between session signing, webhook verification and encryption at rest means all three rotate together and all three fall together.
Store it where the platform stores secrets — the deployment's environment settings, a secrets manager, a sealed file — rather than in a checked-in <code>.env</code>. The .env output above is for your own machine, not for the repository.
None of that is about this tool. It is about the thirty seconds after you paste the value, which is where every key that has ever leaked actually leaked.
Frequently asked questions
tr '+/' '-_' the output can contain + and /, which a URL percent-encodes, and above 48 bytes openssl wraps its output at 64 characters, putting a newline inside the key.+ and /; base64url uses - and _ instead. Everything else is identical, the entropy included — which is why switching between them here respells the key rather than drawing a new one.