惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google Online Security Blog
Google Online Security Blog
D
Docker
人人都是产品经理
人人都是产品经理
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
L
LangChain Blog
月光博客
月光博客
U
Unit 42
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
Recent Announcements
Recent Announcements
MyScale Blog
MyScale Blog
N
Netflix TechBlog - Medium
D
DataBreaches.Net
T
Tailwind CSS Blog
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
V
Visual Studio Blog
B
Blog
G
Google Developers Blog
有赞技术团队
有赞技术团队
Y
Y Combinator Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
云风的 BLOG
云风的 BLOG
Recorded Future
Recorded Future
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
Jina AI
Jina AI
Engineering at Meta
Engineering at Meta
C
Check Point Blog
V
V2EX
爱范儿
爱范儿
Microsoft Azure Blog
Microsoft Azure Blog
T
The Blog of Author Tim Ferriss
博客园 - 聂微东
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Full Disclosure
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Proofpoint News Feed
罗磊的独立博客
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
量子位
博客园 - 司徒正美
博客园 - 叶小钗

运维开发绿皮书

OBS虚拟摄像头重命名 Ubuntu备份为LiveOS 使用Powershell卸载windows默认程序 Cisco路由器OSPF配置 汽车的分类和特点 Windows11跳过TPM2.0 SSL 证书工具 字数统计 BMI 计算 颜色转换 Hash 生成器 JSON 格式化 JWT 解码 房贷计算 时间戳转换 URL 编码 UUID 生成 牛马时钟 二维码批量生成器 CKS Simulator Kubernetes 1.25 FRP TOTP验证码生成器 direct-ssh-passthrough-nat 脚本使用说明 Python软件授权 Linux命令行百度网盘 为 Containerd 配置 Harbor 无证书镜像站 Docker一键部署Meta和MetacubexD面板 必应搜索屏蔽垃圾网站 VMware的ubuntu完整安装vm-tools支持粘贴板 Docker Desktop 安装到其他位置 Dell EMC PowerEdge R740服务器内存插槽使用说明及正确安装方法 tc常用命令总结 OpenStack排错 稳定币监管与投资指南 VMware ESXi 密码恢复指南 VMware 加密 state.tgz 文件解密教程 六百万数据 MySQL count(*) 优化 使用iptables禁止特定子网访问指定端口 解决 iptables DNAT 无法转发到 127.0.0.1 的问题及安全风险分析 Linux 一键测试脚本 Gitlab配置详解 Gitlab批量创建用户 Ubuntu安装VNC教程 Windows11跳过微软账号登录 vue配置Nginx伪静态 Linux下Ollama开放端口 模型详解配置 修改Git提交历史中的作者信息 在PowerShell中启动Git Bash的方法 一键安装OpenVPN 大语言模型 模型架构-Transformer模型 训练相关资料 GPT&DeepSeek模型 大模型技术基础 Git Bash 中一键安装 pacman Gitlab-Runner的一些问题 Docker运行kwaivgi-liveportrait Firefox密码提取 在x86-64上构建和运行多种架构的Docker容器 flutter配置镜像站 VMware最新下载地址(纪念碑) 一键安装Conda Docker老版本runc报错无法启动 清理Rancher节点 aapanel 7.x 中文语言配置指南 Ubuntu 22 安装 Kubernetes 1.29 集群指南 Ubuntu修复CNVD-2024-4920726 GRUB锁定密码 Grub2手动引导Linux Cron在线表达式生成器 GitLab一键设置镜像源 文本字符串倒序 Windows禁用任务管理器 UOS(1070a)服务器版本部署Kubernetes1.28 Windows11恢复Windows10右键菜单 Ubuntu一键设置镜像源 CentOS 7 一键设置Vault镜像 Base64在线编码解码 代码差异对比 为Docker分配物理网卡 解密CFSSL生成的CRL数据 已经在谷底了 怎么走都是向上 GitLab统计提交代码行数Python代码 Windows11的24H2出现扩展错误 Windows绕过MicroSoft Store直接下载应用 Fossy平台离线部署分析 在WSL中移除Windows环境变量 重装MicroSoft Store Sony Xperia 10 IV(pdx225)AOSP14编译和刷机方法 pnpm在DevContainer中存储出错解决方法 从SVN迁移仓库到Git Ubuntu中移除Snap软件包 使用Netcat检测UDP端口连通性 rke2集群命令行调试方法 Ubuntu隐私优化-关闭公共门户连接检查 Cisco路由器配置ipv4和ipv6的VTY账户密码 Python实现九九乘法表 Windows10中用多网卡链路聚合来解决网卡网速瓶颈 CentOS7 安装 OpenResty
HTTPS 双向认证与 USB 加密锁配置实战
Paper-Dragon · 2026-03-15 · via 运维开发绿皮书

