0% прочитано

How I Renewed a Sectigo SSL Certificate in Nginx and Debugged DCV

While renewing a Sectigo SSL certificate, DCV stayed in progress even though the verification file returned 200 OK. I traced it to a stale token and different HTTP and HTTPS validation paths in Nginx, then completed the renewal safely.

18 серпня 2026 р. 5 хв читанняNginx

While renewing a Sectigo SSL certificate in Nginx, the process got stuck at DCV validation even though the verification URL returned `200 OK`. At first everything looked correct, but the real cause turned out to be much more interesting: a stale token, different physical sources for HTTP and HTTPS, and a subtle trap around the validation file.

What the Problem Was

From the outside, this looked like a normal certificate renewal. In the SSL panel, the certificate stayed in the domain validation stage, and Sectigo would not complete DCV.

What confused me most was that the verification file was available both in the browser and through curl. I kept seeing 200 OK, so my first assumption was simple: if the file opens, validation must already be configured correctly.

In practice, that was not enough. In my case, the issue was not the fact that the file was reachable, but that the CA had to receive the current validation content, not just any response from the expected URL.

`200 OK` does not automatically mean DCV will succeed. If the filename stayed the same after a renewal or reissue, you need to verify not only the response status but also the exact content of the validation file.

What I Checked First

My first step was to check which certificate production was actually serving, instead of relying only on what the SSL panel showed.

bash
echo | openssl s_client \  -connect example.com:443 \  -servername example.com \  2>/dev/null \  | openssl x509 \      -noout \      -subject \      -issuer \      -serial \      -dates

That immediately showed me that production was still serving the old certificate. So even if the renewal was progressing in the SSL panel, the website itself had not been updated yet.

Next, I checked the validation URL for the main domain and for `www`, separately over HTTP and HTTPS.

bash
FILE="<VALIDATION_FILE>.txt" curl -i --max-redirs 0 \  "http://example.com/.well-known/pki-validation/$FILE" curl -i --max-redirs 0 \  "https://example.com/.well-known/pki-validation/$FILE" curl -i --max-redirs 0 \  "http://www.example.com/.well-known/pki-validation/$FILE" curl -i --max-redirs 0 \  "https://www.example.com/.well-known/pki-validation/$FILE"

At that stage I was getting exactly what I expected to see at first glance: 200 OK. And that was the easiest moment to make the wrong conclusion and assume the whole DCV setup was already correct.

But when the renewal still would not complete, it became obvious that I had to look deeper: not just at the URL itself, but at which physical file was actually being served and what content the CA was really receiving.

Where the Real Cause Was

The real cause turned out to be two separate issues.

First, I still had a stale validation token. The filename could remain the same as in a previous renewal, but the content had already changed. That is very easy to miss, especially when the URL itself still looks correct.

Second, HTTP and HTTPS were not initially using the same source of truth. One validation endpoint served the file from a dedicated technical directory, while the other one still exposed it through an application path. From the outside both endpoints could look functional, but the CA was not necessarily receiving the same response depending on the scheme or hostname.

That was the point where I stopped treating 200 OK as final proof and started checking the current token, the physical file source, and one consistent DCV flow for every endpoint.

До

The validation URL returned 200 OK, but HTTP and HTTPS could still serve the file from different physical sources.

One of those sources still exposed a stale token, which prevented the renewal from completing even though the response looked correct.

Після

Both HTTP and HTTPS started serving the validation file from one dedicated directory managed directly by Nginx.

After that, Sectigo received the same current token everywhere, and the renewal could complete without extra confusion.

SSL panel screenshot showing the certificate renewal before domain validation is completed
The certificate status before domain validation was completed: the renewal was still waiting for DCV to succeed.
SSL panel screenshot showing the certificate after the renewal completed successfully
After fixing the validation file and the Nginx configuration, the renewal completed and the certificate moved to an active state.

How I Fixed It

The fix had a few parts.

First, I updated the validation file so it contained the current renewal token rather than leftover data from a previous issuance. Then I removed the routing ambiguity and kept a single dedicated DCV path that Nginx serves directly for both HTTP and HTTPS.

Once validation became consistent, I installed the new certificate: I verified that the certificate matched the private key, rebuilt the fullchain correctly, ran nginx -t, and only then reloaded Nginx.

Nginx - PKI validation location
location ^~ /.well-known/pki-validation/ {    auth_basic off;    root /var/www/ssl-validation;    default_type "text/plain";    try_files $uri =404;}

The main idea for me was simple: DCV should not depend on the application deployment. When the validation path is served directly by Nginx from a dedicated directory, the whole process becomes much more predictable.

Verifying the certificate, key, and reloading Nginx
openssl x509 \  -in /tmp/cert/example_com.crt \  -pubkey -noout \  | openssl pkey -pubin -outform DER 2>/dev/null \  | sha256sum openssl pkey \  -in /etc/nginx/ssl/example.com/example.com.key \  -pubout -outform DER 2>/dev/null \  | sha256sum sudo nginx -tsudo systemctl reload nginx
Do not reload Nginx until `nginx -t` passes successfully and the new certificate matches the private key. At that point it is no longer a minor mistake but a real risk to break working HTTPS in production.

What Changed After the Fix

After fixing the validation file, unifying the DCV path, and verifying the certificate, production started serving the renewed Sectigo SSL certificate and the renewal completed without any extra surprises.

What I Kept for the Next Renewal

  • I no longer treat 200 OK as sufficient proof that DCV is configured correctly.

  • If the validation filename matches a previous renewal, I verify the file content separately.

  • HTTP and HTTPS should use one source of truth for /.well-known/pki-validation/.

  • It is safer to keep DCV outside the application deployment and let Nginx serve it directly.

  • Before any reload, I verify the certificate, the private key, and the fullchain, and only then update production.

For me, this was a good reminder that SSL renewal issues are often hidden not in the certificate itself, but in small details around validation and configuration. In this case, the most useful step was not retrying the renewal again, but carefully checking which exact file Nginx was really serving and from where.