certd证书续期
你是否还在为3个月的免费证书而烦恼?现在Certd可以解决你的问题。
[Certd](#https://github.com/certd/certd)
是一个免费全自动申请和自动部署更新SSL证书的管理系统。 后缀d取自linux守护进程的命名风格,意为证书守护进程。
项目不仅支持证书申请过程自动化,还可以自动化部署更新证书,让你的证书永不过期。
- 全自动申请证书(支持所有注册商注册的域名)
- 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等,目前已支持60+部署插件)
- 支持DNS-01、HTTP-01、CNAME代理等多种域名验证方式
- 支持通配符域名/泛域名,支持多个域名打到一个证书上,支持pem、pfx、der、jks等多种证书格式
- 邮件通知、webhook通知
- 私有化部署,数据保存本地,授权信息加密存储,镜像由Github Actions构建,过程公开透明
# docker部署
此次和官方示例略有差异,修改了挂载路径
version: '3.3'
services:
certd:
#指定版本
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:1.31.2
# 最新版本
#image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
container_name: certd
restart: unless-stopped
volumes:
- ./data:/app/data
ports:
- "7001:7001"
- "7002:7002"
environment:
- certd_system_resetAdminPasswd=false
提示
- http 访问
http://你的IP:7001
- https访问
https://你的IP:7002
默认账号密码:`admin/123456
注意
首次登录需要强制修改密码
# 创建流水线
登录后查看使用教程
非常详细
提示
可将多个域名打在一个证书中:
提示
证书申请成功后执行主机命令,如nginx重启等操作
提示
一切结束后执行操作。发送通知什么的
# 数据备份
数据默认存在/data/certd
目录下,可配置自定义流水线
自动备份。
# 备份恢复
将备份的db.sqlite
及同目录下的其他文件一起覆盖到原来的位置,重启certd即可
# 补一个钉钉推送JS
查看代码
代码目录
const crypto = await import('crypto');
//钉钉授权
const auth = {
"accessToken": "xxx",
"secret": "xxx"
};
//根路径
const URL = "https://oapi.dingtalk.com/robot/send";
// 使用标准 URL 编码
function urlEncode(data) {
return encodeURIComponent(data);
}
// 生成签名
function sign(timestamp) {
const secret = auth.secret;
// 构造待签名字符串
const stringToSign = `${timestamp}\n${secret}`;
// 创建 HmacSHA256 哈希
const hmacHash = crypto.createHmac('sha256', secret)
.update(stringToSign)
.digest(); // 得到二进制数据
// 进行 Base64 编码
const base64Encoded = Buffer.from(hmacHash).toString('base64');
// URL Encode 得到最终的签名
return urlEncode(base64Encoded);
}
// 构造请求 URL
function getUrl(timestamp) {
return `${URL}?access_token=${auth.accessToken}×tamp=${timestamp}&sign=${sign(timestamp)}`;
}
// 获取当前时间戳(毫秒)
const timestamp = Math.round(Date.now());
// 生成请求 URL
const url = getUrl(timestamp);
ctx.logger.info("开始向钉钉机器人推送通知")
// 构造消息内容
const msg = `
### 证书更新通知
**你有一张证书即将过期**,<font color="#FF0000">[去处理](http://localhose:7001)</font></br></br>
`;
// 准备要发送的数据
const data = {
msgtype: "markdown",
markdown: {
title: "证书通知",
text: msg
}
};
//axios发起http请求上传证书
const res = await ctx.http.request({
url:url,
data:JSON.stringify(data),
method:"post",
headers: {
"Content-Type": "application/json"
}
})
if(!res || res.errcode !== 0){
ctx.logger.info("调用失败",res)
//抛异常才能让任务失败
throw new Error(res)
}
//不能用console.log,需要用ctx.logger 才能把日志打印在ui上
ctx.logger.info("推送钉钉机器人成功",res.data)
# 自动备份脚本
查看代码
dingding-bot/
├── dingdingbot.py
├── main.py
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
docker-compose.yaml
version: '3.8'
services:
task-bot:
image: python:3.11-slim # 使用官方 Python 镜像
container_name: task-bot
working_dir: /app # 设置工作目录
volumes:
- .:/app # 将当前目录挂载到容器中的 /app 目录
- /root/certd/data:/app/certd # 挂载certd存储路径
command: sh -c "pip install --no-cache-dir -r requirements.txt && python main.py"
environment:
- PYTHONUNBUFFERED=1 # 确保 Python 输出是无缓冲的,方便调试
main.py
import paramiko
import os
import shutil
from datetime import datetime
from schedule import every, repeat, run_pending
import time
from dingdingbot import send_message
files = [
{
"prefix":"ssl",
"path":'/app/certd/db.sqlite'
}
]
def open_ssh_client():
# 创建SSH客户端实例
ssh_client = paramiko.SSHClient()
# 自动添加远程主机的主机密钥到本地known_hosts文件中
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
remote_host = '123456'
pwd = 'xxx'
# 连接到远程服务器
ssh_client.connect(
hostname=remote_host, # 远程服务器的IP地址或域名
port=22, # SSH端口号,默认为22
username='root', # 远程服务器的用户名
password=pwd # 远程服务器的密码
)
print(f"初始化服务器:{remote_host} SSH 成功")
return ssh_client
def get_back_filename(prefix,index):
# 生成备份文件名(包含时间戳)
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
return f"backup_{prefix}_{timestamp}_{index}"
def del_back_file(local_backup_path):
# 删除本地压缩文件(可选)
os.remove(local_backup_path)
print(f"本地压缩文件 {local_backup_path} 已删除")
def check_local_file(local_file):
if not os.path.exists(local_file):
raise FileNotFoundError(f"本地文件 {local_file} 不存在")
def check_remote_path(ssh_client,remote_file_path):
# 确保远程备份目录存在
stdin, stdout, stderr = ssh_client.exec_command(f'mkdir -p {remote_file_path}')
# 检查命令执行是否成功
if stdout.channel.recv_exit_status() != 0:
error_message = stderr.read().decode()
raise Exception(f"创建远程目录失败: {error_message}")
def main():
ssh_client = open_ssh_client()
# 创建SFTP客户端
sftp_client = ssh_client.open_sftp()
n =0
for item in files:
prefix = item.get("prefix")
zip_name = get_back_filename(prefix,n)
# 次数加1
n+=1
# 压缩本地文件
local_file = item.get("path")
local_zip = f"{os.path.dirname(local_file)}/{zip_name}"
print(f"开始压缩:{local_file} 为=>{local_zip} 压缩路径=>{os.path.dirname(local_file)}")
shutil.make_archive(local_zip, 'zip', os.path.dirname(local_file),os.path.basename(local_file))
# 后续使用补上zip后缀
local_zip+=".zip"
# 检查本地文件
check_local_file(local_zip)
# 远程服务器上的目标路径
remote_file_path = f'/root/back/{prefix}'
# 检查远程路径
check_remote_path(ssh_client,remote_file_path)
remote_file=f"{remote_file_path}/{zip_name}.zip"
# ftp上传
sftp_client.put(local_zip, remote_file)
print(f"文件 {local_zip} 已成功上传到远程服务器 {remote_file} 路径下")
#删除压缩包
del_back_file(local_zip)
# 关闭SFTP客户端和SSH连接
sftp_client.close()
ssh_client.close()
print("执行结束,下发通知到钉钉机器人")
# 此次不能缩进
msg = f"""### 证书备份通知
**备份时间**:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
**证书更新系统备份成功**
"""
send_message(msg)
@repeat(every().day.at("00:00"))
# @repeat(every().minutes)
def job_one():
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]开始定时执行备份任务...")
main()
if __name__ == "__main__":
#立即执行一次
main()
#开始定时执行
print("开始定时执行 每天 00:00 运行备份")
while True:
run_pending()
time.sleep(1)
dingdingbot.py
import hmac
import hashlib
import base64
import urllib.parse
import time
import requests
import json
from datetime import datetime
# 假设 TALK 是包含 accessToken 和 secret 的对象
TALK = {
"accessToken": "xx",
"secret": "xx"
}
URL = "https://oapi.dingtalk.com/robot/send"
def url_encode(data):
# 使用 urllib.parse.quote 进行 URL 编码
return urllib.parse.quote(data)
def sign(timestamp):
secret = TALK["secret"]
# 构造待签名字符串
string_to_sign = f"{timestamp}\n{secret}"
# 创建 HmacSHA256 哈希
hmac_hash = hmac.new(secret.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha256)
digest = hmac_hash.digest() # 得到二进制数据
# 进行 Base64 编码
base64_encoded = base64.b64encode(digest)
# URL Encode 得到最终的签名
return url_encode(base64_encoded.decode('utf-8'))
def get_url(timestamp):
return f"{URL}?access_token={TALK['accessToken']}×tamp={timestamp}&sign={sign(timestamp)}"
def send_message(msg):
# 示例调用
timestamp = int(round(time.time() * 1000))
# print(timestamp)
url = get_url(timestamp)
# print(url)
data = {
"msgtype":"markdown",
"markdown":{
"title":"备份通知",
"text":msg,
}
}
resp = requests.post(url=url,data=json.dumps(data),headers={
"Content-Type": "application/json"
})
if not resp.ok:
print("fail")
print(resp.text)
print("success")
requirements.txt
requests
paramiko
schedule
也可以使用Dockerfile
# 使用官方 Python 镜像作为基础镜像
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 将当前目录下的文件复制到容器中的 /app 目录
COPY . /app
# 安装依赖模块
RUN pip install --no-cache-dir -r requirements.txt
# 设置默认命令
CMD ["python", "main.py"]
更多用法后续补充..
上次更新: 2025/03/27, 13:05:53