1. 生成根证书
1.1 生成根证书私钥
# 使用 openssl 的 ecparam 子命令(用于椭圆曲线参数/密钥)
# -genkey:根据指定的椭圆曲线参数直接生成 EC 私钥
# -name prime256v1:指定椭圆曲线 prime256v1(NIST P-256 / secp256r1)
# -out root/root.key:输出生成的 EC 私钥文件路径openssl ecparam -genkey -name prime256v1 -out root/root.key1.2 生成根证书(方式一)
1.2.1 创建root.cnf文件
vim root/root.cnf# ========================
# req 段:用于 openssl req
# ========================
# prompt = no
# 表示不进行交互式输入
# 证书主题信息全部从 distinguished_name 指定的段中读取
[req]
prompt = no
# default_md
# 指定生成 CSR 或证书时使用的默认摘要算法
# 这里使用 SHA-256(当前主流、安全)
default_md = sha256
# distinguished_name
# 指定证书主题(DN)所在的配置段
distinguished_name = req_dn
# ==============================
# req_dn 段:证书主题(DN)信息
# ==============================
[req_dn]
# 国家(Country Name,ISO 3166-1 两位国家代码)
C = CN
# 省 / 州(State or Province Name,用于逻辑或行政区域标识)
ST = Internet
# 城市 / 地区(Locality Name,城市或逻辑区域名称)
L = World
# 组织名(Organization Name,证书主体所属的组织)
O = Lesslog-CA
# 组织单位(Organizational Unit Name,组织内部的部门或角色)
# 常用于区分 CA 层级、用途或责任主体
OU = Certificate Authority
# 通用名(Common Name,证书主体的主要标识)
# 对 CA 证书来说通常表示 CA 的名称,而非域名
CN = Lesslog-Root-CA
# ==========================================
# root_ext 段:CA 证书扩展(X.509 v3 Extensions)
# ==========================================
# 该段通过 -extensions root_ext 指定,在使用
# 用于声明这是一个 CA 证书及其能力边界
[root_ext]
# basicConstraints
# 声明该证书是 CA 证书
# pathlen:1 表示最多只能再签发 1 层下级 CA
# critical 表示客户端必须理解该扩展
basicConstraints = critical,CA:TRUE,pathlen:1
# keyUsage
# 限定该 CA 私钥的用途
# keyCertSign:允许签发证书
# cRLSign:允许签发证书吊销列表(CRL)
# critical 表示这是强制限制
keyUsage = critical,keyCertSign,cRLSign
# subjectKeyIdentifier
# 生成 Subject Key Identifier(SKI)
# 用于唯一标识当前证书的公钥
subjectKeyIdentifier = hash
# authorityKeyIdentifier
# 生成 Authority Key Identifier(AKI)
# keyid:always 表示始终包含签发者的 key identifier
# 对 Root CA 来说,AKI 会指向自身
authorityKeyIdentifier = keyid:always1.2.2 根据root.cnf文件生成csr文件
# 使用 openssl req 生成一个新的证书签名请求(CSR)
# -new:生成新的 CSR
# -key root.key:指定用于生成 CSR 的私钥
# -config root.cnf:指定 openssl 配置文件(包含 req / req_dn 等段)
# -out root.csr:输出生成的 CSR 文件openssl req -new -key root/root.key -config root/root.cnf -out root/root.csr1.2.3 根据root.cnf文件生成root.crt根证书
# 使用 openssl x509 根据 CSR 生成最终的 X.509 证书
#
# 【核心原因说明(务必记住)】
# -extfile 只负责“加载配置文件”,不会自动生效任何扩展
# -extensions 用来“明确指定”从 extfile 中哪一个扩展段写入证书
#
# 即使在生成 CSR 时已经在 root.cnf 中定义并使用了 root_ext:
# - CSR 中的扩展只是“请求”,不会自动复制到证书
# - 证书实际包含哪些扩展,只由签发命令决定
#
# 因此必须同时使用:
# -extfile root.cnf (告诉 openssl 扩展在哪里)
# -extensions root_ext (告诉 openssl 用哪一段扩展)
#
# 否则结果是:
# - root.cnf 会被读取
# - 但 root_ext 不会被应用
#
# 这是 PKI 的安全设计:CA 永远控制证书能力边界
#
# -req:输入文件是 CSR(Certificate Signing Request)
# -in root.csr:指定 CSR 文件
# -signkey root.key:使用私钥对 CSR 进行签名(自签名场景)
# -out root.crt:输出生成的证书文件
# -days 10950:证书有效期,10950 天 ≈ 30 年
# -sha256:证书签名摘要算法使用 SHA-256
# -extfile root.cnf:指定包含 X.509 扩展定义的配置文件
# -extensions root_ext:明确指定将 root_ext 段中的扩展写入证书openssl x509 -req \
-in root/root.csr \
-signkey root/root.key \
-out root/root.crt \
-days 10950 \
-sha256 \
-extfile root/root.cnf \
-extensions root_ext1.3 生成根证书(方式二)
# 使用 openssl req 生成一个自签名的 X.509 证书(Root CA)
# -x509:直接生成证书,而不是只生成 CSR
# -new:创建新证书
# -nodes:不对私钥进行加密(这里实际使用的是已有的 root.key)
# -key root/root.key:指定 Root CA 的私钥
# -sha256:证书签名摘要算法使用 SHA-256
# -days 10950:证书有效期,10950 天 ≈ 30 年
# -out root/root.crt:输出 Root CA 证书文件
# -subj:证书主题(DN),非交互方式指定
# -addext basicConstraints:声明这是 CA 证书,且 pathlen=1
# -addext keyUsage:限定 CA 只能用于签发证书和 CRL
# -addext subjectKeyIdentifier:生成 Subject Key Identifier
# -addext authorityKeyIdentifier:生成 Authority Key Identifier(自签 CA 指向自身)openssl req -x509 -new -nodes \
-key root/root.key \
-sha256 \
-days 10950 \
-out root/root.crt \
-subj "/C=CN/ST=Internet/L=World/O=Lesslog-CA/CN=Lesslog-Root-CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:1" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "subjectKeyIdentifier=hash" \
-addext "authorityKeyIdentifier=keyid:always"2. 生成中间证书
2.1 生成中间证书私钥
openssl ecparam -genkey -name prime256v1 -out intermediate/intermediate.key2.2 生成中间证书
2.2.1 创建intermediate.cnf
vim intermediate/intermediate.cnf[req]
prompt = no
default_md = sha256
distinguished_name = req_dn
req_extensions = intermediate_ext
[req_dn]
C = CN
ST = Internet
L = World
O = Lesslog-Intermediate
CN = Lesslog-Intermediate-CA
[intermediate_ext]
# 标识这是一个 CA 证书
# CA:TRUE -> 表示该证书具备签发下级证书的能力
# pathlen:0 -> 只允许再签发“终端证书”,不允许再创建下级 CA
basicConstraints = critical,CA:TRUE,pathlen:0
# CA 必须具备的关键用途
# keyCertSign -> 允许签发证书
# cRLSign -> 允许签发 CRL
keyUsage = critical,keyCertSign,cRLSign
# 使用公钥派生 Subject Key Identifier(SKI)
# 用于唯一标识“这个证书本身”
subjectKeyIdentifier = hash
# authorityKeyIdentifier 表示“我是由哪个 CA 签发的”
# ❌ 这里不能启用,原因如下:
#
# 1. 当前阶段生成的是 CSR(证书签名请求),并不是证书
# 2. CSR 阶段还不存在“签发者(issuer)证书”
# 3. authorityKeyIdentifier 需要从上级 CA 证书中计算(keyid / issuer)
# 4. 在 req 阶段 OpenSSL 无法获取 issuer,因此会直接报错:
# v2i_AUTHORITY_KEYID:no issuer certificate
#
# ✅ 正确做法:
# - 在“Root CA 签发中间 CA 证书”时
# - 通过 openssl x509 -extfile -extensions 添加
# - 由 Root CA 根据自身证书自动生成 authorityKeyIdentifier
#
# authorityKeyIdentifier = keyid,issuer2.2.2 根据intermediate.cnf文件生成csr文件
openssl req -new \
-key intermediate/intermediate.key \
-out intermediate/intermediate.csr \
-config intermediate/intermediate.cnf2.2.3 创建root-sign-intermediate.cnf文件(用于生成intermediate.crt,原因见intermediate.cnf文件最后注释)
vim intermediate/root-sign-intermediate.cnf[ v3_intermediate_ca ]
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer2.2.4 根据csr文件和root-sign-intermediate.cnf生成intermediate.crt中间证书
# -CA root/root.crt:指定用于签发的 Root CA 证书
# -CAkey root/root.key:指定 Root CA 的私钥
# -CAcreateserial:不存在序列号文件时自动创建
# -days 3650:证书有效期,3650 天 ≈ 10 年openssl x509 -req \
-in intermediate/intermediate.csr \
-CA root/root.crt \
-CAkey root/root.key \
-CAcreateserial \
-out intermediate/intermediate.crt \
-days 3650 \
-extfile intermediate/root-sign-intermediate.cnf \
-extensions v3_intermediate_ca3. 生成服务器证书
3.1 生成服务器证书私钥
openssl ecparam -genkey -name prime256v1 -out server/server.key3.2 生成服务器证书
3.2.1 创建server.cnf文件
vim server/server.cnf[req]
prompt = no
default_md = sha256
distinguished_name = req_dn
req_extensions = server_ext
[req_dn]
C = CN
ST = Internet
L = World
O = LessLog
OU = Prometheus
CN = lesslog.com
# =====================================
# req_ext 段:证书请求扩展(CSR Extensions)
# =====================================
# 这些扩展会写入 CSR
# 是否最终进入证书由签发阶段决定
[server_ext]
# basicConstraints
# 声明该证书不是 CA 证书
basicConstraints = CA:FALSE
# keyUsage
# 指定证书允许的密钥用途
# digitalSignature:用于 TLS 握手签名
# keyEncipherment:用于密钥交换
keyUsage = critical,digitalSignature,keyEncipherment
# extendedKeyUsage
# 指定证书的扩展用途
# serverAuth:用于 TLS 服务器身份认证
extendedKeyUsage = serverAuth
# subjectAltName
# 指定使用 alt_names 段作为 SAN 列表
subjectAltName = @alt_names
# ==========================
# alt_names 段:SAN(Subject Alternative Name)列表
# ==========================
# SAN 是 TLS 客户端进行主机名/IP 校验时使用的唯一依据
# 现代 TLS 实现(OpenSSL / Go / Java / 浏览器)都会忽略 CN,
# 只校验 subjectAltName 中是否包含正在访问的域名或 IP
#
# 只要客户端访问的 Host / IP 出现在 SAN 中:
# - TLS 握手才会通过
# - 否则即使 CN 匹配也会被判定为证书无效
#
# DNS.x:
# - 用于域名匹配
# - 支持通配符(*.example.com)
# - 通配符仅匹配一级子域,不匹配根域名本身
#
# IP.x:
# - 用于 IP 地址匹配
# - 必须写真实 IP,不能使用通配符
#
# 编号规则:
# - x 从 1 开始递增
# - DNS 和 IP 可混用,顺序无关
[alt_names]
DNS.1 = lesslog.com
DNS.2 = *.lesslog.com
IP.1 = 192.168.88.88
IP.2 = 127.0.0.13.2.2 根据server.cnf文件生成csr文件
openssl req -new \
-key server/server.key \
-out server/server.csr \
-config server/server.cnf3.2.3 通过中间证书生成server.crt服务器证书
openssl x509 -req \
-in server/server.csr \
-CA intermediate/intermediate.crt \
-CAkey intermediate/intermediate.key \
-CAcreateserial \
-out server/server.crt \
-days 3650 \
-sha256 \
-extensions server_ext \
-extfile server/server.cnf3.3 生成服务器证书链(证书链作用建议自查)
# 生成服务器证书链(Full Chain)
# 顺序必须为:服务器证书 → 中间 CA 证书
# 证书链作用说明
# 证书链用于在 TLS 握手过程中,向客户端提供从服务器证书到受信任根证书的完整信任路径。
# 客户端通过逐级校验证书链,确认服务器证书是否可信
# 根证书不需要包含在证书链中,因为根证书是客户端本地信任的,中间证书中有指定根证书是谁,客户端会从本地找到根证书验证,验证中间证书是否合法cat server/server.crt intermediate/intermediate.crt > server/fullchain.crt4. 虚拟机部署nginx验证服务器证书
4.1 查看ip是否是192.168.88.88
root@lesslog:~# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:85:97:43 brd ff:ff:ff:ff:ff:ff
altname enp2s1
altname ens33
inet 192.168.88.88/24 brd 192.168.88.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe85:9743/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether d2:83:d3:cc:b4:54 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
root@lesslog:~#4.2 安装nginx以及证书准备
安装教程:https://nginx.org/en/docs/install.html
root@lesslog:~# apt update && apt install nginx创建证书目录
root@lesslog:~# mkdir -p /usr/local/share/certs上传证书至虚拟机
macos@macos:~# scp server/fullchain.crt server/server.key root/root.crt root@192.168.88.88:/usr/local/share/certs/4.3 在nginx中配置443端口证书
root@lesslog:~# vim /etc/nginx/nginx.confserver {
listen 443 ssl reuseport;
listen 443 quic reuseport;
server_name lesslog.com.;
http2 on;
access_log /var/log/nginx/access.log main;
ssl_certificate "/usr/local/share/certs/fullchain.crt"; # 证书位置
ssl_certificate_key "/usr/local/share/certs/server.key"; #私钥位置
ssl_protocols TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
client_header_timeout 5m;
keepalive_timeout 5m;
root /usr/share/nginx/html;
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
deny all;
}
location ~* \.log$ {
deny all;
}
}检查nginx语法
root@lesslog:~# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful重启nginx
root@lesslog:~# systemctl restart nginx4.4 本地通curl命令测试证书配置是否正常
root@lesslog:/usr/local/share/certs# pwd
/usr/local/share/certs
root@lesslog:/usr/local/share/certs# ls
fullchain.crt root.crt server.key
root@lesslog:/usr/local/share/certs# curl -I --cacert root.crt https://127.0.0.1
HTTP/2 200
server: nginx/1.28.0
date: Sun, 21 Dec 2025 14:47:40 GMT
content-type: text/html
content-length: 615
last-modified: Wed, 23 Apr 2025 11:48:54 GMT
etag: "6808d3a6-267"
accept-ranges: bytesroot@lesslog:/usr/local/share/certs# curl -I --cacert root.crt https://192.168.88.88
HTTP/2 200
server: nginx/1.28.0
date: Sun, 21 Dec 2025 14:49:05 GMT
content-type: text/html
content-length: 615
last-modified: Wed, 23 Apr 2025 11:48:54 GMT
etag: "6808d3a6-267"
accept-ranges: bytes
root@lesslog:/usr/local/share/certs#4.5 在宿主机上通过域名验证证书
添加域名和ip地址
macos@macos:~# sudo vim /etc/hosts
192.168.88.88 lesslog.com通过域名调用验证
macos@macos:~# curl -v -I --cacert root.crt https://lesslog.com
* Host lesslog.com:443 was resolved.
* IPv6: (none)
* IPv4: 192.168.88.88
* Trying 192.168.88.88:443...
* Connected to lesslog.com (192.168.88.88) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: root.crt
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
* subject: C=CN; ST=Internet; L=World; O=LessLog; OU=Prometheus; CN=lesslog.com
* start date: Dec 21 13:54:51 2025 GMT
* expire date: Dec 19 13:54:51 2035 GMT
* subjectAltName: host "lesslog.com" matched cert's "lesslog.com"
* issuer: C=CN; ST=Internet; L=World; O=Lesslog-Intermediate; CN=Lesslog-Intermediate-CA
* SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://lesslog.com/
* [HTTP/2] [1] [:method: HEAD]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: lesslog.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> HEAD / HTTP/2
> Host: lesslog.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
HTTP/2 200
< server: nginx/1.28.0
server: nginx/1.28.0
< date: Sun, 21 Dec 2025 14:54:57 GMT
date: Sun, 21 Dec 2025 14:54:57 GMT
< content-type: text/html
content-type: text/html
< content-length: 615
content-length: 615
< last-modified: Wed, 23 Apr 2025 11:48:54 GMT
last-modified: Wed, 23 Apr 2025 11:48:54 GMT
< etag: "6808d3a6-267"
etag: "6808d3a6-267"
< accept-ranges: bytes
accept-ranges: bytes
<
* Connection #0 to host lesslog.com left intact修改/etc/hosts的域名,但是ip仍然指向192.168.88.88
macos@macos:~# sudo vim /etc/hosts
192.168.88.88 test.com再次调用测试
macos@macos:~# curl -I --cacert root.crt https://test.com
curl: (60) SSL: no alternative certificate subject name matches target host name 'test.com'
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
macos@macos:~#调用失败原因:test.com不包含在SAN中
最后,如果不使用fullchain.crt,直接使用server.crt,将会无法使用root.crt证书验证,因为缺少中间证书,中间的链断了,自然就无法验证了,更多细节建议大家自行了解,就不再详细赘述了。刚开始本想写个笔记的,写到最后验证的时候,有点变教程的味道了。