nginx配置mTLS双向认证

环境 almalinux9.3
openssl版本 3.x

证书生成简化脚本

#!/bin/bash

# 1、生成私钥公钥,自己作为CA机构。
echo "#############################################################################################################"
echo "生成CA证书"
mkdir ca && cd ca
openssl genpkey -algorithm RSA -out ca.key -pkeyopt rsa_keygen_bits:2048
openssl req -new -key ca.key -out ca.csr

cat <<EOF > v3_ca.ext
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
EOF

openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt -days 3650 -extfile v3_ca.ext
# 注意这里明确声明自己作为CA,CA:TRUE。
echo "#############################################################################################################"
cd ..
 
 
# 给服务器(域名)颁发证书
echo "#############################################################################################################"
echo "生成域名证书"
mkdir server && cd server
openssl genpkey -algorithm RSA -out server.key -pkeyopt rsa_keygen_bits:2048
openssl req -new -key server.key -out server.csr
 
# 创建一个SAN文件
cat <<EOF > san.conf 
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
 
[alt_names]
DNS.1 = www.test.com
DNS.2 = test.x.com
IP.1 = 127.0.0.1
IP.1 = 192.168.6.114
EOF
 
# 用ca的证书给csr颁发证书
openssl x509 -req -in server.csr -CA ../ca/ca.crt -CAkey ../ca/ca.key  -CAcreateserial -out server.crt -days 365 -extfile san.conf
 
 
# 验证证书链
echo "验证证书链"
openssl verify -CAfile ../ca/ca.crt server.crt
#如果输出 server.crt: OK,说明证书链正确。
echo "#############################################################################################################"
 
cd ..

echo "#############################################################################################################"
echo "生成客户端证书"
mkdir client && cd client
openssl genpkey -algorithm RSA -out client.key -pkeyopt rsa_keygen_bits:2048
openssl req -new -key client.key -out client.csr
openssl x509 -req -in client.csr -CA ../ca/ca.crt -CAkey ../ca/ca.key  -CAcreateserial -out client.crt -days 365

