阅读视图

发现新文章,点击刷新页面。

南墙 WAF 系列(二)– 网站证书自动更新

相对管理后台的 ssl来说,其实网站的 ssl 证书才是正事,毕竟这个关系到网站的访问。按照官方的说法在开放 80 端口的情况下,南墙可以自动申请更新证书,不过后台没找到配置的地方,我的 v4 的 80 也是不通的,所以就需要自己去维护管理证书了。

然而,上午在问了管理之后,得到的答复是没有 api,可以自己抓包进行修改。

嗐,这么看来其实也没啥,最起码说明后台的 api 接口是可以直接拿来用的。即使是有 api 文档,也是得自己去看,去写,没有的话 curl 抓包一样能解决问题。按照之前的方法,只直接复制 curl 给 cursor 就可以了。

api 文件baby_nanqiang_api_tools.py内容:

#!/usr/bin/env python3
import requests
import json
import jwt
from datetime import datetime
import os
import urllib3

# 禁用 SSL 验证警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class NanQiangAPI:
    def __init__(self, base_url="https://lang.bi:443"):
        self.base_url = base_url
        self.session = requests.Session()
        self.session.verify = False  # 忽略SSL证书验证
        self.token = None
        self._setup_headers()

    def _setup_headers(self):
        """设置请求头"""
        self.headers = {
            'accept': 'application/json, text/plain, */*',
            'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
            'cache-control': 'no-cache',
            'content-type': 'application/json',
            'origin': self.base_url,
            'pragma': 'no-cache',
            'priority': 'u=1, i',
            'referer': f'{self.base_url}/',
            'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"macOS"',
            'sec-fetch-dest': 'empty',
            'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-origin',
            'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
        }

    def _update_headers_with_token(self):
        """更新请求头,添加token"""
        if self.token:
            self.headers['Authorization'] = self.token  # 直接使用token,不添加'Bearer '前缀

    def delete_cert(self, cert_id):
        """
        删除指定ID的证书
        :param cert_id: 证书ID
        :return: 删除结果
        """
        if not self.is_logged_in():
            print("请先登录")
            return None

        url = f"{self.base_url}/api/v1/certs/{cert_id}"

        try:
            response = self.session.delete(
                url,
                headers=self.headers
            )
            
            response_data = response.json()
            
            if 'err' in response_data:
                print(f"删除证书失败: {response_data['err']}")
                return None
            
            # 检查删除是否成功
            if response_data.get('result') == 'success' and response_data.get('RowsAffected') > 0:
                print(f"证书 {cert_id} 删除成功")
                return True
            else:
                print(f"证书 {cert_id} 删除失败: 未找到证书或删除操作未生效")
                return False
            
        except requests.exceptions.RequestException as e:
            print(f"删除证书请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def parse_cert_list(self, cert_list):
        """
        解析证书列表数据
        :param cert_list: 证书列表数据
        :return: 解析后的证书信息列表
        """
        if not cert_list:
            return None

        parsed_certs = []
        for cert in cert_list:
            try:
                # 解析SNI字段(JSON字符串)
                sni_list = json.loads(cert.get('sni', '[]'))
                
                parsed_cert = {
                    'id': cert.get('id'),
                    'sni': sni_list,
                    'expire_time': cert.get('expire_time'),
                    'update_time': cert.get('update_time')
                }
                parsed_certs.append(parsed_cert)
            except json.JSONDecodeError as e:
                print(f"解析SNI字段失败: {str(e)}")
                continue
            except Exception as e:
                print(f"解析证书数据失败: {str(e)}")
                continue

        return parsed_certs

    def get_cert_list(self):
        """
        获取证书列表
        :return: 证书列表
        """
        if not self.is_logged_in():
            print("请先登录")
            return None

        url = f"{self.base_url}/api/v1/certs/"

        try:
            response = self.session.get(
                url,
                headers=self.headers
            )
            
            response_data = response.json()
            
            if 'err' in response_data:
                print(f"获取证书列表失败: {response_data['err']}")
                return None
                
            return response_data
            
        except requests.exceptions.RequestException as e:
            print(f"获取证书列表请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def login(self, username, password, otp=""):
        """
        登录接口
        :param username: 用户名
        :param password: 密码
        :param otp: 双因素认证码(可选)
        :return: 登录响应
        """
        url = f"{self.base_url}/api/v1/users/login"
        data = {
            "usr": username,
            "pwd": password,
            "otp": otp
        }

        try:
            response = self.session.post(
                url,
                headers=self.headers,
                json=data
            )
            
            # 获取响应数据
            response_data = response.json()
            
            # 检查是否有错误信息
            if 'err' in response_data:
                print(f"登录失败: {response_data['err']}")
                return None
            
            # 保存token
            if 'token' in response_data:
                self.token = response_data['token']
                self._update_headers_with_token()
                
                # # 解析token信息
                # try:
                #     # 使用 jwt.decode 替代 jwt.decode_complete
                #     token_data = jwt.decode(self.token, options={"verify_signature": False})
                #     exp_timestamp = token_data.get('exp')
                #     if exp_timestamp:
                #         exp_date = datetime.fromtimestamp(exp_timestamp)
                #         print(f"Token 有效期至: {exp_date}")
                # except Exception as e:
                #     print(f"无法解析token信息: {str(e)}")
                
            return response_data
            
        except requests.exceptions.RequestException as e:
            print(f"登录请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def check_cert(self, cert_content, key_content, mode=0):
        """
        检查证书
        :param cert_content: 证书内容
        :param key_content: 私钥内容
        :param mode: 模式,默认为0
        :return: 检查结果
        """
        if not self.is_logged_in():
            print("请先登录")
            return None

        url = f"{self.base_url}/api/v1/certs/check"
        
        # 准备multipart/form-data数据
        files = {
            'mode': (None, str(mode)),
            'cert': (None, cert_content),
            'key': (None, key_content)
        }

        try:
            # 临时移除content-type,让requests自动设置
            headers = self.headers.copy()
            headers.pop('content-type', None)
            
            response = self.session.post(
                url,
                headers=headers,
                files=files
            )
            
            response_data = response.json()
            
            if 'err' in response_data:
                print(f"证书检查失败: {response_data['err']}")
                return None
                
            return response_data
            
        except requests.exceptions.RequestException as e:
            print(f"证书检查请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def check_cert_from_files(self, cert_file_path, key_file_path, mode=0):
        """
        从文件检查证书
        :param cert_file_path: 证书文件路径
        :param key_file_path: 私钥文件路径
        :param mode: 模式,默认为0
        :return: 检查结果
        """
        try:
            with open(cert_file_path, 'r') as f:
                cert_content = f.read()
            with open(key_file_path, 'r') as f:
                key_content = f.read()
                
            return self.check_cert(cert_content, key_content, mode)
            
        except FileNotFoundError as e:
            print(f"文件不存在: {str(e)}")
            return None
        except Exception as e:
            print(f"读取文件失败: {str(e)}")
            return None

    def submit_cert_config(self, check_result):
        """
        提交证书配置
        :param check_result: 证书检查的结果数据
        :return: 提交结果
        """
        if not self.is_logged_in():
            print("请先登录")
            return None

        if not check_result:
            print("无效的证书检查结果")
            return None

        url = f"{self.base_url}/api/v1/certs/config"
        
        # 准备提交数据
        data = {
            "id": check_result.get("id", 0),
            "sni": check_result.get("sni", "[]"),
            "cert": check_result.get("cert", ""),
            "key": check_result.get("key", ""),
            "expire_time": check_result.get("expire_time", ""),
            "update_time": check_result.get("update_time", "")
        }

        try:
            response = self.session.post(
                url,
                headers=self.headers,
                json=data
            )
            
            response_data = response.json()
            
            if 'err' in response_data:
                print(f"证书配置提交失败: {response_data['err']}")
                return None
                
            return response_data
            
        except requests.exceptions.RequestException as e:
            print(f"证书配置提交请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def is_logged_in(self):
        """
        检查是否已登录
        :return: bool
        """
        return self.token is not None

def main():
    # 使用示例
    api = NanQiangAPI()
    
    # 登录信息
    username = "obaby"
    password = "obaby@mars"
    
    # 执行登录
    result = api.login(username, password)
    
    if result:
        print("登录成功:")
        print(json.dumps(result, indent=2, ensure_ascii=False))
        print(f"Token: {api.token}")
        
        # 获取证书列表
        cert_list = api.get_cert_list()
        if cert_list:
            # 解析证书列表
            parsed_certs = api.parse_cert_list(cert_list)
            if parsed_certs:
                print("解析后的证书列表:")
                print(json.dumps(parsed_certs, indent=2, ensure_ascii=False))
                
                # # 删除证书示例
                # cert_id = 4  # 要删除的证书ID
                # delete_result = api.delete_cert(cert_id)
                # if delete_result:
                #     print(f"证书 {cert_id} 删除成功")
                # else:
                #     print(f"证书 {cert_id} 删除失败")
        
        # 证书检查示例
        cert_file = "path/to/cert.pem"
        key_file = "path/to/key.pem"
        
        if os.path.exists(cert_file) and os.path.exists(key_file):
            return
            # 先检查证书
            cert_result = api.check_cert_from_files(cert_file, key_file)
            if cert_result:
                print("证书检查结果:")
                print(json.dumps(cert_result, indent=2, ensure_ascii=False))
                
                # 提交证书配置
                submit_result = api.submit_cert_config(cert_result)
                if submit_result:
                    print("证书配置提交成功:")
                    print(json.dumps(submit_result, indent=2, ensure_ascii=False))
                else:
                    print("证书配置提交失败")
    else:
        print("登录失败")

if __name__ == "__main__":
    main()

账号不要设置动态密码,如果设置了,那就创建一个新账号。

获取证书的脚本参考上一篇文章,对应的路径自己调整。更新证书的代码site_cert_auto_update_tool.py:

#!/usr/bin/env python3
import os
import subprocess
import hashlib
import json
from datetime import datetime
import logging
from baby_nanqiang_api_tools import NanQiangAPI

# Configuration
CERT_SOURCE_DIR = "/root/.acme.sh/h4ck.org.cn_ecc"
CERT_FILE = "fullchain.cer"
KEY_FILE = "h4ck.org.cn.key"
HASH_FILE = "web_cert_hash.json"
CERT_SCRIPT = "get_web_cert.sh"

def setup_logging():
    """设置日志"""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler('web_cert_update.log'),
            logging.StreamHandler()
        ]
    )

def get_file_hash(file_path):
    """计算文件的SHA-256哈希值"""
    sha256_hash = hashlib.sha256()
    with open(file_path, "rb") as f:
        for byte_block in iter(lambda: f.read(4096), b""):
            sha256_hash.update(byte_block)
    return sha256_hash.hexdigest()

def save_cert_hash(cert_hash, key_hash):
    """保存证书和私钥的哈希值到JSON文件"""
    with open(HASH_FILE, 'w') as f:
        json.dump({
            'cert_hash': cert_hash,
            'key_hash': key_hash
        }, f)

def load_cert_hash():
    """从JSON文件加载证书和私钥的哈希值"""
    try:
        with open(HASH_FILE, 'r') as f:
            data = json.load(f)
            return data.get('cert_hash'), data.get('key_hash')
    except (FileNotFoundError, json.JSONDecodeError):
        return None, None

def run_get_cert_script(script_path=None):
    """
    执行获取证书的脚本
    :param script_path: 脚本路径,如果为None则使用默认的get_web_cert.sh
    :return: bool 是否执行成功
    """
    try:
        # 如果没有指定脚本路径,使用默认的get_web_cert.sh
        if script_path is None:
            script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), CERT_SCRIPT)
        
        # 检查脚本是否存在
        if not os.path.exists(script_path):
            logging.error(f"错误: 脚本文件 {script_path} 不存在")
            return False
            
        # 检查脚本是否可执行
        if not os.access(script_path, os.X_OK):
            logging.error(f"错误: 脚本文件 {script_path} 没有执行权限")
            return False
            
        # 执行脚本
        result = subprocess.run(['sh', script_path], 
                              capture_output=True, 
                              text=True)
        
        # 检查执行结果
        if result.returncode == 0:
            logging.info("证书获取脚本执行成功")
            if result.stdout:
                logging.info("脚本输出:\n%s", result.stdout)
            return True
        else:
            logging.error(f"证书获取脚本执行异常,返回码: {result.returncode}")
            if result.stderr:
                logging.error("异常输出:\n%s", result.stderr)
            return True
            
    except Exception as e:
        logging.error(f"执行证书获取脚本时发生错误: {str(e)}")
        return False

def read_file_content(file_path):
    """读取文件内容"""
    try:
        with open(file_path, 'r') as f:
            return f.read()
    except Exception as e:
        logging.error(f"读取文件 {file_path} 失败: {str(e)}")
        return None

def is_cert_expired(expire_time_str):
    """
    检查证书是否过期或即将过期(7天内)
    :param expire_time_str: 过期时间字符串
    :return: bool 是否过期或即将过期
    """
    try:
        expire_time = datetime.strptime(expire_time_str, "%Y-%m-%d %H:%M:%S")
        now = datetime.now()
        days_until_expire = (expire_time - now).days
        return days_until_expire <= 7
    except Exception as e:
        logging.error(f"解析过期时间失败: {str(e)}")
        return False

def process_same_sni_certs(api, parsed_certs, current_sni, current_cert_id):
    """
    处理具有相同SNI的证书
    :param api: API实例
    :param parsed_certs: 解析后的证书列表
    :param current_sni: 当前证书的SNI
    :param current_cert_id: 当前证书的ID
    :return: None
    """
    # 筛选出相同SNI的证书
    same_sni_certs = [cert for cert in parsed_certs 
                     if cert['sni'] == current_sni and cert['id'] != current_cert_id]
    
    if not same_sni_certs:
        return
        
    # 按过期时间排序(从早到晚)
    same_sni_certs.sort(key=lambda x: datetime.strptime(x['expire_time'], "%Y-%m-%d %H:%M:%S"))
    
    # 检查是否有过期或即将过期的证书
    for cert in same_sni_certs:
        if is_cert_expired(cert['expire_time']):
            logging.info(f"删除过期证书 ID: {cert['id']}")
            if not api.delete_cert(cert['id']):
                logging.error(f"删除证书 {cert['id']} 失败")
    
    # 检查是否有过期时间相同的证书
    if len(same_sni_certs) > 1:
        # 获取第一个证书的过期时间作为基准
        base_expire_time = same_sni_certs[0]['expire_time']
        
        # 删除过期时间相同的证书(保留第一个)
        for cert in same_sni_certs[1:]:
            if cert['expire_time'] == base_expire_time:
                logging.info(f"删除重复过期时间的证书 ID: {cert['id']}")
                if not api.delete_cert(cert['id']):
                    logging.error(f"删除证书 {cert['id']} 失败")

def main():
    # 设置日志
    setup_logging()
    
    try:
        # 执行证书获取脚本
        if not run_get_cert_script():
            logging.error("获取证书失败,退出程序")
            return
            
        # 检查证书文件是否存在
        cert_path = os.path.join(CERT_SOURCE_DIR, CERT_FILE)
        key_path = os.path.join(CERT_SOURCE_DIR, KEY_FILE)
        
        if not (os.path.exists(cert_path) and os.path.exists(key_path)):
            logging.error("证书文件不存在,退出程序")
            return
            
        # 计算新文件的哈希值
        new_cert_hash = get_file_hash(cert_path)
        new_key_hash = get_file_hash(key_path)
        
        # 获取旧的哈希值
        old_cert_hash, old_key_hash = load_cert_hash()
        
        # 检查文件是否发生变化
        if new_cert_hash != old_cert_hash or new_key_hash != old_key_hash:
            logging.info("证书文件已发生变化,开始更新流程")
            
            # 读取证书和私钥内容
            cert_content = read_file_content(cert_path)
            key_content = read_file_content(key_path)
            
            if not cert_content or not key_content:
                logging.error("读取证书文件失败")
                return
                
            # 初始化API
            api = NanQiangAPI()
            
            # 登录
            if not api.login("obaby", "obaby@mars"):
                logging.error("登录失败")
                return
                
            # 检查证书
            check_result = api.check_cert(cert_content, key_content)
            if not check_result:
                logging.error("证书检查失败")
                return
                
            # 提交证书配置
            if not api.submit_cert_config(check_result):
                logging.error("提交证书配置失败")
                return
                
            # 获取证书列表
            cert_list = api.get_cert_list()
            if not cert_list:
                logging.error("获取证书列表失败")
                return
                
            # 解析证书列表
            parsed_certs = api.parse_cert_list(cert_list)
            if not parsed_certs:
                logging.error("解析证书列表失败")
                return
                
            # 获取当前证书的SNI
            current_sni = check_result.get('sni', '[]')
            try:
                current_sni = json.loads(current_sni)
            except json.JSONDecodeError:
                logging.error("解析当前证书SNI失败")
                return
                
            # 处理相同SNI的证书
            process_same_sni_certs(api, parsed_certs, current_sni, check_result.get('id'))
            
            # 保存新的哈希值
            save_cert_hash(new_cert_hash, new_key_hash)
            logging.info("证书更新完成")
        else:
            logging.info("证书文件未发生变化,无需更新")
            
    except Exception as e:
        logging.error(f"程序执行出错: {str(e)}", exc_info=True)

if __name__ == "__main__":
    main()

添加定时任务,每天,或者每几天:

0 2 * * * /usr/bin/python3 /home/soft/baby-nanqiang-cert-tools/site_cert_auto_update_tool.py >> /home/soft/baby-nanqiang-cert-tools/web_cert_manager.log 2>&1

最终效果:

The post 南墙 WAF 系列(二)– 网站证书自动更新 appeared first on obaby@mars.

差点…废了

................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

这次只有一个问题——OpenSSH < 9.8 RCE.........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

还好今天这个问题解决很简单,只需要升级openssh到最新版本就可以了。

奈何我强迫症啊,升级控啊,手贱啊!差点把服务器干废了,ssh都连不上了,导致今晚网站服务挂了三个多小时。openssh和openssl是相关联的,看到openssl都到了3.3.0版本了,我就忍不住想升级。不出意外的意外出现了,“/lib/x86_64-linux-gnu/libcrypto.so.3 not found”,于是乎我直接从openssl安装包里复制一个出来“cp libcrypto.so.3 /lib/x86_64-linux-gnu/libcrypto.so.3”,就从这开始问题一波接一波,接着我又试图卸载“libssl-dev,libssl3”,不卸载还好,卸完更糟糕了,系统都出问题了。apt安装出现“dpkg returned an error code”的问题。此时脑瓜子嗡嗡的,都有重装系统的想法了,想想重装系统加配置环境可能一晚上不睡觉都解决不完,还好没有放弃,最后还是找到解决方法(详细可以见此贴)。

20240828231520 scaled.jpg
这么多屏幕看哪个好?

皇天不负有心人啊!然后再反操作一番,把刚才卸载的东西又给装回去了,嘿嘿,又好了!不折腾了,不敢折腾了,openssl暂时不升级了。即使折腾也得先在虚拟机里试一试,没问题才能在生产环境里应用。后面我想尽快搞定用系统apt的方式搭建环境,apt速度很快,就是配置麻烦了一些,这方面可以参考lnmp和宝塔。然后把我的笔记本用起来,作为备用环境,如果小主机环境干废了,可以迅速转移到笔记本上。

That's All!不知不觉又熬夜了,嗯,赶紧睡了。

版权声明: 本文采用 BY-NC-SA 协议进行授权,如无注明均为原创,转载请注明转自 皇家元林
本文链接: 差点…废了

折腾瑞芯微RV1126嵌入式开发板

2020年前后,当华为海思被美国制裁时,国内安防芯片缺口很大(安防领域,当时海思IPC SoC大约占有70%市场,DVR/NVR SoC大约占有90%市场)。之后群雄逐鹿,瑞芯微也适时推出了两款面向IPC的SoC芯片,RV1126(4K800万IPC)及RV1109(500万IPC)。RV1126采用四核32位ARM Cortex A7架构,有2T算力的NPU,适合用来做视频编解码,跑与视频相关的算法模型。

手边有一块闲置很久的基于RV1126的嵌入式开发板,1G内存,自带8G eMMC硬盘。闲暇之余,想用来安装宝塔面板,搭建nginx等环境,然后跑web应用。厂家提供的固件是基于 buildroot的,如此,需要从源代码开始,编译和配置自己需要各种软件和库。对此我一窍不通,难度太大,只能作罢。

RV1126开发板

最近发现厂家更新了固件,提供了基于Ubuntu的底层固件,之前的想法又冒出来了,试了试,居然成功了,这里做些回顾记录。

刷机

刷机方法与刷安卓手机类似。电脑首先安装usb驱动,瑞芯微有提供驱动安装助手–DriverAssitant_v5.0(下载)。驱动安装好后,开发板通过USB与电脑连接,识别到设备。

瑞芯微的刷机工具–RKDevTool(下载),刷机过程中可能出现的问题与安卓刷机基本一样,包括不限于USB线,设备连接,识别,驱动等方面。

RV1126开发板厂商提供的基于Ubuntu的固件–RV1126-Ubuntu-20.04-firmware_20240227(下载)。

环境搭建配置

刷完机,插上网线,因为设备默认DHCP,搜索查询到设备的IP,然后SSH登录。

SSH 登录

1panel面板

因为时常操作云服务器,此时第一想法是安装宝塔面板。却被提示在线安装的宝塔,不支持这个arm 32位设备。让去试试宝塔5.9。简单搜索了下,不知道去哪弄宝塔5.9的安装包,官方的离线安装服务里可能有旧版的,但要收费。(写此博文时,又在网络搜索一番,发现有好心人搜集整理了旧版本的宝塔,此处

转念想到何不试试其他的面板程序,比如1panel。1panel面板官网写着支持armv7l服务器架构。试了下,果然安装成功。有宝塔的经验在先,1panel面板的安装,使用上手很快。终端SSH及文件管理,很直观,与宝塔的使用基本一致。

frp

为了方便SSH远程登录,及面板的远程管理以及后续web站点能外网访问,首先用上frp。用一台阿里云香港服务器(2C2G30M)做frp的服务端,配置好。虽然第一次使用frp,因工作原因,对p2p,nat穿透,端口映射,DDNS知识了解很多,所以对frp理解,上手,使用起来很容易。

frp的设计理念可能是要保持服务端配置的精简,统一(在客户端做各种区分),比如保持服务端唯一的对外http端口,https端口,ssh端口等。如果有多个对外服务(比如web应用),只需要在frp客户端做配置即可,通过绑定不同域名来区分不同web应用(而非常规的采用不同端口区分不同web应用),如此思路很清晰,就是有点费域名。:-)

持续将近半个月的frp使用下来,很稳定,速度也很好。当然,这可靠性多半要归我这台2C2G30M的阿里云香港服务器。看网上的讨论,这个系列的阿里云香港服务器很抢手,性价比很高。我比较看重的,回国内延时非常低,在广州ping值延时只有8-9ms,比广州服务器ping值还低。(对于我用来科学上网,非常完美)。

说个题外话,我曾持续(从一个月到一年时间不等)测试各种云的香港服务器(比如狗云,马云,鸡云,草云。当然还有大厂的,腾讯云,华为云,天翼云等),回大陆线路,表现最好最稳定,性价比最高的还是阿里云。

应用

解决了服务器远程管理,外网访问问题。剩下的就是搭建网站了。使用宝塔时,习惯一键安装NMP等环境。1panel也有类似功能,不过其提供的默认web环境是OpenResty,安装时却出了点状况,始终安装失败。

1panel的理念是一切皆docker。安装的所有应用均是基于docker的。安装OpenResty失败,起初以为是docker镜像源,网络问题。不过,这个很快排除了。

前段时间网络上讨论很多,docker hub及国内的众多加速服务在国内完全无法用,当然解决办法也很多。我比较喜欢的办法,对于个人用户,可以将常用的镜像通过GitHub action同步到阿里云容器镜像服务 ACR,然后选择公开或者私有,需要时从ACR拉取容器镜像。(见 docker_image_pusher

这种方法,我使用多次,国内服务器上拉取容器镜像,速度很快,也稳定可靠。当然,更常规的做法是使用docker hub镜像加速,比如1panel提供的临时加速地址:https://docker.1panel.live 。目前大环境下,国内的docker hub镜像站几乎都关了。我上面提到的docker image pusher方法对于个人用户是非常实用的。

排除网络问题,查看日志,发现安装OpenResty失败的原因是因为1panel商店里上架的这个版本不支持我使用的armv7l处理器。此时恍然大悟,然后又有点忧心。相较于x86,armv7l算比较小众,很多docker应用可能没有适配。如果通过1panel自带的商店安装,估计很多安装不了。如此,对于这个开发版使用1panel面板作用不大了。当然1G内存,外加4核心处理器,本身有点鸡肋和尴尬。也只能用来跑些简单的web应用。

找到问题以后,手动安装了nginx/1.18.0,PHP 7.4.3-4ubuntu2.23 (cli),这样能来跑静态及PHP站点了。

目前这个开发板上运行的web应用有:

之前在网友小宋的博客看见介绍的用来监控树莓派状态的应用Pi Dashboard,UI比较好看,起初试了在云服务器上跑,但是x86的服务器获取不到CPU温度。如今这个是arm的开发版,安排上。

通过docker安装的twikoo,uptime-kuma可以成功运行。

作为我这个hexo博客节点之一(目前我这个博客节点有:阿里云广州,华为云北京,京东云北京,海外netlify以及此开发板的香港服务器frp反代)。通过Github action构建,然后分发到不同节点服务器上,方法参见我之前的博文:博客网站更新总结-2023–Github-action通过GitHub Action将博客网站等静态文件同步到云服务器

考虑停电,断网,设备重启等意外情况,给相关程序加上开机启动,进程守护。同时因为只有1G内存,加上定时任务,定时清理内存,缓存。此时1panel面板的价值作用体现了。

前面提到frp是通过不同域名区分不同网站的,最后各web网站的访问地址是域名+端口,比如one.jiangyu.org:8090,two.jiangyu.org:8090。给这些源站套上CDN,就能不带端口,直接用绑定的CDN域名访问对应的web站点。只是这样一来,就更费域名了。:-)

还有一个问题。绑定CDN以后,我习惯强制https,如此需要给CDN上传SSL证书。如今SSL证书有效期已缩短到90天(我曾在6个月前的博文 SSL证书,部署及相关知识中总结过与SSL证书相关的知识,彼时证书有效期还多是1年,半年不到普遍都是90天了),如果自己的VPS部署站点,开启https,申请SSL证书,然后绑定,不断更新,这都能自动化完成。可是在CDN应用里,需要自己上传SSL证书,如果过期,需要重新再上传新的证书。如果使用的是大厂CDN,这个问题比较好解决,厂家开放了各种api,基于此很多大神帮忙造好了轮子,SSL证书申请,上传到CDN,部署,更新都能通过脚本自动化完成。可是对于小厂CDN,一般没开放api或者没有现成的造好的轮子,需要自己频繁手动更新SSL证书,是个大问题。

最近看到有某CDN小厂提供了名为证书无忧的服务,能很好解决上述问题。在CDN配置里,第一次先上传自己的SSL证书,在证书到期前,会自动更新有效期为 90 天的免费证书。目前公测期间这个功能免费,以后可能每次成功更新证书收费1-2元。这是个很好的功能,不知道会不会有其他CDN厂家跟进。

其他

这个开发板尺寸100 x 60mm,比树莓派略大,买来一个树莓派的亚格力外壳(外壳整体尺寸与这个开发板相当),重新钻孔,保护起来。买外壳时,看见店家有与外壳配套的散热风扇,也买了一个。测试了一段时间,对比发现,这个5V 0.2A的小风扇能给CPU降温5-6℃。不过因为RV1126这颗SoC面向的是安防,消费类IPC领域,低功耗,温度控制这块做得比较好。即使不加散热风扇,在室温30℃时,一般负载下运行时的最高温度也只在50℃上下,离会降频的85℃还很远。

这个开发板有个40pin 的 gpio接口,厂家文档说兼容树莓派的接口,除此没有更多资料。本来想着如同树莓派一样,接几个诸如温度,湿度这样的传感器,调试一下,资料欠缺,需要补的课很多,只能暂时作罢。

SSL证书,部署及相关知识

在老麦的博客看见他有关SSL证书的博文(Certbot申请泛域名证书与续期),想起自己经常需要更新部署SSL证书的一个痛点。每次只是按部就班的操作,这次停下来,全面了解下关于SSL证书的来龙去脉及相关知识。

SSL/TLS协议

Netscape在1994年推出其首版网页浏览器-Netscape Navigator时,推出https协议,以SSL(Secure Sockets Layer,安全套接层)对传输数据进行加密,这是SSL的起源。

SSL利用数据加密技术,可确保数据在网络传输过程中不会被截取,从而能保证数据的安全性和完整性。SSL 协议位于 TCP/IP 协议与各种应用层协议之间。

1999 年经过 IETF(The Internet Engineering Task Force,国际互联网工程任务组)讨论和规范后,SSL改名为TLS(Transport Layer Security)。发展至今已经有TLS1.0,TLS1.1,TLS1.2等多个版本,目前最新版本是TLS1.3。

SSL协议发展历程

协议发布时间状态
SSL 1.0未公布未公布
SSL 2.01995年已于2011年弃用
SSL 3.01996年已于2015年弃用
TLS 1.01999年于2021年弃用
TLS 1.12006年于2021年弃用
TLS 1.22008年
TLS 1.32018年

(数据来源:维基百科:传输层安全性协议,2024年1月17日访问)

SSL协议的基础是公钥基础设施(Public Key Infrastructure,PKI),而公钥基础设施普遍商业运营。

主流浏览器对TLS v1.3的支持情况
(数据来源:Can I use…,2024年1月17日访问)

SSL证书

SSL证书就是遵守SSL协议的服务器数字证书,通过验证域名、服务器身份后,由受信任的数字证书授权机构(Certificate Authority,CA)颁发,具有给通过https等协议进行数据传输的各方(主要是浏览器与服务端)进行身份验证和数据传输加密等功能。

在PKI中,CA是负责发放和管理数字证书的权威机构,承担公钥体系中公钥的合法性检验的责任。

SSL证书的有效期

SSL证书在诞生时并没有效期的限制,可以是5年,10年甚至更久,但随着SSL证书的广泛使用,特别是2005年5月17日由VeriSign和Comodo牵头成立国际标准组织–CA/Browser Forum后,出台了一系列标准,SSL证书的有效期开始缩短。

关于CA/Browser Forum
Organized in 2005, we are a voluntary group of certification authorities (CAs), vendors of Internet browser software, and suppliers of other applications that use X.509 v.3 digital certificates for SSL/TLS, code signing, and S/MIME.

2018年3月,有浏览器厂商试图将SSL证书的有效期从3年缩短到1年,但在CA的抵制下妥协为2年。

2019年,浏览器厂商再次提出同样的提案,再次遭受了几乎所有CA的抵制。

2020年2月,苹果公司在违反CA/Browser Forum标准操作程序,没有呼吁投票的情况下,单方面宣布,决定在其设备上将可以使用的SSL证书有效期缩短为398天(一年时间加上一个月的缓冲期),数周后火狐就宣布跟进,而数月后谷歌的跟进则使得CA机构的抵制没有什么实际意义了。

2023年3月,谷歌仿效苹果曾经的操作,在其“共同前进”(Moving Forward, Together)路线图中宣布,将SSL证书的最长有效期从现有的398天减少到90天,甚至准备向10天的目标迈进。

SSL证书有效期

浏览器厂商为什么一直在推动SSL证书有效期缩短呢?其实谷歌在其“共同前进”路线图对其动机进行了一些阐述:

缩短SSL证书生命周期一方面是为了鼓励证书部署自动化。证书自动化将推动https生态系统摆脱“巴洛克式”(巴洛克式是一种艺术风格,巴洛克的本意就是不规则的,不常规的意思),耗时且容易出错的颁发流程,降低人力成本和工作难度规避人为失误导致的事故。

另一方面,SSL证书有效期的减少,促使网站更频繁地续订证书,而续订的证书会采用最新的算法并进行密钥的轮换,从而将生态系统快速过渡到具备抗量子算法所需的加密敏捷性。

总结起来就是,缩短SSL证书的有效期,可以提高SSL证书的安全性和可信度。

Google网站现用证书的有效期确实只有90天

SSL证书自动化部署

SSL证书有效期缩短,提高可靠性和安全性的同时,却给网络运维人员带了需要频繁更新证书的压力。随着证书有效期可能缩短到10天,证书部署自动化将是未来大势。

对于独立服务器,采用acme协议,可以实现自动申请,验证,签发,安装(更新续签)证书。acme目前已支持很多CA机构,其中包括比较著名的提供免费证书的Let’s Encrypt以及Google Trust Services。宝塔面板等很多第三方续签证书功能的实现基本都是依托acme协议。

对于CDN服务,自动续签证书目前没有太多的好办法。但是一般CDN服务商会提供证书自动续签的功能。阿里云,腾讯云的CDN服务里要实现自动续签证书,需要付费版的证书。又拍云CDN提供免费的Let’s Encrypt DV SSL 单域名证书的自动续签功能。

对于serveless服务(比如netlify,vercel等),一般可以使用其默认的https服务,无需自己部署更新证书。

对于CDN,对象云储存等部署的站点,免费证书如何自动化部署更新?在Github上找到几个不错的开源方案:certd,acme-qcloud-scfTencent CDN Cert Manager

CertD能部署到自己服务器上,支持从Let’s Encrypt申请证书(支持通配符域名,域名解析支持阿里云,腾讯云,华为云等),全自动部署到阿里云,腾讯云等云服务及主机,证书自动续期更新,所有过程支持邮件通知。

CertD支持的部署功能

而acme-qcloud-scf主要借助腾讯云云函数实现通过acme协议的Let’s Encrypt证书的自动部署和更新(Node.js 版本)。证书生成后能自动上传到腾讯云 SSL 托管平台并绑定到相关 CDN 加速域名(如上,腾讯云自带的SSL证书管理更新是付费证书才能支持的功能)。

Tencent CDN Cert Manager,顾名思义就是支持给腾讯云CDN绑定的CDN域名自动更新证书的服务,支持Docker部署。证书提供商自然也是Let’s Encrypt。

扩展阅读:

  1. 谷歌对根CA策略的“共同前进”建议:更频繁地轮换ICA
  2. 阿里云将免费SSL证书的有效期调整为3个月
  3. 腾讯云SSL证书自动化管理方案
  4. 维基百科:根证书公开密钥认证
  5. 又拍云SSL证书服务
  6. digicert:TLS/SSL证书的有效期有多长?
  7. Google:Automate Public Certificates Lifecycle Management via RFC 8555 (ACME)
❌