{"id":5944,"date":"2026-10-30T09:07:34","date_gmt":"2026-10-30T06:07:34","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/?p=5944"},"modified":"2026-10-30T06:26:56","modified_gmt":"2026-10-30T03:26:56","slug":"ssl-private-key-compromise-replace-reissue-revoke","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/ssl-private-key-compromise-replace-reissue-revoke\/","title":{"rendered":"SSL Private Key Exposed: Replace the Key, Reissue and Revoke Safely"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><div class=\"aiw-summary\">\n<p class=\"aiw-box-title\">Quick summary<\/p>\n<p>If your SSL private key is exposed, deleting it or renewing the certificate is not enough. Restrict access to the exposed file, obtain a certificate with a new key, and revoke the certificates that use the exposed key-ask their issuer to mark them as no longer valid.<\/p>\n<ul>\n<li>Preserve records showing when and where exposure occurred.<\/li>\n<li>Verify that the replacement uses a different key.<\/li>\n<li>Prioritize immediate revocation during active misuse, even if it disrupts access to your site.<\/li>\n<li>Check the deployed certificate from outside your server&#8217;s network and fix what allowed the exposure.<\/li>\n<\/ul>\n<\/div>\n<p>Your private key is the secret your server uses to prove its identity over HTTPS. If it reaches a Git repository, support ticket or public directory, deleting it cannot invalidate downloaded copies. A browser showing a secure connection does not prove the key remains private.<\/p>\n<p>You will identify affected certificates, deploy a new key and certificate, and confirm revocation of the old certificates. The walkthrough follows an explicitly hypothetical example scenario using Ubuntu, the Nginx web server and Certbot, a tool that automates certificate requests and renewals. It assumes only the private key leaked to Git. If the server or certificate management account is also compromised, you need a trusted, clean environment and secure account access before replacing the key.<\/p>\n<div id=\"toc_container\" role=\"navigation\" aria-label=\"Table of Contents\" data-nosnippet class=\"toc_transparent no_bullets toc_numbered toc_title_center\"><p class=\"toc_title\">\u0130\u00e7indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#First_response_to_an_SSL_private_key_compromise\"><span class=\"toc_number toc_depth_1\">1.<\/span> First response to an SSL private key compromise<\/a><ul><li><a href=\"#Restrict_access_first_and_preserve_incident_evidence\"><span class=\"toc_number toc_depth_2\">1.1.<\/span> Restrict access first and preserve incident evidence<\/a><\/li><li><a href=\"#Identify_certificates_that_share_the_key\"><span class=\"toc_number toc_depth_2\">1.2.<\/span> Identify certificates that share the key<\/a><\/li><li><a href=\"#Decide_how_urgently_to_revoke\"><span class=\"toc_number toc_depth_2\">1.3.<\/span> Decide how urgently to revoke<\/a><\/li><\/ul><\/li><li><a href=\"#Generate_a_new_private_key_and_certificate_in_a_clean_environment\"><span class=\"toc_number toc_depth_1\">2.<\/span> Generate a new private key and certificate in a clean environment<\/a><ul><li><a href=\"#Install_the_tools_and_save_the_current_settings\"><span class=\"toc_number toc_depth_2\">2.1.<\/span> Install the tools and save the current settings<\/a><\/li><li><a href=\"#Prepare_DNS_and_HTTP_validation\"><span class=\"toc_number toc_depth_2\">2.2.<\/span> Prepare DNS and HTTP validation<\/a><\/li><li><a href=\"#Create_a_separate_record_and_verify_that_the_key_changed\"><span class=\"toc_number toc_depth_2\">2.3.<\/span> Create a separate record and verify that the key changed<\/a><\/li><\/ul><\/li><li><a href=\"#Deploy_the_new_certificate_and_revoke_compromised_certificates\"><span class=\"toc_number toc_depth_1\">3.<\/span> Deploy the new certificate and revoke compromised certificates<\/a><ul><li><a href=\"#Check_the_key_match_and_update_Nginx_paths\"><span class=\"toc_number toc_depth_2\">3.1.<\/span> Check the key match and update Nginx paths<\/a><\/li><li><a href=\"#Verify_the_certificate_served_externally\"><span class=\"toc_number toc_depth_2\">3.2.<\/span> Verify the certificate served externally<\/a><\/li><li><a href=\"#Revoke_old_certificates_because_of_the_exposed_key\"><span class=\"toc_number toc_depth_2\">3.3.<\/span> Revoke old certificates because of the exposed key<\/a><\/li><\/ul><\/li><li><a href=\"#Troubleshoot_the_setup_and_prevent_another_exposure\"><span class=\"toc_number toc_depth_1\">4.<\/span> Troubleshoot the setup and prevent another exposure<\/a><ul><li><a href=\"#Distinguish_stale_certificates_from_validation_errors\"><span class=\"toc_number toc_depth_2\">4.1.<\/span> Distinguish stale certificates from validation errors<\/a><\/li><li><a href=\"#Correct_key_permissions_without_broadening_access\"><span class=\"toc_number toc_depth_2\">4.2.<\/span> Correct key permissions without broadening access<\/a><\/li><li><a href=\"#Test_renewal_and_reloading_together\"><span class=\"toc_number toc_depth_2\">4.3.<\/span> Test renewal and reloading together<\/a><\/li><li><a href=\"#Clean_Git_history_and_check_other_endpoints\"><span class=\"toc_number toc_depth_2\">4.4.<\/span> Clean Git history and check other endpoints<\/a><\/li><\/ul><\/li><li><a href=\"#Frequently_Asked_Questions\"><span class=\"toc_number toc_depth_1\">5.<\/span> Frequently Asked Questions<\/a><ul><li><a href=\"#Can_an_exposed_private_key_decrypt_past_HTTPS_traffic\"><span class=\"toc_number toc_depth_2\">5.1.<\/span> Can an exposed private key decrypt past HTTPS traffic?<\/a><\/li><li><a href=\"#Should_you_also_reset_WordPress_and_customer_passwords\"><span class=\"toc_number toc_depth_2\">5.2.<\/span> Should you also reset WordPress and customer passwords?<\/a><\/li><li><a href=\"#Does_sharing_a_CSR_file_expose_your_private_key\"><span class=\"toc_number toc_depth_2\">5.3.<\/span> Does sharing a CSR file expose your private key?<\/a><\/li><li><a href=\"#Does_enabling_HSTS_protect_you_against_private_key_exposure\"><span class=\"toc_number toc_depth_2\">5.4.<\/span> Does enabling HSTS protect you against private key exposure?<\/a><\/li><\/ul><\/li><li><a href=\"#Checklist_before_closing_the_incident\"><span class=\"toc_number toc_depth_1\">6.<\/span> Checklist before closing the incident<\/a><\/li><\/ul><\/div>\n<h2><span id=\"First_response_to_an_SSL_private_key_compromise\">First response to an SSL private key compromise<\/span><\/h2>\n<p>Once you confirm that an SSL private key has been exposed, assume someone else may have copied it. Deleting the file limits further access; it does not invalidate copies already taken. Routine certificate renewal may also reuse the same key. Your goal is to <strong>switch to a new key and revoke the certificates tied to the exposed key.<\/strong> Revocation tells clients that the issuing certificate authority no longer considers those certificates valid.<\/p>\n<figure class=\"aiw-figure\" data-aiw-visual-id=\"5940\" data-aiw-visual-key=\"de9e493f4ebe414babe18789a39003ad51833c1a456b706a39053bd0eab9d3b4\"><img decoding=\"async\" src=\"https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-0af5343f790a648bc402.png\" alt=\"Response flow from restricting access and identifying affected certificates to choosing revocation priority based on active misuse.\" width=\"840\" height=\"987\" loading=\"lazy\" class=\"aiw-article-visual wp-image-5940\" srcset=\"https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-0af5343f790a648bc402.png 840w, https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-0af5343f790a648bc402-255x300.png 255w, https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-0af5343f790a648bc402-768x902.png 768w\" sizes=\"auto, (max-width: 840px) 100vw, 840px\" \/><figcaption>Workflow diagram: The response order changes when active misuse is detected: immediate revocation may be necessary even if it disrupts access to the site.<\/figcaption><\/figure>\n<div class=\"aiw-note aiw-note-example\">\n<p class=\"aiw-box-title\">Example scenario<\/p>\n<p>In this hypothetical environment, you use Ubuntu 24.04 with the Nginx web server and Certbot to manage certificates. example.com and www.example.com point to the same server. There is no content delivery network (CDN), which would otherwise sit between visitors and your server. Ports 80 and 443 are open for HTTP and HTTPS, and you have sudo privileges to run administrative commands.<\/p>\n<p>Only the private key has leaked to a Git repository, and no active misuse has been detected. The server and the ACME account-the account Certbot uses for automated certificate operations-are assumed to remain secure. If the server is compromised, generate the replacement key on a clean server instead. If the ACME account is compromised, secure or replace that account before requesting the replacement certificate.<\/p>\n<\/div>\n<h3><span id=\"Restrict_access_first_and_preserve_incident_evidence\">Restrict access first and preserve incident evidence<\/span><\/h3>\n<p>In your Git hosting service&#8217;s repository settings, find the visibility or access controls and restrict access to authorized people. If the key is attached to a support ticket, ask for access to the attachment to be blocked. If it is in a web directory, move the file outside the web root-the directory whose files your web server makes available to visitors. Open the old file or attachment address in a browser where you are not signed in and confirm that it can no longer be read or downloaded.<\/p>\n<p>Before cleaning the repository history, record the earliest exposure time you can establish, the file path, the relevant commit ID-the identifier for the recorded Git change-and any available access logs. Do not paste the key into another support ticket. This example assumes the current certificate is at the path below. In the server terminal, create an incident directory accessible only to the root administrator account and copy the old certificate into it:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo install -d -m 700 \/root\/ssl-incident\nsudo cp -L \/etc\/letsencrypt\/live\/example.com\/cert.pem \/root\/ssl-incident\/old-cert.pem<\/code><\/pre>\n<p>When the commands finish without errors, the incident directory contains a copy of the old certificate. You will use this copy to identify the certificate when requesting revocation. It is not a rollback point for reusing the exposed key.<\/p>\n<h3><span id=\"Identify_certificates_that_share_the_key\">Identify certificates that share the key<\/span><\/h3>\n<p>Different filenames do not mean the keys are different. With OpenSSL installed on the server, run the following commands in its terminal. Each produces a SHA-256 hash, a fingerprint you can compare: the first uses the public key derived from the private key, and the second uses the public key contained in the certificate. Neither command prints the private key itself:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo openssl pkey -in \/etc\/letsencrypt\/live\/example.com\/privkey.pem -pubout -outform DER | openssl dgst -sha256\nopenssl x509 -in \/etc\/letsencrypt\/live\/example.com\/cert.pem -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256<\/code><\/pre>\n<p>If every operation in both commands completes without errors, matching hashes show that the certificate matches this private key. A hash line alone does not prove that the check succeeded. If you see a file-reading or permission error, resolve access to the specified file and rerun the check before comparing outputs. Use administrative privileges where needed rather than making the files more widely readable.<\/p>\n<p>For each other existing certificate, replace the certificate path in the second command and compare its resulting hash with the first command&#8217;s hash. Review these findings alongside your certificate provider&#8217;s records. List the unexpired certificates that use the exposed key and the servers where they are installed. Include certificates from before the latest renewal: obtaining a new certificate does not mean the previous one has expired or been revoked.<\/p>\n<h3><span id=\"Decide_how_urgently_to_revoke\">Decide how urgently to revoke<\/span><\/h3>\n<p>If there are no signs of active misuse, promptly prepare and deploy a new key and certificate, then revoke the old certificates without delay. Deploying the replacement first reduces the risk of certificate errors for visitors. Keep this transition short: until you switch, your site still relies on the exposed key.<\/p>\n<p><strong>If there is active misuse, prioritize the revocation request without waiting for the replacement setup.<\/strong> Use the revocation process provided by the issuing certificate authority-the organization that issued your certificate-and select <code>keyCompromise<\/code> as the reason. Revocation may disrupt access to the site while it still serves the old certificate. Browsers and other clients check revocation differently, so it does not guarantee that misuse stops immediately everywhere. After submitting the request, complete the switch to the new key and certificate.<\/p>\n<h2><span id=\"Generate_a_new_private_key_and_certificate_in_a_clean_environment\">Generate a new private key and certificate in a clean environment<\/span><\/h2>\n<p>In this hypothetical example, the server and the ACME account used for automated certificate operations remain secure, so you can generate the new key on the same Ubuntu server. Connect to the server through SSH with sudo privileges. You will create a separate Certbot record for the replacement certificate, allowing you to prepare the files without changing the certificate paths Nginx currently uses.<\/p>\n<h3><span id=\"Install_the_tools_and_save_the_current_settings\">Install the tools and save the current settings<\/span><\/h3>\n<p>The example uses Ubuntu 24.04, with Nginx already serving the site and Certbot installed through Ubuntu packages. Certbot requests the certificate, OpenSSL compares keys, and the remaining tools check DNS and connectivity. Run the following commands in the server terminal to install the tools, save a copy of the site configuration in the root-only incident directory, and list Certbot&#8217;s existing certificates. Keep this configuration copy so you can undo preparation changes if needed:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo apt update\nsudo apt install certbot openssl dnsutils curl\nsudo install -d -m 700 \/root\/ssl-incident\nsudo cp -a \/etc\/nginx\/sites-available\/example.com \/root\/ssl-incident\/example.com.nginx.before\nsudo certbot certificates<\/code><\/pre>\n<p>The last command should list the old <code>example.com<\/code> record. The new name, <code>example-com-incident<\/code>, must not already appear. If it does, stop and identify which certificate it belongs to and where it is used. This check prevents you from accidentally changing an existing certificate record.<\/p>\n<h3><span id=\"Prepare_DNS_and_HTTP_validation\">Prepare DNS and HTTP validation<\/span><\/h3>\n<p>In your DNS management panel, check that the <code>A<\/code> records for both domain names point to this server&#8217;s public IPv4 address. If an <code>AAAA<\/code> record exists, it publishes an IPv6 address, and the same site must be reachable at that address too. Run the first four commands below to inspect the DNS responses. If a record is wrong, save its current value so you can undo the change, then correct it in the panel. Wait until the responses show the intended addresses before requesting the certificate.<\/p>\n<p>The last two commands create the directory for domain-validation files and open the Nginx site configuration in the nano text editor. If nano is not installed, install it through Ubuntu&#8217;s package manager before running the final command:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">dig +short example.com A\ndig +short example.com AAAA\ndig +short www.example.com A\ndig +short www.example.com AAAA\nsudo install -d -m 755 \/var\/www\/acme\/.well-known\/acme-challenge\nsudo nano \/etc\/nginx\/sites-available\/example.com<\/code><\/pre>\n<p>HTTP-01 validation lets Let&#8217;s Encrypt confirm that you control a domain by reading a temporary file through its website. Port 80 must be reachable from the internet for this check. In the configuration file you opened, add the following section to the <code>listen 80<\/code> server block that handles both domain names. A server block contains Nginx settings for a site; the section below tells it where to find validation files. Here, <code>root<\/code> sets the base directory used to locate those files:<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">location ^~ \/.well-known\/acme-challenge\/ {\n    root \/var\/www\/acme;\n    default_type text\/plain;\n    try_files $uri =404;\n}<\/code><\/pre>\n<p>If the server block has a <code>return<\/code> directive that redirects to HTTPS outside its location sections, that redirect also applies to validation requests. To serve the file directly over HTTP, move that redirect into the existing <code>location \/<\/code> section; do not create a second <code>location \/<\/code>. Save the file in nano with Ctrl+O, press Enter to confirm the filename, and exit with Ctrl+X.<\/p>\n<p>Run the following commands to test the configuration, reload Nginx only if the test passes, and create a temporary file for checking external access:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo nginx -t &amp;&amp; sudo systemctl reload nginx\nprintf &#039;acme-ok\\n&#039; | sudo tee \/var\/www\/acme\/.well-known\/acme-challenge\/probe<\/code><\/pre>\n<p>The configuration test should report that its syntax is OK and the test is successful. If it fails, the reload command will not run. Correct the reported line before continuing. To undo the preparation changes, copy example.com.nginx.before from the incident directory back over the original site configuration file, then repeat the test-and-reload command. This restores the earlier configuration only; it does not make the exposed key safe to use.<\/p>\n<p>Next, run these two commands in a terminal on a computer with curl installed, using another internet connection. Both should return <code>acme-ok<\/code>. That response confirms that each domain is reachable from outside the server and that Nginx serves the test file from the directory prepared for validation. If either request fails, resolve that access problem before requesting the certificate:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">curl --fail example.com\/.well-known\/acme-challenge\/probe\ncurl --fail www.example.com\/.well-known\/acme-challenge\/probe<\/code><\/pre>\n<h3><span id=\"Create_a_separate_record_and_verify_that_the_key_changed\">Create a separate record and verify that the key changed<\/span><\/h3>\n<p>Once both addresses return the expected response, return to the server terminal. The following command requests a certificate for both domain names using the web root you prepared. If prompted, enter an email address you monitor and review the terms of service. <code>--duplicate<\/code> allows a separate Certbot record for the same domain names; <code>certonly<\/code> obtains the certificate without changing Nginx&#8217;s certificate paths. The new record is named example-com-incident. The command below spans three lines; join them with spaces before running it:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo certbot certonly --webroot -w \/var\/www\/acme \n  --cert-name example-com-incident --duplicate \n  -d example.com -d www.example.com<\/code><\/pre>\n<p><strong>Run the command above as a single line in the terminal.<\/strong> On success, Certbot should report the new files under <code>\/etc\/letsencrypt\/live\/example-com-incident\/<\/code>. If the request fails, resolve the reported error before continuing. Next, compare the SHA-256 hashes of the old certificate&#8217;s public key and the public key derived from the new private key. The first two commands display these hashes; the last removes the temporary access test file:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo openssl x509 -in \/root\/ssl-incident\/old-cert.pem -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256\nsudo openssl pkey -in \/etc\/letsencrypt\/live\/example-com-incident\/privkey.pem -pubout -outform DER | openssl dgst -sha256\nsudo rm \/var\/www\/acme\/.well-known\/acme-challenge\/probe<\/code><\/pre>\n<p>Every operation in the first two commands must complete without errors, and the hashes must be <strong>different<\/strong>. A hash can still appear after a file-reading or key-processing error, so resolve any such error and rerun the comparison. Different hashes from successful commands confirm that the old key was not reused. If the hashes match, stop: the replacement must use a different key. Nginx still uses the old certificate paths at this point. Before changing them, you will check that the new certificate matches the new private key.<\/p>\n<h2><span id=\"Deploy_the_new_certificate_and_revoke_compromised_certificates\">Deploy the new certificate and revoke compromised certificates<\/span><\/h2>\n<h3><span id=\"Check_the_key_match_and_update_Nginx_paths\">Check the key match and update Nginx paths<\/span><\/h3>\n<p>Before changing Nginx, check that the new certificate matches the new private key. Run the following commands in the server&#8217;s SSH terminal. Every operation must complete without errors, and both commands must return the same SHA-256 hash of the public key. If reading the certificate produces a permission error, run the check in a terminal with root privileges; do not broaden the file permissions.<\/p>\n<figure class=\"aiw-figure\" data-aiw-visual-id=\"5942\" data-aiw-visual-key=\"83ae3c3898412c0bf9e3294faeaea599c54b9134c9b4b02851b82d441755d97e\"><img decoding=\"async\" src=\"https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-d46786a987bac89c544c.png\" alt=\"Five steps from matching the new key and certificate through Nginx deployment, external verification and revocation of old certificates.\" width=\"840\" height=\"1149\" loading=\"lazy\" class=\"aiw-article-visual wp-image-5942\" srcset=\"https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-d46786a987bac89c544c.png 840w, https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-d46786a987bac89c544c-219x300.png 219w, https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-d46786a987bac89c544c-749x1024.png 749w, https:\/\/www.dchost.com\/blog\/wp-content\/uploads\/2026\/10\/dchost-flow-en-d46786a987bac89c544c-768x1051.png 768w\" sizes=\"auto, (max-width: 840px) 100vw, 840px\" \/><figcaption>Workflow diagram: In the example scenario, no active misuse is detected, so the new certificate is deployed first. Certificates tied to the exposed key are then revoked.<\/figcaption><\/figure>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">openssl x509 -in \/etc\/letsencrypt\/live\/example-com-incident\/cert.pem -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256\nsudo openssl pkey -in \/etc\/letsencrypt\/live\/example-com-incident\/privkey.pem -pubout -outform DER | openssl dgst -sha256<\/code><\/pre>\n<p>If the hashes differ, stop and check that both file paths belong to the certificate record you just created. Once the hashes match without errors, open the site configuration to update the paths Nginx uses:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo nano \/etc\/nginx\/sites-available\/example.com<\/code><\/pre>\n<p>In the blocks serving HTTPS for <code>example.com<\/code> and <code>www.example.com<\/code>, replace the two existing certificate lines with the following. <code>ssl_certificate<\/code> points to the chain file containing your server certificate and the intermediate certificates that connect it to a trusted certificate authority. <code>ssl_certificate_key<\/code> points to the private key. Replace the existing paths rather than adding another pair beside the old lines.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">ssl_certificate \/etc\/letsencrypt\/live\/example-com-incident\/fullchain.pem;\nssl_certificate_key \/etc\/letsencrypt\/live\/example-com-incident\/privkey.pem;<\/code><\/pre>\n<p>In nano, save with Ctrl+O, press Enter to confirm the filename, and exit with Ctrl+X. Then run the following command on the server. It tests the Nginx configuration and reloads the running service only if the test succeeds:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo nginx -t &amp;&amp; sudo systemctl reload nginx<\/code><\/pre>\n<p>The test should report <code>syntax is ok<\/code> and <code>test is successful<\/code>. If it reports an error, the reload does not run, and Nginx continues serving with its previously loaded configuration. Correct the reported file or line and try again. Use the configuration backup from the previous section for comparison; <strong>do not redeploy the exposed key as a rollback.<\/strong> After the test passes, check that the reload also completes without errors before checking the certificate externally.<\/p>\n<h3><span id=\"Verify_the_certificate_served_externally\">Verify the certificate served externally<\/span><\/h3>\n<p>A successful Nginx test alone does not prove that visitors receive the new certificate. First, run the following command on the server to obtain the new certificate&#8217;s fingerprint for comparison. Unlike the public key hash you checked earlier, this fingerprint identifies the entire certificate:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">openssl x509 -in \/etc\/letsencrypt\/live\/example-com-incident\/cert.pem -noout -fingerprint -sha256<\/code><\/pre>\n<p>If the file cannot be read because of its permissions, repeat the check with root privileges rather than changing those permissions. Keep the resulting fingerprint for the external comparison.<\/p>\n<p>Next, check both domain names from a Bash terminal on a computer with OpenSSL installed, using another internet connection. <code>-servername<\/code> tells the server which site&#8217;s certificate to present. The verification options check that the certificate matches the domain name and has a chain of trust leading to a trusted certificate authority. The checking computer&#8217;s store of trusted root certificates must be up to date.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">set -o pipefail\nfor host in example.com www.example.com; do\n  printf &#039;\\n%s\\n&#039; &quot;$host&quot;\n  openssl s_client -connect &quot;${host}:443&quot; -servername &quot;$host&quot; -verify_hostname &quot;$host&quot; -verify_return_error &lt;\/dev\/null | openssl x509 -noout -fingerprint -sha256\ndone<\/code><\/pre>\n<p>For each domain, the displayed fingerprint must match the value obtained on the server, and the connection must complete without verification errors. Inspect the error output for both connections; a printed fingerprint alone does not establish that verification passed. If both IPv4 and IPv6 are published, repeat the checks with <code>-4<\/code> and then <code>-6<\/code> added to the <code>s_client<\/code> command. Both connection paths must serve the new certificate and pass verification.<\/p>\n<h3><span id=\"Revoke_old_certificates_because_of_the_exposed_key\">Revoke old certificates because of the exposed key<\/span><\/h3>\n<p>In this hypothetical example scenario, no active misuse has been detected, so submit the revocation request for the old certificate immediately after verifying the new certificate externally. Run the following command in the server&#8217;s SSH terminal. It identifies the certificate to revoke using the copy you saved in the incident directory. <code>keycompromise<\/code> is the revocation reason that reports an exposed private key:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo certbot revoke --cert-path \/root\/ssl-incident\/old-cert.pem --reason keycompromise --no-delete-after-revoke<\/code><\/pre>\n<p>The request is authorized through the existing ACME account-the account used to manage automated certificate operations-which is assumed to remain secure in this example. The <code>--no-delete-after-revoke<\/code> option keeps the local certificate record after revocation. Keeping or deleting local files does not change the certificate&#8217;s revocation status. Repeat the operation for every other unexpired certificate tied to the exposed key, using the corresponding certificate file and the issuing authority&#8217;s revocation process for each.<\/p>\n<p>Certbot should report that revocation succeeded. If it reports an error or the response is unclear, inspect <code>\/var\/log\/letsencrypt\/letsencrypt.log<\/code> for details and, if necessary, seek confirmation through the issuing certificate authority&#8217;s revocation channel. Save the success response in your incident record. <strong>The browser&#8217;s padlock icon does not prove that the old certificate has been revoked;<\/strong> browsers and other clients differ in how they check and enforce revocation information.<\/p>\n<h2><span id=\"Troubleshoot_the_setup_and_prevent_another_exposure\">Troubleshoot the setup and prevent another exposure<\/span><\/h2>\n<h3><span id=\"Distinguish_stale_certificates_from_validation_errors\">Distinguish stale certificates from validation errors<\/span><\/h3>\n<p>If the external check shows the old certificate and your domains publish both IPv4 and IPv6 addresses, compare the results for each connection path first. If only one serves the old certificate, its DNS record may point to another server. If both show the old certificate, inspect the configuration Nginx reads and its reload logs. Run these commands in the Ubuntu server&#8217;s SSH terminal:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo nginx -T 2&gt;&amp;1 | grep -E &#039;server_name|ssl_certificate&#039;\nsudo journalctl -u nginx -n 30 --no-pager<\/code><\/pre>\n<p>The domain&#8217;s HTTPS block should use the <code>example-com-incident<\/code> paths. The first command displays domain names and certificate settings; the second shows recent Nginx service logs, where you can look for reload failures. If you find an incorrect path or a conflicting block handling the same domain, save a copy of the current site configuration and correct only the relevant setting. Then run <code>sudo nginx -t &amp;&amp; sudo systemctl reload nginx<\/code> and repeat the external fingerprint check. The served fingerprint should now match the new certificate, confirming that visitors receive it.<\/p>\n<p>If domain validation fails, recreate the <code>probe<\/code> file from the earlier section and request its address through both domain names from outside the server. A <code>404<\/code> response means the request reached a web server but the validation file was not served; inspect the web root, the server block handling the request, and redirects. For a timeout, check DNS responses, the firewall, and access to port 80. Correct the setting indicated by these checks, then confirm that both addresses return the expected file contents before repeating the certificate request.<\/p>\n<h3><span id=\"Correct_key_permissions_without_broadening_access\">Correct key permissions without broadening access<\/span><\/h3>\n<p>If Nginx reports a <code>Permission denied<\/code> error when reading the new key, first inspect its symbolic link-the reference that points to the actual key file-and the permissions on each directory in that path. Run these commands on the server to inspect the path and the account configured to start the Nginx service:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo namei -l \/etc\/letsencrypt\/live\/example-com-incident\/privkey.pem\nsudo systemctl show nginx -p User<\/code><\/pre>\n<p>In the standard Ubuntu Nginx service used here, the main process starts as root; an empty <code>User=<\/code> field is expected. This main process reads the certificate and key, so you do not need to grant the <code>www-data<\/code> user access to the key. If the output shows incorrect ownership or file permissions for the new key, record the current values in the incident log before changing them. The following commands set ownership and permissions on this key&#8217;s target file, then test whether Nginx can load its configuration:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo chown root:root \/etc\/letsencrypt\/live\/example-com-incident\/privkey.pem\nsudo chmod 600 \/etc\/letsencrypt\/live\/example-com-incident\/privkey.pem\nsudo nginx -t<\/code><\/pre>\n<p>These settings allow only root to read and write the key. A successful Nginx test confirms that the configuration can load with these permissions; apply the pending change using the earlier test-and-reload step. If the test still fails, inspect the exact path in the error message and the permissions on its parent directories. Use the recorded values to review any change you need to undo, but do not restore permissions that expose the key. Correct the affected file or directory rather than granting access across the entire <code>\/etc\/letsencrypt<\/code> tree.<\/p>\n<h3><span id=\"Test_renewal_and_reloading_together\">Test renewal and reloading together<\/span><\/h3>\n<p>A working certificate today does not prove that its next renewal will succeed. In the server&#8217;s SSH terminal, run a test renewal for the new record, then check the Certbot timer. This timer schedules automatic renewal checks and is installed by the Ubuntu package used in this example:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo certbot renew --cert-name example-com-incident --dry-run\nsudo systemctl status certbot.timer --no-pager<\/code><\/pre>\n<p><code>--dry-run<\/code> tests the renewal process without changing the live certificate; the output should report a successful test. If the timer is inactive, run <code>sudo systemctl enable --now certbot.timer<\/code> and use the status command to confirm that it is active.<\/p>\n<p>Next, add a <em>deploy hook<\/em>-a script that runs after a successful renewal-so Nginx reads the renewed files. The commands below create reload-nginx.sh in Certbot&#8217;s deploy-hook directory. If that file already exists, copy it to the protected incident directory and inspect its contents first. The commands overwrite it, so retain any other required operations rather than replacing the file blindly. If your changes cause problems, restore the saved script; this does not require returning to the exposed key.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo install -d -m 755 \/etc\/letsencrypt\/renewal-hooks\/deploy\nsudo tee \/etc\/letsencrypt\/renewal-hooks\/deploy\/reload-nginx.sh &gt;\/dev\/null &lt;&lt;&#039;EOF&#039;\n#!\/bin\/sh\n\/usr\/sbin\/nginx -t &amp;&amp; \/usr\/bin\/systemctl reload nginx\nEOF\nsudo chmod 750 \/etc\/letsencrypt\/renewal-hooks\/deploy\/reload-nginx.sh\nsudo certbot renew --cert-name example-com-incident --dry-run --run-deploy-hooks<\/code><\/pre>\n<p>This second test runs the hook after a successful simulated renewal. The hook tests Nginx&#8217;s configuration and, if it passes, reloads Nginx with the current live certificate. Check the output for a successful renewal test and no hook errors. If the hook fails, correct the reported configuration or script error and repeat this test.<\/p>\n<p>Before removing the old record, confirm that revocation succeeded and that no service, including Nginx, still uses the old certificate paths. Preserve the necessary incident records in the restricted directory, then use <code>sudo certbot delete --cert-name example.com<\/code> to remove the old Certbot record and its associated local certificate files. This does not replace revocation; it prevents the unused record from being renewed again.<\/p>\n<h3><span id=\"Clean_Git_history_and_check_other_endpoints\">Clean Git history and check other endpoints<\/span><\/h3>\n<p>Deleting the file from the latest commit-the latest recorded change-does not remove historical copies. Temporarily pause writes to the repository so new changes do not conflict with cleanup. Before rewriting history, preserve a copy in the restricted incident archive. This gives you a recovery copy if cleanup removes needed project data, but it may also contain the exposed key: do not share it like an ordinary project backup or restore it to the public repository.<\/p>\n<p>Use <code>git-filter-repo<\/code>, a tool for rewriting Git history, to remove the file from every path it occupied in the past; cleaning only its current filename may miss earlier names. Follow your repository hosting service&#8217;s sensitive-data removal process to update branches and tags and address retained copies. Ask your team to clone the cleaned repository again so old history is not accidentally pushed back. You cannot retrieve every repository fork, cached copy, or downloaded file. History cleanup therefore does not replace key replacement and certificate revocation.<\/p>\n<p>Do not put the new key in Git, a support ticket, or an application package. <code>.gitignore<\/code> is only an additional safeguard; it does not protect files Git already tracks. Follow the approach to <a href=\"https:\/\/www.dchost.com\/blog\/en\/managing-env-files-and-secrets-on-a-vps-safely\/\">managing secrets on a VPS<\/a>: keep the key outside the web root and restrict access to the services that need it. Add a scan for secrets such as private keys and passwords before pushing to the repository to help prevent the same mistake from recurring.<\/p>\n<p>The example scenario has no CDN, or content delivery network. If your CDN handles visitors&#8217; HTTPS connections, the certificate presented to visitors and the certificate on the origin server-the server hosting your site-are managed separately. Replace and revoke certificates wherever the exposed key is used; if both sides use the same key, include both in the response. With Cloudflare&#8217;s Full (Strict) mode, the visitor-to-CDN and CDN-to-origin connections use separate TLS connections, and Cloudflare validates the origin certificate. That setting does not replace either side&#8217;s compromised key.<\/p>\n<p>If you use cPanel or Plesk, install the new private key, certificate, and intermediate certificate chain together in the domain&#8217;s SSL\/TLS management screen. The intermediate certificates connect your certificate to a trusted certificate authority. Reissuing a certificate alone may not change the key; regardless of provider, request the replacement using a new key. For a Let&#8217;s Encrypt wildcard certificate, which covers subdomains at the same level, you must use DNS-01 validation instead of HTTP-01. DNS-01 proves domain control through a DNS record. Account for this alternative validation process when <a href=\"https:\/\/www.dchost.com\/blog\/en\/ssl-certificate-automation-tools-how-to-choose-and-implement-them-safely\/\">choosing SSL certificate automation tools<\/a>, and protect the DNS access keys used by automation as secrets.<\/p>\n<h2><span id=\"Frequently_Asked_Questions\">Frequently Asked Questions<\/span><\/h2>\n<h3><span id=\"Can_an_exposed_private_key_decrypt_past_HTTPS_traffic\">Can an exposed private key decrypt past HTTPS traffic?<\/span><\/h3>\n<p>Not necessarily. The answer depends on the key exchange method used to establish the connection. <strong>Forward secrecy<\/strong> protects past traffic by using temporary, session-specific keys, even if the certificate&#8217;s private key is exposed later. TLS is the protocol that secures HTTPS. Connections using TLS 1.3, and TLS 1.2 connections using temporary key exchange methods such as ECDHE, cannot be decrypted solely by obtaining the certificate&#8217;s private key.<\/p>\n<p>If older TLS connections used RSA key exchange, an attacker could use the exposed key to decrypt previously recorded traffic. Having an RSA certificate alone does not establish this risk; the connection&#8217;s key exchange method is what matters. To assess past exposure, review the TLS settings from the incident period and any available connection logs. Today&#8217;s settings do not prove how past connections were established.<\/p>\n<h3><span id=\"Should_you_also_reset_WordPress_and_customer_passwords\">Should you also reset WordPress and customer passwords?<\/span><\/h3>\n<p>Not automatically. An SSL private key compromise alone does not show that WordPress passwords or the database were compromised. If the exposure is limited to one file, as in the hypothetical example scenario, and there is no other evidence of misuse, you do not necessarily need to require password resets for every user.<\/p>\n<p>If the same repository also exposed a database password, access key, or signing secret used to verify sessions, replace each of those separately. If there is evidence of credentials being collected through a fake site, unauthorized server access, or the ability to decrypt traffic containing passwords, reset the affected accounts&#8217; passwords and terminate their sessions. Base your decision on the data exposed and the scope of unauthorized access, not just the private key exposure.<\/p>\n<h3><span id=\"Does_sharing_a_CSR_file_expose_your_private_key\">Does sharing a CSR file expose your private key?<\/span><\/h3>\n<p>No. A <strong>CSR<\/strong>, or certificate signing request, contains the public key and certificate application details, not the private key. The private key cannot be extracted from a normal CSR. You may still prefer to keep the domain or organization details it contains out of public view.<\/p>\n<p>Check the header line inside the file rather than trusting its extension. <code>BEGIN CERTIFICATE REQUEST<\/code> indicates a request, while <code>BEGIN PRIVATE KEY<\/code> or <code>BEGIN RSA PRIVATE KEY<\/code> indicates a private key. These are common header examples, not a complete list; a different header does not mean the file is safe to share. Before attaching a package to a support ticket, also check that it does not contain a private key file alongside the CSR.<\/p>\n<h3><span id=\"Does_enabling_HSTS_protect_you_against_private_key_exposure\">Does enabling HSTS protect you against private key exposure?<\/span><\/h3>\n<p>No. <strong>HSTS<\/strong>, or HTTP Strict Transport Security, is a security policy that tells browsers to connect to your site only over HTTPS. It helps prevent connections from being downgraded to unencrypted HTTP; it neither replaces the private key nor revokes the certificate.<\/p>\n<p>If an attacker can redirect visitors&#8217; traffic to a server they control and use the exposed key with a certificate the browser still accepts, HSTS alone is not enough. Keep HSTS enabled to maintain your HTTPS connection policy, but do not treat it as a substitute for replacing the key and revoking the affected certificates.<\/p>\n<h2><span id=\"Checklist_before_closing_the_incident\">Checklist before closing the incident<\/span><\/h2>\n<p>A site that opens without browser warnings does not, by itself, mean your response is complete. Check each item below before closing the incident record. Save verification outputs in the protected incident directory without including the private key.<\/p>\n<ul>\n<li><strong>The new key is genuinely different:<\/strong> The public key fingerprints compared earlier differ between the old certificate and the new private key. The new certificate&#8217;s public key also matches the new private key. Together, these checks confirm that you replaced the exposed key and paired the new certificate with the correct key.<\/li>\n<li><strong>Every address serves the correct certificate:<\/strong> External checks are complete for both <code>example.com<\/code> and <code>www.example.com<\/code>. If you publish both IPv4 and IPv6 addresses, you have tested both connection paths. The served certificate&#8217;s fingerprint matches the new certificate, and the hostname and certificate chain checks pass without errors.<\/li>\n<li><strong>Old certificates tied to the exposed key are revoked:<\/strong> For every identified, unexpired certificate using the exposed key, you have recorded a successful revocation response with <code>keyCompromise<\/code> as the reason. Removing a certificate from Nginx or deleting its old Certbot record does not count as revocation.<\/li>\n<li><strong>The exposure route is closed:<\/strong> You have removed the file and its historical copies from the Git repository and followed up with the repository hosting service on access restrictions and cache removal. Your incident record acknowledges that cleanup cannot retrieve previously downloaded copies. The new key is not in the Git repository, web root, or support attachments, and only authorized people can access the incident evidence.<\/li>\n<li><strong>Renewal and reloading have been tested:<\/strong> The test renewal for <code>example-com-incident<\/code> succeeds, the Certbot timer is active, and the script that reloads Nginx after renewal completes without errors. No active service or rollback plan depends on the exposed key.<\/li>\n<\/ul>\n<p>If any check remains incomplete, keep it open in the incident record and assign an owner and target completion date. Track certificate replacement and the fix for the exposure route as separate tasks. Restoring HTTPS with a new key does not resolve the mistake that allowed the old key to leak.<\/p>\n<p>Before closing the incident, establish how the key entered Git and address that cause. If a deployment script copies server keys into the project directory, remove that step. If the file was added accidentally, introduce a scan and review rule for secrets such as private keys and passwords before pushing to the repository. Check that the safeguard catches the original mistake without sharing the real key again, and document the change in the incident record.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Deleting an exposed SSL private key does not invalidate copies. Follow an Ubuntu and Nginx walkthrough to contain the leak, generate a new key, deploy and verify its certificate, revoke affected certificates and prevent another exposure.<\/p>\n","protected":false},"author":4,"featured_media":5933,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[180],"tags":[670,590,673,503,672,671],"class_list":["post-5944","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to","tag-certbot","tag-incident-response","tag-lets-encrypt","tag-nginx","tag-private-key-security","tag-ssl-tls"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/5944","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=5944"}],"version-history":[{"count":1,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/5944\/revisions"}],"predecessor-version":[{"id":5946,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/5944\/revisions\/5946"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/5933"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=5944"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=5944"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=5944"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}