原文链接
前言
众所周周知,目前由于安全性的考量,各个平台供应商的SSL
免费证书有效期都变成了三个月。在之前一年的证书有效期的条件下,如果有三五七个域名,一年更新一次,似乎也费不了什么功夫,但是如果是三个月更新一次,这个工作量就没有那么容易让人接受了,所以我这里需要有一个自动化的SSL
证书更新方案
不想写服务的小伙伴也可以使用外部的解决方案,比如CertSvc,但是这种毕竟是别人的平台,在安全性方面肯定是没有自己写来的更好的,而且自定义的的逻辑也没有很多,这里我们以腾讯云为例,简单展示下证书自动运维方案
实现
概述
我们定时任务每天拉取证书列表,当检查到需要维护的证书马上过期时候,申请新的证书,然后安装到本地即可。这里可以附加上删除已吊销/过期证书以及对于快过期的证书的直接吊销的清理流程
接口
这里我们主要需要以下接口,小伙伴可以根据自己需求自行增减:
链接部分使用的是腾讯云的链接,其他服务器平台可以自行参考该平台接口文档,应该差别不大
申请证书
示例如下:
public List<TencentSslList.TencentSsl> getSslList() {
try {
//init
Credential cred = new Credential(secretId, secretKey);
SslClient client = new SslClient(cred, null);
//build
DescribeCertificatesRequest req = new DescribeCertificatesRequest();
req.setLimit(LIMIT_DATA_ROWS);
DescribeCertificatesResponse resp = client.DescribeCertificates(req);
//ret
TencentSslList list = JSONObject.parseObject(
DescribeCertificatesResponse.toJsonString(resp), TencentSslList.class);
return list.getCertificates();
} catch (TencentCloudSDKException exception) {
log.error("", exception);
}
return List.of();
}
public List<String> needApplyDomain(Date date, List<TencentSslList.TencentSsl> sslList) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, RENEW_SSL_DAYS);
List<String> list = sslList.stream()
.filter(ssl -> Objects.equals(TencentSslStatusEnum.AUDIT.getCode(), ssl.getStatus())
|| (calendar.getTime().before(ssl.getCertEndTime()) &&
Objects.equals(TencentSslStatusEnum.PASS.getCode(), ssl.getStatus()))
)
.map(TencentSslList.TencentSsl::getDomain)
.toList();
List<String> needApplyDomain = new ArrayList<>(domainList);
needApplyDomain.removeIf(list::contains);
return needApplyDomain;
}
public void applySsl(String domain) {
try {
Credential cred = new Credential(secretId, secretKey);
SslClient client = new SslClient(cred, null);
//build
ApplyCertificateRequest req = new ApplyCertificateRequest();
req.setDvAuthMethod(APPLY_SSL_METHOD);
req.setDomainName(domain);
ApplyCertificateResponse resp = client.ApplyCertificate(req);
if (null != resp && !ObjectUtils.isEmpty(resp.getCertificateId())) {
redisBiz.getStrInstance().opsForValue().set(
SSL_APPLYING_PREFIX + domain, resp.getCertificateId(),
SSL_APPLYING_WAIT_DAYS, TimeUnit.DAYS);
}
} catch (TencentCloudSDKException exception) {
log.error("", exception);
}
}
@Scheduled(cron = "0 1 0 * * ?")
public void doSomething() {
List<TencentSslList.TencentSsl> sslList = tencentSslBiz.getSslList();
List<String> toApplyList = tencentSslBiz.needApplyDomain(now, sslList);
for (String domain : toApplyList) {
tencentSslBiz.applySsl(domain);
}
}
其中domainList
为希望维护的域名列表,可以使用热配置提供实时修改。在申请证书后将域名和证书唯一键记录到缓存当中
证书安装
使用容器的小伙伴需要注意在容器中进行映射,将安装目录映射到对应宿主机目录:
private static void saveBytesToFile(byte[] bytes, String filePath) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filePath)) {
fos.write(bytes);
}
}
private static void unzip(String zipFilePath, String destDir) throws IOException {
File dir = new File(destDir);
if (!dir.exists()) {
boolean ignore = dir.mkdirs();
}
try (ZipFile ignored = new ZipFile(zipFilePath);
ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(Paths.get(zipFilePath)))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
String filePath = destDir + File.separator + entry.getName();
if (!entry.isDirectory()) {
extractFile(zipIn, filePath);
} else {
File dirPath = new File(filePath);
boolean ignore = dirPath.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
}
}
private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {
byte[] bytesIn = new byte[4096];
int read;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
}
}
private static boolean deleteFile(String filePath) {
File file = new File(filePath);
if (file.exists()) {
return file.delete();
}
return true;
}
public void updateSslList(String certificateId, String domainKey) {
try {
Credential cred = new Credential(secretId, secretKey);
SslClient client = new SslClient(cred, null);
//build
DownloadCertificateRequest req = new DownloadCertificateRequest();
req.setCertificateId(certificateId);
DownloadCertificateResponse resp = client.DownloadCertificate(req);
//ret
if (!ObjectUtils.isEmpty(resp.getContent())) {
String base64Data = resp.getContent();
byte[] decodedBytes = Base64.getDecoder().decode(base64Data);
boolean ignore = deleteFile(sslPackageFullName);
saveBytesToFile(decodedBytes, sslPackageFullName);
unzip(sslPackageFullName, sslPath);
redisBiz.getStrInstance().delete(domainKey);
}
} catch (IOException | TencentCloudSDKException exception) {
log.error("", exception);
}
}
public void installSsl() {
Set<String> domains = redisBiz.getStrInstance().keys(SSL_APPLYING_PREFIX + "*");
if (null == domains) return;
for (String domainKey : domains) {
updateSslList(redisBiz.getStrInstance().opsForValue().get(domainKey), domainKey);
}
}
从缓存中读出证书唯一键,查询已经下发完成,当已经下发完成,安装证书并且删除缓存
证书吊销(可选)
当证书即将过期时,直接吊销证书,清理证书数据。注意,这里的吊销时间需要小于申请的时间,最好有个三五天的安全值,比如设置申请时间为到期14天内触发,那么吊销时间最好为到期7天内触发,否则可能会有一段时间的无有效证书:
public List<String> needRevokeCertificateId(Date date, List<TencentSslList.TencentSsl> sslList) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, REVOKE_SSL_DAYS);
return sslList.stream()
.filter(ssl -> null != ssl.getCertEndTime() &&
calendar.getTime().after(ssl.getCertEndTime()) &&
Objects.equals(ssl.getStatus(), TencentSslStatusEnum.PASS.getCode()))
.map(TencentSslList.TencentSsl::getCertificateId)
.toList();
}
public void revokeSsl(String certificateId) {
try {
Credential cred = new Credential(secretId, secretKey);
SslClient client = new SslClient(cred, null);
//build
RevokeCertificateRequest req = new RevokeCertificateRequest();
req.setCertificateId(certificateId);
RevokeCertificateResponse ignore = client.RevokeCertificate(req);
} catch (TencentCloudSDKException exception) {
log.error("", exception);
}
}
@Scheduled(cron = "0 1 0 * * ?")
public void doSomething() {
List<TencentSslList.TencentSsl> sslList = tencentSslBiz.getSslList();
List<String> toRevokeList = tencentSslBiz.needRevokeCertificateId(now, sslList);
for (String certificateId : toRevokeList) {
tencentSslBiz.revokeSsl(certificateId);
}
}
删除证书(可选)
删除过期以及吊销证书:
public List<String> needDeleteCertificatedId(List<TencentSslList.TencentSsl> sslList) {
return sslList.stream()
.filter(ssl -> List.of(TencentSslStatusEnum.REVOKED.getCode(),
TencentSslStatusEnum.EXPIRED.getCode()).contains(ssl.getStatus()))
.map(TencentSslList.TencentSsl::getCertificateId)
.toList();
}
public void deleteSSl(String certificateId) {
try {
Credential cred = new Credential(secretId, secretKey);
SslClient client = new SslClient(cred, null);
//build
DeleteCertificateRequest req = new DeleteCertificateRequest();
req.setCertificateId(certificateId);
DeleteCertificateResponse ignore = client.DeleteCertificate(req);
} catch (TencentCloudSDKException exception) {
log.error("", exception);
}
}
@Scheduled(cron = "0 1 0 * * ?")
public void doSomething() {
List<TencentSslList.TencentSsl> sslList = tencentSslBiz.getSslList();
List<String> toDeleteList = tencentSslBiz.needDeleteCertificatedId(sslList);
for (String certificateId : toDeleteList) {
tencentSslBiz.deleteSSl(certificateId);
}
}
反向代理
当安装完成后需要重启反向代理,重新加载证书文件,可以使用定时任务cron
完成