用python批量申请https证书

1. 背景

HTTP(HyperText Transfer Protocol)是一种用于传输超文本的协议,但其传输内容是明文的,这意味着在数据传输过程中,任何中间人都可以轻松截获、篡改或窃取信息。因此,HTTP协议被认为是不安全的。
HTTPS(HyperText Transfer Protocol Secure)在HTTP的基础上增加了加密功能,通过SSL/TLS协议对通信内容进行加密和身份认证,提供数据的保密性、完整性和身份验证能力,显著提高了通信的安全性。对于自己开发的网站,使用HTTPS协议不仅能保护用户隐私,还能提高搜索引擎排名,并满足备案和法律合规的要求。
免费HTTPS证书的普及主要得益于Let’s Encrypt等组织的推动,其核心目标是让每个网站都能免费、便捷地获得HTTPS证书,从而实现一个更安全的互联网。

2. 原理

Let’s Encrypt是一个免费、自动化和开放的证书颁发机构(CA)。其核心是ACME(Automated Certificate Management Environment)协议,允许用户通过脚本或工具自动化申请、验证和续期证书。

核心流程:

  1. 域名验证:证明您对该域名的控制权,支持HTTP-01(通过HTTP访问验证文件)或DNS-01(通过DNS记录验证)。
  2. 证书颁发:验证成功后,Let’s Encrypt向用户颁发有效期为90天的证书。
  3. 自动续期:用户需要在证书到期前完成续期,通常通过脚本实现。

3. 脚本自动申请HTTPS证书

3.1 安装Nginx

我是通过oneinstack一键安装的环境所需要的基本数据,作为开发者,一键部署脚本,给我们提供了极大的方便。使用oneinstack可以快速部署nginx、java,mysql、redis、mongodb,node,ftp等服务。
oneinstack官网

目前我们只需要安装nginx,复制这段,他会自动安装

1
wget -c http://mirrors.oneinstack.com/oneinstack.tar.gz && tar xzf oneinstack.tar.gz && ./oneinstack/install.sh --nginx_option 1 --memcached  --reboot 

安装完成之后出现这个oninstack的主目录

他的nginx配置是在/usr/local/nginx这个路径下

创建ssl目录,存放ssl的证书

创建vhost,存放你的网站nginx配置文件

3.2 安装 Acme

1
curl https://get.acme.sh | sh -s email=aaa@gmail.com

安装完成之后出有.acme.sh的隐藏目录出现

3.3 安装Python3

1
2
3
4
5
6
7
8
9
10
11
12
# 安装必要的软件包
yum install gcc openssl-devel bzip2-devel libffi-devel zlib-devel

# 下载python3.8
wget https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tgz

#编译安装
./configure --enable-optimizations --prefix=/usr/local/python3 && make install

# 创建软连接
sudo ln -sf /usr/local/python3/bin/python3.8 /usr/bin/python3
sudo ln -sf /usr/local/python3/bin/pip3.8 /usr/bin/pip3

3.4 脚本编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227

# -*- coding: utf-8 -*-

import os
import random
import subprocess
import time
from datetime import datetime
from urllib.parse import urlparse


def task_log(message):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"{timestamp} - {message}")


def extract_main_domain(url):
# 解析URL
parsed_url = urlparse(url)
domains = parsed_url.netloc if parsed_url.netloc else parsed_url.path
main_domain = domains.split('.')[0]
return main_domain


