You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Node.js中监听Certbot生成的Let's Encrypt证书变更?

解决Certbot续期后Node.js自动加载新证书的问题

为什么监听符号链接无效?

Node.js的fs.watch和fs.watchFile默认监听符号链接本身的变化,而Certbot续期时是先将新证书写入archive/目录,再替换live/下的符号链接指向新文件。这个替换操作会改变符号链接的inode,但部分系统下fs.watch无法稳定捕捉到该变化;如果直接监听符号链接指向的目标文件,续期后目标文件会被替换成新文件,旧文件的监听也会失效。

可靠的解决方案

1. 监听live/目录而非单个符号链接

监听证书所在的live/your-domain/目录,当Certbot替换符号链接时,目录的内容变化会触发事件。事件触发后重新读取证书文件并更新Express的安全上下文:

const fs = require('fs');
const https = require('https');
const express = require('express');

const app = express();
const certDir = '/etc/letsencrypt/live/your-domain/';
let lastCertStat = null;

// 初始化HTTPS服务器
function createServer() {
  const options = {
    cert: fs.readFileSync(`${certDir}/fullchain.pem`),
    key: fs.readFileSync(`${certDir}/privkey.pem`)
  };
  return https.createServer(options, app);
}

let server = createServer();
server.listen(443);

// 监听目录变化
fs.watch(certDir, (eventType) => {
  if (eventType !== 'change' && eventType !== 'rename') return;
  
  fs.stat(`${certDir}/fullchain.pem`, (err, stat) => {
    if (err) return console.error('读取证书状态失败:', err);
    if (lastCertStat && stat.ino === lastCertStat.ino) return;
    
    lastCertStat = stat;
    console.log('证书已更新,重新加载...');
    
    const newServer = createServer();
    newServer.listen(443, () => {
      server.close(() => {
        server = newServer;
        console.log('新证书已生效');
      });
    });
  });
});

2. 利用Certbot的续期钩子(最推荐)

Certbot支持在续期完成后执行自定义脚本,这是最可靠的方式,无需依赖文件监听。

步骤1:编写证书重载脚本

创建/usr/local/bin/reload-node-cert.sh,内容如下:

#!/bin/bash
# 发送USR2信号给Node.js进程(替换成你的Node进程PID或进程名)
PID=$(pgrep -f "node your-app.js")
if [ -n "$PID" ]; then
  kill -USR2 $PID
  echo "已通知Node.js进程重新加载证书"
fi

给脚本添加执行权限:chmod +x /usr/local/bin/reload-node-cert.sh

步骤2:配置Certbot钩子

编辑你的域名续期配置文件/etc/letsencrypt/renewal/your-domain.conf,添加:

renew_hook = /usr/local/bin/reload-node-cert.sh

或者执行Certbot续期时添加参数:certbot renew --renew-hook "/usr/local/bin/reload-node-cert.sh"

步骤3:Node.js监听信号并重载证书

在你的Express应用中添加信号监听:

const fs = require('fs');
const https = require('https');
const express = require('express');

const app = express();
const certDir = '/etc/letsencrypt/live/your-domain/';

function updateSecureContext(server) {
  const options = {
    cert: fs.readFileSync(`${certDir}/fullchain.pem`),
    key: fs.readFileSync(`${certDir}/privkey.pem`)
  };
  server.setSecureContext(options);
  console.log('证书已成功重载');
}

const server = https.createServer({
  cert: fs.readFileSync(`${certDir}/fullchain.pem`),
  key: fs.readFileSync(`${certDir}/privkey.pem`)
}, app);
server.listen(443);

// 监听USR2信号
process.on('SIGUSR2', () => {
  updateSecureContext(server);
});

3. 定期轮询(备选/测试用)

如果以上方法都不适用,可以采用定期检查符号链接目标的方式,虽然不够高效但简单可靠:

const fs = require('fs');
const https = require('https');
const express = require('express');

const app = express();
const certDir = '/etc/letsencrypt/live/your-domain/';
let lastCertPath = null;

function checkAndReload() {
  fs.readlink(`${certDir}/fullchain.pem`, (err, currentPath) => {
    if (err) return console.error('读取符号链接失败:', err);
    
    if (currentPath !== lastCertPath) {
      lastCertPath = currentPath;
      console.log('证书已更新,重新加载...');
      const options = {
        cert: fs.readFileSync(`${certDir}/fullchain.pem`),
        key: fs.readFileSync(`${certDir}/privkey.pem`)
      };
      server.setSecureContext(options);
    }
  });
}

const server = https.createServer({
  cert: fs.readFileSync(`${certDir}/fullchain.pem`),
  key: fs.readFileSync(`${certDir}/privkey.pem`)
}, app);
server.listen(443);

// 每小时检查一次(可调整间隔)
setInterval(checkAndReload, 3600000);

// 初始化时记录当前路径
checkAndReload();

快速测试方法

不用等待数月证书过期,直接用Certbot的模拟续期命令测试:

certbot renew --dry-run

这个命令会模拟完整的续期流程,生成测试证书并替换live/下的符号链接,你可以立刻验证监听或钩子是否生效。

内容的提问来源于stack exchange,提问作者Axel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 16:47:41