Heroku部署Express无法调用req.socket.getPeerCertificate获取客户端证书
Heroku部署Node.js mTLS服务时SSL终止后获取客户端证书的方案
问题背景
- 本地使用Node.js原生
https模块搭配自签名服务端/客户端证书实现双向TLS认证(mTLS)时功能完全正常,部署到Heroku平台后相关逻辑失效 - 根因是Heroku平台默认会在边缘路由节点统一完成SSL终止,向后端应用转发的是解密后的明文HTTP请求,应用无法直接通过
https模块对外提供HTTPS服务获取TLS层的客户端证书信息 - 故障现象:Heroku环境下
req.socket是普通TCP套接字,不存在getPeerCertificate()方法,req.client.authorized字段也不存在,原有证书校验逻辑完全无法运行
本地可正常运行的HTTPS服务代码
const fs = require('fs'); const https = require('https'); const express = require('express'); const app = express(); const port = process.env.PORT || 4545; app.get('/', (req, res) => { console.log(req.socket.getPeerCertificate()); if (!req.client.authorized) { return res.status(401).send('You are not authorized'); } return res.send('Hello, world!'); }); https .createServer( { requestCert: true, rejectUnauthorized: false, ca: fs.readFileSync('ca.crt'), cert: fs.readFileSync('server.crt'), key: fs.readFileSync('server.key') }, app) .listen(port, () => { console.log('listening on port ', port) });
Heroku部署的故障版本代码
const fs = require('fs'); const https = require('https'); const express = require('express'); const app = express(); const port = process.env.PORT || 4545; app.get('/', (req, res) => { console.log(req.socket.getPeerCertificate()); if (!req.client.authorized) { return res.status(401).send('You are not authorized'); } return res.send('Hello, world!'); }); app.listen(port, () => { console.log(`Example app listening on port ${port}`) });
客户端测试代码
const fs = require('fs'); const https = require('https'); process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 const req = https.request( { hostname: 'localhost', port: 4545, method: 'GET', path: '/', cert: fs.readFileSync('client.crt'), key: fs.readFileSync('client.key') }, res => { res.on('data', function(data) { console.log("recieved data ", data.toString()); }); } ); req.end();
可行解决方案
方案1:使用Heroku原生客户端证书透传能力
该功能为Heroku付费dyno专属能力,免费层级不支持。开启后Heroku边缘节点会在TLS握手阶段接收客户端证书,将URL编码后的PEM格式证书内容通过X-Client-Cert请求头透传给后端应用,你需要在Express侧自行完成证书校验,替代原有TLS层的校验逻辑。
改造后的服务代码示例:
const fs = require('fs'); const express = require('express'); const crypto = require('crypto'); const app = express(); const port = process.env.PORT || 4545; // 加载你信任的CA根证书 const trustedCA = fs.readFileSync('ca.crt'); const caCert = new crypto.X509Certificate(trustedCA); app.get('/', (req, res) => { const clientCertHeader = req.headers['x-client-cert']; if (!clientCertHeader) { return res.status(401).send('No client certificate provided'); } // 解码Heroku编码后的证书内容 const clientCertPem = decodeURIComponent(clientCertHeader); try { const clientCert = new crypto.X509Certificate(clientCertPem); // 校验证书是否由信任CA签发 const isTrusted = clientCert.verify(caCert.publicKey); // 校验证书是否在有效期内 const isNotExpired = new Date(clientCert.validTo) > new Date(); if (!isTrusted || !isNotExpired) { return res.status(401).send('Invalid client certificate'); } // 校验通过,可从clientCert中提取主题、序列号等信息做身份识别 console.log('Valid client certificate, subject:', clientCert.subject); return res.send('Hello, world!'); } catch (err) { return res.status(401).send('Invalid client certificate format'); } }); app.listen(port, () => { console.log(`Example app listening on port ${port}`) });
注意:使用该方案时必须配置Heroku的路由规则,强制所有请求使用HTTPS,禁止HTTP明文访问。
方案2:端到端TLS穿透(无平台付费依赖)
如果不想使用Heroku的付费功能,可以配置TCP路由或者支持SNI穿透的路由规则,让TLS握手直接穿透Heroku边缘节点到达你的Node.js应用,此时你可以完全沿用本地运行的https.createServer逻辑实现mTLS,不需要修改原有证书校验代码。
如果没有配置TCP路由的权限,也可以在Heroku应用外层增加支持mTLS的反向代理层,由反向代理完成客户端证书校验,校验通过后再将请求转发到Heroku应用,反向代理通过自定义请求头传递证书校验结果和证书信息。
重要提醒:使用反向代理透传校验结果的方案,必须配置Heroku应用的访问规则,仅允许反向代理的IP段访问应用,避免攻击者伪造请求头绕过证书校验逻辑。
内容的提问来源于stack exchange,提问作者Zivxx
相关产品推荐
相关产品推荐

