Secrets, PKI & CertificatesI · Secrets and Identity FoundationsFoundations
What actually counts as a secret
What you'll learn
- Apply a two-part capability test that separates a secret from configuration
- Classify a composite value such as a connection string by its most sensitive component
- Explain why a public key needs integrity protection rather than confidentiality
- Decide what a certificate discloses and what its private key alone protects
Prerequisites
None — start here.
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
A secret is a value whose disclosure changes what somebody can do. Configuration is a value whose disclosure changes only what somebody knows. That one sentence settles the easy cases and quietly fails on the interesting ones. Get the boundary wrong in one direction and the secret manager fills with hostnames nobody will ever rotate; get it wrong in the other and the production database password sits in a wiki page that thirty people can read.
The capability test
The test has two halves, and each half decides something different. The first half asks about capability: if somebody held this value and nothing else, could they cause an effect on a running system? Not whether it would help them. Whether they could act. The second half asks about rotatability: if the value appeared on a public paste site at 08:00 tomorrow, what would it cost you to make the published copy useless before lunch?
Capability on its own gives you a two-way split, which is where most teams stop. Adding rotatability gives four classes, and the four classes carry genuinely different operational obligations.
| Grants capability alone | Cheap to invalidate | Class | What the class obliges you to do |
|---|---|---|---|
| Yes | Yes | Secret | Store in a secret manager, restrict by policy, rotate on suspicion without ceremony |
| Yes | No | Key material or trust anchor | Protect harder, plan the replacement in advance, rehearse it before you need it |
| No | Not applicable | Sensitive configuration | Keep out of public places, version it, never pretend rotation is a control |
| No | Not applicable | Public configuration | Publish freely, review it like code |
flowchart TD
A["A value you must place"] --> B{"Alone, does it\ncause an effect?"}
B -- "no" --> C{"Does it help\nan attacker aim?"}
C -- "no" --> D["Public configuration"]
C -- "yes" --> E["Sensitive configuration"]
B -- "yes" --> F{"Can you invalidate it\nin an hour?"}
F -- "yes" --> G["Secret"]
F -- "no" --> H["Key material\nor trust anchor"]
Read the diagram as a placement decision rather than a philosophical one. You are not deciding how bad a value is. You are deciding which register it goes in, who is allowed to fetch it, and what you are able to do at three in the morning when somebody tells you it has been published. A value you cannot invalidate in an hour belongs in the fourth box no matter how uncomfortable that feels, because the honest answer changes your plan: you protect it more and you rehearse the replacement, because you will not be improvising it under pressure.
A hostname, an internal URL, and the difference between them
Take db-03.internal.example.com. It fails the capability test
outright. Holding the name lets nobody connect, authenticate or
read a row; the name is an argument to a connection, not a
credential for one. It passes the second question comfortably,
though: it names a live target, confirms a naming scheme that
predicts db-01 and db-04, and tells a scanner exactly where
to point. That combination is sensitive configuration.
The obligation that follows is specific. You do not put it in a secret manager, because a lease on a hostname is theatre and because you cannot change the name under pressure without changing every client that resolves it. You do keep it out of public repositories, out of stack traces that reach browsers, and out of screenshots in tickets. And you keep it out of the same field as anything that does grant capability, for reasons the next section makes concrete.
Now take two values that look identical. The first is
https://internal.example.com/reports/quarterly, sitting behind
a proxy that demands an authenticated session. Knowing the path
achieves nothing; the proxy still asks who you are. That is
configuration. The second is
https://hooks.internal.example.com/t/9f2c1b7e4a5d/publish,
where the path segment is the entire authorisation and any HTTP
client that sends the right POST body gets an effect. That is a
password wearing a URL costume, and the test classifies it as a
secret without hesitating.
The distinction matters operationally because capability URLs leak through channels that no team has hardened. A URL ends up in proxy access logs, in browser history, in the request path attribute that every tracing and log pipeline indexes by default, and in the copy of the runbook somebody pasted into a chat channel. A password field at least gets treated as one by the tooling around it. A URL is treated as metadata by everything it touches, which is why webhook endpoints of this shape have to be enrolled and rotated as credentials rather than documented as endpoints.
A composite value inherits its worst component
The rule is short and there is no averaging in it: a value assembled from several parts carries the classification of its most sensitive part. A connection string is the canonical case.
postgresql://appsvc:REDACTED@db-03.internal.example.com:5432/appdb?sslmode=verify-full
scheme postgresql public configuration
user appsvc sensitive configuration
password REDACTED secret
host db-03.internal... sensitive configuration
port 5432 public configuration
database appdb sensitive configuration
parameter sslmode=verify-full public, and load bearing
Six of the seven parts are things you would happily write on a whiteboard. The seventh drags the whole string across the line, so the assembled value is a secret and every rule that applies to the password now applies to the string: not in a ConfigMap, not in a log line, not in an exception message, not in a Terraform output.
That leaves a real design choice. You can store the whole string
as one secret, which is simple and which makes every password
change a rewrite of a value that also carries the host and the
TLS policy. Routine rotation then becomes a configuration change,
and configuration changes are where sslmode=verify-full quietly
becomes sslmode=require because somebody retyped the line.
Alternatively you store only the password as a secret and
assemble the string at start-up from configuration plus that one
fetched value. The password then rotates on its own schedule, and
the connection policy lives in version control where a reviewer
can see it change. The second design costs a few lines of code
and repays them the first time you rotate.
A public key is not a secret, and that is not the end of it
An asymmetric key pair separates two properties that people
normally bundle together. The private half must stay
confidential, and if it leaves the host it is compromised whether
or not anybody has used it yet. The public half is designed to be
published: you paste it into authorized_keys, you hand it to a
certificate authority inside a signing request, and you serve it
to complete strangers during a TLS handshake.
Confidentiality is not the only property a value can need,
though. The public half needs integrity and authenticity. An
attacker who can append one line to authorized_keys does not
need your private key; they use their own. An attacker who can
drop a certificate into a system trust store does not need to
break anybody’s key; they mint their own and every client on that
host believes them. Neither attack involves a confidentiality
failure anywhere. This is why the sentence “it is public, so it
does not matter” tends to appear in the timeline of a trust store
incident rather than in the postmortem’s list of things that went
well.
A certificate follows the same logic one level up. It is a signed statement binding names to a public key, and it is public by construction: the same file is served to everyone who connects, and publicly trusted certificates are published to Certificate Transparency logs shortly after issuance. Everything inside it is readable.
CERT=/etc/ssl/certs/app.lab.example.pem
openssl x509 -in "$CERT" -noout -subject -issuer -dates
openssl x509 -in "$CERT" -noout -ext subjectAltName
subject=CN=app.lab.example
issuer=O=RunBook Academy Lab, CN=RunBook Lab Server Issuing CA
notBefore=Aug 26 21:19:00 2026 GMT
notAfter=Nov 24 21:19:00 2026 GMT
X509v3 Subject Alternative Name:
DNS:app.lab.example, DNS:www.app.lab.example
Subject, issuer, validity window and every name the certificate
vouches for are all in the clear, and anyone who can reach the
service can retrieve them without authenticating. That has a
practical consequence for naming: a publicly trusted certificate
that covers admin-staging.example.com publishes that name to
the world through Certificate Transparency, which is a good
reason to issue internal names from an internal authority. The
one file that must never leave the host is the private key that
matches the public key embedded in that certificate.
Production discipline
- Classify by capability, never by discomfort. Ask whether the value alone produces an effect. A value that merely embarrasses you is sensitive configuration, and putting it in the vault buys nothing while adding a fetch that can fail.
- Split composites at the storage boundary. Store the one component that grants capability, assemble the rest from configuration, and let the two rotate on different clocks.
- Give sensitive configuration a real home. Private repositories, reviewed changes and restricted dashboards are the right controls for it. Rotation is not one of them.
- Protect public key material for integrity. Trust store
contents,
authorized_keysfiles and pinned CA bundles need ownership, mode, change review and alerting on modification, because confidentiality was never the property at risk. - Record the classification beside the value. A key named
DB_DSNtells a future engineer nothing. A registry entry naming the class, the owner and the rotation procedure tells them everything they need at the moment they are least able to work it out.
Cross-course references
- Linux for Production Sysadmins - Part LXXII (Secrets) covers where a value physically rests on a host, including unit environment files and the ownership and mode decisions that decide who can read them.
- Git, CI/CD & GitOps for Infrastructure Engineers - Part XLII (CISecrets) covers how a forge stores a declared secret and injects it into a job, which is the storage half of the classification you make here.
- Kubernetes for Production Sysadmins - Part LXV (SecretsSec) covers why a Kubernetes Secret is encoded rather than encrypted, and therefore who inside a namespace can read one.
Quiz
Knowledge check · 4 questions
Q1. A team publishes an internal webhook endpoint whose path segment is the only authorisation: any client that sends the correct POST body to that path triggers a deployment. How should the URL be classified?
Q2. A server certificate can be treated as public information, while the private key that corresponds to it cannot.
Q3. State the two questions the capability test asks about a value, and say what each one decides.
Q4. Decide what has actually been exposed, and what has to change.
At 14:20 on 2026-08-26 a reviewer notices that the deployment repository contains a file named app-config.yaml with the entry dsn: postgresql://appsvc:s3rvice-pw@db-03.internal.example.com:5432/appdb?sslmode=require. The repository is private but is readable by everyone in the engineering organisation, roughly 120 people. The same repository also holds the public half of the deployment SSH key and the internal root CA certificate.
Passing score: 75%. Answers are checked in this browser.