def run_shell_command(commands):
try:
result = subprocess.run(commands, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return result.stdout, result.stderr
except subprocess.CalledProcessError as e:
return e.stdout, e.stderr


def format_nginx_https(base_domain):
ret_str = """
server {
server_name base_domain; #将locaihost修改为您证书绑定的域名,例如:www.example.com。
listen 443 ssl; #SSL协议访问端口号为443。此处如未添加ssl,可能会造成Nginx无法启动。
access_log /data/wwwlogs/access_nginx.log combined;
root /data/wwwroot/base_domain;
index index.html index.htm collection.html;
ssl_certificate /usr/local/nginx/ssl/base_domain/fullchain.cer; #将domain name.pem替换成您证书的文件名。
ssl_certificate_key /usr/local/nginx/ssl/base_domain/base_domain.key; #将domain name.key替换成您证书的密钥文件名。
ssl_session_timeout 1m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; #使用此加密套件。
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #使用该协议进行配置。
ssl_prefer_server_ciphers on;

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
expires 1d;
access_log off;
}
location ~ .*\.(js|css)?$ {
expires 1d;
access_log off;
}
location /.well-known {
allow all;
}
}

server {
listen 80;
server_name base_domain;
access_log /data/wwwlogs/access_nginx.log combined;
#return 301 https://$server_name$request_uri;
root /data/wwwroot/base_domain;
index index.html index.htm collection.html;
#error_page 404 /404.html;
#error_page 502 /502.html;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
expires 1d;
access_log off;
}
location ~ .*\.(js|css)?$ {
expires 1d;
access_log off;
}
location ~ ^/(\.user.ini|\.ht|\.git|\.svn|\.project|LICENSE|README.md) {
deny all;
}
location /.well-known {
allow all;
}
}
"""
ret_str = ret_str.replace("base_domain", base_domain)
return ret_str


def format_nginx_http(base_domain):
ret_str = """
server {
listen 80;
server_name base_domain;
access_log /data/wwwlogs/access_nginx.log combined;
#return 301 https://$server_name$request_uri;
root /data/wwwroot/base_domain;
index index.html index.htm;
#error_page 404 /404.html;
#error_page 502 /502.html;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
expires 30d;
access_log off;
}
location ~ .*\.(js|css)?$ {
expires 7d;
access_log off;
}
location /.well-known {
allow all;
}
}
"""
ret_str = ret_str.replace("base_domain", base_domain)
return ret_str


if __name__ == '__main__':
# 0. acme.sh --set-default-ca --server letsencrypt 设置加密证书使用这个
# 1. wwwroot下新增一个文件 设置 index.html
# 2. /usr/local/nginx/conf/vhost 新增一个 domain.config ,acme http方式验证 需要访问 .well-known
# 2.1 创建对应的domain.config
# 3. /usr/local/nginx/ssl 下新建对应的domain目录
# 4. 重启nginx
# 5. 申请证书 acme.sh --issue --domain bluewhite.fun --domain www.bluewhite.fun --webroot /data/wwwroot/domain
# 6. 移动证书到指定的domain下
# acme.sh --install-cert -d domain.xxx --key-file /usr/local/nginx/ssl/domain/domain.xxx.key
# --fullchain-file /usr/local/nginx/ssl/domain/fullchain.cer
# 7. 重新生成带证书的domain.conf

# parser = argparse.ArgumentParser(description="Process a comma-separated string.")
# parser.add_argument('base_domains', type=str, help='domain list , separated')
# args = parser.parse_args()
# domain_list = args.base_domains.split(',')

domain_list = []
file_name = "domain.txt"
with open(file_name, 'r') as file:
for line in file:
doms = line.strip()
domain_list.append(doms)
print(len(domain_list))
try:
for base_domain in domain_list:
# 1
root_dir = "/data/wwwroot" + "/" + base_domain
file_name = "index.html"
if not os.path.exists(root_dir):
os.mkdir(root_dir)
task_log(f"【1】wwwroot : \t create dir is : {root_dir}")
else:
task_log(f"【1】wwwroot :\t {root_dir} exist,skip it")

file_path = os.path.join(root_dir, file_name)

# 2 wwwroot dir and index.html
if not os.path.exists(file_path):
with open(file_path, 'w') as file:
content = "<h1>{} hello world</>".format(base_domain)
file.write(content)
task_log(f"【2】{base_domain} index.html:\t create file:{file_path}")
else:
task_log(f"【2】{base_domain} index.html: \t '{file_path}' is exist")

# 3 vhost domain_config
domain_conf = "/usr/local/nginx/conf/vhost" + "/" + base_domain + ".conf"

if not os.path.exists(domain_conf):
with open(domain_conf, 'w') as file:
content_n = format_nginx_http(base_domain)
file.write(content_n)
task_log(f"【3】vhost domain_config:\t create file is: {domain_conf}")
else:
task_log(f"【3】vhost domain_config: \t {domain_conf} is exist")
# 4
ssl_path = "/usr/local/nginx/ssl" + "/" + base_domain
if not os.path.exists(ssl_path):
os.mkdir(ssl_path)
task_log(f"【4】ssl dir is: \t create {root_dir}")
else:
task_log(f"【4】ssl dir is:\t {root_dir} exist,skip it")
command = ["/usr/local/nginx/sbin/nginx", "-s", "reload"]
stdout, stderr = run_shell_command(command)
task_log("【5】http nginx test: \n" + str(stdout).rstrip("\n") + "stderr:" + str(stderr).rstrip("\n"))
rand_time = random.randint(1, 4)

# 7 application ssl
command = ["/root/.acme.sh/acme.sh", "--set-default-ca", "--server", "letsencrypt"]
stdout, stderr = run_shell_command(command)
task_log(
"【6】set-default-ca letsencrypt: \n" + str(stdout).rstrip("\n") + "stderr:" + str(stderr).rstrip("\n"))

print(f" please waiting {rand_time}s")
time.sleep(rand_time)
command = ["/root/.acme.sh/acme.sh", "--issue", "--domain", base_domain, "--domain", base_domain,
"--webroot",
root_dir]
stdout, stderr = run_shell_command(command)
task_log("【7】application ssl: \n" + str(stdout).rstrip("\n") + "stderr:" + str(stderr).rstrip("\n"))

print(f" please waiting 5s")
time.sleep(5)
ssl_key_name = os.path.join(ssl_path, base_domain + ".key")
ssl_full_name = os.path.join(ssl_path, "fullchain.cer")
command = ["/root/.acme.sh/acme.sh", "--install-cert", "-d", base_domain, "--key-file", ssl_key_name,
"--fullchain-file",
ssl_full_name]
stdout, stderr = run_shell_command(command)
task_log("【8】ssl move target stdout: \n" + str(stdout).rstrip("\n") + "stderr:" + str(stderr).rstrip("\n"))

# 9 https替换
with open(domain_conf, 'w') as file:
content_n = format_nginx_https(base_domain)
file.write(content_n)
task_log(f"【9】vhost https config:\t create file is: {domain_conf}")

task_log(
"【10】^^^^^^^^^domain 【{}】 is complete, go to the next one ,sleep {}s ^^^^^^^^^ \n".format(base_domain,
rand_time))
time.sleep(rand_time)
# 整体再重启一次
command_end = ["/usr/local/nginx/sbin/nginx", "-s", "reload"]
stdout, stderr = run_shell_command(command_end)
except Exception as e:
print(e)
task_log("【11】All Domain Application SSL success")

这样就算完成了,脚本的main函数开始之前,把整体思路已经描述了。

4. 验证

在验证之前一定要把你的域名解析到对应的机器上。

最后执行
python3 ssl_application.py
会从Let’s Encrypt 申请你写入的domain.txt里面的域名ssl证书。
比如我有一个xxxpeak的域名,申请完成证书之后会放到这个ssl目录里面的,自己正常访问网站就会显示https了

5. 自动化续期

acme.sh默认会添加自动续期任务到cron,每60天检测证书状态并尝试续期。如何检查呢,查看crontab

通过acme.sh和Python脚本,可以轻松实现HTTPS证书的免费申请与管理。Let’s Encrypt的免费服务为中小型网站和个人开发者提供了经济高效的解决方案,显著降低了部署HTTPS的门槛。但是还是希望如果可以,去购买付费的ssl证书。


用python批量申请https证书
https://vaughnn.github.io/posts/6d34a0a6/
作者
vaughnn
发布于
2024年12月5日
更新于
2024年12月5日