HTTPS 双向认证与 USB 加密锁配置实战

HTTPS 单向认证只验证服务端身份,双向认证(mTLS)要求客户端也提供证书,服务端会验证客户端的合法性。

本文基于真实项目,使用 Nginx + Docker 实现 mTLS,并将客户端证书写入 USB 硬件加密锁(ET199),实现"不插锁就无法访问"的硬件级访问控制。

整体流程:

  • 搭建本地 CA,签发服务端和客户端证书
  • 配置 Nginx 开启 mTLS,Docker 容器化部署
  • 将客户端证书导入 USB 加密锁
  • 验证效果

环境说明

组件说明
Web 服务器Nginx (Alpine Docker 镜像)
证书工具OpenSSL
加密锁ET199(PKI 模式,约 30 元)
部署方式Docker Compose

项目结构

et199/
├── certs/
│   ├── generate_ca.sh        # 生成根 CA 脚本
│   ├── generate_certs.sh     # 生成服务端/客户端证书脚本
│   ├── openssl_ca.cnf        # OpenSSL 配置文件
│   ├── nginx.conf            # Nginx mTLS 配置
│   ├── demoCA/               # CA 证书存储
│   ├── server.crt / server.key
│   ├── server.key.unsecure   # 无密码私钥(供 Nginx 使用)
│   ├── client.crt / client.key
│   └── client.p12            # 导入浏览器/加密锁用
├── html/
│   ├── index.html
│   └── 403.html
├── Dockerfile
├── docker-compose.yml
└── test.sh

第一步:OpenSSL 配置文件

所有证书操作都依赖这份配置,先把它准备好,放在 certs/openssl_ca.cnf

