Skip to main content
RunBook Academy

← All break/fix scenarios in Secrets, PKI & Certificates

intermediatekey-compromise~40 min

A support bundle uploaded twelve days ago contains the live server private key

Reported symptoms

  • At 08:52 UTC on 26 August a recently joined engineer opens support ticket 4471 to read a reverse proxy configuration and finds a file named privkey.pem inside the attached diagnostic archive
  • The archive was created on web-01 on 14 August at 07:26 UTC during a latency investigation and uploaded to a third-party vendor support portal fifteen minutes later
  • The identical archive is attached to the internal ticket, where anybody with a staff login has been able to download it for twelve days
  • Nothing has failed: the service is healthy, every client validates the certificate, and no alert has fired because there is nothing here for an alert to observe
  • A renewal run at 09:05 UTC reports success and deploys a certificate with a new serial number and a new expiry date, and the team believes the exposure is closed
  • The private key in the archive was generated on 3 November 2025 and has been carried unchanged through six renewals since
  • A Certificate Transparency search for the domain returns only certificates the team can account for, so there is no sign the key was used to obtain another one

Evidence

  • · tar -tzf on the archive lists etc/ssl/private/api.example.com/privkey.pem beside the configuration files, so the key travelled inside the upload rather than being referenced by it
  • · openssl pkey -pubout on the key extracted from the archive, piped into openssl sha256, returns a digest ending 1af66874b
  • · openssl x509 -noout -pubkey on the certificate read from the socket, piped into the same digest, returns exactly that value, which proves the archived key is the key in production and not a superseded copy
  • · openssl x509 -noout -serial -dates on the certificate in service returns notAfter Sep 15 09:14:00 2026 GMT, leaving twenty days of validity at the moment of discovery
  • · After the 09:05 renewal the certificate serial has changed and the notAfter has moved to Oct 10 09:05:00 2026 GMT, which is why the change looked like a rotation
  • · After the same renewal the public key digest of the new certificate is unchanged, so the renewal issued a second certificate for the compromised key
  • · openssl x509 -noout -ext authorityInfoAccess on the certificate shows a CA Issuers URI and no OCSP responder URI at all, so there is nothing for an OCSP query to ask
  • · The vendor portal audit trail records three downloads of the attachment by accounts in the vendor support organisation, and the internal ticket records eleven distinct viewers, so the number of copies cannot be established
Diagnosis and resolutionclick to reveal

Root cause

Two defects, and the quieter one is why this incident survived its first remediation. The first is that an ordinary operational tool could reach the key. The diagnostic bundle is built by a script that archives the reverse proxy configuration directory and everything it refers to, which is a sensible thing for a support bundle to do and is exactly how the key ended up inside it. Nobody made a mistake in the sense of doing something they had been told not to do. An engineer under time pressure ran the documented tool and uploaded what it produced, and the tool had the file permissions to read a key that the reverse proxy also had to read. The second defect is that the key was effectively permanent. The renewal automation reuses the existing key pair, so every renewal since November has produced a fresh certificate over a key that is now two hundred and ninety-six days old. That is why the archive from 14 August contains the key that was live on 26 August, why the 09:05 renewal changed nothing that mattered, and why the exposure is not bounded by any certificate lifetime. It also means the same key sits in every backup, every configuration snapshot and every earlier diagnostic bundle taken since November. The first defect made the copy possible. The second made the copy durable, and turned a bad twenty minutes into an exposure with no natural end date.

Remediation

Fix the date first, not the certificate. Write down the moment the archive was created, because that is when the key stopped being yours, and every calculation afterwards runs from it rather than from the moment somebody noticed. Then generate a new key pair. Not a renewal, not a reissue against the existing key, a genuinely new private key, and prove it is new by comparing its public key digest with the recorded digest of the compromised one before you request anything. Issue a certificate against that new key, deploy it to all three hosts, and reload each service. Revoke the old certificate as well, and do it for the right reason: it is the correct thing to place on the record, it protects the minority of clients that check, and inside a private PKI where you operate and enforce a CRL it can be a real control. On the public web it is not the control, and a plan whose safety rests on it is a plan with a gap in it. What actually bounds this exposure is that the old certificate is replaced within hours and expires within weeks. Then find the copies: the vendor portal, the internal ticket, every previous bundle, the backups of web-01, and any image built from that configuration tree. Ask the vendor to delete the attachment and get the confirmation in writing with a date on it. Do not renew and stop. Do not disable certificate verification anywhere to buy time, and do not treat the absence of evidence of misuse as evidence of absence.

