前言

我的网站证书三个月更新一次.由于懒癌, 现在连证书都不想手动部署了, 于是决定写一个脚本配合 acme 自动更新我的网站证书

本文默认读者会使用acme签发域名证书, 环境为已经签发了一次证书后的linux服务器.

证书关联与需求分析

我有两个域名

一个域名证书在七牛云cdn上会用到

另一个域名在nginx上会使用到

于是流程分为
1.acme签发证书
2.七牛云上传证书
3.七牛云部署证书
4.将证书存储到nginx配置文件指定的ssl证书位置
5.nginx重启

七牛云自动部署

根据官方文档需要接口
七牛云证书上传api

七牛云证书修改api

这里我考虑的是运用七牛云官方python sdk一部分.官方sdk不支持这两个功能,需要手动获取token后调用接口

这里可以看七牛云官方python sdk的源码, auth部分, 位于lib/sites-packages/qiniu/auth.py

先配置虚拟环境然后安装应该就不用我说了

python -m venv .venv && .venv/bin/pip install qiniu

关键代码如下

class Auth(object):
"""七牛安全机制类

该类主要内容是七牛上传凭证、下载凭证、管理凭证三种凭证的签名接口的实现,以及回调验证。

Attributes:
__access_key: 账号密钥对中的accessKey,详见 https://portal.qiniu.com/user/key
__secret_key: 账号密钥对重的secretKey,详见 https://portal.qiniu.com/user/key
"""

def __init__(self, access_key, secret_key, disable_qiniu_timestamp_signature=None):
"""初始化Auth类"""
self.__checkKey(access_key, secret_key)
self.__access_key = access_key
self.__secret_key = b(secret_key)
self.disable_qiniu_timestamp_signature = disable_qiniu_timestamp_signature

def get_access_key(self):
return self.__access_key

def get_secret_key(self):
return self.__secret_key

def __token(self, data):
data = b(data)
hashed = hmac.new(self.__secret_key, data, sha1)
return urlsafe_base64_encode(hashed.digest())

def token(self, data):
return '{0}:{1}'.format(self.__access_key, self.__token(data))

def token_of_request(self, url, body=None, content_type=None):
"""带请求体的签名(本质上是管理凭证的签名)

Args:
url: 待签名请求的url
body: 待签名请求的body
content_type: 待签名请求的body的Content-Type

Returns:
管理凭证
"""
parsed_url = urlparse(url)
query = parsed_url.query
path = parsed_url.path
data = path
if query != '':
data = ''.join([data, '?', query])
data = ''.join([data, "\n"])

if body:
mimes = [
'application/x-www-form-urlencoded'
]
if content_type in mimes:
data += body

return '{0}:{1}'.format(self.__access_key, self.__token(data))

那么获取token的示例就是

from qiniu import Auth
import requests
AccessKey = "your_access_key"
SecretKey = "your_access_secret_key"
q = Auth(access_key=AccessKey, secret_key=SecretKey)
def uploadCert(key, crt):
host = "api.qiniu.com"
method = "POST"

data = {
"name": str(uuid.uuid1()),
"common_name": "*.voidval.com",
"pri": key,
"ca": crt
}

header = {
'Content-Type': 'application/json',
}
path = "/sslcert"
url = f"https://{host}{path}"
token = q.token_of_request(url=url, body=data, content_type="application/json")
header['Authorization'] = f"QBox {token}"
resp = requests.post(url, json=data, headers=header, verify=False)
print(resp.json())
return resp.json()['certID']

同理写出刷新域名证书代码即可

完整代码如下

# @Time    : 2024/12/12 13:59
# @Author : TwoOnefour
# @File : refreshcert.py
import hmac
import hashlib
import base64
import json
import requests
import uuid
import urllib.parse

import urllib3
from qiniu import Auth
AccessKey = "xxxxx"
SecretKey = "xxxxxx"
q = Auth(access_key=AccessKey, secret_key=SecretKey)

urllib3.disable_warnings()
def uploadCert(key, crt):
host = "api.qiniu.com"
method = "POST"

data = {
"name": str(uuid.uuid1()),
"common_name": "*.pursuecode.cn",
"pri": key,
"ca": crt
}

header = {
'Content-Type': 'application/json',
}
path = "/sslcert"
url = f"https://{host}{path}"
# token = getAuthToken(method=method, path=path, body=data, header=header, host=host)

token = q.token_of_request(url=url, body=data, content_type="application/json")
header['Authorization'] = f"QBox {token}"
resp = requests.post(url, json=data, headers=header, verify=False)
print(resp.json())
return resp.json()['certID']

def setcert(CertID):
host = "api.qiniu.com"
method = "PUT"
header = {
'Content-Type': 'application/json',
}

domains = [
"www.example.com",
"bucket.example.com"
]

paths = [
f"/domain/{domain}/httpsconf" for domain in domains
]

data = {
"certId": CertID,
"forceHttps": True,
"http2Enable": True
}
for path in paths:
url = f"https://{host}{path}"
token = q.token_of_request(url=url, body=data, content_type="application/json")
header['Authorization'] = f"QBox {token}"
resp = requests.put(url, headers=header, json=data, verify=False)
print(resp.json())

if __name__ == "__main__":
cer = None
key = None
with open(r"/root/.acme.sh/*.example.com/fullchain.cer") as f:
cer = f.read().strip()
with open(r"/root/.acme.sh/*.example.com/_.example.com.key") as f:
key = f.read().strip()
certid = uploadCert(crt=cer, key=key)
setcert(certid)

这里后面open语句填acme生成得到的证书路径,将此python路径记住备用,我这里是/root/qiniu/refreshcert.py

acme签发证书

如果你已经运行过一次acme且成功签发证书,在证书签发的文件夹可以找到*.example.com.conf这个配置

example.com.conf配置

这里主要是看Le_reloadCmdLe_realKeyPath

  • Le_reloadCmd 是在执行完acme签发证书命令后会执行的命令, 这里是经过base64编码的, 格式如下
    __ACME_BASE64__START_base64(cmd_plain_string)__ACME_BASE64__END_
    也就是说要将命令经过一次base64编码
    例如我的需求是systemctl restart nginx
    将他编码为base64后就是
    c3lzdGVtY3RsIHJlc3RhcnQgeHJheSYmc3lzdGVtY3RsIHJlc3RhcnQgbmdpbng=
    一整串就是
    __ACME_BASE64__START_c3lzdGVtY3RsIHJlc3RhcnQgeHJheSYmc3lzdGVtY3RsIHJlc3RhcnQgbmdpbng=__ACME_BASE64__END_

  • Le_realKeyPath 是acme签发证书的文件位置

这里就把nginx的ssl证书位置填上对应的即可

比如我的证书配置是这样的

nginx.conf

那么 Le_realKeyPath就填/etc/nginx/cerkey.key

接下来crontab中一般会含有如下语句,这是acme用来自动刷新证书的

crontab自动刷新语句

这样就部署好了,可以尝试运行一下crontab里写的这串命令

"/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" --force

在七牛云的证书cdn域名

在nginx本地服务器上会使用到的域名

至此懒人部署证书逻辑大功告成