自签 TLS 双向证书配置

梨子2022-10-19,更新于 2022-10-24
本文全长 1023 字,全部读完大约需要 3 分钟。

首先生成根证书私钥,很多教程都会教你用 RSA 的密钥,但是其实 RSA 已经过时了。它越来越被认为不安全, 因此需要越来越长的密钥。而当下最为推荐的是 ECC 即椭圆曲线的非对称算法,它在同样的密钥长度下被认为安全性高于 RSA 数十倍,在同样的安全性下运算效率远高于 RSA。在众多椭圆曲线中,实现最安全简单,被工业标准认可,最受广泛支持的是 prime256v1 曲线。

openssl ecparam -out ca.key -name prime256v1 -genkey

然后生成根证书请求文件,注意默认的 SHA1 算法已经不再安全,需要手动指定 SHA256 签名算法,以下都是。此处会要求填写一些东西,可根据自己的喜好填写,

  • Country Name (2 letter code) []:
  • State or Province Name (full name) []:
  • Locality Name (eg, city) []:
  • Organization Name (eg, company) []:
  • Organizational Unit Name (eg, section) []:
  • Common Name (eg, fully qualified host name) []:

其中 Common Name 就是你根证书的名字,可以叫「YourName Root CA」之类的。密码可以不填。

openssl req -new -sha256 -key ca.key -out ca.csr

自签根证书。方便起见可以设置过期时间为 10 年。要访问使用此根证书颁发的证书的网站,需要在客户端上导入根证书并信任。

openssl x509 -req -sha256 -days 3650 -in ca.csr -signkey ca.key -out ca.crt

然后我们签发服务器证书,先生成服务器证书私钥。

openssl ecparam -genkey -name prime256v1 -out server.key

签发服务器证书请求文件。此处 Common Name 应该填写你的域名,如果没有域名则填 IP。密码可以不填。

openssl req -new -sha256 -key server.key -out server.csr

此处注意,如果使用 IP 作为 Common Name,需要加一个扩展文件参数来设置 subjectAltName,否则浏览器会提示证书无效。创建一个文件 http.ext 写入以下内容

keyUsage=nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage=serverAuth, clientAuth
subjectAltName=@SubjectAlternativeName

[SubjectAlternativeName]
IP.1=10.0.0.1
# 如果是域名证书,也可以在此添加多域名
# DNS.1=example.org
# DNS.2=www.example.org

用根证书签名服务器证书。注意浏览器不接受超过 825 天有效期的证书,会提示证书无效。因此最大天数只能是 825 天。

openssl x509 -req -days 825 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -sha256 -extfile http.ext

签发客户端证书,和服务器证书类似

openssl ecparam -genkey -name prime256v1 -out client.key
openssl req -new -sha256 -key client.key -out client.csr
openssl x509 -req -days 825 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -sha256

导出客户端证书,以便导入到钥匙串中。

openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out crh.p12

配置 Nginx,并把客户端证书 Common Name 作为 Header 传入。以下配置只启用 TLS 1.3,读者实际需要的配置可以通过 https://ssl-config.mozilla.org/ 来生成。

map $ssl_client_s_dn $ssl_client_s_dn_cn {
     default "";
     ~(^|,)CN=(?<CN>[^,]+) $CN;
}

server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;
    root         /usr/share/nginx/html;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;

        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}

server {
    listen       443 ssl http2 default_server;
    listen       [::]:443 ssl http2 default_server;
    server_name  _;
    root         /usr/share/nginx/html;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

    ssl_certificate "/etc/pki/nginx/server.crt";
    ssl_certificate_key "/etc/pki/nginx/private/server.key";
    ssl_protocols TLSv1.3;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 10m;
    ssl_prefer_server_ciphers off;
    ssl_verify_client on;
    ssl_client_certificate "/etc/pki/nginx/ca.crt";
    ssl_verify_depth 6;
    ssl_trusted_certificate "/etc/pki/nginx/ca.crt";

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;

        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_set_header X-Tls-Certificate-CN $ssl_client_s_dn_cn;
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}

参考资料

除特殊说明以外,本网站文章采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。