前言
以Alpine Linux 的Docker 镜像为基础,创建一个SSL 证书配置测试环境,用于熟悉Nginx 的SSL 配置。
准备工作
Dockerfile GitHub
FROM alpine:3.6
RUN apk add --no-cache python3 nginx curl openssl socat bash openrc &&\
mkdir /run/nginx &&\
touch /root/.bashrc &&\
echo "export PS1='\h:\w\\\$ '" >> /root/.bashrc &&\
echo "alias r='fc -e -'" >> /root/.bashrc &&\
echo "set -o vi" >> /root/.bashrc &&\
echo 'rc_provide="loopback net"' >> /etc/rc.conf &&\
rc-update add nginx default
VOLUME ["/tmp/sys/fs/cgroup","/sys/fs/cgroup"]
WORKDIR /root
CMD ["/sbin/init"]
创建Docker 镜像alssltest
# 获取Dockerfile 并创建镜像
git clone git@github.com:suzhi82/Alpine-SSLTEST.git &&\
cd Alpine-SSLTEST &&\
docker build -t alssltest .
# 查看Docker 镜像列表
docker image ls
# 或者
docker images
运行Docker 镜像alssltest
# 宿主机的8888 就当成80 来用,8001、8002 及8443 测试不同应用时用
docker run --name alssltest -itd \
-p 8888:80 -p 8443:8443 -p 8001:8001 -p 8002:8002 \
alssltest
参数 | 解释 |
---|---|
–name | 给容器命名,以便后面使用,否则只能用容器的ID |
-i | 获取标准输入STDIN,容器会一直等待用户输入而不退出 |
-t | 给docker attach 准备,表用户连上来的是一个tty,否则连输密码都显示明文 |
-d | detach,分离,即让容器后台运行 |
-p | 端口映射,宿主机端口:容器端口,该参数可多次使用来映射多个端口 |
连接Docker 容器alssltest
docker exec -it alssltest /bin/bash
容器alssltest 内部调整
cat /etc/nginx/nginx.conf
http 内部最后可看到include /etc/nginx/conf.d/*.conf;
所以,我们可以将测试用的.conf 配置文件都写在/etc/nginx/conf.d/
下面,然后
# 将默认的配置备份起来不启用
mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak
OpenSSL 自签SSL 证书
SSL 自签证书生成
# 创建保存SSL 文件的目录
mkdir -p /etc/nginx/ssl_keys/openssl
cd /etc/nginx/ssl_keys/openssl
# 生成服务器端的私钥 (key 文件)
openssl genrsa -out server.key 1024
# 生成服务器端证书签名请求文件 (csr 文件),certificate signing request,自定义区域详见下表
openssl req -new -key server.key -out server.csr
# 生成证书文件 (crt 文件),csr 文件至此就没有用了
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
csr 文件属性
自定义区域
Country Name (2 letter code) [XX]:CN--------------------------------- 证书持有者所在国家
State or Province Name (full name) []:BJ----------------------------- 证书持有者所在州或省份(可省略不填)
Locality Name (eg, city) []:BJ--------------------------------------- 证书持有者所在城市(可省略不填)
Organization Name (eg, company) []:NH-------------------------------- 证书持有者所属组织或公司
Organizational Unit Name (eg, section) []:.-------------------------- 证书持有者所属部门(可省略不填)
Common Name (eg, your name or your server's hostname) []:ceshi.com--- 域名
Email Address []:---------------------------------------------------- 邮箱(可省略不填)
challenge password:-------------------------------------------------- 自定义密码
An optional company name:-------------------------------------------- 可选公司名称
Nginx SSL 配置
# Nginx 配置文件
cat > /etc/nginx/conf.d/openssl.conf << EOF
server {
listen 8443 ssl;
server_name ceshi.com;
ssl on;
ssl_certificate /etc/nginx/ssl_keys/openssl/server.crt;
ssl_certificate_key /etc/nginx/ssl_keys/openssl/server.key;
index index.html index.htm;
location / {
root /home/app/code;
}
}
EOF
访问测试文件
mkdir -p /home/app/code/
cat > /home/app/code/index.html << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<title>Index</title>
</head>
<body>
<h1>What a good day!</h1>
</body>
</html>
EOF
# 测试Nginx 配置
nginx -t
# 重启Nginx 服务
rc-service nginx restart
# 或者
nginx -s reload
重启完毕后就可以在浏览器上访问https://服务器IP:8443,出现Your connection is not private
点Help me understand
,再点击下方的Proceed to 服务器IP (unsafe)
,出现What a good day!
表示访问成功,但不被浏览器所信任。
OpenSSL 使用补充
# 去除key 文件中的密钥
openssl rsa -in jesonc.key -out jesonc_nopwd.key
# 符合苹果标准的签名文件,直接生成私钥key 和公钥crt,由于是自签所以省略了请求文件csr 的生成
openssl req -days 3650 -x509 -sha256 -nodes -newkey rsa:2048 -keyout new_server.key -out new_server.crt
# rsa:2048 是服务器私钥的算法,-sha256 是发给浏览器证书即公钥的算法,可用以下命令查看
openssl x509 -noout -text -in new_server.crt
Nginx 排错补充
Nginx 启动的用户须拥有页面文件所在文件夹的读取权限,否则会报403 错误
cat /etc/nginx/nginx.conf
# 启动nginx 所用的用户
user nginx;
# 错误日志的存放路径
error_log /var/log/nginx/error.log warn;
OpenSSL 使用结论
由于是自签,证书需手工导入浏览器才会被信任,在生成证书申请的时候所有自定义信息都可以直接回车略过,只要浏览器输入的是服务器的IP 或指向该IP 的域名及对应端口即可访问到网站内容,用户的安全性可想而知。?
acme.sh 获取SSL 证书
获取正规渠道CA 颁发的SSL 证书
Freenom 域名转Cloudflare 管理
虽然根据cme.sh 提示在freenom 里添加txt 类型的DNS 记录也可申请到证书,但是每次续期都得操作一次,相较没有使用DNS 的API 自动化来得方便。
先从https://www.freenom.com 获取到免费域名,注意要挂美国IP 的VPN。
无非就是注册然后输入想要的域名 -->Check Availability,一般tk/ml/gq 等结尾的都是免费的。
注册登录https://www.cloudflare.com,Home 下面点击Add a Site
,输入签名获得的域名例如maybook.tk
,再按Add site,Select a plan,选择Free $0/month,一路continue,直至Change your nameservers
界面。
登录到https://www.freenom.com,Services -->My Domains,选中maybook.tk
的Manage Domain,–>Management Tools -->Nameservers,选择Use custom nameservers
,写入如下两个服务器。
amit.ns.cloudflare.com
amy.ns.cloudflare.com
最后点击Change Nameservers
即可。
回到Cloudflare 的Change your nameservers
页面,点击Done, check nameservers,如果页面跳转的话则点击Re-check now 按钮。可能要等待几个小时Freenom 的域名才能交给Cloudflare 接管。
Cloudflare 接管之后会有Email 通知。登录Cloudflare 之后就会出现状态为Active 的maybook.tk
了,点击它,
Overview 中的Get your API key 的API Tokens 的Global API Key
可以授权第三方程序管理dns 的设置,例如用acme.sh
自动申请、续期SSL 证书。
设置域名记录
Cloudflare -->Home 选中maybook.tk -->DNS 依次添加以下Records 以便后面做试验用?
Type | Name | Content | TTL | Proxy status |
---|---|---|---|---|
A | maybook.tk | 139.180.167.52 | Auto | DNS only |
A | app1.maybook.tk | 139.180.167.52 | Auto | DNS only |
A | app2.maybook.tk | 139.180.167.52 | Auto | DNS only |
acme.sh 安装及使用
# 安装acme.sh
curl https://get.acme.sh | sh
# 使acme.sh 生效,重新进入容器也行
source ~/.bashrc
# 导入Cloudflare Token,即Global API Key,让acme.sh 控制DNS 记录以自动申请证书
export CF_Key="ba1a821xxxxxx42b603064xxxxxxx3443"
export CF_Email="xxxxxx@gmail.com"
# 将非泛型域名放在第一位方便以后部署,因为* 是通配符。
# 用以下命令申请的SSL 证书会包含maybook.tk 及泛型*.maybook.tk 域名
acme.sh --issue --dns dns_cf -d maybook.tk -d *.maybook.tk
执行过acme.sh 后DNS 的API 信息会保存到~/.acme.sh/account.conf
,如果写错了去该文件里删除相关内容,然后unset 环境变量后重新export 环境变量即可。
# 安装部署SSL 证书
acme.sh --installcert -d maybook.tk \
--key-file /etc/nginx/ssl_keys/maybook.tk.key.pem \
--fullchain-file /etc/nginx/ssl_keys/maybook.tk.cert.pem \
--reloadcmd "service nginx restart"
每运行acme.sh --installcert
安装的路径及其他配置会被保存到~/.acme.sh/<domain>/<domain>.conf
里面。
其他发行版可能要用--reloadcmd "service nginx force-reload"
才能让Nginx 重新读取配置文件。
Nginx SSL 配置
# 新建一个Nginx 配置acme.sh.conf
cat > /etc/nginx/conf.d/acme.sh.conf << EOF
server {
listen 8001 ssl;
server_name maybook.tk;
ssl on;
ssl_certificate /etc/nginx/ssl_keys/maybook.tk.cert.pem;
ssl_certificate_key /etc/nginx/ssl_keys/maybook.tk.key.pem;
index index.html index.htm;
location / {
root /home/app/code;
}
}
EOF
# 让Nginx 重新读取配置
nginx -s reload
新证书访问验证
还是使用之前创建的页面,配置上CA 颁发的新SSL 证书。
浏览器中输入https://maybook.tk:8001
,出现What a good day!
且浏览器显示绿色锁表示SSL 证书被信任了。
验证泛型域名
Nginx 配置文件
# 创建两个server 都监听80 端口,根据不同的请求host 临时重定向302 到对应的https 链接
cat > /etc/nginx/conf.d/acme.sh.x.conf << EOF
server {
listen 80;
server_name app1.maybook.tk;
# 不使用301 永久重定向,因为如果后期地址改变需要清理浏览器端的Cookie 才能生效
return 302 https://$host:8001$request_uri;
}
server {
listen 80;
server_name app2.maybook.tk;
return 302 https://$host:8002$request_uri;
}
# 创建两个开启了SSL 的server,并将请求代理转发到对应的后台应用
server {
listen 8001 ssl;
server_name app1.maybook.tk;
ssl_certificate /etc/nginx/ssl_keys/maybook.tk.cert.pem;
ssl_certificate_key /etc/nginx/ssl_keys/maybook.tk.key.pem;
location / {
proxy_pass http://127.0.0.1:8801;
}
}
server {
listen 8002 ssl;
server_name app1.maybook.tk;
ssl_certificate /etc/nginx/ssl_keys/maybook.tk.cert.pem;
ssl_certificate_key /etc/nginx/ssl_keys/maybook.tk.key.pem;
location / {
proxy_pass http://127.0.0.1:8802;
}
}
EOF
访问测试文件
# 模拟应用1
mkdir /home/web1/
cd /home/web1/
cat > index.html << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<title>Index</title>
</head>
<body>
<h1>
<a href="/a1.html">a1.html</a><br/>
<a href="/a2.html">a2.html</a>
</h1>
</body>
</html>
EOF
cat > a1.html << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<title>A1</title>
</head>
<body>
<a href="/index.html">index.html</a>
<h1>A1A1A1</h1>
</body>
</html>
EOF
cat > a2.html << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<title>A2</title>
</head>
<body>
<a href="/index.html">index.html</a>
<h1>A2A2A2</h1>
</body>
</html>
EOF
# 模拟应用2
mkdir /home/web2/
cd /home/web2/
cat > index.html << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<title>Index</title>
</head>
<body>
<h1>
<a href="/b1.html">b1.html</a><br/>
<a href="/b2.html">b2.html</a>
</h1>
</body>
</html>
EOF
cat > b1.html << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<title>B1</title>
</head>
<body>
<a href="/index.html">index.html</a>
<h1>B1B1B1</h1>
</body>
</html>
EOF
cat > b2.html << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<title>B2</title>
</head>
<body>
<a href="/index.html">index.html</a>
<h1>B2B2B2</h1>
</body>
</html>
EOF
模拟后台API
nohup python3 -m http.server 8801 > /dev/null 2>&1 &
nohup python3 -m http.server 8802 > /dev/null 2>&1 &
测试结果及结论
URL | 内容 |
---|---|
http://maybook.tk:8888 | https 形式的“What a good day!” 页面 |
http://app1.maybook.tk:8888 | https 形式含有a1、a2 的index 页面 |
http://app2.maybook.tk:8888 | https 形式含有b1、b2 的index 页面 |
Nginx 中可以多个server 同时监听同一个端口,再用server_name 即域名或子域名的方式来区分不同的业务。
监听容器的80 端口(映射到宿主机8888),然后将http 的请求转到https(8001-8002),再根据server_name 转到后台应用(8801-8802)。
浏览器会对比地址栏输入的域名是否跟SSL 证书里的域名相同,如果相同且证书是受信任的则允许访问,否则出现自签证书那样的安全提示。
SSL 双向认证简单介绍
双向认证 SSL 协议要求服务器和用户双方都有证书。单向认证 SSL 协议不需要客户拥有CA证书,具体的过程相对于上面的步骤,只需将服务器端验证客户证书的过程去掉,以及在协商对称密码方案,对称通话密钥时,服务器发送给客户的是没有加过密的(这并不影响 SSL 过程的安全性)密码方案。这样,双方具体的通讯内容,就是加过密的数据,如果有第三方攻击,获得的只是加密的数据,第三方要获得有用的信息,就需要对加密的数据进行解密,这时候的安全就依赖于密码方案的安全。而幸运的是,目前所用的密码方案,只要通讯密钥长度足够的长,就足够的安全。这也是我们强调要求使用128位加密通讯的原因。
一般Web应用都是采用SSL单向认证的,原因很简单,用户数目广泛,且无需在通讯层对用户身份进行验证,一般都在应用逻辑层来保证用户的合法登入。但如果是企业应用对接,情况就不一样,可能会要求对客户端(相对而言)做身份验证。这时就需要做SSL双向认证。
清理工作
停止并删除Docker 容器
docker stop alssltest && docker rm alssltest
删除Docker 镜像alssltest
docker rmi alssltest:latest
参考文档
Nginx 入门实战
使用 acme.sh 安装 Let’ s Encrypt 提供的免费 SSL 证书
线上服务器部署(前后端)