如何在Node.js中监听Certbot生成的Let's Encrypt证书变更?
为什么监听符号链接无效?
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

