「免费」玩转HTTPS、SSL证书,一键申请、更新续期、部署,webhook自动部署

779 阅读4分钟

免费ssl证书哪里获取?

雾中街:33种免费获取SSL证书的方式

我选择了 OHTTPS。

OHTTPS - 免费HTTPS证书、自动更新、自动部署

怎么玩?

一、域名申请

SSL 证书一般需要有域名。(无域名,仅公网IP,没有尝试过,未知可行否)

建议在大平台上申请,OHTTPS 这类平台对其支持度会好点。

笔者在阿里云申请了域名:life666.top 。其子域名是一款程序员代码生成转换的在线工具 tool.life666.top/ , 欢迎点击使用(^.^))

二、创建https证书

快速上手资料:OHTTPS - 免费HTTPS证书、自动更新、自动部署

按照上文教程,基本没啥问题。

大致步骤:

1、创建 DNS 授权

这一步是为了证明,你对证书要绑定的域名有控制权,即域名确实归你所有。两种方式:

1)手动添加一条它指定的文本域名解析;

2)添加DNS授权,这样OHTTPS便可以自动完成你本需要手动完成的操作。

2、创建 HTTPS 证书

​

编辑切换为居中

添加图片注释,不超过 140 字(可选)

按照指引一步步操作即可。

3 部署

OHTTPS 平台提供了一些云平台的自动部署:「部署节点」菜单,可实现一键部署。

但是,如果你和笔者一样,比较寒酸,需要部署到刚氪金购买的小小的云主机或VPS上,就需要手动部署了。

下载证书文件到本地,下面两个文件:

cert.key 私钥
fullchain.cer 证书

上传到你的服务器上。

如何配置安装证书?这个依据你的服务而定,如果是 Nginx 可参考 如何在Nginx或Tengine服务器上安装证书_数字证书管理服务-阿里云帮助中心

如何0成本续期?

免费证书,一般初次申请,这份辛苦基本可以接受。

但是免费的午餐总是有期限,这个 OHTTPS 90天有效期,如何0成本续期?

证书管理》点击证书》更新证书,可更新证书。

但是,如果你和我一样需要手动部署,3个月就要手动上传一次,会略微有点小小的不爽。

OHTTPS 提供了 webhook 自动部署能力,就是,你可以提供一个 http 回调地址,当你点击「部署证书」时,调用你的接口,将证书传给你(json)。

为此,笔者在我的vps上常驻了一个小小的web服务,提供回调接口,接收证书请求参数,自动写到 Nginx 的证书目录下,实现自动一键更新和部署的目的。

​

编辑切换为居中

添加图片注释,不超过 140 字(可选)

自动部署 webhook 服务源码

采用 golang gin web 框架编写。

1、项目结构

​

编辑

添加图片注释,不超过 140 字(可选)

2、main.go

package main

import (
	_ "github.com/CodyGuo/godaemon"
	"github.com/gin-gonic/gin"
	"log"
	"os"
	"ssl-webhook/src"
)

