普通视图

付费的 SSL 证书是不是智商税

2023年12月15日 18:32

最近要给公司网站的 SSL 证书续费了,以前一直用的是高大上的 DigiCert 泛域名 OV 证书服务。为啥要选这个服务商,理由很简单,当时看到 GitHub 用的就是它家的证书,所以就无脑为信仰充值了(按理说 DigiCert 应该给 GitHub 不少广告费才对)。年复一年,每次续费似乎已经成了习惯动作,直到最近我瞟了一眼账单。

IMG_9658.PNG

好嘛,从 2017 年的 1k 刀出头,直接来到了一千七百多刀,结合汇率的涨幅,这个使用成本直接高出了一倍。我寻摸着 SSL 证书这玩意也没啥技术和资源投入吧,咋涨这么狠呢?当然也不能怪 DigiCert 心太黑,其实 OV 证书本来价格就比较高,更何况还是泛域名。证书快到期的这段时间里,国内供应商的销售电话轰炸就没停过,我看了看他们的报价1,其实也没有更良心。

IMG_9680.PNG

本来我是决定吃下这个哑巴亏的,但是由于我是在周五提交的续费申请,OV 证书还有一个人工审核步骤。后面我的证书就一直卡在这一步下发不了,跟他们客服咨询后告诉我,中国区的代理都提前下班去搞团建了,最快也要等到下周一才能审核。好吧,外资公司的福利果然让人羡慕😂。

但是我的证书周日就要过期了,这是我不能忍受的。所以我决定用免费证书先顶上💡,之前一直听说免费证书可能有兼容性问题,但这次也顾不了那么多了,有总比挂了好。

使用免费的 SSL 证书

2023-12-15T08:51:06.png

免费证书当然是 Let's Encrypt 的最出名,直接 certbot 一把梭

certbot certonly --manual -d xxxxxx.com -d *.xxxxxx.com --agree-tos --manual-public-ip-logging-ok --preferred-challenges dns-01 --server https://acme-v02.api.letsencrypt.org/directory --register-unsafely-without-email

我直接选择了 DNS 验证,因为不用再去改服务器配置,缺点是三个月过期以后2还要去 DNS 那里重新配置一下,不过我就是临时用一下,这样反而是最方便的。命令输入后系统会提示你给 DNS 记录里加上两条 _acme-challenge.xxxxxx.comTXT 记录用于验证(因为同时申请了根域名和泛域名所以是两条)。添加完毕后确认验证通过以后,证书就已经下发到本地了,下发的证书存储在目录

/etc/letsencrypt/live/xxxxxx.com/

里面有这么几个文件:

cert.pem
chain.pem
fullchain.pem
privkey.pem

privkey.pem 是你的私钥,cert.pem 是你的证书,但是为了提高兼容性我建议你使用带有完整证书链的证书 fullchain.pemcertbot是基于 Python 的,你可能还需要搞定运行环境,现在貌似 acme.sh 更流行,这个直接在 shell 里就可以运行,兼容性更高。

不管怎么样现在我已经拿到了一个可用的 SSL 证书,大多数云厂商都提供了很方便的证书更新服务,把这个证书上传上去然后等待更新完毕即可。我在周五下班前,花了半个小时干完了上面说的所有事情。然后在所有用户都不知情的情况下,网站已经运行在了一个成本为 0 的安全证书上。

三天以后周一,DigiCert 通知我新的证书已经下发下来了,而在这期间我们没有接到任何的关于安全证书方面的用户反馈,网页,手机端,小程序都没有。于是我决定再等几天,看看会发生什么事情。一周下来,什么事情都没有发生🤷‍♂️。当然最后我还是把 DigiCert 的证书换上去了,毕竟钱已经花了🤦‍♂️。

最后的思考

其实免费证书已经广泛商用了,很多 CDN 服务商在你使用自己的域名时已经不要求必须上传证书了,它们会跟我一样为客户生成一个免费证书。而像 Cloudflare 这样的厂商,干脆就自己签发免费证书了。而我自己的个人网站,也一直在使用免费证书。

所以我就在想,我使用收费证书到底能带来什么?

兼容性?根据我的实践,没遇到这方面的问题。提高安全性?这就是骗骗小白的话术,实际上从加密手段上来讲这两者没啥区别。更方便?确实能省一点时间,免费证书大多过期时间在三个月,但收费证书的最长过期时间也从两年缩短到了一年多一点,而从我的实践来看,每次更新证书也花不了多少时间,所以你觉得省的这些时间值这么多钱么?提高置信度?这可能有点玄学了,以前如果你买更贵的 EV 证书,在浏览器上会显示一个 Green Bar,告诉用户这个网站属于哪个企业,但现在也已经取消了,现在你要查询证书里的验证信息,还得查看证书详情才看得到,我想没有人吃饱了撑的去靠这个来验证安全性。

