|
|
<!doctypehtml><html lang=zh-CN><meta charset=UTF-8><meta content=width=device-width name=viewport><meta media="(prefers-color-scheme: light)" content=#222 name=theme-color><meta media="(prefers-color-scheme: dark)" content=#222 name=theme-color><meta content="Hexo 7.3.0" name=generator><link href=https://cdnjs.cloudflare.com rel=preconnect><link href=/images/apple-touch-icon-next.png rel=apple-touch-icon sizes=180x180><link href=/images/favicon-32x32-next.png rel=icon sizes=32x32 type=image/png><link href=/images/favicon-16x16-next.png rel=icon sizes=16x16 type=image/png><link color=#222 href=/images/logo.svg rel=mask-icon><meta content=2X6S9P7CAjXjVvw8YyQR8pCu-B0oEu7O9quLgxXuWyA name=google-site-verification><meta content=dV8JGNzi0c name=baidu-site-verification><script>!function(){var t=!1;function e(){var e;t||(t=!0,(e=document.createElement("script")).setAttribute("data-ad-client","ca-pub-7480618470784058"),e.async=!0,e.src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js",document.head.appendChild(e))}function a(){"requestIdleCallback"in window?window.requestIdleCallback(e,{timeout:5e3}):window.setTimeout(e,5e3)}"complete"===document.readyState?a():window.addEventListener("load",a,{once:!0})}()</script><link href=/css/main.css rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.0/css/all.min.css integrity=sha256-VHqXKFhhMxcpubYf9xiWdCiojEbY9NexQ4jh8AxbvcM= rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/6.1.14/fancybox/fancybox.css integrity=sha256-mdaMxrItEW6hMIVJJNPKuX0B7KCSJQVEv3mVY4N2we4= rel=stylesheet><script class=next-config data-name=main type=application/json>{"hostname":"nicksxs.me","root":"/","images":"/images","scheme":"Pisces","darkmode":true,"version":"8.29.0","exturl":false,"sidebar":{"position":"left","width_expanded":320,"width_dual_column":240,"display":"post","padding":18,"offset":12},"hljswrap":true,"codeblock":{"theme":{"light":"default","dark":"stackoverflow-dark"},"prism":{"light":"prism","dark":"prism-dark"},"copy_button":{"enable":true,"style":"default"},"fold":{"enable":false,"height":500},"language":false},"bookmark":{"enable":false,"color":"#222","save":"auto"},"mediumzoom":false,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":true,"nav":null},"stickytabs":false,"motion":{"enable":false,"async":false,"duration":200,"transition":{"menu_item":"fadeInDown","post_block":"fadeIn","post_header":"fadeInDown","post_body":"fadeInDown","coll_header":"fadeInLeft","sidebar":"fadeInUp"}},"prism":false,"i18n":{"placeholder":"搜索...","empty":"没有找到任何搜索结果:${query}","hits_time":"找到 ${hits} 个搜索结果(用时 ${time} 毫秒)","hits":"找到 ${hits} 个搜索结果"},"algolia":{"appID":"663Q99OQQD","apiKey":"e461a1371d12cec1491c1153b288a9ed","indexName":"nicksxs","hits":{"per_page":10}}}</script><script defer src=/js/config.js></script><meta content="从腾讯云已有新证书、网站仍使用旧证书的问题讲起,结合 ssl-sync 的 Go 源码,解释证书查询与下载、域名和私钥校验、文件摘要比较、原子替换、TLS 入口验证与失败回滚,以及 Docker 挂载和定时任务的实现边界。" name=description><meta content=article property=og:type><meta content="用 Go 自动同步腾讯云 SSL 证书到 Traefik:下载、校验与失败回滚" property=og:title><meta content=https://nicksxs.me/2026/10/18/%E7%94%A8Go%E8%87%AA%E5%8A%A8%E5%90%8C%E6%AD%A5%E8%85%BE%E8%AE%AF%E4%BA%91SSL%E8%AF%81%E4%B9%A6%E5%88%B0Traefik-%E4%B8%8B%E8%BD%BD%E6%A0%A1%E9%AA%8C%E4%B8%8E%E5%A4%B1%E8%B4%A5%E5%9B%9E%E6%BB%9A/index.html property=og:url><meta content="Nicksxs's Blog" property=og:site_name><meta content="从腾讯云已有新证书、网站仍使用旧证书的问题讲起,结合 ssl-sync 的 Go 源码,解释证书查询与下载、域名和私钥校验、文件摘要比较、原子替换、TLS 入口验证与失败回滚,以及 Docker 挂载和定时任务的实现边界。" property=og:description><meta content=zh_CN property=og:locale><meta content=https://nicksxs.me/uploads/tencent-ssl-sync/deployment-flow.png property=og:image><meta content=2026-10-17T16:00:00.000Z property=article:published_time><meta content=2026-10-17T16:00:00.000Z property=article:modified_time><meta content=Nicksxs property=article:author><meta content=Docker property=article:tag><meta content=腾讯云 property=article:tag><meta content=Go property=article:tag><meta content=HTTPS property=article:tag><meta content=Traefik property=article:tag><meta content=summary name=twitter:card><meta content=https://nicksxs.me/uploads/tencent-ssl-sync/deployment-flow.png name=twitter:image><link href=https://nicksxs.me/2026/10/18/%E7%94%A8Go%E8%87%AA%E5%8A%A8%E5%90%8C%E6%AD%A5%E8%85%BE%E8%AE%AF%E4%BA%91SSL%E8%AF%81%E4%B9%A6%E5%88%B0Traefik-%E4%B8%8B%E8%BD%BD%E6%A0%A1%E9%AA%8C%E4%B8%8E%E5%A4%B1%E8%B4%A5%E5%9B%9E%E6%BB%9A/ rel=canonical><script class=next-config data-name=page type=application/json>{"sidebar":"","isHome":false,"isPost":true,"lang":"zh-CN","comments":true,"permalink":"https://nicksxs.me/2026/10/18/%E7%94%A8Go%E8%87%AA%E5%8A%A8%E5%90%8C%E6%AD%A5%E8%85%BE%E8%AE%AF%E4%BA%91SSL%E8%AF%81%E4%B9%A6%E5%88%B0Traefik-%E4%B8%8B%E8%BD%BD%E6%A0%A1%E9%AA%8C%E4%B8%8E%E5%A4%B1%E8%B4%A5%E5%9B%9E%E6%BB%9A/","path":"2026/10/18/用Go自动同步腾讯云SSL证书到Traefik-下载校验与失败回滚/","title":"用 Go 自动同步腾讯云 SSL 证书到 Traefik:下载、校验与失败回滚"}</script><script class=next-config data-name=calendar type=application/json>""</script><title>用 Go 自动同步腾讯云 SSL 证书到 Traefik:下载、校验与失败回滚 | Nicksxs's Blog</title><script async src=https://www.googletagmanager.com/gtag/js?id=UA-61358619-1></script><script class=next-config data-name=google_analytics type=application/json>{"tracking_id":"UA-61358619-1","only_pageview":false,"measure_protocol_api_secret":null}</script><script defer src=/js/third-party/analytics/google-analytics.js></script><script defer src=/js/third-party/analytics/baidu-analytics.js></script><script async src=https://hm.baidu.com/hm.js?20f33b3c0c0eff9b1522999c0015646d></script><script crossorigin=anonymous defer integrity=sha256-QnJV4HT5r8i89ovZGRD86iynZqHerEbT2xt69vDM36I= src=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/6.1.14/fancybox/fancybox.umd.js></script><script defer src=/js/utils.js></script><script defer src=/js/sidebar.js></script><script defer src=/js/next-boot.js></script><script data-algolia-integrity=sha256-o49zoHLDPBm9WLg6AiliAVi3axCRTqe3ltTxLvhzAVg= data-algolia-src=https://cdnjs.cloudflare.com/ajax/libs/algoliasearch/5.36.0/lite/builds/browser.umd.js data-search-src=/js/third-party/search/algolia-search.js src=/js/third-party/search/algolia-search-loader.js></script><script defer src=/js/third-party/fancybox.js></script><noscript><link href=/css/noscript.css rel=stylesheet></noscript><link title="Nicksxs's Blog" href=/atom.xml rel=alternate type=application/atom+xml><body itemscope itemtype=http://schema.org/WebPage><div class=headband></div><main class=main><div class=column><header class=header itemscope itemtype=http://schema.org/WPHeader><div class=site-brand-container><div class=site-nav-toggle><div aria-label=切换导航栏 class=toggle role=button><span class=toggle-line></span><span class=toggle-line></span><span class=toggle-line></span></div></div><div class=site-meta><a class=brand href=/ rel=start><i class=logo-line></i><p class=site-title>Nicksxs's Blog</p><i class=logo-line></i></a><p class=site-subtitle itemprop=description>Java 运行时、生产诊断与 AI 工程实践</div><div class=site-nav-right><div class="toggle popup-trigger" aria-label=搜索 role=button><i class="fa fa-search fa-fw fa-lg"></i></div></div></div><nav class=site-nav><ul class="main-menu menu"><li class="menu-item menu-item-home"><a href=/ rel=section><i class="fa fa-home fa-fw"></i>首页</a><li class="menu-item menu-item-专题"><a href=/topics/ rel=section><i class="fa fa-layer-group fa-fw"></i>专题</a><li class="menu-item menu-item-工具"><a href=/tools/ rel=section><i class="fa fa-toolbox fa-fw"></i>工具</a><li class="menu-item menu-item-about"><a href=/about/ rel=section><i class="fa fa-user fa-fw"></i>关于我</a><li class="menu-item menu-item-tags"><a href=/tags/ rel=section><i class="fa fa-tags fa-fw"></i>标签</a><li class="menu-item menu-item-categories"><a href=/categories/ rel=section><i class="fa fa-th fa-fw"></i>分类</a><li class="menu-item menu-item-archives"><a href=/archives/ rel=section><i class="fa fa-archive fa-fw"></i>归档</a><li class="menu-item menu-item-search"><a class=popup-trigger role=button><i class="fa fa-search fa-fw"></i>搜索</a></ul></nav><div class=search-pop-overlay><div class="popup search-popup"><div class=search-header><span class=search-icon><i class="fa fa-search"></i></span><div class=search-input-container><input autocapitalize=off autocomplete=off class=search-input maxlength=80 placeholder=搜索... spellcheck=false type=search></div><span class=popup-btn-close role=button><i class="fa fa-times-circle"></i></span></div><div class=search-result-container><div class=search-result-icon><i class="fab fa-algolia fa-5x"></i></div></div></div></div></header><aside class=sidebar><div class="sidebar-inner sidebar-nav-active sidebar-toc-active"><ul class=sidebar-nav><li class=sidebar-nav-toc>文章目录<li class=sidebar-nav-overview>站点概览</ul><div class=sidebar-panel-container><div class="post-toc-wrap sidebar-panel"><div class="post-toc animated"><ol class=nav><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%85%88%E7%9C%8B%E8%AF%81%E4%B9%A6%E4%BB%8E%E5%93%AA%E9%87%8C%E6%9D%A5%EF%BC%8C%E6%9C%80%E5%90%8E%E7%94%B1%E8%B0%81%E6%8F%90%E4%BE%9B><span class=nav-number>1.</span> <span class=nav-text>先看证书从哪里来,最后由谁提供</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E6%80%8E%E6%A0%B7%E4%BB%8E%E8%85%BE%E8%AE%AF%E4%BA%91%E9%80%89%E5%87%BA%E8%A6%81%E9%83%A8%E7%BD%B2%E7%9A%84%E8%AF%81%E4%B9%A6><span class=nav-number>2.</span> <span class=nav-text>怎样从腾讯云选出要部署的证书</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#API-%E7%AD%BE%E5%90%8D%E6%80%8E%E6%A0%B7%E8%AF%81%E6%98%8E%E8%AF%B7%E6%B1%82%E6%9D%A5%E8%87%AA%E6%9C%89%E6%9D%83%E9%99%90%E7%9A%84%E8%B0%83%E7%94%A8%E8%80%85><span class=nav-number>3.</span> <span class=nav-text>API 签名怎样证明请求来自有权限的调用者</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#ZIP-%E4%B8%8B%E8%BD%BD%E6%88%90%E5%8A%9F%E5%90%8E%EF%BC%8C%E8%BF%98%E8%A6%81%E6%A3%80%E6%9F%A5%E5%93%AA%E4%BA%9B%E5%86%85%E5%AE%B9><span class=nav-number>4.</span> <span class=nav-text>ZIP 下载成功后,还要检查哪些内容</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%90%8C%E6%97%B6%E6%AF%94%E8%BE%83%E8%AF%81%E4%B9%A6%E5%92%8C%E7%A7%81%E9%92%A5%E7%9A%84%E6%91%98%E8%A6%81><span class=nav-number>5.</span> <span class=nav-text>为什么要同时比较证书和私钥的摘要</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E6%96%B0%E6%96%87%E4%BB%B6%E6%80%8E%E6%A0%B7%E6%9B%BF%E6%8D%A2%E6%97%A7%E6%96%87%E4%BB%B6><span class=nav-number>6.</span> <span class=nav-text>新文件怎样替换旧文件</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%87%8D%E5%90%AF%E6%88%90%E5%8A%9F%E4%BB%A5%E5%90%8E%EF%BC%8C%E8%BF%98%E8%A6%81%E5%81%9A-TLS-%E8%BF%9E%E6%8E%A5><span class=nav-number>7.</span> <span class=nav-text>为什么重启成功以后,还要做 TLS 连接</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E9%83%A8%E7%BD%B2%E6%80%8E%E6%A0%B7%E7%A1%AE%E8%AE%A4%EF%BC%8C%E5%A4%B1%E8%B4%A5%E6%80%8E%E6%A0%B7%E6%81%A2%E5%A4%8D><span class=nav-number>8.</span> <span class=nav-text>部署怎样确认,失败怎样恢复</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E6%94%BE%E5%88%B0-Docker-%E9%87%8C%EF%BC%8C%E8%A6%81%E5%88%86%E6%B8%85%E4%B8%A4%E5%A5%97%E8%B7%AF%E5%BE%84><span class=nav-number>9.</span> <span class=nav-text>放到 Docker 里,要分清两套路径</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E6%9C%AC%E6%9C%BA%E8%BF%90%E8%A1%8C%E3%80%81SSH-%E5%92%8C%E5%AE%9A%E6%97%B6%E6%A3%80%E6%9F%A5%E5%90%84%E8%B4%9F%E8%B4%A3%E4%BB%80%E4%B9%88><span class=nav-number>10.</span> <span class=nav-text>本机运行、SSH 和定时检查各负责什么</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%AE%9E%E9%99%85%E6%8E%A5%E5%85%A5%E6%97%B6%EF%BC%8C%E5%85%88%E6%A0%B8%E5%AF%B9%E8%BF%99%E5%87%A0%E4%B8%AA%E9%85%8D%E7%BD%AE><span class=nav-number>11.</span> <span class=nav-text>实际接入时,先核对这几个配置</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%BB%8E%E6%BA%90%E7%A0%81%E5%BC%80%E5%A7%8B%EF%BC%8C%E6%80%8E%E6%A0%B7%E5%AE%9E%E9%99%85%E8%BF%90%E8%A1%8C><span class=nav-number>12.</span> <span class=nav-text>从源码开始,怎样实际运行</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E7%BC%96%E8%AF%91%E7%A8%8B%E5%BA%8F><span class=nav-number>12.1.</span> <span class=nav-text>编译程序</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%BD%BF%E7%94%A8%E5%AE%89%E8%A3%85%E5%99%A8%EF%BC%8C%E4%BF%9D%E7%95%99%E5%B7%B2%E6%9C%89%E9%85%8D%E7%BD%AE%E5%92%8C%E5%87%AD%E6%8D%AE><span class=nav-number>12.2.</span> <span class=nav-text>使用安装器,保留已有配置和凭据</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E9%9C%80%E8%A6%81%E9%80%90%E6%AD%A5%E6%A0%B8%E5%AF%B9%E6%97%B6%EF%BC%8C%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85%E7%A8%8B%E5%BA%8F%E5%92%8C%E9%85%8D%E7%BD%AE><span class=nav-number>12.3.</span> <span class=nav-text>需要逐步核对时,手动安装程序和配置</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E5%85%88%E6%89%A7%E8%A1%8C%E4%B8%80%E6%AC%A1%E6%A3%80%E6%9F%A5%E6%A8%A1%E5%BC%8F><span class=nav-number>12.4.</span> <span class=nav-text>先执行一次检查模式</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E5%AE%89%E8%A3%85-service%EF%BC%8C%E5%86%8D%E6%89%8B%E5%8A%A8%E6%AD%A3%E5%BC%8F%E8%BF%90%E8%A1%8C%E4%B8%80%E6%AC%A1><span class=nav-number>12.5.</span> <span class=nav-text>安装 service,再手动正式运行一次</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E7%A1%AE%E8%AE%A4%E5%8D%95%E6%AC%A1%E8%BF%90%E8%A1%8C%E5%90%8E%EF%BC%8C%E5%86%8D%E5%BC%80%E5%90%AF%E5%AE%9A%E6%97%B6%E5%99%A8><span class=nav-number>12.6.</span> <span class=nav-text>确认单次运行后,再开启定时器</span></a></ol></ol></div></div><div class="site-overview-wrap sidebar-panel"><div class="site-author animated" itemprop=author itemscope itemtype=http://schema.org/Person><img alt=Nicksxs class=site-author-image itemprop=image src=/uploads/avatar.jpg><p class=site-author-name itemprop=name>Nicksxs<div class=site-description itemprop=description>Nicksxs 的技术博客,深入 Java 运行时与生产诊断、低成本大模型推理和 Agent 工程实践。</div></div><div class="site-state-wrap animated"><nav class=site-state><div class="site-state-item site-state-posts"><a href=/archives/><span class=site-state-item-count>394</span> <span class=site-state-item-name>日志</span></a></div><div class="site-state-item site-state-categories"><a href=/categories/><span class=site-state-item-count>181</span> <span class=site-state-item-name>分类</span></a></div><div class="site-state-item site-state-tags"><a href=/tags/><span class=site-state-item-count>347</span> <span class=site-state-item-name>标签</span></a></div></nav></div><div class="links-of-author animated"><span class=links-of-author-item><a rel="noopener me" title="GitHub → https://github.com/nicksxs" href=https://github.com/nicksxs target=_blank><i class="fab fa-github fa-fw"></i>GitHub</a> </span><span class=links-of-author-item><a rel="noopener me" title="E-Mail → mailto:nicksxs1202@gmail.com" href=mailto:nicksxs1202@gmail.com target=_blank><i class="fa fa-envelope fa-fw"></i>E-Mail</a></span></div><div class="cc-license animated" itemprop=license><a class=cc-opacity href=https://creativecommons.org/licenses/by-nc-sa/4.0/ rel=noopener target=_blank><img alt="Creative Commons" src=https://cdnjs.cloudflare.com/ajax/libs/creativecommons-vocabulary/2020.11.3/assets/license_badges/small/by_nc_sa.svg></a></div><div class="widget-wrap sidebar-tag-cloud"><div class=tag-cloud-tags><a class=tag-cloud-10 href=/tags/Java/ style=font-size:16px>Java</a><a class=tag-cloud-9 href=/tags/leetcode/ style=font-size:15.62px>leetcode</a><a class=tag-cloud-8 href=/tags/LLM/ style=font-size:15.23px>LLM</a><a class=tag-cloud-8 href=/tags/%E7%94%9F%E6%B4%BB/ style=font-size:14.85px>生活</a><a class=tag-cloud-7 href=/tags/%E9%A2%98%E8%A7%A3/ style=font-size:14.46px>题解</a><a class=tag-cloud-6 href=/tags/SpringBoot/ style=font-size:14.08px>SpringBoot</a><a class=tag-cloud-6 href=/tags/MySQL/ style=font-size:14.08px>MySQL</a><a class=tag-cloud-5 href=/tags/C/ style=font-size:13.69px>C++</a><a class=tag-cloud-5 href=/tags/Redis/ style=font-size:13.31px>Redis</a><a class=tag-cloud-4 href=/tags/Tomcat/ style=font-size:12.92px>Tomcat</a><a class=tag-cloud-4 href=/tags/Mybatis/ style=font-size:12.92px>Mybatis</a><a class=tag-cloud-3 href=/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ style=font-size:12.54px>小技巧</a><a class=tag-cloud-2 href=/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ style=font-size:12.15px>数据结构</a><a class=tag-cloud-2 href=/tags/%E6%BA%90%E7%A0%81/ style=font-size:12.15px>源码</a><a class=tag-cloud-2 href=/tags/MQ/ style=font-size:11.77px>MQ</a><a class=tag-cloud-2 href=/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ style=font-size:11.77px>消息队列</a><a class=tag-cloud-2 href=/tags/RocketMQ/ style=font-size:11.77px>RocketMQ</a><a class=tag-cloud-2 href=/tags/%E8%BF%90%E5%8A%A8/ style=font-size:11.77px>运动</a><a class=tag-cloud-1 href=/tags/php/ style=font-size:11.38px>php</a><a class=tag-cloud-0 href=/tags/Spring/ style=font-size:11px>Spring</a><a class=tag-cloud-0 href=/tags/linux/ style=font-size:11px>linux</a><a class=tag-cloud-0 href=/tags/Docker/ style=font-size:11px>Docker</a><a class=tag-cloud-0 href=/tags/%E5%87%8F%E8%82%A5/ style=font-size:11px>减肥</a><a class=tag-cloud-0 href=/tags/%E8%B7%91%E6%AD%A5/ style=font-size:11px>跑步</a></div></div><div class="affiliate-creative affiliate-creative--sidebar" data-affiliate-id=tencent-cloud-server><a rel="sponsored noopener" aria-label=腾讯云服务器优惠 href=https://cloud.tencent.com/act/cps/redirect?redirect=6544&cps_key=2e33a1eedb380997d03618b4a5ba6ca7&from=console target=_blank><img alt="腾讯云 AI 焕新云服务器优惠" decoding=async height=241 loading=lazy src=/uploads/affiliate/tencent-cloud-server.webp width=690> </a><span class=affiliate-disclosure>推广</span></div></div></div></div><div class="sidebar-inner sidebar-blogroll"><div class="links-of-blogroll animated"><div class=links-of-blogroll-title><i class="fa fa-globe fa-fw"></i> 链接</div><ul class=links-of-blogroll-list><li class=links-of-blogroll-item><a href=https://covermusic.cn/ rel=noopener target=_blank title=https://covermusic.cn>69伙伴</a></ul></div></div></aside></div><div class="main-inner post posts-expand"><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article lang=zh-CN><link href=https://nicksxs.me/2026/10/18/%E7%94%A8Go%E8%87%AA%E5%8A%A8%E5%90%8C%E6%AD%A5%E8%85%BE%E8%AE%AF%E4%BA%91SSL%E8%AF%81%E4%B9%A6%E5%88%B0Traefik-%E4%B8%8B%E8%BD%BD%E6%A0%A1%E9%AA%8C%E4%B8%8E%E5%A4%B1%E8%B4%A5%E5%9B%9E%E6%BB%9A/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="Nicksxs 的技术博客,深入 Java 运行时与生产诊断、低成本大模型推理和 Agent 工程实践。" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="用 Go 自动同步腾讯云 SSL 证书到 Traefik:下载、校验与失败回滚 | Nicksxs's Blog" itemprop=name><meta content="从腾讯云已有新证书、网站仍使用旧证书的问题讲起,结合 ssl-sync 的 Go 源码,解释证书查询与下载、域名和私钥校验、文件摘要比较、原子替换、TLS 入口验证与失败回滚,以及 Docker 挂载和定时任务的实现边界。" itemprop=description></span><header class=post-header><h1 itemprop="name headline" class=post-title>用 Go 自动同步腾讯云 SSL 证书到 Traefik:下载、校验与失败回滚</h1><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-10-18 00:00:00" datetime=2026-10-18T00:00:00+08:00>2026-10-18</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/%E6%8A%80%E6%9C%AF/ itemprop=url rel=index><span itemprop=name>技术</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-title="用 Go 自动同步腾讯云 SSL 证书到 Traefik:下载、校验与失败回滚" data-increment=true data-path=/2026/10/18/%E7%94%A8Go%E8%87%AA%E5%8A%A8%E5%90%8C%E6%AD%A5%E8%85%BE%E8%AE%AF%E4%BA%91SSL%E8%AF%81%E4%B9%A6%E5%88%B0Traefik-%E4%B8%8B%E8%BD%BD%E6%A0%A1%E9%AA%8C%E4%B8%8E%E5%A4%B1%E8%B4%A5%E5%9B%9E%E6%BB%9A/ title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span></span></div><div class=post-description>从腾讯云已有新证书、网站仍使用旧证书的问题讲起,结合 ssl-sync 的 Go 源码,解释证书查询与下载、域名和私钥校验、文件摘要比较、原子替换、TLS 入口验证与失败回滚,以及 Docker 挂载和定时任务的实现边界。</div></div></header><div class=post-body itemprop=articleBody><p>网站的 HTTPS 证书快到期时,通常要先取得新证书,再更新服务器上的 <code>.crt</code> 和 <code>.key</code> 文件,让入口服务加载它们。腾讯云控制台里已经出现新证书,服务器还要完成这一轮部署。漏掉后半段,访问网站时仍可能看到旧证书。<p>如果入口使用 Docker 里的 Traefik,还要确认文件放到了宿主机的正确目录,容器能看到更新后的内容,Traefik 也确实加载了新证书。单看下载成功或者重启命令执行成功,都还缺少一步验证。<p>这里用一个叫 <code>ssl-sync</code> 的 Go 工具,把这些步骤连起来。它查询腾讯云已签发的证书,下载并检查内容,更新目标文件,再通过一次实际的 TLS 连接确认结果。<p>当前代码只调用证书列表和下载接口。证书申请、域名验证、续期与重签发,需要由已有的云端流程完成。本文根据 2026 年 10 月 18 日读取的源码分析,文中域名、地址和路径均为示例,不代表生产服务器的实际配置。</p><span id=more></span><h2 id=先看证书从哪里来,最后由谁提供><a class=headerlink href=#先看证书从哪里来,最后由谁提供 title=先看证书从哪里来,最后由谁提供></a>先看证书从哪里来,最后由谁提供</h2><p>HTTPS 连接里,服务器会向客户端出示证书。证书包含它覆盖的域名、公钥和有效期等信息;服务器还要持有相配的私钥,才能完成相应的身份认证过程。私钥应只保留在获准使用它的系统中。<p>证书文件可能包含多张证书。网站自身的那张叫叶证书,其他中间证书用于帮助客户端建立信任链。后面判断网站有没有换证时,工具比较的就是叶证书。<p>在本文的示例里,访问关系是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>浏览器 → Traefik 的 HTTPS 入口 → Gitea 等后端服务</span><br></pre></table></figure><p>Traefik 作为反向代理,接收访问请求,再转给后端。TLS 在 Traefik 这一层结束时,浏览器看到的是 Traefik 加载的证书。因此,更新文件和检查新证书是否生效,都要围绕这个入口进行。<p>Traefik 可以通过动态配置引用用户提供的证书:<figure class="highlight yaml"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=attr>tls:</span></span><br><span class=line> <span class=attr>certificates:</span></span><br><span class=line> <span class=bullet>-</span> <span class=attr>certFile:</span> <span class=string>/certs/gitea.crt</span></span><br><span class=line> <span class=attr>keyFile:</span> <span class=string>/certs/gitea.key</span></span><br></pre></table></figure><p>这里的路径属于 Traefik 所在的运行环境。放在 Docker 容器里时,它们就是容器内路径。配置格式可对照 <a href=https://doc.traefik.io/traefik/reference/routing-configuration/http/tls/tls-certificates/ rel=noopener target=_blank>Traefik 的用户证书说明</a>。<p><code>ssl-sync</code> 负责把云端证书送到这些路径对应的宿主机文件,并让入口重新加载。一次执行的主要流程如下:<p><img alt="ssl-sync 从查询新证书到确认入口生效的流程" loading=lazy src=/uploads/tencent-ssl-sync/deployment-flow.png><p>读源码时,可以按下面几个文件寻找对应步骤:<table><thead><tr><th>文件<th>负责的事情<tbody><tr><td><code>main.go</code><td>组织同步流程,决定跳过、提交或回滚<tr><td><code>tencent.go</code><td>腾讯云 API 签名、证书查询与下载<tr><td><code>certificate.go</code><td>从 ZIP 选择证书和私钥,检查内容<tr><td><code>deploy.go</code><td>本机或 SSH 部署、备份与恢复<tr><td><code>verify.go</code><td>连接 TLS 入口并比较证书指纹<tr><td><code>config.go</code><td>读取配置、补默认值与校验参数</table><h2 id=怎样从腾讯云选出要部署的证书><a class=headerlink href=#怎样从腾讯云选出要部署的证书 title=怎样从腾讯云选出要部署的证书></a>怎样从腾讯云选出要部署的证书</h2><p>同一个域名可能有多张证书。续期或重签发之后,新旧证书也可能同时出现在列表里,因此不能只拿搜索结果中的第一条。<p>工具调用 <code>DescribeCertificates</code>,请求参数在 <code>ListCertificates</code> 中:<figure class="highlight go"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>payload := <span class=keyword>map</span>[<span class=type>string</span>]any{</span><br><span class=line> <span class=string>"Offset"</span>: <span class=number>0</span>,</span><br><span class=line> <span class=string>"Limit"</span>: <span class=number>1000</span>,</span><br><span class=line> <span class=string>"SearchKey"</span>: domain,</span><br><span class=line> <span class=string>"CertificateType"</span>: <span class=string>"SVR"</span>,</span><br><span class=line>}</span><br></pre></table></figure><p><code>SVR</code> 表示服务器证书。<code>SearchKey</code> 是搜索关键词,会模糊匹配证书 ID、备注和域名,搜索命中还需要继续核对。参数定义见<a href=https://cloud.tencent.com/document/product/400/41671 rel=noopener target=_blank>腾讯云证书列表接口</a>。<p>取得列表后,代码保留 <code>Status == 1</code> 的候选,按 <code>CertEndTime</code> 降序排列;到期时间相同时,再比较 <code>CertBeginTime</code>。当前实现直接比较接口返回的时间字符串,依赖这些字段保持统一、可按字典序比较的格式。<p>这里的选择规则是优先尝试到期更晚的候选。随后逐张下载、解析,取第一张通过内容校验的证书。某个候选下载失败,或者证书不覆盖目标域名,程序会记录原因并尝试下一张。<p>所以,“选中的证书”还经过了下载包校验。列表里的名称和时间只负责排列尝试顺序。<p>配置也允许指定 <code>certificate_id</code>。填了以后会直接下载这张证书,跳过列表选择。它适合需要明确固定证书的场景;后续出现新 ID 时,这条配置仍指向原来的 ID,需要另行更新。<p>查询本身还有范围限制:当前只请求 <code>Offset = 0</code> 的一页,最多 1000 条,没有继续翻页。通配符或多域名证书能否进入候选,也取决于 <code>SearchKey</code> 的搜索结果。域名校验可以检查已下载的候选,无法补回根本没有搜到的证书。<h2 id=API-签名怎样证明请求来自有权限的调用者><a title="API 签名怎样证明请求来自有权限的调用者" class=headerlink href=#API-签名怎样证明请求来自有权限的调用者></a>API 签名怎样证明请求来自有权限的调用者</h2><p>腾讯云 API 凭据和网站私钥分别用于两件事。前者授权程序查询、下载云端证书;后者用于网站的 TLS 连接。<p><code>ssl-sync</code> 从环境变量读取 <code>SecretId</code>、<code>SecretKey</code>,使用临时凭据时还可以读取 Token。配置文件保存的是环境变量名称:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"secret_id_env"</span><span class=punctuation>:</span> <span class=string>"TENCENTCLOUD_SECRET_ID"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"secret_key_env"</span><span class=punctuation>:</span> <span class=string>"TENCENTCLOUD_SECRET_KEY"</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>这让普通配置与凭据值分开,降低把密钥随配置一起提交的概率。实际运行时,环境文件、进程权限和日志仍需要由部署环境管理。<p>代码使用 <code>TC3-HMAC-SHA256</code> 签名。它先把请求方法、路径、参与签名的请求头和请求体摘要拼成规范请求,再构造待签名字符串。接下来,用密钥按日期和服务名逐级派生签名密钥:<figure class="highlight go"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>secretDate := hmacSHA256([]<span class=type>byte</span>(<span class=string>"TC3"</span>+secretKey), []<span class=type>byte</span>(date))</span><br><span class=line>secretService := hmacSHA256(secretDate, []<span class=type>byte</span>(tencentService))</span><br><span class=line>secretSigning := hmacSHA256(secretService, []<span class=type>byte</span>(<span class=string>"tc3_request"</span>))</span><br><span class=line></span><br><span class=line>signature := hex.EncodeToString(hmacSHA256(secretSigning, []<span class=type>byte</span>(stringToSign)))</span><br></pre></table></figure><p>本工具的服务名是 <code>ssl</code>。请求头里的 <code>Authorization</code> 包含调用者 ID、凭证范围、签名头列表和计算出的签名,<code>SecretKey</code> 本身没有作为请求参数发送。腾讯云根据同样的规则验证签名,再检查调用权限。<p>签名中的日期按 UTC 计算,必须与请求时间戳对应。源码先调用 <code>UTC()</code>,再生成日期,避免本地日期与 UTC 日期跨日时不一致。这些规则可对照<a href=https://cloud.tencent.com/document/product/213/30654 rel=noopener target=_blank>腾讯云签名方法 v3</a>。<p>现有实现只用了 Go 标准库,没有引入腾讯云 SDK。这样可以直接看到签名过程;开发新的云 API 集成时,也可以采用官方 SDK,让 SDK 处理签名与请求细节。<h2 id=ZIP-下载成功后,还要检查哪些内容><a title="ZIP 下载成功后,还要检查哪些内容" class=headerlink href=#ZIP-下载成功后,还要检查哪些内容></a>ZIP 下载成功后,还要检查哪些内容</h2><p><code>DownloadCertificate</code> 返回的 <code>Content</code> 是 Base64 编码的 ZIP。程序先解码,再在内存里打开压缩包。<a href=https://cloud.tencent.com/document/api/400/41670 rel=noopener target=_blank>下载接口文档</a>说明了这一返回格式。<p>压缩包可能包含适配不同服务器的软件格式。工具会提高 Nginx 目录以及 <code>_bundle.crt</code> 等文件的候选优先级,但文件名只用于排序,实际内容还要通过检查。<p><code>extractCertificateBundle</code> 主要检查:<table><thead><tr><th>检查<th>要避免的问题<tbody><tr><td>解析 PEM 和 X.509 证书<td>文件存在,但内容无法作为证书读取<tr><td><code>VerifyHostname(domain)</code><td>备注或搜索命中正确,实际证书却不覆盖目标域名<tr><td>检查 <code>NotBefore</code><td>新证书还没有到生效时间<tr><td>检查 <code>NotAfter</code><td>证书已过期,或者剩余时间不够<tr><td><code>tls.X509KeyPair</code><td>证书和私钥没有配对</table><p>域名检查使用证书里的域名信息,具体匹配规则由 Go 的 <a href=https://pkg.go.dev/crypto/x509#Certificate.VerifyHostname rel=noopener target=_blank>VerifyHostname</a> 实现。匹配证书与私钥则调用 <a href=https://pkg.go.dev/crypto/tls#X509KeyPair rel=noopener target=_blank>X509KeyPair</a>。<p>剩余有效期的判断在源码里是:<figure class="highlight go"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=keyword>if</span> !leaf.NotAfter.After(now.Add(minimumValidity)) {</span><br><span class=line> reasons = <span class=built_in>append</span>(reasons, certFile.name+<span class=string>": 剩余有效期不足 "</span>+minimumValidity.String())</span><br><span class=line> <span class=keyword>continue</span></span><br><span class=line>}</span><br></pre></table></figure><p>例如,示例配置把 <code>minimum_validity</code> 设为 <code>168h</code>,要求证书剩余有效期大于 7 天。代码未配置时的默认值则是 <code>24h</code>。两者要分开看,不能把示例值当成程序默认值。<p>通过检查后,工具保留原来的证书 PEM 内容和匹配的私钥。它还限制 ZIP 内单文件及解压后总内容的大小,并且不按压缩包里的路径直接解压到目标目录。<p>这一阶段检查的是证书内容、域名、时间和密钥配对,没有单独调用 <code>x509.Verify</code> 构建完整信任链。部署后的 TLS 探测在默认配置下,还会经过客户端的常规证书验证。<h2 id=为什么要同时比较证书和私钥的摘要><a class=headerlink href=#为什么要同时比较证书和私钥的摘要 title=为什么要同时比较证书和私钥的摘要></a>为什么要同时比较证书和私钥的摘要</h2><p>定时任务会反复执行。没有新内容时,重复写文件、重启 Traefik 都会造成额外操作。<p>工具为下载得到的证书文件和私钥分别计算 SHA-256,再读取目标位置的两个文件,比较它们的完整内容摘要:<figure class="highlight go"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line><span class=keyword>if</span> strings.EqualFold(current.Cert, bundle.CertHash) &&</span><br><span class=line> strings.EqualFold(current.Key, bundle.KeyHash) {</span><br><span class=line> log.Printf(<span class=string>"[%s] 目标文件已是最新版本,无需重启"</span>, deployment.Name)</span><br><span class=line> <span class=keyword>return</span> <span class=literal>nil</span></span><br><span class=line>}</span><br></pre></table></figure><p>两个摘要都相同,这次同步就结束。这使得重复执行通常只发生查询、下载、校验和比较,不再触发部署。<p>完整文件摘要比较得很严格。即便叶证书相同,证书链内容或 PEM 文件的换行变化,也可能让摘要不同。私钥的编码形式改变同样可能触发更新。<p>这里还有一个运行边界:走到“内容相同”的分支后,代码直接返回,不再连接 HTTPS 入口。因此,这个定时工具不能代替持续的 HTTPS 监控。磁盘文件已经更新、服务却还在使用旧证书时,仅靠这条跳过分支发现不了问题。<h2 id=新文件怎样替换旧文件><a class=headerlink href=#新文件怎样替换旧文件 title=新文件怎样替换旧文件></a>新文件怎样替换旧文件</h2><p>本机部署从 <code>localDeployer.Begin</code> 开始。它先为旧证书和私钥创建备份,再写入新内容,随后执行配置的重启命令。<p>备份存在的目的,是在新证书无法正常投入使用时恢复原来的文件。原来不存在的文件也会被记录下来,恢复时应把新建文件删除,而不是保留半套新配置。<p>写入时,程序在目标文件的同一目录创建临时文件。以私钥为例,主要顺序是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>在私钥目录创建临时文件</span><br><span class=line> → 设置权限为 0600</span><br><span class=line> → 写入完整的新私钥</span><br><span class=line> → Sync 并关闭临时文件</span><br><span class=line> → Rename 到目标文件名</span><br><span class=line> → Sync 所在目录</span><br></pre></table></figure><p><code>0600</code> 表示只有文件所有者能读写。证书和私钥都写好后,工具才显式执行重启命令。<p>在面向 Linux 的这套路径里,同目录的重命名让单个目标文件从旧内容切换到完整新内容,减少其他进程读到半个文件的机会。<code>Sync</code> 则用于把文件内容和目录变化请求同步到存储层。相关接口见 Go 的 <a href=https://pkg.go.dev/os#Rename rel=noopener target=_blank>Rename</a> 和 <a href=https://pkg.go.dev/os#File.Sync rel=noopener target=_blank>File.Sync</a>。<p>这里需要保留两个区别。证书文件和私钥文件各自完成替换,两个文件没有在同一个文件系统事务里同时切换。如果另一个进程恰好在两次替换之间读取,仍可能短暂遇到新旧不配套的内容。<p>备份和恢复逻辑也依赖程序继续运行。它没有持久化事务日志,在进程被强制终止或机器断电以后,不会自动扫描上次的半成品并完成恢复。要进一步处理这类故障,需要另外设计版本目录、状态记录和启动恢复流程。<h2 id=为什么重启成功以后,还要做-TLS-连接><a title="为什么重启成功以后,还要做 TLS 连接" class=headerlink href=#为什么重启成功以后,还要做-TLS-连接></a>为什么重启成功以后,还要做 TLS 连接</h2><p>文件更新成功、Docker 命令退出码为 0,分别证明了文件操作和命令执行的结果。网站实际提供哪张证书,还要从客户端连接时观察。<p><code>verify.go</code> 会连接 <code>verify.address</code>,在 TLS 配置里设置 <code>ServerName</code>。它既用于域名验证,也让同一入口上的多个站点可以按 SNI 选择对应证书。配置还把最低 TLS 版本设为 1.2。<p>连接完成后,程序取得服务端给出的叶证书,再比较它与本次准备部署的叶证书:<figure class="highlight go"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br></pre><td class=code><pre><span class=line>servedHash := sha256.Sum256(certificates[<span class=number>0</span>].Raw)</span><br><span class=line>wantHash := sha256.Sum256(bundle.Leaf.Raw)</span><br><span class=line><span class=keyword>if</span> servedHash != wantHash {</span><br><span class=line> <span class=keyword>return</span> fmt.Errorf(<span class=string>"%s 仍在提供旧证书(当前 SHA256 %s,期望 %s)"</span>,</span><br><span class=line> cfg.Address,</span><br><span class=line> strings.ToUpper(hex.EncodeToString(servedHash[:<span class=number>8</span>])),</span><br><span class=line> strings.ToUpper(hex.EncodeToString(wantHash[:<span class=number>8</span>])))</span><br><span class=line>}</span><br></pre></table></figure><p><code>Raw</code> 是证书的 DER 编码。程序比较的是完整 SHA-256 值,错误信息只展示摘要的前 8 个字节,方便识别当前和期望的证书。<p>前面用于决定是否部署的是两个文件的摘要;这里用于确认生效的是叶证书的指纹。它们比较的对象不同。<p>在默认配置下,Go TLS 客户端会先校验证书链和域名,然后工具再核对指纹。配置虽然允许设置 <code>insecure_skip_verify</code>,但开启后会跳过常规链和域名检查,剩下的指纹比较不能等同于正常的信任链验证。相关行为见 <a href=https://pkg.go.dev/crypto/tls#Config rel=noopener target=_blank>Go TLS 配置说明</a>。<p>如果暂时没有观察到新证书,程序会等待 2 秒后再次尝试,默认验证窗口是 <code>60s</code>。这个窗口之外,单次连接本身还可能耗时,因此它不构成精确到秒的总耗时保证。<p>验证地址要指向真正要检查的 TLS 终止点。域名前面有 CDN 或其他负载均衡时,直接连接公网域名可能看到上游服务的证书。若目标是检查源站,可以把 <code>address</code> 配为明确的源站地址,同时保留目标域名作为 <code>server_name</code>。一次连接观察到的入口,也不能证明所有节点都已更新。<p>工具没有在这一步发送业务 HTTP 请求。TLS 指纹一致说明连接到的入口提供了预期证书,登录、页面内容和后端服务是否正常,还需要各自的检查。<h2 id=部署怎样确认,失败怎样恢复><a class=headerlink href=#部署怎样确认,失败怎样恢复 title=部署怎样确认,失败怎样恢复></a>部署怎样确认,失败怎样恢复</h2><p>主流程把部署结果分成 <code>Begin</code>、<code>Commit</code> 和 <code>Rollback</code>:<table><thead><tr><th>阶段<th>当前实现做什么<tbody><tr><td><code>Begin</code><td>保存旧文件,写入新文件,执行重启;备份继续保留<tr><td>TLS 验证<td>观察入口是否提供本次准备部署的叶证书<tr><td><code>Commit</code><td>接受本次更新,删除旧文件备份<tr><td><code>Rollback</code><td>恢复原文件,再次执行重启命令</table><p>本地文件写入或重启失败时,部署器内部会进入恢复逻辑。<code>Begin</code> 已成功、但后续 TLS 验证失败时,则由 <code>main.go</code> 调用 <code>Rollback</code>。<p>主流程给这次回滚创建了独立的 context:<figure class="highlight go"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>rollbackCtx, cancel := context.WithTimeout(context.Background(), <span class=number>5</span>*time.Minute)</span><br><span class=line>rollbackErr := transaction.Rollback(rollbackCtx)</span><br><span class=line>cancel()</span><br></pre></table></figure><p>原来的执行可能已经因为 Ctrl+C 等原因取消。这里从 <code>context.Background()</code> 开始,给后续恢复单独留出执行窗口。进程如果已经被强制终止,这段代码仍然没有机会运行。<p>回滚本身也会失败。例如,旧文件恢复了,但再次重启仍然失败。源码会把两层错误一起返回,不能只看“已尝试回滚”就判定网站恢复正常。<p>还有一种状态是新证书已经通过验证,但清理备份失败。主流程会返回“证书已生效,但清理备份失败”。这和入口仍提供旧证书是两类问题,处理时应该保留这一区别。<h2 id=放到-Docker-里,要分清两套路径><a title="放到 Docker 里,要分清两套路径" class=headerlink href=#放到-Docker-里,要分清两套路径></a>放到 Docker 里,要分清两套路径</h2><p>假设 Compose 把宿主机的证书目录映射进容器:<figure class="highlight yaml"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=attr>services:</span></span><br><span class=line> <span class=attr>traefik:</span></span><br><span class=line> <span class=attr>volumes:</span></span><br><span class=line> <span class=bullet>-</span> <span class=string>/opt/traefik/certs:/certs:ro</span></span><br></pre></table></figure><p>那么,两边使用的路径分别是:<table><thead><tr><th>使用位置<th>证书路径示例<tbody><tr><td><code>ssl-sync</code> 在宿主机写入<td><code>/opt/traefik/certs/gitea.crt</code><tr><td>Traefik 在容器内读取<td><code>/certs/gitea.crt</code></table><p>只读挂载 <code>:ro</code> 约束的是容器通过这个挂载进行写入,宿主机仍能更新对应目录。<p>这套实现采用临时文件加重命名,部署时应挂载整个证书目录。单独绑定挂载某个文件时,宿主机把文件重命名替换后,容器里的挂载可能继续关联旧文件,导致宿主机看到了新内容、容器仍读取旧内容。<p>Traefik 的 <a href=https://doc.traefik.io/traefik/reference/install-configuration/providers/others/file/ rel=noopener target=_blank>File provider 文档</a>也说明了绑定挂载、文件重命名和变更通知之间的限制,并建议使用父目录挂载。该限制不能靠“重启命令成功”来排除,实际证书指纹检查仍有必要。<p><code>ssl-sync</code> 示例选择在更新后重启 Traefik。这是当前工具采用的加载方式,不代表 Traefik 所有证书更新场景都必须重启。是否采用动态配置重载,需要结合 File provider 和实际挂载方式单独验证。<h2 id=本机运行、SSH-和定时检查各负责什么><a title="本机运行、SSH 和定时检查各负责什么" class=headerlink href=#本机运行、SSH-和定时检查各负责什么></a>本机运行、SSH 和定时检查各负责什么</h2><p>本机模式让程序直接运行在 Traefik 所在服务器,读取和替换本地文件。SSH 模式把校验后的证书和私钥打成 tar,通过 SSH 标准输入交给远端脚本。<p>两种部署器都实现相同的接口:<figure class="highlight go"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=keyword>type</span> Deployer <span class=keyword>interface</span> {</span><br><span class=line> CurrentHashes(context.Context, DeploymentConfig) (FileHashes, <span class=type>error</span>)</span><br><span class=line> Begin(context.Context, DeploymentConfig, *CertificateBundle) (Transaction, <span class=type>error</span>)</span><br><span class=line>}</span><br></pre></table></figure><p>主流程只负责比较内容和决定提交、回滚,具体文件操作交给部署器。SSH 的参数开启了 <code>BatchMode=yes</code> 和 <code>StrictHostKeyChecking=yes</code>,需要事先完成密钥登录和主机指纹登记。需要提权时使用 <code>sudo -n</code>,拿不到免密权限就失败,不等待输入密码。<p>远端使用临时文件加 <code>mv</code> 替换,并用 shell trap 处理部署脚本执行中的失败。它没有逐项实现本地部署器的文件和目录 <code>Sync</code> 调用,所以两种模式的落盘细节不能完全等同。<p>仓库还提供 systemd service 和 timer 模板。service 采用一次执行后退出的方式,timer 的主要设置是:<figure class="highlight ini"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line><span class=section>[Timer]</span></span><br><span class=line><span class=attr>OnCalendar</span>=*-*-* <span class=number>03</span>:<span class=number>17</span>:<span class=number>00</span></span><br><span class=line><span class=attr>OnCalendar</span>=*-*-* <span class=number>15</span>:<span class=number>17</span>:<span class=number>00</span></span><br><span class=line><span class=attr>RandomizedDelaySec</span>=<span class=number>30</span>m</span><br><span class=line><span class=attr>Persistent</span>=<span class=literal>true</span></span><br><span class=line><span class=attr>Unit</span>=ssl-sync.service</span><br></pre></table></figure><p>未另写时区时,这两个时间按服务器本地时区解释。随机延迟让实际执行时间向后分散;<code>Persistent=true</code> 则让定时器恢复激活时,有机会补做停机期间错过的日历任务,补做仍受随机延迟影响。<a href=https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html rel=noopener target=_blank>systemd timer 手册</a>说明了这些行为。<p>程序自身还用 <code>flock</code> 锁住配置的锁文件,避免使用同一把锁的进程同时执行。不同机器各自持有本机锁时,不能靠它协调对同一远端目标的并发更新。<p>多域名配置会依次执行。一项失败后,程序继续处理其他项目,最终用非零退出码表示本轮存在失败。每个项目各自提交或回滚,没有把所有域名打包成一个整体事务。<h2 id=实际接入时,先核对这几个配置><a class=headerlink href=#实际接入时,先核对这几个配置 title=实际接入时,先核对这几个配置></a>实际接入时,先核对这几个配置</h2><p>下面是一份本机模式的示例。它要求程序运行位置能访问腾讯云 API,也能读写目标证书目录和执行重启命令:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"tencent_cloud"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"minimum_validity"</span><span class=punctuation>:</span> <span class=string>"168h"</span></span><br><span class=line> <span class=punctuation>}</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"targets"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"traefik-server"</span><span class=punctuation>:</span> <span class=punctuation>{</span> <span class=attr>"type"</span><span class=punctuation>:</span> <span class=string>"local"</span> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>}</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"deployments"</span><span class=punctuation>:</span> <span class=punctuation>[</span></span><br><span class=line> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"name"</span><span class=punctuation>:</span> <span class=string>"gitea"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"target"</span><span class=punctuation>:</span> <span class=string>"traefik-server"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"domain"</span><span class=punctuation>:</span> <span class=string>"git.example.com"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"cert_file"</span><span class=punctuation>:</span> <span class=string>"/opt/traefik/certs/gitea.crt"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"key_file"</span><span class=punctuation>:</span> <span class=string>"/opt/traefik/certs/gitea.key"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"restart"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"working_directory"</span><span class=punctuation>:</span> <span class=string>"/opt/traefik"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"command"</span><span class=punctuation>:</span> <span class=punctuation>[</span><span class=string>"docker"</span><span class=punctuation>,</span> <span class=string>"compose"</span><span class=punctuation>,</span> <span class=string>"restart"</span><span class=punctuation>,</span> <span class=string>"traefik"</span><span class=punctuation>]</span></span><br><span class=line> <span class=punctuation>}</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"verify"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"enabled"</span><span class=punctuation>:</span> <span class=literal><span class=keyword>true</span></span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"address"</span><span class=punctuation>:</span> <span class=string>"git.example.com:443"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"server_name"</span><span class=punctuation>:</span> <span class=string>"git.example.com"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"timeout"</span><span class=punctuation>:</span> <span class=string>"60s"</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>]</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p><code>domain</code> 用于选择和校验证书;<code>cert_file</code>、<code>key_file</code> 指向宿主机实际文件;<code>working_directory</code> 对应 Compose 项目目录;<code>verify</code> 则决定从哪个入口观察哪一个域名的证书。把这些字段分别对上,比只确认 JSON 能解析更有用。<h2 id=从源码开始,怎样实际运行><a class=headerlink href=#从源码开始,怎样实际运行 title=从源码开始,怎样实际运行></a>从源码开始,怎样实际运行</h2><p>下面按 Linux 服务器、systemd 和 Docker Compose 的环境说明,把工具直接放在 Traefik 所在服务器运行。这样可以直接操作宿主机文件。新证书需要已经在腾讯云签发并允许下载,申请和续期仍由已有流程完成。<h3 id=编译程序><a class=headerlink href=#编译程序 title=编译程序></a>编译程序</h3><p>如果源码已经在目标服务器上,而且已有 Go 1.21 或更新版本,在包含 <code>go.mod</code>、<code>Makefile</code> 的源码目录执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>make <span class=built_in>test</span></span><br><span class=line>make build</span><br></pre></table></figure><p>程序会生成在 <code>bin/ssl-sync</code>。这个项目没有第三方 Go 模块依赖。<p>如果在 Mac 上编译后上传 Linux 服务器,需要指定目标系统和 CPU 架构。在服务器运行 <code>uname -m</code>:<code>x86_64</code> 对应 Go 的 <code>amd64</code>,<code>aarch64</code> 对应 <code>arm64</code>。例如,为 x86_64 的 Linux 服务器编译:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o bin/ssl-sync-linux-amd64 .</span><br></pre></table></figure><p>ARM64 服务器把 <code>GOARCH</code> 改成 <code>arm64</code>,输出文件名也相应改为 <code>ssl-sync-linux-arm64</code>。不能把默认编译出来的 macOS 程序直接放到 Linux 上运行。<p>在 Mac 编译时,将下面 4 个文件传到目标服务器的同一准备目录:<ul><li>对应架构的 Linux 程序。<li><code>config.example.json</code>。<li><code>deploy/systemd/ssl-sync.service</code>。<li><code>deploy/systemd/ssl-sync.timer</code>。</ul><p>后面的安装命令按在源码目录中执行书写。如果使用单独的上传目录,调整这些输入文件的路径即可。<h3 id=使用安装器,保留已有配置和凭据><a class=headerlink href=#使用安装器,保留已有配置和凭据 title=使用安装器,保留已有配置和凭据></a>使用安装器,保留已有配置和凭据</h3><p>项目已增加 <code>deploy/install.sh</code>。它需要目标机已有 Python 3、Docker 和 systemd,使用预先编译的程序,不自动安装这些软件包。在源码目录中可以执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>sudo ./deploy/install.sh --binary ./bin/ssl-sync \</span><br><span class=line> --domain git.example.com \</span><br><span class=line> --cert-file /opt/traefik/certs/gitea.crt \</span><br><span class=line> --key-file /opt/traefik/certs/gitea.key \</span><br><span class=line> --compose-dir /opt/traefik</span><br></pre></table></figure><p>也可以把完整配置作为 <code>--config ./config.json</code> 传入,适合多个域名。安装器保留已存在的 <code>/etc/ssl-sync/config.json</code> 和 <code>/etc/ssl-sync/env</code>;参数和示例只用于首次生成配置。升级时会备份被替换的程序、管理入口和 systemd 单元,文件写入失败时尝试恢复。<p>安装完成后,统一使用管理入口加载凭据和操作服务:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>sudoedit /etc/ssl-sync/env</span><br><span class=line>sudo /usr/local/sbin/ssl-sync-ctl check</span><br><span class=line>sudo /usr/local/sbin/ssl-sync-ctl activate</span><br><span class=line>sudo /usr/local/sbin/ssl-sync-ctl status</span><br></pre></table></figure><p><code>check</code> 运行检查模式;<code>activate</code> 依次检查、正式同步一次、开启定时器。任一步失败,后面的步骤不再执行。默认安装只放置文件,不自动触发证书替换或重启;已有定时器在升级时会暂停,并恢复其原有运行状态。<p>API 凭据可以使用专门的 CAM 子用户。以管理员身份进入腾讯云的“访问管理 → 用户 → 用户列表”,选择“新建用户 → 自定义创建”,创建可访问资源的子用户。再通过自定义策略关联下面两个操作,完整策略示例已放入 <code>deploy/tencent-policy.json</code>:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"version"</span><span class=punctuation>:</span> <span class=string>"2.0"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"statement"</span><span class=punctuation>:</span> <span class=punctuation>[</span></span><br><span class=line> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"effect"</span><span class=punctuation>:</span> <span class=string>"allow"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"action"</span><span class=punctuation>:</span> <span class=punctuation>[</span><span class=string>"ssl:DescribeCertificates"</span><span class=punctuation>,</span> <span class=string>"ssl:DownloadCertificate"</span><span class=punctuation>]</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"resource"</span><span class=punctuation>:</span> <span class=punctuation>[</span><span class=string>"*"</span><span class=punctuation>]</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>]</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>这份策略的资源范围是账号证书。给子用户关联策略后,在其详情页创建 API 密钥,将生成的 SecretId 和 SecretKey 填入服务器的凭据文件。SecretKey 需要在创建时保存。具体入口见<a href=https://cloud.tencent.com/document/product/598/13674 rel=noopener target=_blank>创建子用户</a>、<a href=https://cloud.tencent.com/document/product/598/80789 rel=noopener target=_blank>创建自定义策略</a>和<a href=https://cloud.tencent.com/document/product/598/37140 rel=noopener target=_blank>子账号 API 密钥管理</a>。<h3 id=需要逐步核对时,手动安装程序和配置><a class=headerlink href=#需要逐步核对时,手动安装程序和配置 title=需要逐步核对时,手动安装程序和配置></a>需要逐步核对时,手动安装程序和配置</h3><p>在服务器上执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>sudo install -m 0755 bin/ssl-sync /usr/local/bin/ssl-sync</span><br><span class=line>sudo install -d -m 0700 /etc/ssl-sync</span><br><span class=line>sudo install -m 0600 config.example.json /etc/ssl-sync/config.json</span><br><span class=line>sudo <span class=built_in>touch</span> /etc/ssl-sync/env</span><br><span class=line>sudo <span class=built_in>chmod</span> 0600 /etc/ssl-sync/env</span><br><span class=line>sudoedit /etc/ssl-sync/env /etc/ssl-sync/config.json</span><br></pre></table></figure><p>如果上传的是 <code>ssl-sync-linux-amd64</code>,第一条命令的输入文件替换为它,安装后的名字仍是 <code>/usr/local/bin/ssl-sync</code>。这些命令面向首次安装;已经有配置时,保留原配置并核对字段,避免用示例覆盖现有内容。<p><code>env</code> 文件填入专门用于这项任务的 API 凭据,下面的中文内容需要替换为真实值:<figure class="highlight plaintext"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>TENCENTCLOUD_SECRET_ID='填入专用子用户的SecretId'</span><br><span class=line>TENCENTCLOUD_SECRET_KEY='填入专用子用户的SecretKey'</span><br></pre></table></figure><p>保持每行 <code>名称=值</code> 的写法,不在行首添加 <code>export</code>。这里用简单的单引号赋值,兼容下文的 shell 加载和 service 的 <code>EnvironmentFile</code>。临时凭据还要增加 <code>TENCENTCLOUD_SESSION_TOKEN</code>,并由调用方负责更新过期凭据。<p>该身份需要允许 <code>ssl:DescribeCertificates</code> 和 <code>ssl:DownloadCertificate</code>。列表与下载接口的资源授权粒度不同,可以按<a href=https://cloud.tencent.com/document/product/598/98168 rel=noopener target=_blank>腾讯云 SSL 的 CAM 接口授权表</a>配置访问范围。凭据只在服务器的受限文件里填写,示例和文章中不放真实值。<p>程序当前通过环境变量接收凭据。文件权限能限制直接读取,不能替代完整的密钥管理;systemd 对环境变量和 <code>EnvironmentFile</code> 的行为可查<a href=https://www.freedesktop.org/software/systemd/man/257/systemd.exec.html rel=noopener target=_blank>执行环境文档</a>。<p>接着在 <code>config.json</code> 改好前一节列出的域名、宿主机证书路径、Compose 目录、服务名和验证入口。<code>restart.command</code> 中的 <code>traefik</code> 必须与实际 Compose 服务名一致。通常保留 <code>certificate_id</code> 为空,让程序每次重新选择候选。<h3 id=先执行一次检查模式><a class=headerlink href=#先执行一次检查模式 title=先执行一次检查模式></a>先执行一次检查模式</h3><p>直接运行程序不会自动加载 <code>/etc/ssl-sync/env</code>。下面用 root shell 显式读取刚才创建的赋值文件,再将变量传给程序:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>sudo sh -c <span class=string>'</span></span><br><span class=line><span class=string> set -eu</span></span><br><span class=line><span class=string> set -a</span></span><br><span class=line><span class=string> . /etc/ssl-sync/env</span></span><br><span class=line><span class=string> set +a</span></span><br><span class=line><span class=string> exec /usr/local/bin/ssl-sync -config /etc/ssl-sync/config.json -dry-run</span></span><br><span class=line><span class=string>'</span></span><br></pre></table></figure><p>这次会读取凭据、查询并下载证书、校验内容和读取目标文件摘要,也会使用进程锁;发现变化后停在部署之前,不覆盖目标证书或执行重启。SSH 模式也需要能连接目标机器读取摘要。检查模式通过后,写入权限、重启和入口验证仍需要在实际执行时确认。<p>如果提示缺少凭据,先核对文件中的变量名称和加载方式;提示没有可用证书时,再看查询结果、域名覆盖、下载权限和剩余有效期。示例的 <code>168h</code> 会拒绝剩余不足 7 天的候选。<h3 id=安装-service,再手动正式运行一次><a title="安装 service,再手动正式运行一次" class=headerlink href=#安装-service,再手动正式运行一次></a>安装 service,再手动正式运行一次</h3><p>确认检查模式的证书与目标路径符合预期后,安装 systemd 模板,再执行正式部署:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>sudo install -m 0644 deploy/systemd/ssl-sync.service /etc/systemd/system/ssl-sync.service</span><br><span class=line>sudo install -m 0644 deploy/systemd/ssl-sync.timer /etc/systemd/system/ssl-sync.timer</span><br><span class=line>sudo systemctl daemon-reload</span><br><span class=line>sudo systemctl start ssl-sync.service</span><br><span class=line>sudo journalctl -u ssl-sync.service -n 100 --no-pager</span><br></pre></table></figure><p>service 会通过 <code>EnvironmentFile=/etc/ssl-sync/env</code> 加载凭据,无需在终端再次导出变量。有证书变化时,这一步会覆盖目标文件并执行配置的重启命令,入口服务可能短暂中断。重启与验证的结果以这次实际日志为准。<p>这是一次执行后退出的 service,无需保持常驻。可以查看 systemd 记录的结果:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>sudo systemctl show ssl-sync.service -p Result -p ExecMainStatus</span><br></pre></table></figure><p>没有证书变化时,日志会记录无需重启;执行了更新并通过验证时,才会记录同步成功。前文提到的跳过分支仍然存在,任务成功退出不能替代持续的 HTTPS 监控。<h3 id=确认单次运行后,再开启定时器><a class=headerlink href=#确认单次运行后,再开启定时器 title=确认单次运行后,再开启定时器></a>确认单次运行后,再开启定时器</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>sudo systemctl <span class=built_in>enable</span> --now ssl-sync.timer</span><br><span class=line>sudo systemctl list-timers --all ssl-sync.timer</span><br></pre></table></figure><p>模板按服务器本地时间每天检查两次,并加入随机延迟。停止后续定时检查可以执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>sudo systemctl <span class=built_in>disable</span> --now ssl-sync.timer</span><br></pre></table></figure><p>停止 timer 不会撤销已经完成的证书更新,也不会终止一项已经在运行的 service。后续查看执行结果,仍使用前面的 <code>journalctl</code> 命令。<p>如果改从 Mac 经 SSH 操作服务器,使用本机编译的 macOS 程序和 <code>config.ssh.example.json</code>,填好目标主机、登录用户、密钥与 <code>known_hosts</code> 路径;其中证书路径和 Compose 目录都属于远端服务器。SSH 方式仍需要当前运行机器能读取 API 凭据并保持运行,远端权限和主机指纹也要事先配置好。<p>本文核对了现有实现,并运行其已有的 12 个 Go 单元测试和 <code>go vet</code>,结果通过。测试覆盖配置、签名固定样例、证书筛选、本地文件提交与恢复、部分 SSH 脚本恢复路径。安装器另有 10 项测试通过,覆盖配置与凭据保留、重复安装、升级恢复,以及检查或正式运行失败时不启用定时器。SSH 单元测试用本地假命令执行脚本,没有连接真实服务器;签名测试也没有调用腾讯云 API。<p>上述测试结果没有包含真实腾讯云账号联调、生产 Docker 重启、线上 TLS 换证验证或进程崩溃后的恢复验证。正式接入时,可以按配置检查、检查模式、一次实际部署和入口证书确认的顺序逐项记录结果。</div><footer class=post-footer><div class="affiliate-creative affiliate-creative--post-end" data-affiliate-id=tencent-cloud-server><a rel="sponsored noopener" aria-label=腾讯云服务器优惠 href=https://cloud.tencent.com/act/cps/redirect?redirect=6544&cps_key=2e33a1eedb380997d03618b4a5ba6ca7&from=console target=_blank><img alt="腾讯云 AI 焕新云服务器优惠" decoding=async height=241 loading=lazy src=/uploads/affiliate/tencent-cloud-server.webp width=690> </a><span class=affiliate-disclosure>推广</span></div><div class=reward-container><div>请我一杯咖啡吧!</div><button>赞赏</button><div class=post-reward><div><img alt="Nicksxs 微信" src=https://i.loli.net/2020/01/12/NUlWanT31E8LQGx.png><span>微信</span></div><div><img alt="Nicksxs 支付宝" src=https://ooo.0o0.ooo/2016/08/17/57b421e773057.jpg><span>支付宝</span></div></div></div><div class=post-copyright><ul><li class=post-copyright-author><strong>本文作者: </strong>Nicksxs<li class=post-copyright-link><strong>本文链接:</strong> <a title="用 Go 自动同步腾讯云 SSL 证书到 Traefik:下载、校验与失败回滚" href=https://nicksxs.me/2026/10/18/%E7%94%A8Go%E8%87%AA%E5%8A%A8%E5%90%8C%E6%AD%A5%E8%85%BE%E8%AE%AF%E4%BA%91SSL%E8%AF%81%E4%B9%A6%E5%88%B0Traefik-%E4%B8%8B%E8%BD%BD%E6%A0%A1%E9%AA%8C%E4%B8%8E%E5%A4%B1%E8%B4%A5%E5%9B%9E%E6%BB%9A/>https://nicksxs.me/2026/10/18/用Go自动同步腾讯云SSL证书到Traefik-下载校验与失败回滚/</a><li class=post-copyright-license><strong>版权声明: </strong>本博客所有文章除特别声明外,均采用 <a href=https://creativecommons.org/licenses/by-nc-sa/4.0/ rel=noopener target=_blank><i class="fab fa-fw fa-creative-commons"></i>BY-NC-SA</a> 许可协议。转载请注明出处!</ul></div><div class=post-tags><a href=/tags/Docker/ rel=tag># Docker</a><a href=/tags/%E8%85%BE%E8%AE%AF%E4%BA%91/ rel=tag># 腾讯云</a><a href=/tags/Go/ rel=tag># Go</a><a href=/tags/HTTPS/ rel=tag># HTTPS</a><a href=/tags/Traefik/ rel=tag># Traefik</a></div><div class=post-nav><div class=post-nav-item><a title="腾讯 Octop 是什么:WorkBuddy 对比、Agent Loop 与本地部署" href=/2026/10/10/octop-local-deployment/ rel=prev><i class="fa fa-angle-left"></i> 腾讯 Octop 是什么:WorkBuddy 对比、Agent Loop 与本地部署</a></div><div class=post-nav-item><a title="为什么专家缓存救不了稠密模型:16GB 显存跑 27B Dense 模型的推理优化实践" href=/2026/10/25/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%93%E5%AE%B6%E7%BC%93%E5%AD%98%E6%95%91%E4%B8%8D%E4%BA%86%E7%A8%A0%E5%AF%86%E6%A8%A1%E5%9E%8B-16GB%E6%98%BE%E5%AD%98%E8%B7%9127B-Dense%E6%A8%A1%E5%9E%8B%E7%9A%84%E6%8E%A8%E7%90%86%E4%BC%98%E5%8C%96%E5%AE%9E%E8%B7%B5/ rel=next>为什么专家缓存救不了稠密模型:16GB 显存跑 27B Dense 模型的推理优化实践 <i class="fa fa-angle-right"></i></a></div></div></footer></article></div><div class=comments id=disqus_thread><noscript>Please enable JavaScript to view the comments powered by Disqus.</noscript></div></div></main><footer class=footer><div class=footer-inner><div class=beian><a href=https://beian.miit.gov.cn/ rel=noopener target=_blank>浙ICP备16002580号-2 </a><img alt src=/uploads/beian.png><a href=https://beian.mps.gov.cn/#/query/webSearch?code=33010602011094 rel=noopener target=_blank>浙公网安备 33010602011094号</a></div><div class=copyright>© <span itemprop=copyrightYear>2026</span><span class=with-love><i class="fa fa-heart"></i> </span><span class=author itemprop=copyrightHolder>Nicksxs</span></div></div></footer><div class="toggle sidebar-toggle" role=button><span class=toggle-line></span><span class=toggle-line></span><span class=toggle-line></span></div><div class=sidebar-dimmer></div><div aria-label=返回顶部 class=back-to-top role=button><i class="fa fa-arrow-up fa-lg"></i><span>0%</span></div><a aria-label="在 GitHub 上关注我" title="在 GitHub 上关注我" class=github-corner href=https://github.com/nicksxs rel=noopener target=_blank><svg viewbox="0 0 250 250" aria-hidden=true height=80 width=80><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" style="transform-origin:130px 106px" class=octo-arm fill=currentColor></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" class=octo-body fill=currentColor></path></svg></a><noscript><div class=noscript-warning>Theme NexT works best with JavaScript enabled</div></noscript><script data-cloudflare-page-counter-config type=application/json>{"enable":true,"endpoint":"https://counter.nicksxs.me","site":"nicksxs","local_cache":true,"timeout":5000,"icon":"far fa-eye","item_text":true}</script><script src=/js/cloudflare-page-counter.js></script><script class=next-config data-name=disqus type=application/json>{"enable":true,"shortname":"nicksxs","count":false,"i18n":{"disqus":"disqus"}}</script><script defer src=/js/third-party/comments/disqus.js></script>
|