[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = ./demoCA
certs             = $dir/certs
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand
private_key       = $dir/private/cakey.pem
certificate       = $dir/cacert.pem
default_days      = 3650
default_crl_days  = 30
default_md        = sha256
preserve          = no
policy            = policy_match

[ policy_match ]
countryName            = match
stateOrProvinceName    = match
organizationName       = match
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[ req ]
default_bits       = 2048
default_keyfile    = privkey.pem
distinguished_name = req_distinguished_name
x509_extensions    = v3_ca

[ req_distinguished_name ]
countryName_default            = CN
stateOrProvinceName_default    = Guangdong
localityName_default           = Shenzhen
0.organizationName_default     = Technology Co., Ltd.
organizationalUnitName_default = IT Department
commonName_default             = example.com
emailAddress_default           = admin@example.com

[ v3_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer:always
basicConstraints       = CA:true

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage         = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName   = @alt_names

[ alt_names ]
DNS.1 = example.com
DNS.2 = *.example.com

第二步:生成根 CA

certs/generate_ca.sh — 只需运行一次,CA 是签发所有证书的根基:

#!/bin/bash
set -e

CERT_DIR="./certs"
PASSWORD="111111"
CERT_DAYS=3650

cd "$CERT_DIR"

# 初始化 CA 目录结构
mkdir -p demoCA/newcerts demoCA/private
touch demoCA/index.txt
echo "1000" > demoCA/serial
echo "unique_subject = no" > demoCA/index.txt.attr

# 生成 CA 私钥(2048 位,带密码保护)
openssl genrsa -des3 -passout pass:"$PASSWORD" -out demoCA/private/cakey.pem 2048

# 自签名 CA 证书,有效期 10 年
openssl req -new -x509 -days $CERT_DAYS \
    -key demoCA/private/cakey.pem \
    -passin pass:"$PASSWORD" \
    -out demoCA/cacert.pem \
    -config openssl_ca.cnf <<EOF
CN
Guangdong
Shenzhen
Technology Co., Ltd.
IT Department
Root CA
admin@example.com
EOF

echo "CA 证书生成完成"
openssl x509 -in demoCA/cacert.pem -noout -subject -dates

运行:

bash certs/generate_ca.sh

第三步:签发服务端和客户端证书

certs/generate_certs.sh — 支持传入域名参数,一键生成所有需要的证书:

#!/bin/bash
set -e

CERT_DIR="./certs"
DOMAIN="${1:-example.com}"
PASSWORD="111111"
CERT_DAYS=3650

cd "$CERT_DIR"

# 检查 CA 是否存在
if [ ! -f "demoCA/private/cakey.pem" ] || [ ! -f "demoCA/cacert.pem" ]; then
    echo "错误: CA 证书不存在,请先运行 generate_ca.sh"
    exit 1
fi

WILDCARD_DOMAIN="*.${DOMAIN#*.}"

# 用时间戳做序列号,避免重复签发时冲突
echo "$(date +%s)" > demoCA/serial
echo "unique_subject = no" > demoCA/index.txt.attr

# 动态生成包含 SAN 的 openssl 配置
cat > openssl_ca.cnf <<CONFEOF
# ... (同上,将 alt_names 中的域名替换为 $DOMAIN$WILDCARD_DOMAIN)
CONFEOF

# ---- 服务端证书 ----
openssl genrsa -des3 -passout pass:"$PASSWORD" -out server.key 2048

openssl req -new -key server.key -passin pass:"$PASSWORD" \
    -out server.csr -config openssl_ca.cnf <<EOF
CN
Guangdong
Shenzhen
Technology Co., Ltd.
IT Department
$DOMAIN
admin@example.com
$PASSWORD
EOF

openssl ca -in server.csr -out server.crt -config openssl_ca.cnf \
    -extensions v3_req -days $CERT_DAYS \
    -passin pass:"$PASSWORD" -batch -notext

# Nginx 启动不需要输密码,剥离私钥密码
openssl rsa -in server.key -passin pass:"$PASSWORD" -out server.key.unsecure

# ---- 客户端证书 ----
openssl genrsa -des3 -passout pass:"$PASSWORD" -out client.key 2048

openssl req -new -key client.key -passin pass:"$PASSWORD" \
    -out client.csr -config openssl_ca.cnf <<EOF
CN
Guangdong
Shenzhen
Technology Co., Ltd.
IT Department
$DOMAIN
admin@example.com
$PASSWORD
EOF

openssl ca -in client.csr -out client.crt -config openssl_ca.cnf \
    -days $CERT_DAYS -passin pass:"$PASSWORD" -batch -notext

# 导出 .p12 格式,用于浏览器和加密锁导入
openssl pkcs12 -export -clcerts \
    -in client.crt -inkey client.key \
    -out client.p12 \
    -passin pass:"$PASSWORD" -passout pass:"$PASSWORD"

# 同样生成无密码客户端私钥,方便 curl 测试
openssl rsa -in client.key -passin pass:"$PASSWORD" -out client.key.unsecure

echo "证书生成完成,所有密码: $PASSWORD"

运行:

# 使用自定义域名
bash certs/generate_certs.sh example.com

生成后的文件:

certs/
├── demoCA/
│   ├── cacert.pem          # CA 根证书(公钥,分发给客户端信任)
│   └── private/cakey.pem   # CA 私钥(严格保管,不对外)
├── server.crt              # 服务端证书
├── server.key              # 服务端私钥(带密码)
├── server.key.unsecure     # 服务端私钥(无密码,供 Nginx 使用)
├── client.crt              # 客户端证书
├── client.key              # 客户端私钥(带密码)
├── client.key.unsecure     # 客户端私钥(无密码,供 curl 测试)
└── client.p12              # 客户端证书包(导入浏览器/加密锁)

遇到 failed to update database / TXT_DB error number 2,执行:

echo "unique_subject = no" > demoCA/index.txt.attr

第四步:配置 Nginx mTLS

certs/nginx.conf — 核心在于 ssl_verify_clientssl_client_certificate

# 用 map 判断证书验证结果,未通过则重定向到 403 页面
map $ssl_client_verify $cert_check {
    default /403.html;
    SUCCESS "";
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    root /usr/share/nginx/html;
    index index.html;

    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;

    # 服务端证书
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # 用于验证客户端证书的 CA
    ssl_client_certificate /etc/nginx/ssl/ca.crt;

    # optional: 有证书就验证,没有证书也放行(由 map 决定后续行为)
    # require: 强制要求客户端提供证书,否则直接 400
    ssl_verify_client optional;
    ssl_verify_depth  10;

    ssl_protocols             TLSv1.2;
    ssl_ciphers               HIGH:!aNULL:!MD5:!RC4;
    ssl_prefer_server_ciphers off;
    ssl_session_cache         shared:SSL:10m;
    ssl_session_timeout       10m;

    add_header Strict-Transport-Security "max-age=31536000";

    # 403 页面只允许内部跳转
    location = /403.html {
        internal;
    }

    # 反向代理后端(可选)
    location @backend {
        proxy_pass http://host.docker.internal:8080;
        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;
    }

    # 主路由:证书未通过则返回 403,通过则转发后端
    location / {
        try_files $cert_check @backend;
    }

    # 调试接口:查看客户端证书信息
    location /cert-info {
        default_type text/plain;
        return 200 "Client DN: $ssl_client_s_dn\nVerify: $ssl_client_verify\n";
    }
}

ssl_verify_client optional 配合 map 的好处是:没有证书时可以返回自定义的 403 页面,而不是浏览器原生的 SSL 握手错误,用户体验更好。如果想要更严格的控制,改成 ssl_verify_client require 即可。

第五步:Docker 容器化部署

Dockerfile

FROM nginx:alpine

RUN mkdir -p /etc/nginx/ssl /usr/share/nginx/html

# 证书和配置直接打包进镜像
COPY certs/nginx.conf          /etc/nginx/conf.d/default.conf
COPY certs/server.crt          /etc/nginx/ssl/server.crt
COPY certs/server.key.unsecure /etc/nginx/ssl/server.key
COPY certs/demoCA/cacert.pem   /etc/nginx/ssl/ca.crt
COPY html/                     /usr/share/nginx/html/

EXPOSE 443

CMD ["nginx", "-g", "daemon off;"]

docker-compose.yml

services:
  nginx:
    build: .
    container_name: nginx-mtls
    ports:
      - "4433:443"
    extra_hosts:
      - "host.docker.internal:host-gateway"  # 容器内访问宿主机
    restart: unless-stopped

启动:

docker compose build
docker compose up -d

第六步:验证 mTLS 是否生效

test.sh — 覆盖了有证书、无证书、PEM 格式、P12 格式四种场景:

#!/bin/bash

DOMAIN="example.com"
CERT_DIR="./certs"
BASE_URL="https://127.0.0.1:4433"

echo "=========================================="
echo "HTTPS 双向认证测试"
echo "=========================================="

# 测试1:PEM 格式证书访问主页(应返回 200)
echo ""
echo "[1] PEM 证书访问主页"
HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" \
  --cert "$CERT_DIR/client.crt" \
  --key  "$CERT_DIR/client.key.unsecure" \
  --cacert "$CERT_DIR/demoCA/cacert.pem" \
  -H "Host: $DOMAIN" \
  "$BASE_URL/")
echo "  HTTP 状态码: $HTTP_CODE (预期 200)"

# 测试2:查看证书信息
echo ""
echo "[2] 查看客户端证书信息"
curl -k -s \
  --cert "$CERT_DIR/client.crt" \
  --key  "$CERT_DIR/client.key.unsecure" \
  --cacert "$CERT_DIR/demoCA/cacert.pem" \
  -H "Host: $DOMAIN" \
  "$BASE_URL/cert-info"

# 测试3:无证书访问(应显示 403 页面)
echo ""
echo "[3] 无证书访问(应显示 Access Denied)"
RESULT=$(curl -k -s \
  --cacert "$CERT_DIR/demoCA/cacert.pem" \
  -H "Host: $DOMAIN" \
  "$BASE_URL/")
if echo "$RESULT" | grep -q "Access Denied"; then
  echo "  ✓ 正确返回 403 禁止访问"
else
  echo "  ✗ 未返回预期的 403 页面"
fi

# 测试4:P12 格式证书(加密锁导出的格式)
echo ""
echo "[4] P12 证书访问主页"
HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" \
  --cert "$CERT_DIR/client.p12:111111" \
  --cert-type P12 \
  --cacert "$CERT_DIR/demoCA/cacert.pem" \
  -H "Host: $DOMAIN" \
  "$BASE_URL/")
echo "  HTTP 状态码: $HTTP_CODE (预期 200)"

运行测试:

预期输出:

[1] PEM 证书访问主页
  HTTP 状态码: 200 (预期 200)

[2] 查看客户端证书信息
Client DN: CN=example.com,OU=IT Department,O=Technology Co.\, Ltd.,ST=Guangdong,C=CN
Verify: SUCCESS

[3] 无证书访问(应显示 Access Denied)
  ✓ 正确返回 403 禁止访问

[4] P12 证书访问主页
  HTTP 状态码: 200 (预期 200)

第七步:将客户端证书写入 USB 加密锁

加密锁(ET199)需要提前通过管理软件初始化为 PKI 模式。

  1. client.p12 传输到 Windows 机器(Linux 下可用 sz client.p12
  2. 打开加密锁管理软件,输入 PIN 码登录
  3. 点击"导入",选择 client.p12,输入导出密码(111111
  4. 导入成功后,证书和私钥存储在加密锁的安全芯片中,私钥不可导出

插入加密锁访问站点,浏览器弹出证书选择框,选择加密锁中的证书并输入 PIN 码即可正常访问。拔出加密锁后,服务端拒绝连接。

403 页面

html/403.html — 无证书访问时展示的页面,比浏览器原生 SSL 错误更友好:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Access Denied</title>
</head>
<body>
    <h1>Access Denied</h1>
    <p>This website requires a client certificate for authentication.</p>
    <p>Please use a valid client certificate to access.</p>
    <hr>
    <p>Error: 403 Forbidden</p>
</body>
</html>

关键配置说明

配置项说明
ssl_verify_client require强制要求客户端证书,无证书直接 400
ssl_verify_client optional有证书就验证,无证书也放行,配合 map 做软拦截
ssl_verify_depth 10证书链最大验证深度,自签 CA 设 1 即可
ssl_client_certificate指定信任的 CA,只有该 CA 签发的客户端证书才被接受
$ssl_client_verifyNginx 变量,值为 SUCCESS / FAILED / NONE
$ssl_client_s_dn客户端证书的 DN 信息,可用于日志或后端鉴权

为什么用加密锁而不是直接导入浏览器

直接把 .p12 导入浏览器的问题在于:证书文件可以被复制和分发,一旦泄露就失去了访问控制的意义。

加密锁的私钥存储在硬件安全芯片中,私钥不可导出,所有加密运算在锁内完成。即使有人拿到了锁,还需要知道 PIN 码才能使用,实现了"持有硬件 + 知道密码"的双因素认证。

ET199 这类 PKI 加密锁价格不高,加上运费大概 30 元左右,适合对安全性有一定要求的内网管理系统。

更新日志

  • 9057c-docs: 新增 HTTPS 双向认证与 USB 加密锁配置实战