唯一我能想到值得一点的就是稳定性,毕竟 Let's Encrypt 作为一个免费服务是没有任何商业保证的,它哪一天突然不干了不提供服务了也是有可能的,这就是你需要权衡的。

2023-12-15T10:15:58.png

但况且就算你要买 OV 证书也有更便宜的选择,另一大免费 SSL 证书提供商 ZeroSSL 也提供收费的 OV EV 证书选择,但是费用要便宜得多,需要的用户可以考虑一下。

所以对我来说,收费 SSL 证书提供的附加值对我价值有限(某些公司可能有特殊的安全认证要求,必须要收费证书),我的用户也不是金融用户,相反免费证书可以帮我省掉一比费用。所以接下来我会考虑这些做法:

  1. 逐步提高免费证书的使用比率,比如在一些次级域名使用,进一步验证兼容性。
  2. 考查更便宜的泛域名证书选择,三个月更新一次还是有点麻烦,而且我可能不会选择 OV 证书了,普通的就行。对于重要的服务还是需要有商业保证的证书,但是有便宜的选择最好。

收费证书不完全是智商税,有它的必要性,但是付出这么多费用如果你没有任何这方面的回报,那就是不划算的。


  1. 赛门铁克证书服务已经被 DigiCert 收购
  2. Let's Encrypt 免费 SSL 证书的最长有效期是三个月

努力要趁早

2025年10月28日 10:55

年轻就是最宝贵财富年轻就意味着无限可能性

一个二十岁的农民工,想成为程序员,有可能吗?有。一个二十岁的服务员,想成为摄影师,有可能吗?有。年轻就有尝试的资本,有学习的空间。年轻人犯错,社会是可以包容的,年轻人想要尝试,社会是愿意给机会的。

人到中年之后,就不一样了。没有人在意中年人的梦想,没有人会包容中年人的错误,也没有人会相信中年人还有什么可能性。一个四十岁的民工,说他想成为优秀的程序员,又有多少人会相信呢,又有多少人愿意帮助他呢。

努力要趁早。

努力要趁早

2025年10月28日 10:55

年轻就是最宝贵财富年轻就意味着无限可能性

一个二十岁的农民工,想成为程序员,有可能吗?有。一个二十岁的服务员,想成为摄影师,有可能吗?有。年轻就有尝试的资本,有学习的空间。年轻人犯错,社会是可以包容的,年轻人想要尝试,社会是愿意给机会的。

人到中年之后,就不一样了。没有人在意中年人的梦想,没有人会包容中年人的错误,也没有人会相信中年人还有什么可能性。一个四十岁的民工,说他想成为优秀的程序员,又有多少人会相信呢,又有多少人愿意帮助他呢。

努力要趁早。

基于 wp-cron.php 的拒绝服务攻击

2025年11月12日 14:02

这几天不知道是发生什么事了,说是不知道什么事情,但是大概率是被打了。只是这次打的挺高级的,外层的 eo 貌似也没什么反应。只是那个访问量通过 umami 看,直接爆炸了。

平常几百的访问量,昨天的时候,结果到了 2000 多,当然这不是最奇怪的,奇怪的是服务器过了会儿卡死了。之前都是因为请求太多 php-fpm 耗尽 cpu 资源卡死了,这次以为还是同样的问题。然而,并不是,发现 mysql 把 cpu 跑满了,查看日志的时候发现大量的 wp-cron.php 的请求,这尼玛,请求直接透传过来了。

另外还有一大堆 bot 的请求,包括 bing 以及一些乱起八糟的爬虫遍历。

最开始没想到什么好办法,简单粗暴的把 wp-cron.php 改名了,暂时解决了这个问题。

不过这个方法的确是高明,带着参数透传过来,wp 就是疯狂的执行,一条没执行完就到了下一条。然而,对于这种事情直接改名的确是可以解决办法,不过后来想了一下还是直接从 eo 下手吧。

尽管 eo 防住了 22 万次的攻击,但是,这些透传的请求,直接让 mysql 耗尽了 cpu 资源,也是个不错的办法,甚至请求频率都不用太高。流量到了 144g,这也不知道是哪个哥们又闲的蛋疼了,如果真的蛋疼来找姐姐啊,姐姐帮你治疗,直接给你割下来,塞你自己嘴里!

