{"id":3060,"date":"2026-05-06T17:19:48","date_gmt":"2026-05-06T09:19:48","guid":{"rendered":"https:\/\/www.ruianding.com\/blog\/?p=3060"},"modified":"2026-05-06T17:19:49","modified_gmt":"2026-05-06T09:19:49","slug":"ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever","status":"publish","type":"post","link":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/","title":{"rendered":"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever"},"content":{"rendered":"\n<p><em>A field manual for anyone running Trojan-Go (or any 443-hijacking proxy) behind Let&#8217;s Encrypt.<\/em><\/p>\n\n\n\n<p>Three months after deploying the &#8220;Ultimate Stealth Proxy&#8221; setup, my <code>www.ruianding.com<\/code> suddenly went dark. Browsers yelled <code>NET::ERR_CERT_DATE_INVALID<\/code>, Shadowrocket refused to dial out. My first reaction: &#8220;Impossible. I configured <code>certbot<\/code> to auto-renew.&#8221; My second reaction, after 15 minutes of debugging: &#8220;Oh. <em>That&#8217;s<\/em> why.&#8221;<\/p>\n\n\n\n<p>This post explains the exact failure mode, how to diagnose it in under 60 seconds, and the 6-line hook that guarantees it never happens again.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-text-color has-cyan-bluish-gray-color has-alpha-channel-opacity has-cyan-bluish-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">I. The Paradox: Certbot Renewed, But the Cert Is Still Expired<\/h2>\n\n\n\n<p>The first clue was this contradiction:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># Certbot says the cert is fine\n$ sudo certbot certificates\n  Expiry Date: 2026-07-04 01:27:30+00:00 (VALID: 58 days)\n  Certificate Path: \/etc\/letsencrypt\/live\/ruianding.com\/fullchain.pem\n\n# But the live server is serving an expired one\n$ echo | openssl s_client -servername www.ruianding.com -connect www.ruianding.com:443 2>\/dev\/null \\\n    | openssl x509 -noout -dates -subject\nnotBefore=Feb  3 13:35:43 2026 GMT\nnotAfter=May  4 13:35:42 2026 GMT       # &lt;-- EXPIRED\nsubject=CN = ruianding.com              # &lt;-- old cert, no www SAN<\/pre>\n\n\n\n<p>Same domain. Two different certificates. One on disk, one on the wire.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-text-color has-cyan-bluish-gray-color has-alpha-channel-opacity has-cyan-bluish-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">II. The Root Cause: Trojan-Go Holds the Cert in Memory<\/h2>\n\n\n\n<p>Unlike Apache or Nginx, which can <code>reload<\/code> on SIGHUP and re-read cert files from disk without dropping connections, <strong>Trojan-Go reads <code>fullchain.pem<\/code> and <code>privkey.pem<\/code> exactly once \u2014 at process startup \u2014 and keeps them in memory forever.<\/strong><\/p>\n\n\n\n<p>So the real timeline was:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Date<\/th><th>Event<\/th><th>What the client saw<\/th><\/tr><\/thead><tbody><tr><td>Feb 3<\/td><td>Trojan-Go started, loaded cert v1 (expires May 4)<\/td><td>cert v1<\/td><\/tr><tr><td>Apr 5<\/td><td><code>certbot.timer<\/code> fired, cert v2 written to disk (expires Jul 4)<\/td><td>cert v1 (still!)<\/td><\/tr><tr><td>May 4<\/td><td>Cert v1 expires in Trojan-Go&#8217;s memory<\/td><td><strong>broken<\/strong><\/td><\/tr><tr><td>May 6<\/td><td>Me, panicking<\/td><td><strong>broken<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>The renewal <strong>succeeded<\/strong>. The <code>deploy-hook<\/code> to restart the consumer <strong>didn&#8217;t exist<\/strong>. Certbot has no idea that a 443-squatting process is quietly holding the old cert hostage.<\/p>\n\n\n\n<p>The culprit \u2014 or rather, the missing piece \u2014 was visible the whole time in <code>\/etc\/letsencrypt\/renewal\/ruianding.com.conf<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">[renewalparams]\nauthenticator = webroot\nwebroot_path = \/data\/ruianding.com,\nserver = https:\/\/acme-v02.api.letsencrypt.org\/directory\nkey_type = ecdsa\n# \u2190 no renew_hook, no post_hook, no deploy_hook. Nothing.<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-text-color has-cyan-bluish-gray-color has-alpha-channel-opacity has-cyan-bluish-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">III. 60-Second Diagnostic Playbook<\/h2>\n\n\n\n<p>Run these five commands whenever HTTPS dies on a stealth-proxy box. The answer is almost always in the diff between commands 1 and 2.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># 1. What does certbot THINK is deployed?\nsudo certbot certificates\n\n# 2. What is ACTUALLY being served on port 443?\necho | openssl s_client -servername www.yourdomain.com -connect www.yourdomain.com:443 2>\/dev\/null \\\n    | openssl x509 -noout -dates -subject -ext subjectAltName\n\n# 3. Who owns port 443? (Sanity check: not apache, not nginx)\nsudo ss -tulpn | grep :443\n\n# 4. Does the renewal config have a hook?\nsudo cat \/etc\/letsencrypt\/renewal\/yourdomain.com.conf | grep -i hook\n\n# 5. When was the cert file last written vs. when did the proxy start?\nsudo ls -lL \/etc\/letsencrypt\/live\/yourdomain.com\/fullchain.pem\nsystemctl show trojan-go -p ActiveEnterTimestamp<\/pre>\n\n\n\n<p><strong>If cert file mtime is newer than Trojan-Go start time \u2192 you&#8217;ve hit this exact bug.<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-text-color has-cyan-bluish-gray-color has-alpha-channel-opacity has-cyan-bluish-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">IV. The Fix (Two Minutes, Two Steps)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1 \u2014 Restore service immediately<\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">sudo systemctl restart trojan-go\n\n# Verify the wire now matches disk\necho | openssl s_client -servername www.yourdomain.com -connect www.yourdomain.com:443 2>\/dev\/null \\\n    | openssl x509 -noout -dates -subject -ext subjectAltName\n# Expect: notAfter ~ 90 days out, SAN includes both apex + www<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2 \u2014 Install a permanent deploy-hook<\/h3>\n\n\n\n<p>Certbot scans <code>\/etc\/letsencrypt\/renewal-hooks\/deploy\/<\/code> and runs every executable there <strong>only after a successful renewal<\/strong> (never on dry-runs, never on no-op checks). Inside the script, <code>$RENEWED_LINEAGE<\/code> is set to the live directory of the cert that was just renewed, which lets you scope actions per-domain on multi-cert boxes.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">sudo tee \/etc\/letsencrypt\/renewal-hooks\/deploy\/reload-services.sh > \/dev\/null &lt;&lt;'EOF'\n#!\/bin\/bash\n# Fires after every successful Let's Encrypt renewal.\n# $RENEWED_LINEAGE looks like \/etc\/letsencrypt\/live\/yourdomain.com\nif [[ \"$RENEWED_LINEAGE\" == *\"\/yourdomain.com\" ]]; then\n    systemctl reload  apache2    || true   # reload is enough for apache\n    systemctl restart trojan-go  || true   # MUST be restart; SIGHUP won't re-read certs\nfi\nEOF\nsudo chmod +x \/etc\/letsencrypt\/renewal-hooks\/deploy\/reload-services.sh<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3 \u2014 Prove the hook works without waiting 60 days<\/h3>\n\n\n\n<p>Simulate what certbot will do:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># Execute the hook manually with the same env var certbot injects\nsudo RENEWED_LINEAGE=\/etc\/letsencrypt\/live\/yourdomain.com \\\n    \/etc\/letsencrypt\/renewal-hooks\/deploy\/reload-services.sh\n\n# Confirm trojan-go came back up seconds ago\nsystemctl status trojan-go --no-pager | head -5\n# Look for: Active: active (running) since ... Xs ago\n\n# Confirm the full renewal pipeline still works end-to-end\nsudo certbot renew --dry-run\n# Look for: Congratulations, all simulated renewals succeeded<\/pre>\n\n\n\n<p>Done. Next time <code>certbot.timer<\/code> ticks over and a real renewal happens, <code>trojan-go<\/code> gets restarted automatically, picks up the new cert in memory, and you never notice.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-text-color has-cyan-bluish-gray-color has-alpha-channel-opacity has-cyan-bluish-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">V. Why <code>--post-hook<\/code> in the Original Guide Isn&#8217;t Enough<\/h2>\n\n\n\n<p>The original stealth-proxy post ended with:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">sudo certbot renew --post-hook \"systemctl restart trojan-go apache2\"<\/pre>\n\n\n\n<p>This works, but it has two foot-guns I want to flag:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>--post-hook<\/code> is only honored for that single manual invocation.<\/strong> It does <strong>not<\/strong> get saved into <code>\/etc\/letsencrypt\/renewal\/yourdomain.com.conf<\/code>. The next <code>certbot.timer<\/code> run will not know about it. The hook you set this way vanishes the moment the terminal closes.<\/li>\n\n\n\n<li><strong><code>post-hook<\/code> fires even when nothing was renewed.<\/strong> On a healthy system that&#8217;s a no-op, but it means your service restarts on every successful dry-run-ish check \u2014 mildly annoying if you care about uptime counters.<\/li>\n<\/ol>\n\n\n\n<p>The <code>renewal-hooks\/deploy\/<\/code> directory approach is superior because:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>It&#8217;s <strong>persistent<\/strong> (lives on disk, survives reboots, survives certbot package upgrades).<\/li>\n\n\n\n<li>It <strong>only fires on actual successful renewals<\/strong> (<code>deploy-hook<\/code> semantics).<\/li>\n\n\n\n<li>It&#8217;s <strong>per-domain aware<\/strong> via <code>$RENEWED_LINEAGE<\/code>, so one hook can handle multiple certs correctly.<\/li>\n<\/ul>\n\n\n\n<p>If you want the hook saved <em>into<\/em> the renewal config instead, do it once with:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">sudo certbot renew --force-renewal \\\n    --deploy-hook \"systemctl restart trojan-go &amp;&amp; systemctl reload apache2\"<\/pre>\n\n\n\n<p>\u2026but honestly, the <code>renewal-hooks\/deploy\/<\/code> directory is cleaner.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-text-color has-cyan-bluish-gray-color has-alpha-channel-opacity has-cyan-bluish-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">VI. Bonus Land-Mine: Webroot Breaks When Trojan-Go Squats on 443<\/h2>\n\n\n\n<p>Since Trojan-Go owns 443, HTTP-01 validation must go over <strong>port 80<\/strong>, via Apache&#8217;s <code>*:80<\/code> VirtualHost, using the <code>webroot<\/code> authenticator. This creates a silent dependency: your Apache port-80 vhost&#8217;s <code>DocumentRoot<\/code> <strong>must<\/strong> match <code>webroot_path<\/code> in <code>\/etc\/letsencrypt\/renewal\/yourdomain.com.conf<\/code>, or the <code>.well-known\/acme-challenge\/<\/code> file Let&#8217;s Encrypt writes will 404.<\/p>\n\n\n\n<p>Check it once, sleep well forever:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">grep -E 'DocumentRoot|&lt;VirtualHost' \/etc\/apache2\/sites-available\/yourdomain.com.conf\ngrep webroot_path \/etc\/letsencrypt\/renewal\/yourdomain.com.conf<\/pre>\n\n\n\n<p>If they disagree, either fix the <code>DocumentRoot<\/code> or add an explicit alias inside the <code>*:80<\/code> vhost:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Alias \/.well-known\/acme-challenge\/ \/data\/yourdomain.com\/.well-known\/acme-challenge\/\n&lt;Directory \"\/data\/yourdomain.com\/.well-known\/acme-challenge\/\">\n    Require all granted\n&lt;\/Directory><\/pre>\n\n\n\n<p>And always verify end-to-end with:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">sudo certbot renew --dry-run<\/pre>\n\n\n\n<p>A <code>--dry-run<\/code> pass means both the ACME challenge path <strong>and<\/strong> your webroot config are healthy \u2014 the two things that most often rot silently between renewals.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-text-color has-cyan-bluish-gray-color has-alpha-channel-opacity has-cyan-bluish-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">VII. TL;DR Checklist<\/h2>\n\n\n\n<p>Run once on every stealth-proxy box you own:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># 1. Install persistent deploy hook\nsudo tee \/etc\/letsencrypt\/renewal-hooks\/deploy\/reload-services.sh > \/dev\/null &lt;&lt;'EOF'\n#!\/bin\/bash\nif [[ \"$RENEWED_LINEAGE\" == *\"\/yourdomain.com\" ]]; then\n    systemctl reload  apache2    || true\n    systemctl restart trojan-go  || true\nfi\nEOF\nsudo chmod +x \/etc\/letsencrypt\/renewal-hooks\/deploy\/reload-services.sh\n\n# 2. Prove the hook works\nsudo RENEWED_LINEAGE=\/etc\/letsencrypt\/live\/yourdomain.com \\\n    \/etc\/letsencrypt\/renewal-hooks\/deploy\/reload-services.sh\nsystemctl status trojan-go --no-pager | head -5\n\n# 3. Prove the renewal pipeline works\nsudo certbot renew --dry-run\n\n# 4. Prove the wire serves what certbot has on disk\necho | openssl s_client -servername www.yourdomain.com -connect www.yourdomain.com:443 2>\/dev\/null \\\n    | openssl x509 -noout -dates -subject -ext subjectAltName<\/pre>\n\n\n\n<p>Four commands. Two minutes. Zero 3-AM &#8220;why is my site down&#8221; incidents for the next 90 days \u2014 and every 90 days after that.<\/p>\n\n\n\n<p><em>The moral: &#8220;auto-renewal&#8221; only renews the <strong>file<\/strong>. Someone still has to tell the long-running process to re-read it. On a normal LAMP box, that someone is the package&#8217;s postinst script. On a stealth-proxy box where you&#8217;ve hand-rolled the 443 listener, that someone is <strong>you<\/strong> \u2014 exactly once, via the <code>renewal-hooks\/deploy\/<\/code> directory.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A field manual for anyone running Trojan-Go (or any 443-hijacking proxy) behind Let&#8217;s Encrypt. Three months after deploying the &#8220;Ultimate Stealth Proxy&#8221; setup, my www.ruianding.com suddenly went dark. Browsers yelled NET::ERR_CERT_DATE_INVALID, Shadowrocket refused to dial out. My first reaction: &#8220;Impossible. I configured certbot to auto-renew.&#8221; My second reaction, after 15 minutes of debugging: &#8220;Oh. That&#8217;s [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_themeisle_gutenberg_block_has_review":false,"footnotes":""},"categories":[1,12,10],"tags":[],"class_list":["post-3060","post","type-post","status-publish","format-standard","hentry","category-miscellaneous","category-troubleshooting","category-tutorial"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever - \u6781\u7b80IT\uff5cSimpleIT<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever - \u6781\u7b80IT\uff5cSimpleIT\" \/>\n<meta property=\"og:description\" content=\"A field manual for anyone running Trojan-Go (or any 443-hijacking proxy) behind Let&#8217;s Encrypt. Three months after deploying the &#8220;Ultimate Stealth Proxy&#8221; setup, my www.ruianding.com suddenly went dark. Browsers yelled NET::ERR_CERT_DATE_INVALID, Shadowrocket refused to dial out. My first reaction: &#8220;Impossible. I configured certbot to auto-renew.&#8221; My second reaction, after 15 minutes of debugging: &#8220;Oh. That&#8217;s [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/\" \/>\n<meta property=\"og:site_name\" content=\"\u6781\u7b80IT\uff5cSimpleIT\" \/>\n<meta property=\"article:published_time\" content=\"2026-05-06T09:19:48+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-05-06T09:19:49+00:00\" \/>\n<meta name=\"author\" content=\"Ruian Ding\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Ruian Ding\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"4 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/\"},\"author\":{\"name\":\"Ruian Ding\",\"@id\":\"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/440d88575b7dc819a4cefc8c4199db3b\"},\"headline\":\"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever\",\"datePublished\":\"2026-05-06T09:19:48+00:00\",\"dateModified\":\"2026-05-06T09:19:49+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/\"},\"wordCount\":720,\"publisher\":{\"@id\":\"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/440d88575b7dc819a4cefc8c4199db3b\"},\"articleSection\":[\"Miscellaneous\",\"Troubleshooting\",\"Tutorial\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/\",\"url\":\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/\",\"name\":\"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever - \u6781\u7b80IT\uff5cSimpleIT\",\"isPartOf\":{\"@id\":\"https:\/\/www.ruianding.com\/blog\/#website\"},\"datePublished\":\"2026-05-06T09:19:48+00:00\",\"dateModified\":\"2026-05-06T09:19:49+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.ruianding.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.ruianding.com\/blog\/#website\",\"url\":\"https:\/\/www.ruianding.com\/blog\/\",\"name\":\"Ruian's Tech Troubleshooting Toolbox\",\"description\":\"Debug the World.\",\"publisher\":{\"@id\":\"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/440d88575b7dc819a4cefc8c4199db3b\"},\"alternateName\":\"\u4e01\u777f\u5b89\u7684\u6280\u672f\u5206\u4eab\u535a\u5ba2\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.ruianding.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/440d88575b7dc819a4cefc8c4199db3b\",\"name\":\"Ruian Ding\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/www.ruianding.com\/blog\/wp-content\/uploads\/2023\/05\/logo.png\",\"contentUrl\":\"https:\/\/www.ruianding.com\/blog\/wp-content\/uploads\/2023\/05\/logo.png\",\"width\":284,\"height\":284,\"caption\":\"Ruian Ding\"},\"logo\":{\"@id\":\"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/image\/\"},\"description\":\"I am currently a Support Specialist at NIO, focusing on cloud-related issues for NIO Power. Previously, at Microsoft Entra ID, I specialized in identity and access management (IAM), including device registration, Windows Hello for Business (WHfB), multi-factor authentication (MFA), and single sign-on (SSO). In addition to my core expertise, I have a strong foundation in Active Directory, Servers, Cloud Computing, Network Administration, and Front-end Web Development. This diverse technical skill set enables me to effectively handle a wide range of challenges in a fast-paced IT environment.\",\"sameAs\":[\"https:\/\/www.ruianding.com\"],\"url\":\"https:\/\/www.ruianding.com\/blog\/author\/ruiand\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever - \u6781\u7b80IT\uff5cSimpleIT","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/","og_locale":"en_US","og_type":"article","og_title":"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever - \u6781\u7b80IT\uff5cSimpleIT","og_description":"A field manual for anyone running Trojan-Go (or any 443-hijacking proxy) behind Let&#8217;s Encrypt. Three months after deploying the &#8220;Ultimate Stealth Proxy&#8221; setup, my www.ruianding.com suddenly went dark. Browsers yelled NET::ERR_CERT_DATE_INVALID, Shadowrocket refused to dial out. My first reaction: &#8220;Impossible. I configured certbot to auto-renew.&#8221; My second reaction, after 15 minutes of debugging: &#8220;Oh. That&#8217;s [&hellip;]","og_url":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/","og_site_name":"\u6781\u7b80IT\uff5cSimpleIT","article_published_time":"2026-05-06T09:19:48+00:00","article_modified_time":"2026-05-06T09:19:49+00:00","author":"Ruian Ding","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Ruian Ding","Est. reading time":"4 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/#article","isPartOf":{"@id":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/"},"author":{"name":"Ruian Ding","@id":"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/440d88575b7dc819a4cefc8c4199db3b"},"headline":"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever","datePublished":"2026-05-06T09:19:48+00:00","dateModified":"2026-05-06T09:19:49+00:00","mainEntityOfPage":{"@id":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/"},"wordCount":720,"publisher":{"@id":"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/440d88575b7dc819a4cefc8c4199db3b"},"articleSection":["Miscellaneous","Troubleshooting","Tutorial"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/","url":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/","name":"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever - \u6781\u7b80IT\uff5cSimpleIT","isPartOf":{"@id":"https:\/\/www.ruianding.com\/blog\/#website"},"datePublished":"2026-05-06T09:19:48+00:00","dateModified":"2026-05-06T09:19:49+00:00","breadcrumb":{"@id":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.ruianding.com\/blog\/ssl-certificate-auto-renewal-for-trojan-go-why-it-silently-breaks-and-the-one-line-hook-that-fixes-it-forever\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.ruianding.com\/blog\/"},{"@type":"ListItem","position":2,"name":"SSL Certificate Auto-Renewal for Trojan-Go: Why It Silently Breaks and the One-Line Hook That Fixes It Forever"}]},{"@type":"WebSite","@id":"https:\/\/www.ruianding.com\/blog\/#website","url":"https:\/\/www.ruianding.com\/blog\/","name":"Ruian's Tech Troubleshooting Toolbox","description":"Debug the World.","publisher":{"@id":"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/440d88575b7dc819a4cefc8c4199db3b"},"alternateName":"\u4e01\u777f\u5b89\u7684\u6280\u672f\u5206\u4eab\u535a\u5ba2","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.ruianding.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/440d88575b7dc819a4cefc8c4199db3b","name":"Ruian Ding","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/www.ruianding.com\/blog\/wp-content\/uploads\/2023\/05\/logo.png","contentUrl":"https:\/\/www.ruianding.com\/blog\/wp-content\/uploads\/2023\/05\/logo.png","width":284,"height":284,"caption":"Ruian Ding"},"logo":{"@id":"https:\/\/www.ruianding.com\/blog\/#\/schema\/person\/image\/"},"description":"I am currently a Support Specialist at NIO, focusing on cloud-related issues for NIO Power. Previously, at Microsoft Entra ID, I specialized in identity and access management (IAM), including device registration, Windows Hello for Business (WHfB), multi-factor authentication (MFA), and single sign-on (SSO). In addition to my core expertise, I have a strong foundation in Active Directory, Servers, Cloud Computing, Network Administration, and Front-end Web Development. This diverse technical skill set enables me to effectively handle a wide range of challenges in a fast-paced IT environment.","sameAs":["https:\/\/www.ruianding.com"],"url":"https:\/\/www.ruianding.com\/blog\/author\/ruiand\/"}]}},"_links":{"self":[{"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/posts\/3060","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/comments?post=3060"}],"version-history":[{"count":1,"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/posts\/3060\/revisions"}],"predecessor-version":[{"id":3061,"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/posts\/3060\/revisions\/3061"}],"wp:attachment":[{"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/media?parent=3060"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/categories?post=3060"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ruianding.com\/blog\/wp-json\/wp\/v2\/tags?post=3060"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}