openssl rand -hex 32 prints 64 hex characters made from 32 random bytes: 256 bits, more than any token needs. For 128 bits, the usual size of a session ID or an API key, use openssl rand -hex 16. OpenSSL 3 on Linux and the LibreSSL that macOS ships take the same flags.
openssl rand -hex 32
# 6963b23aa6d4a843c050c7e1e3ca7425ceffb7546e559fc3e7664a969c4047dc
openssl rand -hex 16
# 68ba35ff6044bda94bf0217feda2c5c3
The tool below shows the count for the first: 64 characters × log2(16) = 256 bits. OWASP’s Session Management Cheat Sheet sets 64 bits of entropy as the floor for a session ID.
Base64
openssl rand -base64 32 gives 44 characters for the same 32 bytes, with +, / and a trailing =. From 49 bytes up it breaks the output into lines of 64 characters. For a URL or a file name, swap the two symbols and drop the padding:
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
# RM8Nw_SvEyc7TJj0UCuejqtPkGuiTgvkv18rJ_nyNrQ
Straight from /dev/urandom
Without OpenSSL, read the kernel’s random device:
head -c 32 /dev/urandom | base64
# KRpkEAPsB6RP1qKmFvzah6tsORZUmQPrNRACmek0Uw4=
GNU base64 on Linux wraps at 76 characters, which starts at 58 bytes, so add -w 0 for longer tokens. macOS’s base64 doesn’t wrap.
For letters and digits only, let tr delete every other byte:
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32; echo
# jywLOMQZOkVxorVl3A57K8fanBL41Lg7
Each byte tr keeps is one of the 62 characters, all equally likely, so there’s no bias; the tool counts 32 of them as 32 characters × log2(62) = 191 bits. head -c prints no newline, hence the echo.
On macOS, LC_ALL=C is required. In a UTF-8 locale, macOS’s tr reads the bytes as text and stops at the first invalid sequence:
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32
# tr: Illegal byte sequence
It may print a few characters first, and the pipeline still exits 0, so a script gets a short token and no error. GNU tr works on bytes, so Linux doesn’t need the prefix.
Under set -o pipefail the same line fails differently: head exits after 32 bytes, tr is killed by SIGPIPE, and the pipeline’s status is 141. With set -euo pipefail, token=$(…) ends the script there without a message. openssl rand has neither problem.
Not $RANDOM
$RANDOM is a number from 0 to 32767, 15 bits from a seeded generator, and gluing several together doesn’t make it secret. Bash 5.1 added $SRANDOM, 32 bits from the system’s random source, but /bin/bash on macOS is still 3.2, where it expands to nothing.