
在数字化转型浪潮中,企业服务的安全性变得越来越重要。最近,我们完成了从传统HTTP到全链路mTLS的升级改造,将服务间通信的安全等级提升到了一个新的高度。
今天,我将完整分享这次升级改造的全过程,包括架构设计、配置细节、常见问题解决方案,希望能为正在考虑服务安全升级的你提供参考。
我们有两台Ubuntu服务器,运行着不同的服务:
服务器A (192.168.8.111) • 前端:80端口(HTTP) • 后端:127.0.0.1:8888(Sanic服务) • Nginx配置:将/api/路径代理到后端8888端口
服务器B (192.168.8.222) • 前端:8080端口(HTTP) • 后端:127.0.0.1:8889(Sanic服务) • Nginx配置:将/api/路径代理到后端8889端口
✅ 将HTTP升级为HTTPS,启用mTLS双向认证 ✅ 前端Nginx处理SSL/TLS终止和客户端证书验证 ✅ 后端服务只监听127.0.0.1,只能通过Nginx访问 ✅ 每台服务器使用独立的CA,实现证书隔离 ✅ 客户端必须使用正确的证书才能访问服务
在众多安全方案中,我们选择了mTLS,原因如下:
方案 | 优点 | 缺点 |
|---|---|---|
API密钥 | 实现简单 | 密钥管理复杂,易泄露 |
JWT Token | 无状态,易于扩展 | Token可能被盗用 |
OAuth 2.0 | 功能完善,标准协议 | 实现复杂,适合第三方认证 |
mTLS | 强身份验证,自动过期,细粒度控制 | 证书管理稍复杂 |
客户端 (需证书)
↓ HTTPS + mTLS
Nginx (证书验证 + TLS终止)
↓ HTTP (127.0.0.1)
后端服务 (只监听本地)服务器A (192.168.8.111) 服务器B (192.168.8.222)
├── CA-A (ca-111.crt) ├── CA-B (ca-222.crt)
├── 服务器证书 (server-111.crt) ├── 服务器证书 (server-222.crt)
├── 客户端证书 (client-111.crt) ├── 客户端证书 (client-222.crt)
└── 信任CA-B └── 信任CA-A在两台服务器上执行环境准备脚本:
#!/bin/bash
# prepare-environment.sh
set -e
echo "=== 开始准备mTLS环境 ==="
# 检查并安装必要工具
sudo apt-get update
sudo apt-get install -y openssl nginx curl python3-pip
# 安装Python依赖
sudo pip3 install sanic aiohttp
# 创建证书目录
sudo mkdir -p /etc/pki/mtls/{ca,server,client,backup}
sudo mkdir -p /etc/ssl/{certs,private,client}
# 设置权限
sudo chmod 700 /etc/ssl/private
sudo chmod 755 /etc/ssl/{certs,client}
sudo chmod 755 /etc/pki/mtls
echo "=== 环境准备完成 ==="我们编写一个通用证书生成脚本,通过参数生成不同服务器的证书:
#!/bin/bash
# generate-mtls-certs.sh
set -e
# 解析参数
SERVER_IP="$1"
LAST_OCTET=$(echo "$SERVER_IP" | awk -F. '{print $NF}')
CERT_DIR="/etc/pki/mtls"
cd $CERT_DIR
echo "=== 为 $SERVER_IP 生成证书 ==="
# 1. 生成CA证书
sudo openssl genrsa -out ca/ca-${LAST_OCTET}.key 4096
sudo openssl req -x509 -new -nodes -key ca/ca-${LAST_OCTET}.key \
-sha256 -days 36500 -out ca/ca-${LAST_OCTET}.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/CN=CA-${LAST_OCTET}-Root"
# 2. 生成服务器证书
sudo openssl genrsa -out server/server-${LAST_OCTET}.key 2048
sudo openssl req -new -key server/server-${LAST_OCTET}.key \
-out server/server-${LAST_OCTET}.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/CN=server-${LAST_OCTET}.mycompany.com"
# 创建扩展配置文件
cat > /tmp/server.ext << EOF
subjectAltName = DNS:server-${LAST_OCTET}.mycompany.com, IP:${SERVER_IP}, IP:127.0.0.1
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
EOF
sudo openssl x509 -req -in server/server-${LAST_OCTET}.csr \
-CA ca/ca-${LAST_OCTET}.crt -CAkey ca/ca-${LAST_OCTET}.key -CAcreateserial \
-out server/server-${LAST_OCTET}.crt -days 36500 -sha256 -extfile /tmp/server.ext
# 3. 生成客户端证书
sudo openssl genrsa -out client/client-${LAST_OCTET}.key 2048
sudo openssl req -new -key client/client-${LAST_OCTET}.key \
-out client/client-${LAST_OCTET}.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/CN=client-${LAST_OCTET}.mycompany.com"
cat > /tmp/client.ext << EOF
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
sudo openssl x509 -req -in client/client-${LAST_OCTET}.csr \
-CA ca/ca-${LAST_OCTET}.crt -CAkey ca/ca-${LAST_OCTET}.key -CAcreateserial \
-out client/client-${LAST_OCTET}.crt -days 36500 -sha256 -extfile /tmp/client.ext
# 4. 生成PKCS12格式(用于浏览器导入)
CERT_PASS="Client${LAST_OCTET}Pass123!"
sudo openssl pkcs12 -export -in client/client-${LAST_OCTET}.crt \
-inkey client/client-${LAST_OCTET}.key -certfile ca/ca-${LAST_OCTET}.crt \
-out client/client-${LAST_OCTET}.p12 -password pass:"$CERT_PASS"
echo "=== 证书生成完成 ==="在两台服务器上分别执行:
# 在192.168.8.111上执行
sudo ./generate-mtls-certs.sh 192.168.8.111
# 在192.168.8.222上执行
sudo ./generate-mtls-certs.sh 192.168.8.222为了让服务间能够互信(虽然我们不需要服务间直接通信,但为未来扩展考虑),交换CA证书:
# 在111服务器上执行
scp /etc/pki/mtls/ca/ca-111.crt root@192.168.8.222:/etc/pki/mtls/ca/
# 在222服务器上执行
scp /etc/pki/mtls/ca/ca-222.crt root@192.168.8.111:/etc/pki/mtls/ca/在两台服务器上执行安装脚本:
#!/bin/bash
# install-certs.sh
set -e
# 获取服务器IP的最后一段
IP=$(hostname -I | awk '{print $1}')
LAST_OCTET=$(echo "$IP" | awk -F. '{print $NF}')
echo "=== 安装证书到系统位置 (服务器: $IP) ==="
# 创建目录
sudo mkdir -p /etc/ssl/{certs,private,client}
sudo chmod 700 /etc/ssl/private
# 安装服务器证书
sudo cp "/etc/pki/mtls/server/server-${LAST_OCTET}.crt" /etc/ssl/certs/server.crt
sudo cp "/etc/pki/mtls/server/server-${LAST_OCTET}.key" /etc/ssl/private/server.key
# 安装客户端证书
sudo cp "/etc/pki/mtls/client/client-${LAST_OCTET}.crt" /etc/ssl/client/
sudo cp "/etc/pki/mtls/client/client-${LAST_OCTET}.key" /etc/ssl/client/
sudo cp "/etc/pki/mtls/client/client-${LAST_OCTET}.p12" /etc/ssl/client/
# 安装CA证书
sudo cp "/etc/pki/mtls/ca/ca-${LAST_OCTET}.crt" /etc/ssl/certs/
# 设置权限
sudo chmod 600 /etc/ssl/private/*
sudo chmod 600 /etc/ssl/client/*.key
sudo chmod 644 /etc/ssl/certs/*.crt
sudo chmod 644 /etc/ssl/client/*.crt
sudo chmod 600 /etc/ssl/client/*.p12
echo "=== 证书安装完成 ==="这是最关键的一步,我们将原有的HTTP配置升级为HTTPS + mTLS。
# /etc/nginx/sites-available/server-111
# HTTP重定向到HTTPS
server {
listen 80;
server_name 192.168.8.111;
return 301 https://$server_name$request_uri;
}
# HTTPS + mTLS主配置
server {
listen 443 ssl http2;
server_name 192.168.8.111;
# SSL证书配置
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
# 🔐 mTLS配置 - 强制客户端证书验证
ssl_client_certificate /etc/ssl/certs/ca-111.crt; # 只信任自己的CA
ssl_verify_client on; # 开启客户端验证
ssl_verify_depth 2; # 验证深度
# SSL优化参数
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 传递客户端证书信息到后端
proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
# 根路径 - 静态文件服务
root /var/www/html;
index index.html;
# 健康检查端点(无需客户端证书)
location = /health {
access_log off;
add_header Content-Type text/plain;
ssl_verify_client optional;
return 200 "server-111 healthy\n";
}
# 主路径
location / {
# 验证客户端证书
if ($ssl_client_verify != SUCCESS) {
return 403 "Client certificate required or invalid";
}
try_files $uri $uri/ /index.html;
}
# API路径 - 代理到本地后端服务
location /api/ {
# 代理到本地的8888端口
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
# 传递客户端证书信息
proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
# 证书验证检查
if ($ssl_client_verify != SUCCESS) {
return 403 "Client certificate required or invalid";
}
}
# 错误页面
error_page 403 /403.html;
location = /403.html {
root /usr/share/nginx/html;
internal;
}
}# /etc/nginx/sites-available/server-222
# HTTP重定向到HTTPS
server {
listen 8080;
server_name 192.168.8.222;
return 301 https://$server_name:8443$request_uri;
}
# HTTPS + mTLS主配置(使用8443端口)
server {
listen 8443 ssl http2;
server_name 192.168.8.222;
# SSL证书配置
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
# 🔐 mTLS配置
ssl_client_certificate /etc/ssl/certs/ca-222.crt; # 只信任自己的CA
ssl_verify_client on;
ssl_verify_depth 2;
# SSL优化参数
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 传递客户端证书信息
proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
# 根路径
root /var/www/html;
index index.html;
# 健康检查端点
location = /health {
access_log off;
add_header Content-Type text/plain;
ssl_verify_client optional;
return 200 "server-222 healthy\n";
}
# 主路径
location / {
if ($ssl_client_verify != SUCCESS) {
return 403 "Client certificate required or invalid";
}
try_files $uri $uri/ /index.html;
}
# API路径 - 代理到本地后端服务
location /api/ {
# 代理到本地的8889端口
proxy_pass http://127.0.0.1:8889/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
if ($ssl_client_verify != SUCCESS) {
return 403 "Client certificate required or invalid";
}
}
# 错误页面
error_page 403 /403.html;
location = /403.html {
root /usr/share/nginx/html;
internal;
}
}配置Sanic后端服务只监听127.0.0.1,确保只能通过Nginx访问。
# /opt/apps/backend-8888/app.py
from sanic import Sanic, response
from sanic.response import text, json
import logging
app = Sanic("backend-8888")
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.route("/health")
async def health(request):
return text("OK")
@app.route("/api/data")
async def get_data(request):
# 从Nginx头部获取客户端证书信息
client_cert = request.headers.get('X-SSL-Client-Cert', '')
client_verify = request.headers.get('X-SSL-Client-Verify', '')
return json({
"message": "Hello from server-111 backend",
"client_cert_verified": client_verify == 'SUCCESS',
"server_ip": "192.168.8.111",
"backend_port": 8888
})
if __name__ == "__main__":
# 🔐 关键:只监听127.0.0.1,确保只能通过Nginx访问
app.run(host="127.0.0.1", port=8888, workers=2, access_log=True)创建Systemd服务:
# /etc/systemd/system/backend-8888.service
[Unit]
Description=Backend Service on port 8888
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/apps/backend-8888
ExecStart=/usr/bin/python3 -m sanic app.app --host=127.0.0.1 --port=8888 --workers=2
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target# /opt/apps/backend-8889/app.py
from sanic import Sanic, response
from sanic.response import text, json
import logging
app = Sanic("backend-8889")
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.route("/health")
async def health(request):
return text("OK")
@app.route("/api/data")
async def get_data(request):
client_cert = request.headers.get('X-SSL-Client-Cert', '')
client_verify = request.headers.get('X-SSL-Client-Verify', '')
return json({
"message": "Hello from server-222 backend",
"client_cert_verified": client_verify == 'SUCCESS',
"server_ip": "192.168.8.222",
"backend_port": 8889
})
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8889, workers=2, access_log=True)# 启用Nginx配置
sudo ln -sf /etc/nginx/sites-available/server-111 /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
# 测试Nginx配置
sudo nginx -t
# 创建应用目录
sudo mkdir -p /opt/apps/backend-8888
sudo chown -R www-data:www-data /opt/apps
sudo chmod -R 755 /opt/apps
# 启用并启动后端服务
sudo systemctl daemon-reload
sudo systemctl enable backend-8888
sudo systemctl start backend-8888
# 重启Nginx
sudo systemctl restart nginx# 启用Nginx配置
sudo ln -sf /etc/nginx/sites-available/server-222 /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo mkdir -p /opt/apps/backend-8889
sudo chown -R www-data:www-data /opt/apps
sudo chmod -R 755 /opt/apps
sudo systemctl daemon-reload
sudo systemctl enable backend-8889
sudo systemctl start backend-8889
sudo systemctl restart nginx# 在服务器A上
sudo ufw allow 80/tcp # HTTP重定向
sudo ufw allow 443/tcp # HTTPS
sudo ufw allow 22/tcp # SSH
# 在服务器B上
sudo ufw allow 8080/tcp # HTTP重定向
sudo ufw allow 8443/tcp # HTTPS
sudo ufw allow 22/tcp # SSH
# 启用防火墙
sudo ufw --force enable
sudo ufw status verbose# 在服务器A上测试
# 测试健康检查(无需证书)
curl -k https://192.168.8.111/health
# 测试API访问(需要证书)- 应该失败
curl -k https://192.168.8.111/api/data
# 使用证书访问 - 应该成功
curl -v --cert /etc/ssl/client/client-111.crt \
--key /etc/ssl/client/client-111.key \
--cacert /etc/ssl/certs/ca-111.crt \
https://192.168.8.111/api/data
# 在服务器B上测试
curl -k https://192.168.8.222:8443/health
curl -v --cert /etc/ssl/client/client-222.crt \
--key /etc/ssl/client/client-222.key \
--cacert /etc/ssl/certs/ca-222.crt \
https://192.168.8.222:8443/api/data#!/bin/bash
# test-mtls-deployment.sh
set -e
echo "=== mTLS部署验证脚本 ==="
echo
test_server() {
local server_ip=$1
local server_port=$2
local cert_prefix=$3
local test_name=$4
echo "测试 $test_name ($server_ip:$server_port)..."
# 健康检查测试
echo -n " 健康检查: "
if curl -s -k "https://$server_ip:$server_port/health" 2>/dev/null | grep -q "healthy"; then
echo "✓ 通过"
else
echo "✗ 失败"
fi
# 无证书访问测试(应该失败)
echo -n " 无证书访问: "
if curl -s -k -w "%{http_code}" "https://$server_ip:$server_port/api/data" 2>/dev/null | grep -q "403\|400"; then
echo "✓ 被拒绝(符合预期)"
else
echo "⚠ 异常"
fi
# 有证书访问测试
echo -n " 有证书访问: "
if curl -s -w "%{http_code}" \
--cert "/etc/ssl/client/client-$cert_prefix.crt" \
--key "/etc/ssl/client/client-$cert_prefix.key" \
--cacert "/etc/ssl/certs/ca-$cert_prefix.crt" \
"https://$server_ip:$server_port/api/data" 2>/dev/null | grep -q "200"; then
echo "✓ 成功"
else
echo "✗ 失败"
fi
echo
}
# 测试服务器A
test_server "192.168.8.111" "443" "111" "服务器A (192.168.8.111)"
# 测试服务器B
test_server "192.168.8.222" "8443" "222" "服务器B (192.168.8.222)"
echo "=== 验证完成 ===" # 在服务器A上导出
sudo cp /etc/ssl/client/client-111.p12 /tmp/
sudo cp /etc/ssl/certs/ca-111.crt /tmp/
sudo chmod 644 /tmp/client-111.p12 /tmp/ca-111.crt
# 在服务器B上导出
sudo cp /etc/ssl/client/client-222.p12 /tmp/
sudo cp /etc/ssl/certs/ca-222.crt /tmp/
sudo chmod 644 /tmp/client-222.p12 /tmp/ca-222.crt分别导入 CA 根证书到受信任的根证书颁发机构
# 安装CA证书
sudo cp ca-111.crt /usr/local/share/ca-certificates/
sudo cp ca-222.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# 使用curl测试
curl --cert client-111.crt --key client-111.key \
--cacert /etc/ssl/certs/ca-certificates.crt \
https://192.168.8.111/api/data症状:curl: (60) SSL certificate problem 解决:
# 验证证书链
openssl verify -CAfile ca-111.crt client-111.crt
# 检查证书详情
openssl x509 -in client-111.crt -text -noout症状:直接返回403,不弹出证书选择框 解决:
症状:nginx: [emerg] SSL_CTX_load_verify_locations 解决:
# 检查错误日志
sudo tail -f /var/log/nginx/error.log
# 验证CA证书格式
openssl x509 -in /etc/ssl/certs/ca-111.crt -text -noout
# 重新加载配置
sudo nginx -t && sudo nginx -s reload症状:Nginx返回502 Bad Gateway 解决:
# 检查后端服务状态
sudo systemctl status backend-8888
# 检查后端服务日志
sudo journalctl -u backend-8888 -f
# 检查端口监听
sudo netstat -tlnp | grep 8888症状:curl添加证书报错 unable to set private key file 解决:切换到root或者使用sudo执行
# 监控SSL握手性能
cat > /etc/nginx/conf.d/ssl-metrics.conf << 'EOF'
# SSL性能监控
log_format sslmetrics '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'ssl_protocol=$ssl_protocol ssl_cipher=$ssl_cipher '
'ssl_client_verify=$ssl_client_verify '
'ssl_session_reused=$ssl_session_reused';
access_log /var/log/nginx/ssl_access.log sslmetrics;
EOFssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;listen 443 ssl http2;ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;# 定期检查证书过期
cat > /usr/local/bin/check-certs.sh << 'EOF'
#!/bin/bash
find /etc/ssl -name "*.crt" -exec openssl x509 -noout -subject -dates {} \;
EOF
sudo chmod +x /usr/local/bin/check-certs.sh# 添加安全头
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";# 记录客户端证书信息
log_format mtls '$remote_addr - $ssl_client_s_dn [$time_local] '
'"$request" $status $body_bytes_sent';
access_log /var/log/nginx/mtls_access.log mtls;在实施mTLS的过程中,我总结了以下几点经验:
https://nginx.org/en/docs/http/ngx_http_ssl_module.html https://www.feistyduck.com/library/openssl-cookbook/
安全之路,道阻且长,行则将至。 希望本文能为你在企业服务安全建设的道路上提供一些参考和帮助。如果你在实施过程中遇到任何问题,欢迎在评论区交流讨论。