从指定路径更新雷池WAF证书

  雷池(SafeLine) 是长亭科技部分开源的一款 Web 防火墙,社区版本已经有较为完善的 WAF 功能,可以满足个人项目的基本 Web 防护需求。 不过目前(v5.2.0)雷池对于证书的操作,仅支持从 UI 导入或使用 Let’s Encrypt 的 HTTP-01 验证方法 来配置证书, 这对于使用 DNS 验证的短期证书用户来说非常不方便,可以看到社区有这样的需求的小伙伴还是挺多的: [建议] 证书增加使用路径导入方式 | Github issue, 因此便写了个小脚本来实现这个需求。

调研

雷池本身也是基于 Tengine 来做网关的, 证书配置也在 Tengine 来实现, 因此 Alliot 一开始准备直接操作容器中 Tengine 的证书文件来实现, 不过看样子社区中有小伙伴已经尝试过了: [Bug] 手动更新证书文件并重启容器后,【证书管理】界面的有效期时间没有同步更新, 从底下的评论可以知道目前雷池的证书还是以数据库为准,直接操作文件并不能达到更新证书的目的。
这里打算通过操作雷池 API 来达到目的。

动手

官方并没有提供 API 文档,也没有提供生成 API key 的操作,只能通过模拟登录来实现。
雷池登录流程为: 密码登录 -> 校验通过后得到中间 JWT -> 校验 OTP -> 校验通过后得到最终的 JWT, 并且前面过程中都需要携带 CSRF token, 由于逻辑并不复杂,并且也是简单的构建一下请求,这里就直接贴代码吧:

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
import requests
import time
import pyotp
import urllib3

BASE_URL = "https://www.iots.vip" # 登录地址
USERNAME = "admin"
PASSWORD = "123123123"

OTP_SECRET = "ADXXXXXXWB5S3UIWZCDX" # 动态验证码的密钥
CERT_ID = "1" # 证书ID, 一般如果仅有一个证书,ID为1, 多个证书也可以调用下面的list_all_certs来获取ID
CERT_FILE_PATH = "www.iots.vip.crt" # 证书路径
CERT_KEY_PATH = "www.iots.vip.key" # 证书路径


def get_passcode(secret):
totp = pyotp.TOTP(secret)
return totp.now()

class SafeLine:
def __init__(self, base_url):
urllib3.disable_warnings()
self.base_url = base_url
self.csrf_token = None
self.jwt = None
self.session = requests.Session()
self.session.verify = False

def get_csrf_token(self):
response = self.session.get(f"{self.base_url}/api/open/auth/csrf")
data = response.json()
self.csrf_token = data['data']['csrf_token']
return self.csrf_token

def login(self, username, password):
self.get_csrf_token()
payload = {
"username": username,
"password": password,
"csrf_token": self.csrf_token
}
response = self.session.post(f"{self.base_url}/api/open/auth/login", json=payload)
data = response.json()
self.jwt = data['data']['jwt']
return self.jwt

def validate_mfa(self, code):
self.get_csrf_token()
headers = {'authorization': 'Bearer ' + self.jwt}
payload = {
"code": code,
"timestamp": int(time.time() * 1000),
"csrf_token": self.csrf_token
}
response = self.session.post(f"{self.base_url}/api/open/auth/tfa", headers=headers, json=payload)
data = response.json()
if data['err'] is None:
self.jwt = data['data']['jwt']
print("login success")
else:
self.jwt = None
return data['data']['jwt']

def list_all_certs(self):
headers = {'authorization': 'Bearer ' + self.jwt}
response = self.session.get(f"{self.base_url}/api/open/cert", headers=headers)
data = response.json()
return data['data']['nodes']

def update_cert(self, cert_id, crt, key):
headers = {'authorization': 'Bearer ' + self.jwt}
payload = {
"manual": {
"crt": crt,
"key": key
},
"type": 2
}
response = self.session.put(f"{self.base_url}/api/open/cert/{cert_id}", headers=headers, json=payload)
data = response.json()
if data['err'] is None:
print("update success")


if __name__ == '__main__':
safeline = SafeLine(base_url=BASE_URL)
safeline.login(USERNAME, PASSWORD)
safeline.validate_mfa(get_passcode(OTP_SECRET))

with open(CERT_FILE_PATH, 'r') as cert_file:
cert_str = cert_file.read()
with open(CERT_KEY_PATH, 'r') as key_file:
cert_key = key_file.read()
safeline.update_cert(CERT_ID, cert_str, cert_key)

安装依赖:

1
pip install requests pyotp

填好参数后 python main.py 运行,去到控制台便可以看到完成了证书更新。

部署到Certbot

Alliot 是使用 Certbot 自动申请的泛域名证书,配合对应 DNS 提供商的插件,可以实现自动颁发、续签证书。 这里使用 certbot-dns-aliyun 来实现阿里云托管域名的 Let’s Encrypt 证书自动申请。

安装部署Certbot && 阿里云DNS插件

已经有自己的证书的可以跳到下一小节。

1
2
# 确保已经安装Python3运行环境后执行 
pip install certbot certbot-nginx certbot-dns-aliyun

创建配置文件,填入阿里云 AccessKey(申请方式: 获取AccessKey|Aliyun):

1
2
3
4
mkdir /etc/certbot

touch ali_credentials.ini
chmod 600 ali_credentials.ini

ali_credentials.ini 格式如下:

1
2
dns_aliyun_access_key = xxxx
dns_aliyun_access_key_secret = xxxx

之后我们尝试申请一下证书:

1
2
3
4
certbot certonly \
--authenticator=dns-aliyun \
--dns-aliyun-credentials='/etc/certbot/ali_credentials.ini' \
-d "*.example.com,example.com"

添加一个定时任务到 crontab

1
(crontab -l 2>/dev/null; echo "0 0 * * * certbot renew -q") | crontab -

配置自动更新雷池里的证书

现在 Certbot 已经可以为我们自动申请 SSL 证书了,我们可以利用 Certbot 的 renewal-hooks 来达到证书续期后自动更新到雷池:

1
2
3
4
5
6
7
8
9
10
# 复制上面的代码,填入自己的配置
vim /etc/certbot/renew_safeline_cert.py

# 创建一个hook
cd /etc/letsencrypt/renewal-hooks/deploy

cat > update_safeline_ssl.sh << EOF
#!/bin/bash
python /etc/certbot/safeline_renew_cert/main.py
EOF

现在来测试一下 renewal-hooks:

1
2
3
4
5
6
7
8
9
10
11
certbot renew --dry-run --run-deploy-hooks
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/www.iots.vip.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for www.iots.vip
Waiting 30 seconds for DNS changes to propagate
Hook 'deploy-hook' ran with output:
login success
update success

可以看到 deploy-hook 成功执行,大工告成! 之后每次 Certbot 在更新完证书后都会同步更新雷池里托管的 SSL 证书。