SSL证书有效期变更为三个月的自动化运维解决方案

原文链接

欢迎大家对于本站的访问 - AsterCasc

前言

众所周周知,目前由于安全性的考量,各个平台供应商的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完成

原文链接

欢迎大家对于本站的访问 - AsterCasc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值