echo "#############################################################################################################"
rm -f /home/software/openresty/nginx/conf/vhost/server/*
cp ../server/* /home/software/openresty/nginx/conf/vhost/server/
cp ../ca/ca.crt /home/software/openresty/nginx/conf/vhost/server/
/home/software/openresty/nginx/sbin/nginx -s reload

echo "curl访问"
curl --cert client.crt --key client.key --cacert ../ca/ca.crt https://www.test.com


# 为了将证书导入到浏览器,生成p12格式
openssl pkcs12 -export -inkey client.key -in client.crt -out client.p12 -name "ClientCert"


# nginx配置
server {

	listen	80;
	listen	443 ssl;
	http2 on;
	server_name  www.test.com;
	root /www;
	index index.php index.html;
	
	ssl_certificate /home/software/openresty/nginx/conf/vhost/server/server.crt;
	ssl_certificate_key /home/software/openresty/nginx/conf/vhost/server/server.key;
	
	ssl_session_timeout 5m;
	ssl_protocols TLSv1.1 TLSv1.2;
	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
	ssl_prefer_server_ciphers on;
	
	ssl_verify_client on;
	ssl_client_certificate /home/software/openresty/nginx/conf/vhost/server/ca.crt;
	
	client_max_body_size 100M;
	
	server_tokens off;
	fastcgi_hide_header X-Powered-By;

	location ~ \.php$
	{
		fastcgi_split_path_info ^((?U).+.php)(/?.+)$;
		fastcgi_param  PATH_INFO $fastcgi_path_info;
		fastcgi_param  PATH_TRANSLATED $document_root$fastcgi_path_info;
		fastcgi_pass   127.0.0.1:10000;
		fastcgi_index  index.php;
		fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
		fastcgi_param  RGS_COREPATHNAME GameMotors;
		include        fastcgi_params;
	}

	access_log  /var/log/nginx/www.test.com-access.log;
	error_log   /var/log/nginx/www.test.com-error.log;
}

# ssl_verify_client on 是开启验证客户端证书。
# ssl_client_certificate 这个参数告诉nginx我信任这个文件中的 CA,如果客户端提交的证书是由这些 CA 签发的,那我就接受。

1、首先双击 ca.crt 文件,导入到本地,选择 受信任的根证书颁发机构。这样浏览器才会信任,不会报不安全。
2、Chrome浏览器导入p12证书,点击 设置 – 隐私与安全 – 安全 – 管理证书 – 您的证书 – 管理从Windows导入的证书
点击 导入,选择刚才的p12证书文件。 “证书存储” 部分,选择 “将所有的证书都放入下列存储”,选择 “个人” ,完成。
浏览器访问 https://www.test.com/ 会弹窗选择客户端证书。

如果不导入证书,则访问时nginx会报下面错误

ngx_http_realip_module 获取客户端真实ip模块测试

此模块需要在编译安装nginx时加上 –with-http_realip_module 参数

测试环境:
PC主机 -> nginx反向代理 -> 源nginx
192.168.6.88 -> 192.168.6.151 -> 192.168.6.114

# 192.168.6.151 中 nginx 反向代理配置
location / {
	proxy_pass http://192.168.6.114;
	proxy_set_header host t1.test.com;
	proxy_set_header x-forwarded-for $remote_addr,192.168.6.151,1.1.1.1;
}

# 192.168.6.114中nginx配置:
real_ip_header x-forwarded-for;
set_real_ip_from 192.168.6.151;
set_real_ip_from 1.1.1.1;
real_ip_recursive on;

当用PC浏览器访问反向代理nginx ip时,源nginx日志中为:
192.168.6.88 - - [07/Feb/2025:01:31:55 -0500] "GET /

real_ip_header 用于配置从哪个请求头中获取真实ip。
set_real_ip_from 配置指令可以有多个,用于配置受信任的ip,移除 x-forwarded-for 字段中 set_real_ip_from 中定义的值。
real_ip_recursive 为on,先从 x-forwarded-for 中排除 set_real_ip_from 指令指定的ip,然后取最后一个ip作为客户端ip。
real_ip_recursive 为off,取x-forwarded-for中最后一个ip作为客户端ip

当 set_real_ip_from 的值都匹配不到 x-forwarded-for 中的值时,则不处理,直接用上一级代理的ip作为客户端ip(也就是 nginx反向代理机器ip )

这样可以实现在不改动后端代码的情况下获取真实客户端ip
例如php

nginx配置br压缩

# br模块仓库 https://github.com/google/ngx_brotli

注意br模块只能在https中用。http协议是不支持的。

当浏览器发送请求时,会在请求头中携带支持的压缩算法。
比如chrome访问http网站时,请求头中为Accept-Encoding: gzip, deflate
访问https网站时,请求头中为Accept-Encoding: gzip, deflate, br, zstd

nginx可以同时配置gzip和br压缩算法,如果浏览器的请求头中含有br,则优先于gzip。

# 错误:CMake 3.15 or higher is required. You are running version 3.6.0
# 安装新版本cmake,否则可能会提示上面CMake版本过低问题。

[root@web02 ~]# cd /usr/local/
[root@web02 local]# wget -c https://cmake.org/files/v3.30/cmake-3.30.0-linux-x86_64.tar.gz
[root@web02 local]# tar zxvf cmake-3.30.0-linux-x86_64.tar.gz 

[root@web02 local]# echo 'export PATH=$PATH:/usr/local/cmake-3.30.0-linux-x86_64/bin' >> /etc/profile
[root@web02 ~]# source /etc/profile

# 构建依赖
[root@web02 ~]# cat br
git clone --recurse-submodules https://github.com/google/ngx_brotli
cd ngx_brotli/deps/brotli
mkdir out && cd out
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_CXX_FLAGS="-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_INSTALL_PREFIX=./installed ..
cmake --build . --config Release --target brotlienc

[root@web02 ~]# bash br

# nginx添加br的编译参数
./configure --add-module=../ngx_brotli


# nginx虚拟主机配置
server
{

	listen       16666 ssl;
	http2 on;
	server_name  localhost;

	ssl_certificate /home/software/openresty/nginx/conf/vhost/fullchain5.pem;
	ssl_certificate_key /home/software/openresty/nginx/conf/vhost/privkey5.pem;
	
	ssl_session_timeout 5m;
	ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
	ssl_prefer_server_ciphers on;


	index index.html;
	root /home/software/openresty/nginx/html;

	#启用brotli压缩
	brotli on;
	brotli_comp_level 6;
	brotli_buffers 16 8k;
	brotli_min_length 20;
	# 注意要配置压缩的文件类型(content-type)
	brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml application/wasm application/octet-stream;  


	# 反向代理用法
	#location /
	#{
		#proxy_pass http://192.168.12.196:8888;
		#proxy_set_header Accept-Encoding "";

		#启用brotli压缩
		#brotli on;
		#brotli_comp_level 6;
		#brotli_buffers 16 8k;
		#brotli_min_length 20;
		# 注意要配置压缩的文件类型(content-type)
		#brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml application/wasm application/octet-stream;  
	#}
}

html和php的url美化处理

# 访问地址
http://voice/launchGame/23?playerId=123&playMode=real_play&currency=USD&operatorCode=Cordish&jurisdiction=PA&skin=Instance001&loginToken=token123&language=bg&contestRef=321

当浏览器访问时,会提示404错误,因为网站根目录下不存在launchGame目录及23文件
实际这是前段美化url后的访问地址,需要在nginx中将其重定向到 index.html 中,js会获取参数进行处理。
nginx中重定向有2种指令配置
1、rewrite
2、try_files

这里采用try_files

location / 
{
	index index.php index.html;
	try_files $uri $uri/ /index.html;
}

这样html中的js代码会将 /launchGame/23?playerId=123&playMode=real_play&currency=USD&operatorCode=Cordish&jurisdiction=PA&skin=Instance001&loginToken=token123&language=bg&contestRef=321
每一部分进行处理。

php的url美化处理
# 访问地址
http://voice/?user=1&pass=2
可以看到并没有具体到某个资源文件,例如index.php
但是配置文件中index指令指定了默认页为 index.php 所以url中的参数在index.php中是可以$_GET获取的

如果想通过try_files交给别的文件处理,则需要下面的配置

server
{
	listen        80;
	server_name  voice;
	root   "D:/phpstudy_pro/WWW/pt";
	index index.php index.html;
	
	try_files $uri $uri/ /a.php$is_args$args;

	location ~ \.php(.*)$
	{
		fastcgi_pass   127.0.0.1:9001;
		fastcgi_index  index.php;
		fastcgi_split_path_info  ^((?U).+\.php)(/?.+)$;
		fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
		fastcgi_param  PATH_INFO  $fastcgi_path_info;
		fastcgi_param  PATH_TRANSLATED  $document_root$fastcgi_path_info;
		include        fastcgi_params;
	}
}

http://voice/?user=1&pass=2 通过此链接来分析

# 情况1
$uri = /
匹配不到任何文件,找默认页 index.php或者index.html(取决于前后顺序,如果index.php文件不存在,则找index.html)。

# 情况2
$uri/,由于$uri = / 所以$uri/ = //
如果没有index.php或者index.html文件,则会匹配到网站根目录,会将网站根目录文件列出来,但是没加 autoindex on; 参数的话会继续访问 /a.php 且响应403状态码 但是不会传递参数。
如果加上了 autoindex on; 配置,则列出来网站目录列表,不会继续访问下一个匹配(/a.php)。

# 情况3
去掉 $uri/ 配置部分,也就是配置为 try_files $uri /a.php$is_args$args;
如果没有index.php或者index.html文件,会继续匹配/a.php$is_args$args,此时会访问到a.php并且参数也传递过去。

关于nignx中正则括号捕获组

server
{
    listen       80 default_server;
    server_name  ~^(\w+\.)?(test\.com)$;

    root /www/$1$2;
    set $san $1;
    set $er  $2;


    location ~* \.(jpeg|png|gif|jpg)$
	#location /
    {
		types {}
		default_type text/html;
		echo $san "</br>";
		echo $er  "</br>";
		echo $1   "</br>";
		echo $2   "</br>";
		echo $uri;
    }
    
}


# 当访问 http://img.test.com/1.txt 时,返回404错误,日志中报错:
 open() "/www/img.test.com/1.txt" failed (2: No such file or directory)
 说明正则匹配的值为img.test.com($1=img. $2=test.com)去/www/目录下寻找此目录
 
# 当访问 http://img.test.com/1.jpeg 时,页面返回内容:
img.
test.com
jpeg

/1.jpeg
可以发现$2为空,猜测可能是因为出现第二个正则匹配并捕获内容导致$2变为未定义变量(并不知道真实原因是什么,只是猜测)。
因为第一个捕获组定义了,但是没有定义第二个捕获组。

当注释掉location ~* \.(jpeg|png|gif|jpg)$ 开启 #location / 时,则echo的内容全部返回。
如果想用之前的变量可以通过set设置自定义变量。