Secrets, PKI & CertificatesII · The Secret LifecycleLifecycle
Generating secrets safely - entropy, strength and the folklore
What you'll learn
- Choose the correct kernel randomness interface for a given generation context
- Calculate the entropy of a generated value from its generator rather than from its appearance
- Reject the entropy folklore that leads teams to install unnecessary daemons
- Identify the two moments in a machine lifetime when randomness is genuinely at risk
Prerequisites
Verified against OpenSSL 3.5.x teaching target; 3.0+ minimum · OpenSSH 10.x teaching target; 8.2+ minimum for certificate workflows · OpenBao 2.6.x · Smallstep step-ca 0.30.x · Certbot / Pebble Certbot current release; Pebble 2.10.x ACME test server · Kubernetes (cross-course target) 1.36.x · PostgreSQL 17.x · 2026-08-26
Almost every credential in an estate begins as a request for random bytes, and almost every team believes something about that request which stopped being true a decade ago. The belief is costly in a specific way: it sends effort towards entropy daemons and monitoring dashboards that change nothing, and away from the two situations where randomness really can be weak.
What the kernel actually gives you
Linux maintains one cryptographic generator, seeded from timing noise collected across device drivers and, where the hardware offers it, from a processor instruction and a paravirtual device supplied by the hypervisor. Three interfaces reach that same generator, and they differ only in how they behave before it has been seeded for the first time after boot.
getrandom(2). The system call. Its manual page states that it draws entropy from the urandom source by default, and that if the urandom source has not yet been initialised the call will block unlessGRND_NONBLOCKis given. It needs no file descriptor, so it cannot fail inside a chroot or when the process has exhausted its descriptor table./dev/urandom. Never blocks. Its manual page warns that when read during early boot it may return data prior to the entropy pool being initialised, and that is the only caveat attached to it./dev/random. The manual page calls it plainly “a legacy interface which dates back to a time where the cryptographic primitives used in the implementation of/dev/urandomwere not widely trusted”, and records that since Linux 5.6 it no longer blocks except during early boot.
The same page settles the argument that has outlived its own
premise: /dev/urandom “is preferred and sufficient in all use
cases, with the exception of applications which require
randomness during early boot time; for these applications,
getrandom(2) must be used instead, because it will block until
the entropy pool is initialized”.
flowchart LR
N["Device timing noise\nCPU instruction\nvirtio RNG"] --> P["Kernel entropy input"]
P --> C["Seeded CSPRNG"]
C --> A["getrandom(2)\nblocks only until seeded"]
C --> B["/dev/urandom\nnever blocks"]
C --> D["/dev/random\nlegacy path"]
One generator, three doors. The noise sources on the left matter only for reaching the seeded state on the left of the diagram. Once the generator is seeded, all three doors emit output from the same construction, and choosing between them is a question about blocking behaviour at boot rather than a question about output quality.
Where the folklore comes from and what it costs
The file /proc/sys/kernel/random/entropy_avail reports an
integer described by the manual page as the available entropy in
bits. Reading that number as a fuel gauge is the origin of most
of the folklore. It is an internal accounting figure attached to
the legacy blocking path, and on a modern kernel it hovers near
the size of a small pool during entirely normal cryptographic
work. A low reading does not mean the generator is producing
weaker bytes, and a high reading does not mean it is producing
better ones.
The practical cost of the folklore is misdirected effort. Teams graph a number that cannot be actioned, install a userspace entropy daemon on every virtual machine, and consider the subject handled. None of that touches either situation where generation genuinely goes wrong.
The two moments that genuinely matter
First boot, before the generator is seeded. A machine that generates host keys or a bootstrap credential in the first moments of its first boot can be asking for bytes before enough noise has arrived. The fix is the interface, not a daemon: use a mechanism that blocks until the generator is seeded rather than one that returns immediately.
Cloned images that carry a seed forward. Distributions save a seed file across reboots so the generator reaches a good state quickly on the next start. On this authoring host that file is 32 bytes with mode 0600, restored by a unit named “Load/Save OS Random Seed” whose condition excludes containers. The seed is exactly the right idea for one machine and exactly the wrong thing to copy. A golden image built with the seed file still in place ships identical starting material to every instance created from it.
# Image build step: never ship a saved seed inside a template.
SEED=/var/lib/systemd/random-seed
rm -f "$SEED"
# Host keys belong to the instance, not to the template.
rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub
Where strength comes from, and where it does not
Strength is a property of the generator and the number of bytes taken from it. It is not a property of how the result looks.
# 32 bytes from the kernel generator, encoded for transport.
openssl rand -base64 32
head -c 32 /dev/urandom | base64
# Key generation asks the same generator for its material.
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out app.key
Thirty-two bytes drawn from a cryptographic generator carry 256 bits of entropy. Base64 renders those bytes as 44 characters and hexadecimal renders them as 64 characters, and both still carry 256 bits, because encoding rearranges information rather than creating it. This is worth internalising, because it inverts the intuition that a longer string is a stronger one.
Two arithmetic rules follow, and both bite in practice. Entropy per character is the base-two logarithm of the alphabet size, so a hexadecimal character carries 4 bits and a mixed-case alphanumeric character carries slightly under 6. A value squeezed into a field that accepts only 16 hexadecimal characters carries 64 bits no matter how it was generated. Truncating a strong value to fit a schema truncates its strength in exact proportion, and this is the single most common way a well-generated secret becomes a weak one.
Human-chosen values sit far below their apparent length. A passphrase assembled by choosing words uniformly at random from a list of 7,776 entries gains about 12.9 bits per word, so five words reach roughly 64 bits; the same five words chosen because they were memorable gain very much less, because the selection was not uniform.
Generation anti-patterns that survive code review
Each of these ships regularly, and each looks reasonable in a diff.
# ANTI-PATTERN: a timestamp has only the entropy of the guessing
# window, and hashing it does not add any. An attacker who knows
# the deploy happened on a given afternoon has a small search space.
date +%s | sha256sum
# ANTI-PATTERN: $RANDOM is a small non-cryptographic generator.
echo "token-$RANDOM$RANDOM"
- Deriving every environment from one string. A master value with a suffix per environment means the staging credential is a complete description of the production one.
- Treating a UUID as a bearer token. A version 4 UUID from a cryptographic source carries 122 random bits and is defensible; a version 1 UUID encodes a timestamp and a hardware address, and many libraries produce UUIDs from a general-purpose generator that was never intended to resist an adversary. The string looks identical in all three cases.
- Generating on a workstation and pasting the value. The value now exists in shell history, in a clipboard manager, in the terminal scrollback buffer, and usually in the chat message that delivered it. Generation should happen where the value will live.
- Complexity rules that shrink the alphabet. A policy requiring a symbol and forbidding ambiguous characters removes candidates from the space. Machine-generated values should be drawn uniformly and then encoded, never filtered into shape.
- Reusing key material across environments. A key that protects both the test and production estates has the security properties of the test estate.
Production discipline
- Ask for bytes through an interface that blocks until
seeded. In practice this means
getrandom(2)or a library that calls it, and it matters only in early boot, which is exactly where key generation tends to happen. - Take 32 bytes and stop tuning. Two hundred and fifty six bits is beyond any realistic search, and the effort saved belongs in the storage and rotation stages.
- Never truncate a generated value to fit a field. If the field is short, generate to the field length deliberately and record the resulting entropy, so the weakness is a decision rather than an accident.
- Strip seed files and host keys from every image template. Add an assertion to the image build pipeline that fails if either is present.
- Delete the entropy dashboard. Replace it with an alert on image templates containing a seed file, which is a finding that can actually be acted on.
Cross-course references
- Linux for Production Sysadmins - Part XXVI (SSH) covers host key generation on first boot, which is the most common place an unseeded generator would do real damage.
- Linux for Production Sysadmins - Part LXXV (Immutable) covers image build pipelines, where the seed file and host key assertions described here belong as build-time gates.
- Kubernetes for Production Sysadmins - Part LX (ServiceAccounts) covers tokens minted by the control plane rather than by the workload, which removes the generation problem from the application entirely.
Quiz
Knowledge check · 4 questions
Q1. A service generates a token during the first seconds of a machine first boot. Which interface choice addresses the real risk?
Q2. Encoding 32 random bytes as 64 hexadecimal characters rather than 44 base64 characters produces a stronger secret because the resulting string is longer.
Q3. A schema accepts only 16 hexadecimal characters for an API key. State how much entropy such a key can carry and what the correct engineering response is.
Q4. Explain how identical key material could appear across independent hosts, and give the changes that prevent recurrence.
A fleet audit at example.com finds that 22 of 60 application servers present the same SSH host key fingerprint. All 22 were created from a virtual machine template rebuilt in June; the other 38 predate it. The template was built by installing a base image, applying configuration, generating a set of test credentials, and then taking a snapshot while the machine was running. Entropy daemons are installed and reporting healthy on every host.
Passing score: 75%. Answers are checked in this browser.