Verification

Verify from the socket, from outside, and per host. The independent observation is the public key the server actually presents to a client, read across the network rather than from the file you just copied, because a correct file that the process has not picked up looks identical to a working deployment in every other check. Take the digest of the public key in the certificate served by each of the three hosts individually, bypassing the load balancer, and compare each one against the recorded digest of the compromised key. Three differences and no matches is the result you need; two out of three is an unfinished rollout, not a partial success. Confirm the new certificate carries the new serial and the expected validity window. Confirm the old serial is served nowhere by scanning the estate for it and requiring a count of zero. Run the diagnostic bundle script again after the fix and list the archive to confirm the key file is no longer in it, because a procedure change nobody has executed is a document rather than a control. Record what you could not verify: you cannot prove the key was never used, and the report should say so plainly instead of implying otherwise.

Prevention

Generate a new key at every issuance, and make that the default rather than an option someone has to remember. A renewal that reuses the key preserves the one thing you most want to be short-lived, and the cost of a fresh key pair is milliseconds. Add a gate that compares the public key digest of each newly issued certificate against the previous one and fails the deployment when they match. Keep private keys out of the reach of general-purpose tooling: give the diagnostic bundler an explicit allow list of files rather than a directory to walk, run it as an identity that cannot read the key directory, and test that it refuses. Better still, hold the key where it cannot be copied at all, in an HSM or a PKCS#11 token, so that a support archive physically cannot contain it. Keep validity short and renewal automatic, because the remaining lifetime of a leaked certificate is the part of the exposure you can actually bound: forty-five day certificates renewed automatically cap it at weeks, and an annual certificate caps it at nothing useful. Monitor Certificate Transparency for every domain you own and page within 15 minutes on an issuance nobody requested. Measure the time it takes to replace a key and certificate across the fleet, rehearse it every quarter, and hold the number under 60 minutes. That measured number, not revocation, is the control you will rely on when this happens.

Reported symptoms

The certificate for api.example.com comes from a public CA. An ACME client on web-01 obtains it, configuration management copies it to web-02 and web-03, and the whole arrangement has renewed itself on a forty-five day cycle since last November without anyone having to think about it. There is no story here. That is worth saying, because there is still no story at 08:52 UTC when this begins.

An engineer who joined three weeks ago opens support ticket 4471. She is not investigating anything. She wants to see how the reverse proxy is configured, somebody has attached a diagnostic archive taken from web-01, and unpacking it is quicker than asking for access to the host. What she finds is this:

  • etc/ssl/private/api.example.com/privkey.pem, complete and unencrypted, inside an archive created on 14 August at 07:26 UTC.
  • The archive was uploaded to a third-party vendor support portal fifteen minutes after it was made, during a latency investigation that eventually turned out to be a routing problem.
  • The same archive is attached to the internal ticket, where anyone with a staff login has been able to download it ever since.
  • Nothing is broken. Every client validates, every host is healthy, and not one monitoring signal has moved, because nothing about a copied key is visible from the inside.

She says so in the platform channel at 08:58 UTC. The incident is declared at 09:01.

Then comes the part that makes this scenario worth reading slowly. At 09:05 an engineer runs the renewal, because the obvious answer to a leaked certificate is a new certificate. It works. The tooling reports success, a certificate with a different serial and a later expiry lands on all three hosts, the reload is clean, and the channel calms down. Nothing in that sequence is false. The exposure at 09:20 is exactly what it was at 08:52.

Evidence provided

Begin with what was uploaded, because the scope of the incident is decided by the contents of one archive.

