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.
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.
echo | openssl s_client \ -connect example.com:443 \ -servername example.com \ 2>/dev/null \ | openssl x509 \ -noout \ -subject \ -issuer \ -serial \ -datesThat 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.
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.


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.
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.
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 nginxWhat Changed After the Fix
What I Kept for the Next Renewal
I no longer treat
200 OKas 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.