func main() {
	file, err := os.OpenFile("log.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}

	defer file.Close()

	log.SetOutput(file)
	log.Println("==== ssl-webhook ====")

	r := gin.Default()
	r.GET(src.CONTEXT_PATH+"/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

	src.Initialize(r)

	r.Run(":10010")
}

3、handler.go

package src

import (
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"github.com/gin-gonic/gin"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

func Initialize(engine *gin.Engine) {
	engine.GET(CONTEXT_PATH+"/hello", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "world",
		})
	})

	ohttps := engine.Group(CONTEXT_PATH + "/ohttps")
	{
		ohttps.GET("/hello", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{
				"success": "hello ohttps",
			})
		})
		// [OHTTPS - 免费HTTPS证书、自动更新、自动部署](https://ohttps.com/docs/cloud/webhook/webhook)
		ohttps.POST("/deploy", func(c *gin.Context) {
			//request := make(map[string]interface{})
			var request OhttpsSslDeployRequest
			fmt.Println(c.Query("a")) // url 参数
			err := c.ShouldBind(&request)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{
					"success": false,
					"error":   err.Error(),
				})
				log.Panic("[ERROR] ", err)
				return
			}

			log.Println(request.Payload.CertificateName, request.Payload.CertificateDomains)
			// 验签
			md5 := md5.New()
			io.WriteString(md5, strconv.FormatInt(request.Timestamp, 10)+":"+CALLBACK_TOKEN)
			md5sum := hex.EncodeToString(md5.Sum(nil))
			if md5sum != request.Sign {
				c.JSON(http.StatusInternalServerError, gin.H{
					"success": false,
					"error":   "签名验证不通过",
				})
				log.Panic("[ERROR] 签名验证不通过, request.Sign: ", request.Sign, ", request.Timestamp: ", request.Timestamp, ", md5sum: ", md5sum)
				return
			}

			// 按关联的域名, 生成对应的目录 先带 .tmp 后缀,
			// 然后, 正在使用的目录更名带 日期后缀 备份, 新的去掉 tmp 后缀
			for _, domain := range request.Payload.CertificateDomains {
				domainCertPath := domain
				if strings.HasPrefix(domain, "*") {
					// 泛域名
					domainCertPath = domain[2:]
				}
				tmpCertPath := NGINX_CERT_BASE_PATH + "/" + domainCertPath + ".tmp"
				os.MkdirAll(tmpCertPath, os.ModePerm)

				certKeyFile, _ := os.OpenFile(tmpCertPath+"/cert.key", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
				defer certKeyFile.Close()
				certKeyFile.WriteString(request.Payload.CertificateCertKey)

				fullchainFile, _ := os.OpenFile(tmpCertPath+"/fullchain.cer", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
				defer fullchainFile.Close()
				fullchainFile.WriteString(request.Payload.CertificateFullchainCerts)

				// 原目录备份
				os.Rename(NGINX_CERT_BASE_PATH+"/"+domainCertPath, NGINX_CERT_BASE_PATH+"/"+domainCertPath+"."+time.Now().Format("20060102150405"))
				// 启用新的
				os.Rename(tmpCertPath, tmpCertPath[:len(tmpCertPath)-4])
				log.Printf("部署 %s 成功, 路径: %s\n", domain, tmpCertPath[:len(tmpCertPath)-4])
			}

			c.JSON(http.StatusOK, gin.H{
				"success": true,
			})

		})
	}

}

type OhttpsSslDeployRequest struct {
	Timestamp int64 `form:"timestamp"` // 请求时间戳
	Payload   struct {
		CertificateName           string   `form:"certificateName"`           // 证书ID
		CertificateDomains        []string `form:"certificateDomains"`        // 证书关联域名
		CertificateCertKey        string   `form:"certificateCertKey"`        // 证书私钥(PEM格式)
		CertificateFullchainCerts string   `form:"certificateFullchainCerts"` // 证书(包含证书和中间证书)(PEM格式)
		CertificateExpireAt       int64    `form:"certificateExpireAt"`       // 证书过期时间
	} `form:"payload"`
	Sign string `form:"sign"` // 请求签名,`${timestamp}:${回调令牌}`的32位小写md5值
}

3、consts.go

package src

// CONTEXT_PATH 根路径
const CONTEXT_PATH = "/sslwebhook"
const CALLBACK_TOKEN = "回调的token,用于验证签名,OHTTPS 平台「部署节点」菜单下可以查到"

// NGINX_CERT_BASE_PATH nginx 证书根目录
const NGINX_CERT_BASE_PATH = "/etc/nginx/cert"

4、构建脚本

# windows 交叉编译 Linux 相关参数配置
export CGO_ENABLED=0
export GOOS=linux
export GOARCH=amd64


# 不加文件名, 生成和当前路径一致的名字
go build

5、README.md

# SSL 证书自动部署 webhook web服务

> 暂仅仅支持 OHTTPS 平台的自动部署。https://ohttps.com/

1. 构建
```shell
cd {项目根目录}
sh bin\build.sh 
```

2. 上传

生成 ssl-webhook 可执行文件。
上传至服务器。
添加执行权限
```shell
chmod 777 ssl-webhook
```

3. 启动

启动服务(后台)
```shell
./ssl-webhook -d=true
```

4. 部署证书

证书管理》点选要部署的证书》部署证书

结果:
会在 nginx 证书目录下("/etc/nginx/cert"),生成对应域名的目录(域名即使目录名)。
内含两个文件:
```
cert.key // 私钥
fullchain.cer // 证书
```

改域名下原有证书目录,重命名,添加当时时间后缀备份。

![image.png](https://s2.loli.net/2022/11/19/5kI1PARGSaHs32j.png)