Read-only / Safethe attachment on ticket 4471, listed rather than extracted
$ tar -tzf /srv/tickets/4471/web-01-diag-2026-08-14.tar.gz | grep -E 'ssl|nginx.conf'
etc/nginx/nginx.conf
etc/nginx/conf.d/api.example.com.conf
etc/ssl/certs/api.example.com/fullchain.pem
etc/ssl/private/api.example.com/privkey.pem

Illustrative output

The certificate being in there is unremarkable; a certificate is a public document. The fourth line is the incident. The next question is whether it is the key that is live right now or a copy that has since been superseded, and there is a definitive answer to that.

Read-only / Safethe public half of the private key that was uploaded, reduced to one value
$ openssl pkey -in /run/incident/from-bundle/privkey.pem -pubout | openssl sha256
SHA2-256(stdin)= 75061de3387b8969c6c33ba0931ec0e541ad4c90a4ee2ec3e734e031af66874b

Illustrative output

Now the same value taken the other way round, from the certificate the server is presenting to the network at this moment.

Read-only / Safethe same measurement from the far side of the socket, and the same answer
$ echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null | openssl x509 -noout -pubkey | openssl sha256
SHA2-256(stdin)= 75061de3387b8969c6c33ba0931ec0e541ad4c90a4ee2ec3e734e031af66874b

Illustrative output

Two identical digests, one from a private key sitting in a vendor support portal and one from a live production service. This pair is the whole diagnosis. A private key and a certificate belong to each other precisely when their public keys match, so the archived key is not a historical artefact, it is the operative key of the service, and it has been outside the organisation for twelve days.

How long that matters for depends on the certificate.

Read-only / Safe08:59 UTC: twenty days of validity left on the certificate that key can impersonate
$ openssl x509 -in /etc/ssl/certs/api.example.com/fullchain.pem -noout -serial -dates
serial=03A7F41C6B8E250D9247B0FC5E1A6338D40B
notBefore=Aug  1 09:14:00 2026 GMT
notAfter=Sep 15 09:14:00 2026 GMT

Illustrative output

And here is the reading taken at 09:20, after the renewal that everybody believed had resolved it.

Read-only / Safe09:20 UTC: a new serial and forty-five fresh days, which is what convinced the channel
$ openssl x509 -in /etc/ssl/certs/api.example.com/fullchain.pem -noout -serial -dates
serial=0491C3E8207B6D45F0A83E19C7526BB0D82F
notBefore=Aug 26 09:05:00 2026 GMT
notAfter=Oct 10 09:05:00 2026 GMT

Illustrative output

Measured the other way, on exactly the same file, the picture is different.

Read-only / Safe09:20 UTC: the same key as before, wearing a different certificate
$ openssl x509 -in /etc/ssl/certs/api.example.com/fullchain.pem -noout -pubkey | openssl sha256
SHA2-256(stdin)= 75061de3387b8969c6c33ba0931ec0e541ad4c90a4ee2ec3e734e031af66874b

Illustrative output

One more reading, because it decides how much of the response can lean on revocation.

Read-only / Safethe certificate names where to fetch its issuer, and nothing else
$ openssl x509 -in /etc/ssl/certs/api.example.com/fullchain.pem -noout -ext authorityInfoAccess
X509v3 Authority Information Access:
  CA Issuers - URI:http://issuer.example.net/ca.crt

Illustrative output

There is no OCSP responder URI in that extension. That is no longer unusual on the public web. Let’s Encrypt removed the OCSP URI from its certificates on 7 May 2025 and switched its responder off altogether on 6 August 2025, so a runbook step that runs an OCSP query against a certificate from that CA has nothing to talk to.

Work the evidence before reading on

