Коли я продовжувала Sectigo SSL у Nginx, renewal завис на етапі DCV validation, хоча verification URL повертав `200 OK`. Спочатку все виглядало правильно, але реальна причина виявилася значно цікавішою: старий token, різні physical sources для HTTP і HTTPS та неочевидна пастка з validation-файлом.
У чому була проблема
Зовні це виглядало як звичайне продовження сертифіката. У SSL Panel сертифікат залишався в статусі перевірки домену, а Sectigo не завершував DCV.
Найбільше мене збивало з пантелику те, що verification-файл був доступний через браузер і через curl. Я бачила 200 OK, тому перша думка була проста: якщо файл відкривається, то validation налаштований правильно.
На практиці цього виявилося недостатньо. У моєму випадку проблема була не в самому факті доступності файла, а в тому, що CA мав отримати саме актуальний validation content, а не просто будь-яку відповідь із потрібного URL.
Що я перевірила спочатку
Спочатку я перевірила, який сертифікат реально віддає production, а не лише те, що показує SSL Panel.
echo | openssl s_client \ -connect example.com:443 \ -servername example.com \ 2>/dev/null \ | openssl x509 \ -noout \ -subject \ -issuer \ -serial \ -datesТак я побачила, що production ще віддає старий сертифікат. Тобто навіть якщо renewal уже рухався в SSL Panel, сам сайт ще не був оновлений.
Далі я перевірила validation URL для основного домену та для `www`, окремо через HTTP і 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"На цьому етапі я отримувала саме те, що й очікувала побачити на перший погляд: 200 OK. І саме тут було легко зробити хибний висновок, що вся DCV-конфігурація вже правильна.
Але коли renewal не завершувався, стало зрозуміло, що потрібно дивитися глибше: не просто на URL, а на те, який саме physical file віддається у відповідь і який вміст реально бачить CA.
Де насправді була причина
Справжня причина виявилася подвійною.
По-перше, у мене залишився старий validation token. Filename міг збігатися з попереднім renewal, але content уже був іншим. Візуально це дуже легко пропустити, особливо коли сам URL виглядає правильним.
По-друге, HTTP і HTTPS спочатку не використовували один і той самий source of truth. Один validation endpoint віддавав файл з окремого технічного каталогу, а інший - через application path. Ззовні обидва могли виглядати робочими, але фактично CA отримував не ту саму відповідь залежно від схеми або hostname.
Саме після цього я вже перестала дивитися на 200 OK як на остаточний доказ і почала перевіряти актуальний token, physical source файла і єдину логіку для всіх DCV endpoints.
Validation URL повертав 200 OK, але HTTP і HTTPS могли віддавати файл із різних physical sources.
Один із них містив старий token, тому renewal не завершувався, навіть попри зовні правильну відповідь.
І HTTP, і HTTPS почали віддавати validation-файл з одного окремого каталогу під контролем Nginx.
Після цього Sectigo отримував один і той самий актуальний token, а renewal уже можна було завершити без плутанини.


Як я це виправила
Рішення складалося з кількох кроків.
Спочатку я оновила validation-файл так, щоб у ньому був саме поточний token для renewal, а не залишок від попереднього випуску. Далі я прибрала неоднозначність у маршрутизації та залишила один окремий DCV path, який Nginx віддає напряму і для HTTP, і для HTTPS.
Після того, як validation став послідовним, я вже встановила новий сертифікат: перевірила, що certificate відповідає private key, коректно зібрала fullchain, виконала nginx -t і тільки після цього зробила reload.
location ^~ /.well-known/pki-validation/ { auth_basic off; root /var/www/ssl-validation; default_type "text/plain"; try_files $uri =404;}Для мене головна ідея тут проста: DCV не повинен залежати від application deployment. Коли validation path віддається напряму 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Що змінилося після виправлення
Що я залишила собі на майбутнє
Я більше не сприймаю
200 OKяк достатній доказ успішної DCV validation.Якщо filename validation-файла збігається з попереднім renewal, я окремо перевіряю саме content.
HTTP і HTTPS повинні використовувати один source of truth для
/.well-known/pki-validation/.DCV краще тримати поза application deployment і віддавати напряму через Nginx.
Перед reload я завжди перевіряю certificate, private key і
fullchain, а вже потім оновлюю production.
Для мене це було хороше нагадування, що проблеми з SSL renewal часто ховаються не в самому сертифікаті, а в дрібних деталях validation і конфігурації. У цьому випадку найкориснішим виявилося не те, що я ще раз запустила renewal, а те, що я уважно перевірила, який саме файл і звідки реально віддає Nginx.



