Let’s Encrypt 提供各种免费 SSL/TLS 证书

我们主要使用的是通配符证书

直接使用acme.sh申请就行,不需要使用官方的Certbot

  1. 获取腾讯云 API 密钥

    登录 腾讯云控制台访问管理API 密钥管理

  2. 下载acme.sh

    1
    curl https://get.acme.sh | sh -s email=your@email.com
  3. 设置腾讯云 API 环境变量

export Tencent_SecretId=”xxxxxx”
export Tencent_SecretKey=”xxxxxxxx”

  1. 首次申请证书

    1
    2
    3
    4
    5
    6
    # 申请证书(包含所有多个域名)
    acme.sh --issue --dns dns_tencent \
    -d "whoisjory.com" \
    -d "*.whoisjory.com" \
    -d "storage.whoisjory.com" \
    -d "*.storage.whoisjory.com"
    • 这将只获得一个证书,但一个证书全都能用(每个证书最多包含 100个域名
    1
    2
    # 可以查看一下证书包含的域名(记得换路径)
    openssl x509 -in /root/nginx/cert/fullchain.pem -text -noout | grep -A1 "X509v3 Subject Alternative Name"

    现在有证书了, 先不要着急做下面的事件,去运行起来,run!

  2. 配置证书自动部署

    1
    2
    3
    4
    5
    # 设置自动部署命令
    acme.sh --install-cert -d whoisjory.com \
    --key-file /root/nginx/cert/acme/privkey.pem \
    --fullchain-file /root/nginx/cert/acme/fullchain.pem \
    --reloadcmd "docker exec nginx nginx -s reload"
  3. 配置自动续签时间

    Let’s Encrypt 证书有效期默认为 90 天

    1
    2
    3
    4
    5
    6
    # 编辑 acme.sh 的账户配置
    vim ~/.acme.sh/account.conf

    # 添加以下内容:
    AUTO_UPGRADE="1" #自动更新 acme.sh,避免因脚本版本过旧导致与Let's Encrypt API的兼容性问题
    RENEW_DAYS_BEFORE_EXPIRE="60" #证书续签触发时间在证书到期前 60 天 开始

    acme.sh 通过 cron 任务实现自动化,这表示每天午夜(00:00)检查一次证书状态

    1
    crontab -l
  4. 验证续签系统

    1
    2
    3
    4
    5
    # 手动测试续签(不实际续签)
    acme.sh --renew -d whoisjory.com --force --test

    # 强制立即续签(实际测试)
    acme.sh --renew -d whoisjory.com --force

两个命令的核心区别

命令类型 --issue (签发证书) --install-cert (安装证书)
作用 向 Let’s Encrypt 申请新证书 将已申请的证书部署到指定路径并设置续签行为
使用频率 首次申请或需要新增域名时使用 每次证书签发/续签后自动执行
是否修改证书内容 是(生成新证书) 否(仅配置部署规则)
典型使用场景 证书首次申请、新增域名、证书撤销后重新申请 设置证书存储路径、配置服务重启命令
与 account.conf 关系 不受其直接影响 依赖 account.conf 中的 AUTO_UPGRADERENEW_DAYS_BEFORE_EXPIRE 参数控制自动续签行为
let artalkItem = null const option = null const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' const destroyArtalk = () => { if (artalkItem) { artalkItem.destroy() artalkItem = null } } const artalkChangeMode = theme => artalkItem && artalkItem.setDarkMode(theme === 'dark') const initArtalk = (el = document, pageKey = location.pathname) => { artalkItem = Artalk.init({ el: el.querySelector('#artalk-wrap'), server: 'https://interact.whoisjory.com', site: 'Jorysinteract', darkMode: document.documentElement.getAttribute('data-theme') === 'dark', ...option, pageKey: isShuoshuo ? pageKey : (option && option.pageKey) || pageKey, imgUpload: true }) if (GLOBAL_CONFIG.lightbox === 'null') return artalkItem.on('list-loaded', () => { artalkItem.ctx.get('list').getCommentNodes().forEach(comment => { const $content = comment.getRender().$content btf.loadLightbox($content.querySelectorAll('img:not([atk-emoticon])')) }) }) if (isShuoshuo) { window.shuoshuoComment.destroyArtalk = () => { destroyArtalk() if (el.children.length) { el.innerHTML = '' el.classList.add('no-comment') } } } btf.addGlobalFn('pjaxSendOnce', destroyArtalk, 'destroyArtalk') btf.addGlobalFn('themeChange', artalkChangeMode, 'artalk') } const loadArtalk = async (el, pageKey) => { if (typeof Artalk === 'object') initArtalk(el, pageKey) else { await btf.getCSS('https://lib.baomitu.com/artalk/2.9.1/Artalk.min.css') await btf.getScript('https://lib.baomitu.com/artalk/2.9.1/Artalk.min.js') initArtalk(el, pageKey) } } if (isShuoshuo) { 'Artalk' === 'Artalk' ? window.shuoshuoComment = { loadComment: loadArtalk } : window.loadOtherComment = loadArtalk return } if ('Artalk' === 'Artalk' || !false) { if (false) btf.loadComment(document.getElementById('artalk-wrap'), loadArtalk) else setTimeout(loadArtalk, 100) } else { window.loadOtherComment = loadArtalk } })()