Everything in this incident is working correctly, including the renewal, and that is what makes it a good scenario to sit with rather than skim.

  1. Two digests, taken twenty minutes apart across a renewal, are identical while the serial number changes. State in one sentence what the renewal actually replaced.
  2. The certificate had twenty days left when the key was found. In what sense is that number the exposure, and in what sense is it not?
  3. The Certificate Transparency search came back clean and the access logs look ordinary. Name precisely what those two negative results rule out, and what they leave open.
  4. The archive is on a vendor portal, on an internal ticket, and in whatever was downloaded from either. Which parts of the copy count can you establish, and which will you have to write down as unknown?

Before continuing: the key is compromised from the moment it was copied, not from the moment it is used. Say what follows from that about how much of the remediation can be justified by the absence of any sign of misuse.

Root cause

An ordinary tool could read the key

The diagnostic bundler archives the reverse proxy configuration directory and the files it references, which is a reasonable thing for a support bundle to do and is precisely how the key got in. The script runs as an identity that can read the key, because it walks the same tree the reverse proxy walks and the reverse proxy must be able to read it.

No rule was broken. The engineer at 07:26 on 14 August was doing exactly what the runbook says to do during a latency investigation: capture the state, attach it to the ticket, give the vendor something to work with. The upload happened fifteen minutes later under the pressure of an active problem, and nobody opened the archive to see what was in it, because nobody opens an archive to see what is in it.

The general shape is worth naming. Any automated collector whose scope is a directory will eventually collect something that was never meant to leave, and the moment it does, the material is wherever collectors send things.

Renewal preserved the key it should have replaced

The second defect is invisible until you measure it, and the evidence measures it twice. The ACME client is configured to reuse its existing key pair, so every renewal since 3 November 2025 has produced a new certificate over the same private key.

That single setting does three things, none of them good. It means the key in the 14 August archive is the key in production twelve days later. It means the 09:05 renewal produced a second certificate the holder of that key can present, rather than making the archived material worthless. And it means the key is not only in the archive: it is in every backup of web-01 taken since November, in every configuration snapshot, and in any earlier diagnostic bundle built by the same script.

This is the defect that made the incident durable. The first defect produced one bad copy on one afternoon. The second guaranteed that the copy stayed useful, that the obvious remediation would not touch it, and that there is no certificate lifetime anywhere in the system that bounds the exposure by itself.

Revocation is the reflex, and it is not the control

The instinct after a key leak is to revoke, and revoking is correct. It is also much weaker than most operators expect, and a response that quietly depends on it has a gap in it.

The evidence already shows one reason: the certificate carries no OCSP responder URI, because that CA no longer runs one. The wider reason is client behaviour. Chrome does not perform online revocation checks by default and disabled revocation checking for extended validation certificates in 2022. Firefox uses CRLite, a pushed dataset rather than a live query, and does not make OCSP requests for domain-validated certificates that chain to the Mozilla root store. The overall effect is that a certificate you have revoked will continue to be accepted by a large share of the clients on the internet until it expires.

So revoke, and record it, and mean it. Then plan as though it will not be honoured, because for public TLS that is the honest default.

Resolution

  1. Record the moment the archive was created, 07:26 UTC on 14 August, as the start of the compromise. Not the moment of discovery, not the moment of upload. Every window you report and every decision about what else was exposed is measured from the copy, because that is when the key stopped being under your control.
  2. Generate a new private key. This is the step the incident is actually about, and it is not a renewal, not a reissue and not a redeploy of the same material. Create a fresh key pair on the host that will use it, with the same algorithm and parameters as before unless you have a reason to change them.
  3. Take the public key digest of the new key and compare it against the recorded digest of the compromised one before you request anything. Two different values is the gate for continuing. If they match you have reused the key, and everything after this point would be theatre.
  4. Issue a certificate against the new key and deploy it to web-01, web-02 and web-03, reloading each service so the running process picks it up. Treat the reload as the cutover: until a process has re-read its key, that host is still serving the compromised one however correct the files on disk look.
  5. Revoke the old certificate, and be clear with yourself about why. It is the correct entry to place on the record, it protects clients that do check, and in a private PKI where you operate and enforce a CRL it is a genuine control. On the public web it will not stop most clients accepting the old certificate, so it is a step you take in addition to fast replacement rather than instead of it.
  6. Enumerate the copies and pursue each one. The vendor portal attachment, the internal ticket attachment, every earlier bundle produced by the same script, the backups of web-01 taken since November, and any image built from that configuration tree. Ask the vendor in writing to delete the attachment and to confirm the deletion with a date.
  7. Ask what else was in the archive. The key is the headline and the archive is the incident: 214 configuration files went to the same two places, and anything credential-shaped among them joins its own rotation queue with its own owner.
  8. Change the bundler before the next latency investigation, not in a follow-up ticket. Give it an explicit list of files to collect rather than a directory to walk, and run it as an identity that cannot read the key directory at all.
  9. Turn off key reuse in the ACME client configuration so the next renewal produces a new key pair, and add a deployment check that compares the new certificate public key digest against the previous one and fails when they match.
  10. Write the exposure window down as a single number, from 07:26 UTC on 14 August to the moment the new key went live, and put it at the top of the report. Twelve days is the finding. Everything else is detail.