昨天晚上发现这个情况的时候,本来是想去处理下的,结果对象在用电脑,自己又不想去开笔记本,就用手机处理了一下,简单的改下了文件名。

今天早上才处理了一下,加到了 eo 的访问规则里:

尽管如此,还是对这几天的访问记录比较好奇,想看看请求了多少次。去拉 nginx 日志的时候发现文件已经 1.5G 了。直接截取这几天的记录,用 goaccess 跑了一下,但是比较奇怪的是这个 wp-cron.php 的请求竟然没有。

暂时放弃 goaccess 直接使用 ngxtop 进行数据分析:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
使用ngxtop分析Nginx日志中的POST请求
提供交互式菜单和多种分析选项
"""

import subprocess
import sys
import os
from pathlib import Path


def run_ngxtop(cmd_args):
    """运行ngxtop命令"""
    venv_python = Path(__file__).parent / "venv" / "bin" / "python"
    ngxtop_script = Path(__file__).parent / "venv" / "bin" / "ngxtop"
    
    if not ngxtop_script.exists():
        print("错误: ngxtop未安装,请先运行: source venv/bin/activate && pip install ngxtop")
        sys.exit(1)
    
    try:
        result = subprocess.run(
            [str(ngxtop_script)] + cmd_args,
            capture_output=True,
            text=True,
            check=False
        )
        print(result.stdout)
        if result.stderr and "error" in result.stderr.lower():
            print(result.stderr, file=sys.stderr)
        return result.returncode == 0
    except Exception as e:
        print(f"错误: {e}", file=sys.stderr)
        return False


def show_menu():
    """显示菜单"""
    print("\n" + "="*60)
    print("Nginx日志POST请求分析 - ngxtop工具")
    print("="*60)
    print("1. POST请求总览")
    print("2. 按URL统计POST请求 (Top 20)")
    print("3. 按IP统计POST请求 (Top 20)")
    print("4. 按状态码统计POST请求")
    print("5. POST请求中状态码为404的URL")
    print("6. POST请求中状态码为200的URL")
    print("7. 可疑POST请求 (xmlrpc, wp-login等)")
    print("8. POST请求详情示例")
    print("9. 自定义查询")
    print("0. 退出")
    print("="*60)


def analyze_post_requests(log_file):
    """分析POST请求"""
    if not os.path.exists(log_file):
        print(f"错误: 日志文件 {log_file} 不存在")
        return
    
    base_args = ["-l", log_file, "--no-follow", "-i", 'request.startswith("POST")']
    
    while True:
        show_menu()
        choice = input("\n请选择分析选项 (0-9): ").strip()
        
        if choice == "0":
            print("退出分析")
            break
        elif choice == "1":
            print("\n【POST请求总览】")
            print("-" * 60)
            run_ngxtop(base_args + ["--limit", "0"])
        elif choice == "2":
            print("\n【按URL统计POST请求 (Top 20)】")
            print("-" * 60)
            run_ngxtop(base_args + ["--group-by", "request_path", "--limit", "20"])
        elif choice == "3":
            print("\n【按IP统计POST请求 (Top 20)】")
            print("-" * 60)
            run_ngxtop(base_args + ["--group-by", "remote_addr", "--limit", "20"])
        elif choice == "4":
            print("\n【按状态码统计POST请求】")
            print("-" * 60)
            run_ngxtop(base_args + ["--group-by", "status", "--limit", "0"])
        elif choice == "5":
            print("\n【POST请求中状态码为404的URL (Top 10)】")
            print("-" * 60)
            run_ngxtop(["-l", log_file, "--no-follow", 
                       "-i", 'request.startswith("POST") and status == 404',
                       "--group-by", "request_path", "--limit", "10"])
        elif choice == "6":
            print("\n【POST请求中状态码为200的URL (Top 10)】")
            print("-" * 60)
            run_ngxtop(["-l", log_file, "--no-follow",
                       "-i", 'request.startswith("POST") and status == 200',
                       "--group-by", "request_path", "--limit", "10"])
        elif choice == "7":
            print("\n【可疑POST请求统计】")
            print("-" * 60)
            run_ngxtop(["-l", log_file, "--no-follow",
                       "-i", 'request.startswith("POST") and (request_path == "/xmlrpc.php" or request_path == "/wp-login.php" or request_path.startswith("/wp-admin"))',
                       "--group-by", "request_path", "--limit", "0"])
        elif choice == "8":
            print("\n【POST请求详情示例 (前10条)】")
            print("-" * 60)
            run_ngxtop(base_args + ["print", "remote_addr", "time_local", "request", "status", "bytes_sent", "--limit", "10"])
        elif choice == "9":
            print("\n【自定义查询】")
            print("-" * 60)
            print("示例查询:")
            print("  - 查看特定URL: ngxtop -l <file> -i 'request.startswith(\"POST\") and request_path == \"/wp-cron.php\"'")
            print("  - 查看特定IP: ngxtop -l <file> -i 'request.startswith(\"POST\") and remote_addr == \"114.66.247.160\"'")
            print("  - 查看错误请求: ngxtop -l <file> -i 'request.startswith(\"POST\") and status >= 400'")
            print("\n请输入自定义ngxtop命令参数 (用空格分隔):")
            custom_args = input("> ").strip().split()
            if custom_args:
                run_ngxtop(["-l", log_file, "--no-follow"] + custom_args)
        else:
            print("无效的选择,请重试")
        
        input("\n按回车键继续...")


def main():
    """主函数"""
    if len(sys.argv) < 2:
        # 查找默认日志文件
        log_files = list(Path(".").glob("*.txt"))
        if log_files:
            default_log = str(log_files[0])
            print(f"未指定日志文件,使用默认: {default_log}")
            log_file = default_log
        else:
            print("用法: python analyze_with_ngxtop.py <日志文件路径>")
            print("示例: python analyze_with_ngxtop.py 11-08_org.txt")
            sys.exit(1)
    else:
        log_file = sys.argv[1]
    
    analyze_post_requests(log_file)


if __name__ == "__main__":
    main()

运行命令:

python3 analyze_with_ngxtop.py 11-08_org.txt

分析结果:

【按URL统计POST请求 (Top 20)】
------------------------------------------------------------

running for 7 seconds, 23670 records processed: 3508.50 req/sec

Summary:
|   count |   avg_bytes_sent |   2xx |   3xx |   4xx |   5xx |
|---------+------------------+-------+-------+-------+-------|
|   23670 |         2381.924 |  4678 |    21 | 18574 |   397 |

Detailed:
| request_path                    |   count |   avg_bytes_sent |   2xx |   3xx |   4xx |   5xx |
|---------------------------------+---------+------------------+-------+-------+-------+-------|
| /wp-cron.php                    |   16454 |          731.309 |  3413 |     0 | 13034 |     7 |
| /xmlrpc.php                     |    3102 |          416.754 |   248 |     0 |  2853 |     1 |
| /wp-login.php                   |    2519 |        15204.250 |     0 |     0 |  2519 |     0 |
| /wp-admin/admin-ajax.php        |    1017 |          542.043 |   971 |     0 |    44 |     2 |
| /wp-comments-post.php           |     401 |         2551.357 |     0 |    14 |     0 |   387 |
| /xmrpc.php                      |      41 |          915.000 |     0 |     0 |    41 |     0 |
| /tslogin                        |      20 |        30543.150 |    16 |     4 |     0 |     0 |
| /alfacgiapi/perl.alfa           |      11 |        51292.455 |     0 |     0 |    11 |     0 |
| /ALFA_DATA/alfacgiapi/perl.alfa |      11 |        51323.636 |     0 |     0 |    11 |     0 |
| /index.php                      |      10 |        34570.900 |    10 |     0 |     0 |     0 |
| /wp-plain.php                   |       9 |         1331.000 |     0 |     0 |     9 |     0 |
| /                               |       9 |        28609.556 |     7 |     0 |     2 |     0 |
|                                 |       8 |          415.000 |     8 |     0 |     0 |     0 |
| /flow.php                       |       7 |          915.000 |     0 |     0 |     7 |     0 |
| /wp-admin/async-upload.php      |       5 |          736.000 |     5 |     0 |     0 |     0 |
| /php-cgi/php-cgi.exe            |       4 |        33911.500 |     0 |     0 |     4 |     0 |
| /graphql                        |       4 |        33469.750 |     0 |     0 |     4 |     0 |
| /wp-admin/post.php              |       3 |            5.000 |     0 |     3 |     0 |     0 |
| /member/success.aspx            |       2 |        16784.500 |     0 |     0 |     2 |     0 |
| /e/aspx/upload.aspx             |       2 |        16628.500 |     0 |     0 |     2 |     0 |

【按IP统计POST请求 (Top 20)】
------------------------------------------------------------
running for 7 seconds, 23670 records processed: 3586.40 req/sec

Summary:
|   count |   avg_bytes_sent |   2xx |   3xx |   4xx |   5xx |
|---------+------------------+-------+-------+-------+-------|
|   23670 |         2381.924 |  4678 |    21 | 18574 |   397 |

Detailed:
| remote_addr    |   count |   avg_bytes_sent |   2xx |   3xx |   4xx |   5xx |
|----------------+---------+------------------+-------+-------+-------+-------|
| 221.204.26.162 |    4407 |          696.960 |  1125 |     1 |  3279 |     2 |
| 221.204.26.233 |    4291 |          738.947 |  1054 |     1 |  3235 |     1 |
| 101.71.101.44  |    3168 |          686.088 |   911 |     4 |  2252 |     1 |
| 101.71.101.106 |    2564 |          868.693 |   183 |     2 |  2379 |     0 |
| 43.174.53.229  |    2094 |         7795.611 |     6 |     0 |  2088 |     0 |
| 43.174.53.236  |    2090 |         7811.496 |     4 |     0 |  2086 |     0 |
| 114.66.247.160 |    1810 |          743.818 |   520 |     1 |  1288 |     1 |
| 114.66.246.149 |    1123 |          507.375 |   538 |     1 |   582 |     2 |
| 101.71.105.47  |     104 |          574.404 |    57 |     0 |    47 |     0 |
| 43.175.19.192  |      29 |         5430.241 |     1 |     0 |    15 |    13 |
| 43.175.17.169  |      26 |         2520.500 |     0 |     0 |     8 |    18 |
| 43.175.18.81   |      25 |         2049.720 |     1 |     0 |     6 |    18 |
| 43.175.18.253  |      25 |         1835.800 |     1 |     0 |     8 |    16 |
| 43.175.18.195  |      25 |         5997.720 |     0 |     0 |     8 |    17 |
| 43.175.18.137  |      25 |         2101.840 |     1 |     0 |     5 |    19 |
| 43.175.17.87   |      24 |         2210.208 |     0 |     0 |     5 |    19 |
| 43.175.17.47   |      23 |         7488.043 |     0 |     0 |     9 |    14 |
| 43.175.18.51   |      22 |         3213.455 |     0 |     0 |     8 |    14 |
| 43.175.17.205  |      21 |         7011.381 |     1 |     0 |    10 |    10 |
| 43.175.169.137 |      16 |         1386.562 |     3 |     0 |     6 |     7 |

而至于这些 IP 地址,多数都是国内的,这个倒是也在意料之内,毕竟国外的被拦截的概率会更高一些。

然而,goaccess 就无法分析吗?也可以,添加忽略请求参数的参数就可以了:

#!/bin/bash
# 使用goaccess的--no-query-string参数移除查询参数
# 不需要修改日志文件!

LOG_FILE="${1:-11-08_org.txt}"
OUTPUT_FILE="${2:-goaccess_no_query_report.html}"

if [ ! -f "$LOG_FILE" ]; then
    echo "错误: 日志文件 $LOG_FILE 不存在"
    exit 1
fi

echo "=========================================="
echo "使用GoAccess分析(移除查询参数)"
echo "=========================================="
echo "日志文件: $LOG_FILE"
echo "输出文件: $OUTPUT_FILE"
echo ""
echo "使用参数: --no-query-string (或 -q)"
echo "这将移除URL中的查询参数,只保留路径"
echo ""

# 使用--no-query-string参数
goaccess "$LOG_FILE" \
  --log-format='%h %^[%d:%t %^] "%r" %s %b "%R" "%u"' \
  --date-format='%d/%m/%Y' \
  --time-format='%H:%M:%S' \
  --no-query-string \
  -o "$OUTPUT_FILE"

if [ $? -eq 0 ]; then
    echo ""
    echo "✅ 报告生成成功: $OUTPUT_FILE"
    echo ""
    echo "现在wp-cron.php应该能正确合并统计了!"
    echo ""
    echo "在浏览器中打开报告查看:"
    echo "  open $OUTPUT_FILE    # macOS"
    echo "  xdg-open $OUTPUT_FILE  # Linux"
    echo ""
    echo "在交互界面中使用:"
    echo "  goaccess $LOG_FILE \\"
    echo "    --log-format='%h %^[%d:%t %^] \"%r\" %s %b \"%R\" \"%u\"' \\"
    echo "    --date-format='%d/%m/%Y' \\"
    echo "    --time-format='%H:%M:%S' \\"
    echo "    --no-query-string"
else
    echo "❌ 报告生成失败"
    exit 1
fi

主要就是:–no-query-string参数。

实际效果:

文件没改名之前:

文件改名之后:

虽然加起来之后不到两万次,但是却让 mysql 把 cpu 资源耗尽了,这的确不失为一个低成本的攻击方式。

爬虫占比:

这几天也不知道爬虫是发什么疯

今天的访问量:

百度的统计:

咱就是说,有点时间干点正事不好吗?真是闲的。

 

❌