Verification

  1. Read the public key digest from the socket rather than from disk, from a host outside the deployment path. The server is the only authority on what it is presenting; a file that is correct on disk and has not been re-read by the process looks identical to a finished deployment in every other check.
  2. Repeat that reading against web-01, web-02 and web-03 individually rather than through the load balancer. Three digests, all different from the recorded compromised value, is the result. Two out of three is an unfinished rollout, and the load balancer will happily hide the third.
  3. Confirm the certificate now in service carries the expected new serial and the expected validity window, so that the record of what is deployed matches the record of what was issued.
  4. Scan the estate for the old serial and require a count of zero. Include any host that terminates TLS for this name, including ones the change plan did not mention, because the copy of the key was never limited to the hosts on the list.
  5. Confirm the revocation was accepted by the CA and record the timestamp. Then record, in the same place, that you cannot demonstrate any particular client honours it, so nobody later reads the revocation entry as proof of containment.
  6. Run the corrected diagnostic bundler and list the archive it produces. The key file must be absent. A procedure change nobody has executed is a document, and this one is easy to execute.
  7. Search Certificate Transparency for the domain again after the replacement, and set up the monitoring that repeats the search continuously. The check that matters is not the one you ran during the incident, it is the one that will run next month.
  8. State in the report what could not be established: whether the key was ever used, how many copies exist, and who holds them. An incident report that omits its own unknowns invites the reader to assume there were none.

Prevention

  • Generate a new key on every issuance, by default. Key reuse is usually a configuration option that somebody enabled for a reason that made sense at the time, and it silently converts a ninety-second operation into a permanent asset. A fresh key pair costs milliseconds and caps the age of the thing you least want copied.
  • Gate the deployment on the digest. Compare the public key digest of each newly issued certificate against the previous one and fail the deployment when they match. This is three lines of script and it is the difference between a rotation and a renewal being indistinguishable from the outside.
  • Give collectors an allow list, never a directory. Diagnostic bundlers, log shippers, configuration backups and support tools all walk trees, and a tree that contains a key will eventually ship one. Enumerate the files to collect, run the collector as an identity with no read access to the key directory, and test that it refuses.
  • Hold keys where they cannot be copied. A key generated inside an HSM or a PKCS#11 token, marked non-exportable, cannot be in a support archive at all. That converts this entire class of incident from something you respond to into something that cannot be expressed.
  • Keep validity short and renewal automatic. The remaining life of a leaked certificate is the one part of the exposure you can bound in advance, and it is bounded by policy rather than by luck. Forty-five day certificates renewed on schedule cap it at weeks; an annual certificate caps it at nothing you would want to put in a report.
  • Monitor Certificate Transparency for every name you own. Page within 15 minutes on any issuance nobody requested. It is the only external channel that will tell you a key of yours is being used by someone else, and it costs nothing to run.
  • Measure the replacement, then rehearse it. Time a full key and certificate replacement across the fleet, hold it under 60 minutes, and repeat the drill quarterly. That measured number, not the revocation entry, is what will actually be protecting you the next time an archive goes somewhere it should not.