阅读视图

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

别搞“小而美”了!Rust 开发者请愿:求求标准库学学 Go 吧

本文永久链接 – https://tonybai.com/2026/04/09/stop-being-small-and-beautiful-rust-petition-to-learn-from-go

大家好,我是Tony Bai。

如果你之前经常听 Go 社区最火的播客 GoTime(很遗憾,该播客2024年末因平台原因停播了),你一定会熟悉每期节目最后的那个经典环节——“Unpopular Opinion”(非主流观点)。在这个环节,嘉宾们会分享一些看似离经叛道、却往往一针见血的“暴论”。

但就在前几天,这个流行于 Go 社区的“梗”,却被隔壁的 Rust 社区“偷”了过去,并掀起了一场史诗级的“路线之争”。

一位 Rust 开发者,在 r/rust 论坛上发了一篇帖子,标题就叫:《Unpopular opinion: Rust should have a larger standard library》(非主流观点:Rust 应该有一个更大的标准库)。

他在这篇帖子中发出了灵魂拷问:

“我不想为了写一个程序,被迫去拉几百个我根本没时间、也没人去审计的第三方依赖包。看看隔壁的 Go 是怎么做标准库的,你几乎可以不依赖任何三方包就构建出复杂的系统!”

这篇帖子瞬间引爆了 Rust 社区。短短一天,帖子收获了近 700 的高赞和近 300 条激烈辩论。

这看起来像是一场简单的“库多库少”之争,但本质上,它背后是 Rust 这门以“零成本抽象、极致安全”著称的语言,在面对日益猖獗的供应链安全威胁和 Go 语言“开箱即用”的降维打击时,所爆发的一场深刻的身份危机与哲学反思。

“小而美”的代价:悬在每个 Rust 项目头顶的达摩克利斯之剑

长期以来,Rust 社区一直为自己“小核心、强生态”的模式感到自豪。Rust 的标准库(std)极其精简,只提供最基础、最核心的功能。任何稍微高级一点的需求,比如随机数生成、异步运行时、序列化,官方都鼓励你去 crates.io 上找社区“钦定”的“明星库”(Blessed Crates)。

这套模式在早期极大地促进了生态的繁荣。但随着 npm left-pad 事件和各种开源投毒攻击的阴影笼罩全球,这套模式的代价也变得越来越难以承受。

原帖作者一针见血地指出了所有人的噩梦:

“是的,你可以采取各种缓解措施。但等你发现某个藏在你依赖树第三层的、不起眼的包被植入了恶意软件时,你的服务器密钥可能早就被偷光了!”

评论区里的一位开发者用一句话概括了所有人的痛点:

“我完全同意。有时候 std 里就是缺了那么一点至关重要的东西。我能理解这背后的原因,但为了生成一个随机数就要去装一个第三方包,这实在有点小题大做了。”

这正是 Rust 开发者面临的尴尬:当你只是想生成一个 UUID,或者发起一个 HTTP 请求时,你被迫要对 rand、reqwest、tokio 这些由社区个人或小团体维护的库,付出与 Rust 官方核心团队同等级别的“信任”。

而这种信任,正在变得越来越昂贵和危险。

隔壁的诱惑:Go 语言的“大一统”模式

在这场大讨论中,一个名字被反复提及,它就是 Go 语言。

Go 从诞生之初,就选择了与 Rust 截然相反的“自带电池(Batteries Included)”哲学。

  • 你想做 Web 开发?net/http 原生支持,性能强大到可以直接裸奔在生产环境。
  • 你想做 JSON/XML 解析?encoding/json(以及实验性的encoding/json/v2)、encoding/xml 是标配。
  • 你想做并发?goroutine 和 channel 是语言级原生特性。
  • 你想生成随机数?math/rand、crypto/rand 随便用。

评论区里,一位 Rust 开发者的对比极其扎心:

“把恶意代码偷偷塞进一个(流行的)Crate 的第四层依赖里,比把它塞进 Rust 的 std 里要容易得多。”

Go 语言通过一个庞大、稳定、由官方核心团队直接维护的标准库,为开发者提供了一道坚固的“安全护城河”。你可以在不引入任何一个第三方依赖的情况下,构建出一个功能极其完备、性能强大的高并发网络服务。

这种“开箱即用”的安全感和便利性,对于那些深受供应链安全审计折磨的企业开发者来说,是致命的诱惑。

社区的挣扎:当“保守”成为“瓶颈”

面对社区的“呐喊”,Rust 核心团队的成员和社区大佬们也纷纷下场,给出了极其理性和深刻的解释。他们的回复,揭示了 Rust 在标准库扩张上,面临的“三重枷锁”。

枷锁一:向后兼容性的“诅咒”

一位核心成员引用了 Python 社区的一句名言:

“标准库,是模块最终的坟场(The standard library is where modules go to die)。”

一旦一个 API 进入了 std,它就必须背上永不破坏向后兼容的沉重承诺。哪怕 10 年后发现这个设计有缺陷,也只能眼睁睁地看着它腐烂,或者推出一个 urllib2、urllib3 这样极其丑陋的补丁。

Rust 团队宁愿让这些库在社区里自由进化、大浪淘沙,等到它们的设计真正成熟、稳定到可以“永恒”时,再考虑纳入 std。比如 once_cell 和最新的 rand(目前在 nightly 版本中)。

枷锁二:无休止的“维护地狱”

另外一名核心成员指出,将一个库纳入 std,意味着它的维护成本将全部转移到人数本就捉襟见肘的官方维护者身上。而在社区,每个 Crate 都有自己专门的维护者。这是两种完全不同的成本模型。

枷锁三:设计的“过早僵化”

最典型的例子就是异步。原帖作者提议:“Rust 能不能偷一下 Zig 的 IO 思想,这样我们就不需要在 Tokio 和 non-Tokio 生态之间分裂了?”

一位社区大佬立刻反驳:Zig 没有 Rust 的 Send/Sync 标记,两者的异步模型有本质区别。Rust 的异步生态之所以看起来“分裂”,恰恰是语言给了开发者在不同场景下做最优选择的自由。如果过早地在 std 里统一一个官方运行时,反而会扼杀创新。

破局之路:从“大一统”到“邦联制”

在这场激烈的辩论中,一些极具建设性的“折中方案”也开始浮现。这或许预示着 Rust 未来的演进方向。

方案一:官方背书的“准标准库(Semi-official)”

一位开发者提出,Rust 项目组可以借鉴 C++ Boost 库的模式,官方接管 serde、rand、tokio 这些“钦定”的明星库,将它们纳入一个统一的 extd (extended) 命名空间下。

use extd::regex::Regex;
use extd::rand;

这并不会增加 std 的体积,但给了这些库一个“官方认证”的金字招牌,极大地解决了开发者的信任和审计问题。

方案二:引入“孵化期(Incubation Phase)”

一位开发者建议,应该有一个更明确的孵化流程,让那些有潜力进入 std 的库,先在一个类似 Go golang.org/x 的“实验场”里进行检验,而不是直接从某个个人开发者仓库里一步登天。

方案三:强化 Cargo 的安全审计能力

一些核心成员则认为,问题的根源不在于 std 的大小,而在于 crates.io 的分发机制不够安全。与其“因噎废食”地把所有东西都塞进 std,不如去建立更强大的包安全审计机制,比如:

  • 发布隔离期:新发布的包必须经过 72 小时自动化扫描才能被下载。
  • 签名与信任链:通过 cargo 增强包签名和审计者签名,让企业可以选择只使用“可信审计者”批准的依赖列表。

小结:一场关于“灵魂”的拷问

这场由“非主流观点(Unpopular Opinion)”引发的大讨论,表面上是在争论标准库的大小,但其核心,却是一场关于 Rust 与 Go 两种截然不同建国哲学的灵魂拷问。

  • Go 语言,像一个大一统的、中央集权的帝国。它为你提供了从道路、货币到度量衡的一切基础设施。你享受着极高的安全感和便利性,代价是必须忍受它某些时候的“独裁”与“不灵活”。
  • Rust 语言,则更像一个松散的、充满活力的城邦联盟。官方只提供最基础的法律和军队,剩下的一切都交给各个城邦(Crates)自由发展。你拥有无与伦比的自由和选择权,代价是你必须自己承担选择的风险,并时刻提防“外敌入侵”(供应链攻击)。

这两种哲学没有绝对的优劣,只有不同场景下的取舍。

但 Rust 社区的这场“请愿”,无疑为我们所有技术人敲响了警钟:在软件供应链日益脆弱的今天,一个强大、可靠、由顶级专家背书的“官方基础设施”,其价值正在被无限放大。

或许,Rust 的未来,真的需要在“自由”与“安全”之间,找到一个新的平衡点。而隔壁 Go 的作业,他们可能真的需要抄一抄了。

资料链接:https://www.reddit.com/r/rust/comments/1seu7p2/unpopular_opinion_rust_should_have_a_larger/


今日互动探讨:

在你的日常开发中,你是更喜欢 Go 这种“自带电池”的大标准库模式,还是 Rust 这种“小核心+强生态”的自由模式?你是否也曾因为“拉了一堆三方库”而感到安全焦虑?

欢迎在评论区分享你的看法!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

你的 Go 报错信息正在“出卖”你!扒一扒大厂是如何做错误隔离与日志脱敏的

本文永久链接 – https://tonybai.com/2026/03/21/best-practices-for-secure-error-handling-in-go

大家好,我是Tony Bai。

如果要在 Go 语言里选一句被敲击次数最多的代码,if err != nil { return err } 绝对毫无悬念地霸榜第一。

初学 Go 时,我们总觉得这种显式的错误处理极其啰嗦。但随着项目的深入,我们开始理解 Go 团队的良苦用心:错误不是被抛出的异常(Exceptions),错误就是普通的值(Values)。你需要像对待普通变量一样,去传递它、包装它、解包它。

于是,我们成了熟练的“包装工”。当数据库查询失败时,我们习惯性地写下这样的代码:

return fmt.Errorf(“query user failed: %w”, err)

我们以为这样做极其优雅,既保留了底层的堆栈信息,又方便了外层调用的 Debug。

但今天,我必须给你浇一盆冷水。

就在本月初,JetBrains GoLand 的官方博客发布了一篇极其硬核的警告文章:《Best Practices for Secure Error Handling in Go》。这篇文章直指一个让无数微服务架构师冷汗直流的安全盲区:

你引以为傲的“错误包装(Error Wrapping)”,正在把你们公司的核心底裤——数据库架构、内部路径、甚至是认证 Token,全部赤裸裸地暴露在公网之上!

今天,我们就来扒开这层遮羞布,看看那些烂大街的 Go 错误处理教程,到底是如何在无形中“出卖”你的。同时,我将带你重塑大厂级别的“安全错误防线”

你的 Go 错误,是如何变成黑客的“导航图”的?

在绝大多数其他语言(比如 Java 或 Python)中,异常往往会被全局的异常捕获器(Global Exception Handler)拦截,然后向客户端返回一个统一的 500 错误页面。

但在 Go 中,因为错误只是普通的接口值(Interface value),它极其容易随着 HTTP 的 return 一层一层“冒泡”到最顶层,最后被直接序列化成 JSON 吐给了前端。

这就是噩梦的开始。

想象一个真实的业务场景:你的应用需要根据传入的邮箱去查询用户信息。如果数据库连接池满了,或者执行的 SQL 语法有误。

传统的做法是直接将错误 return err 抛给 HTTP 处理器。于是,客户端的屏幕上、或者是抓包工具里,赫然出现了这样一串报错:

{"error": "failed to get profile: pq: duplicate key value violates unique constraint 'users_email_key'"}

看着眼熟吗?这短短的一行报错,给黑客透露了极其致命的情报:

  1. 技术栈裸奔:pq: 明确告诉了黑客,你们后台用的是 PostgreSQL 数据库。
  2. 表结构裸奔:users_email_key 暴露了你们数据库里的核心表名和唯一索引名。
  3. 注入暗示:如果是因为某些非法字符导致的语法错误,黑客就能根据这段详尽的错误信息,极其精准地调试他们的 SQL 注入 payload。

这绝不是危言耸听。在最新的 Kubernetes 漏洞(CVE-2025-7445)中,攻击者仅仅是通过观察 secrets-store-sync-controller 的错误日志 marshal(序列化)过程,就成功窃取了具有高权限的 Service Account Token!

你以为你在输出错误,其实你是在给黑客手把手发系统导航图。

构建“人格分裂”的安全错误对象

既然把错误信息吐给前端这么危险,那我是不是以后不管遇到什么错,都直接返回 {“error”: “Internal Server Error”} 就可以了?

当然不行。 如果你这么干,你的运维兄弟(SRE)会提着刀来找你。因为他们面对满屏的 Internal Error 日志,根本不知道该如何排查线上故障。

安全(不泄露机密)和实用(易于 Debug),似乎是一个不可调和的矛盾。

这就要求我们的 Go 错误必须具备一种“人格分裂”的能力:面对内部日志,它要知无不言;面对外部公网,它要守口如瓶。

大厂的最佳实践,是利用 Go 面向接口编程的特性,在编译层面强制构建一道“安全防火墙”。

不要再到处 return fmt.Errorf(…) 了,去定义一个你自己的 SafeError 结构体(仅是配合讲解的示意定义):

package secure

// SafeError 实现了 error 接口,但在内部做到了机密隔离
type SafeError struct {
    // 【面对公网】:给客户端看的机器码(如 "RESOURCE_NOT_FOUND")
    Code string
    // 【面对公网】:给用户看的安全提示语
    UserMsg string

    // 【面对内部】:最原始的底层报错(绝对不能通过 API 暴露!)
    Internal error

    // 【面对内部】:经过脱敏的上下文数据,用于打结构化日志
    Metadata map[string]string
}

// Error() 方法实现了标准库的 error 接口
// 核心防御:这个方法永远只返回安全的 UserMsg!
// 这样即使被初级程序员直接用 http.Error 输出,也不会泄露内部机密
func (e *SafeError) Error() string {
    return e.UserMsg
}

// LogString() 是专门给 SRE 团队内部使用的日志打印方法
func (e *SafeError) LogString() string {
    return fmt.Sprintf("Code: %s | Msg: %s | Cause: %v | Meta: %v",
        e.Code, e.UserMsg, e.Internal, e.Metadata)
}

通过这个极其简单的设计,我们在代码骨架里埋入了一道物理隔离墙。如果团队里有新人不小心写了 http.Error(w, err.Error(), 500),用户只会看到干瘪的 UserMsg(比如:“无法获取配置文件”),而真正的死因(比如:“连接 redis 10.0.1.5:6379 失败”)则被死死地锁在了 Internal 字段里,只输出到内网的安全日志系统中。

警惕滥用 fmt.Errorf(“%w”),学会“不透明包装”

自从 Go 1.13 引入了 %w 动词以及 errors.Is/As 函数后,整个 Go 社区都陷入了一种“疯狂包装错误”的狂欢。现在 Go 1.26 更是加入了更方便、类型安全的 errors.AsType。

大家都觉得用 %w 把底层错误包起来,外层调用者就可以用 errors.Is() 去追根溯源了。

但这恰恰是微服务架构中最危险的毒药。

在 GoLand 的这篇官方指南中,重点提出了一个名为 Opaque Wrapping(不透明包装) 的防御概念。

想象一下,如果你的“业务层”调用了“数据访问层(DAL)”。数据层报错了,你用 %w 把 SQL 错误包了一下扔给了业务层。

这看起来没问题,但这意味着你的业务层,甚至更上层的 API 网关层,都可以通过 errors.As() 把你的底层 SQL 错误“扒光”看到!

这违反了微服务设计中最底层的“信任边界(Trust Boundary)”原则。

上游服务根本不应该,也没有权利知道下游服务用的是什么数据库、爆了什么错!如果第三方库的错误类型中藏有解析漏洞,上层的恶意调用者甚至可以通过制造特定的错误来触发利用。

在大厂的微服务架构中,处理跨越边界的错误只有一条铁律:

在信任边界处,彻底斩断错误调用链(Break the dependency chain)!

func GetUserProfile(id string) (*Profile, error) {
    user, err := db.QueryUser(id)
    if err != nil {
        // ❌ 危险:暴露了原始 DB 错误
        // return nil, err 

        // ❌ 危险:虽然包装了,但依然可以通过 Unwrap() 被外层脱下衣服看到底裤
        // return nil, fmt.Errorf("db error: %w", err) 

        // ✅ 安全:不透明包装 (Opaque Wrapping)
        // 将底层错误封印在我们自定义的 SafeError 中,对外不暴露 Unwrap() 方法
        return nil, &SafeError{
            Code:     "FETCH_ERROR",
            UserMsg:  "Unable to retrieve user profile.",
            Internal: err, // 原始错误被保留用于打日志,但对调用链彻底隐藏
        }
    }
    return user, nil
}

当你跨越微服务之间的鸿沟(比如从数据库层到业务层,或者从订单服务调用认证服务)时,你必须做一个冷酷的“翻译官”:把具体的 sql.ErrNoRows 翻译成全公司通用的 domain.ErrNotFound。

绝不让任何一行带有底层技术细节的错误代码,流出它所在的微服务。

日志脱敏的生死防线

就算你的错误在返回给用户时做了完美的隔离,如果你在打日志时依然大手大脚,那安全防线同样会崩溃。

GoLand 官方给出了三条极其硬核的日志避坑军规:

1. 抛弃 fmt.Printf,强制推行结构化日志

在内网日志里把错误原因和用户输入的 Query 拼成一个大字符串,是非常危险的“日志注入”行为。必须使用 Go 原生的 log/slog 或是 zap。结构化日志会将参数作为独立的数据类型处理,而不是原始字符串,这能天然防范转义字符引发的安全漏洞。

2. 永不直接打印 Struct

永远不要在 if err != nil 的块里,随手写下 slog.Error(“login failed”, “request”, req)。因为这个 req 结构体里可能明晃晃地写着用户的密码明文!

3. 引入脱敏机制

对于不得不打印的上下文结构体,在你的项目里强制推行 Redact() any 接口:

type Redactor interface {
    Redact() any
}

type LoginRequest struct {
    Username string
    Password string
}

// 强制接管结构体的序列化输出
func (r LoginRequest) Redact() any {
    return struct {
        Username string json:"username"
        Password string json:"password"
    }{
        Username: r.Username,
        Password: "***REDACTED***", // 把底裤遮好
    }
}

// 以后打日志时强制调用:
// logger.Info("login attempt", "req", req.Redact())

小结:别让“偷懒”毁了你的架构

错误处理,一直是区分初级 Go 程序员和高级微服务架构师的一块试金石。

初级程序员写 if err != nil,只是为了消除 IDE 上的红色波浪线警告;

而高级架构师在写下 return err 的那一刻,脑海中思考的却是:“这个错误跨越了哪道信任边界?它包含了哪些敏感状态?如果它一路上浮被打印到公网上,会不会成为摧毁整个业务的一颗炸弹?”

不要用“开发周期的战术性偷懒”,去掩盖“系统安全防御上的战略性溃败”。

今晚下班前,打开你负责的核心微服务,翻一翻那些连接数据库、调用第三方 API 的错误返回。看看那里面,到底藏了多少没穿衣服的机密代码。是时候,给它们穿上名为“SafeError”的防弹衣了!

资料链接:https://blog.jetbrains.com/go/2026/03/02/secure-go-error-handling-best-practices/


今日互动探讨

在你的开发生涯中,有没有遇到过因为“错误日志泄露敏感信息”而引发的线上事故?或者你在公司的日志系统里,看到过哪些让人惊掉下巴的“密码明文/系统底裤”? 欢迎在评论区疯狂吐槽与分享!


读懂底层边界,才能看透高可用架构

一门语言的哲学,往往藏在它最让人“吐槽”的地方。
很多人觉得 Go 的错误处理不够优雅,但当你今天从微服务信任边界的角度重新审视它时,你会发现:Go 强制你显式地对待错误,其实是给了架构师一张极其精密的手术刀,让你能精准地切断每一个可能蔓延的故障与安全危机。

然而,令人遗憾的是,绝大多数 Go 开发者依然停留在“查查文档、调调包、完成 CRUD”的表层。他们对 Go 错误处理背后的安全边界、Goroutine 调度的本质、内存模型的逃逸机制一无所知。

如果你渴望突破这种“低头干活不看天”的瓶颈,想要像硅谷顶级大厂架构师一样,看透 Go 语言背后的系统级设计思维,建立起坚不可摧的技术护城河——

我的全新极客时间专栏 Tony Bai·Go语言进阶课 正是为你量身定制。

在这 30+ 讲极其硬核的内容中,我不仅带你剥开语法糖,深挖并发模型、Channel 哲学;更会带你全面吃透 Go 的工程化实践,把错误处理、边界防御、微服务构建背后的深层逻辑一次性讲透。

目标只有一个:助你完成从“Go 熟练工”到“能做顶级架构决策的 Go 专家”的蜕变!

扫描下方二维码,加入专栏。让我们一起用顶级架构师的视角,重新认识 Go 语言。


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!

我们致力于打造一个高品质的 Go 语言深度学习AI 应用探索 平台。在这里,你将获得:

  • 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。
  • 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等,掌握 AI 时代新技能。
  • 星主 Tony Bai 亲自答疑: 遇到难题?星主第一时间为你深度解析,扫清学习障碍。
  • 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术,碰撞思想火花。
  • 独家资源与内容首发: 技术文章、课程更新、精选资源,第一时间触达。

衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

img{512x368}


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

刚刚,2025图灵奖揭晓!面对即将瘫痪的传统密码学,Go 语言的“抗量子”底牌曝光

本文永久链接 – https://tonybai.com/2026/03/19/2025-turing-award-go-quantum-resistant-cryptography

大家好,我是Tony Bai。

就在昨天(2026 年 3 月 18 日),计算科学界的最高荣誉——ACM A.M. 图灵奖正式揭晓。2025 年的图灵奖,颁给了 Charles H. Bennett 和 Gilles Brassard 两位伟大的科学家,以表彰他们在“量子密码学(Quantum Cryptography)”和量子信息科学领域的开创性贡献。

或许你会觉得,图灵奖、量子力学、薛定谔的猫……这些高大上的词汇离我们每天 CRUD 的业务代码太遥远了。

但实际上,这场发端于理论物理界的革命,正在引发全球软件工程界一场最高级别的“红色预警”。

早期的图灵奖往往颁发给操作系统、数据库或编程语言的设计者(比如Unix 之父、B 语言(C 语言前身)以及 Go 语言联合设计者的Ken Thompson),而这次颁给量子密码学,传递出了一个极其明确的信号:传统的数字世界护城河,马上就要守不住了。

今天,借着图灵奖揭晓的热点,我想和大家聊一个极其硬核、且关乎我们所有后端开发者未来饭碗的话题:当“量子末日(Q-Day)”逼近,作为云原生时代绝对霸主的 Go 语言,手里究竟握着怎样的“抗量子底牌”?

你的数据,正被黑客“先存后破”

在理解 Go 团队的动作之前,我们必须先弄懂,为什么我们需要“后量子密码学(PQC)”?

目前,我们用来保护 HTTPS 流量、验证 JWT 登录、以及签署 Git 提交的底层基石,绝大多数是 RSA 或 ECC(椭圆曲线)算法 。这些算法的安全假设,建立在大质数分解和离散对数计算极其困难的数学事实上。

但早在 1994 年,Peter Shor 就提出了著名的 Shor 算法。该算法在数学上证明了:只要拥有一台足够规模的量子计算机,RSA 和 ECC 算法不仅能被破解,而且破解速度是指数级倍增的!

你可能会想:“量子计算机离真正商用还早着呢,急什么?”

黑客们可不这么想。现在全球的顶级黑客和某些国家级 APT 组织,正在疯狂执行一种名为 “Store Now, Decrypt Later”(先收集,后破解,SNDL) 的战略。

他们把现在截获的、由 RSA/ECC 加密的核心机密数据全部存储在硬盘里。等若干年后量子计算机成熟,他们就能在一瞬间把这些历史机密全部解开。

为了应对这场“降维打击”,美国国家标准与技术研究院(NIST)紧急发布了后量子密码学(PQC)的 FIPS 标准草案。而作为全球云基础设施底层语言的 Go,自然被推到了抗击量子危机的第一线。

Go 团队的“抗量子”谋略

如果你经常关注 Go 社区,你会发现 Go 核心团队早就确定了引入新密码学算法的策略。在 Go 官方仓库的 Issue #64537(crypto: post-quantum support roadmap)中,现任 Go 安全团队负责人 Roland Shoemaker 和 Go 密码学专家 Filippo Valsorda 明确抛出了 Go 在面对量子危机时的三大铁律:

  1. 绝对不当小白鼠:Go 标准库只实现那些结构已经绝对稳定、并在业界(如 WebPKI、TLS)被广泛验证的算法。那些还在实验阶段的半成品,一律拒之门外。
  2. “按需”引入,绝不盲目:PQC 算法分为两类,一类是密钥封装(KEM,用于加密和协商密钥),一类是数字签名(Signature,用于身份认证)。
  3. “内测”转“公测”机制:任何新的 PQC 算法,Go 都会先在 internal 包中悄悄跑几个版本,等把所有可能的开发者“误用坑”都踩平了,才会暴露为 Public API。

基于这套严谨的哲学,Go 团队打出了他们的第一张底牌:优先解决“先收集后破解”的威胁。

在 Go 1.24 中,Go 已经通过提案 #70122 和 #69985,在底层网络库中悄然集成了 ML-KEM(即 Kyber 算法)与 X25519 的混合密钥交换机制。(注:ML-KEM 从 Go 1.23 就以实验特性引入)

这意味着,如果你使用的是最新的 Go 版本构建的 HTTPS 服务,你的连接在建立之初,就已经具备了抵抗未来量子计算机窃听的能力!

密钥交换的问题解决了,那么用来证明身份的数字签名(Digital Signatures)呢?这就引出了 Go 团队即将放出的第二张王炸。

揭开 crypto/mldsa 的硬核源码

数字签名的重要性不言而喻:微服务之间的 mTLS 认证、固件升级包的防篡改、区块链的交易防伪,全靠它。

就在最近,Filippo 在 Go 官方 GitHub 上正式提交了 Issue #77626(proposal: crypto/mldsa: new package),提议在即将到来的 Go 1.27 中,正式向全世界暴露 ML-DSA(NIST FIPS 204 标准)的公有 API。

让我们剥开这层提案,看看顶级大厂架构师是如何设计这套跨时代 API 的。

极简的参数集隔离

ML-DSA 并不是一个单一算法,它包含了不同的安全级别。Go 提案非常干净利落地定义了三个常量函数:

func MLDSA44() Parameters // 推荐日常使用,安全级别相当于 AES-128
func MLDSA65() Parameters // 相当于 AES-192
func MLDSA87() Parameters // 极高安全级别,相当于 AES-256

开发者不需要去记忆复杂的参数结构,只需像拼积木一样调用。

拒绝“半展开密钥”,将安全做到极致

如果你看源码,会发现 NewPrivateKey 除了传入 params 参数集外,只需要传入一个极短的 seed(种子字节),而不是业内的“半展开密钥(Semi-expanded keys)”。

为什么?Filippo 在讨论中给出了让人拍案叫绝的解释:

“半展开密钥是一个极其糟糕的格式。它不仅占用空间更大,加载速度更慢,而且更危险。我们只会支持基于 Seed 的密钥派生。”

这体现了 Go 始终如一的安全哲学:如果一种格式有被开发者误用的风险,那就从 API 层面彻底物理隔绝它。

巧妙应对“预哈希(External μ)”难题

传统签名时,我们通常先用 SHA256 算个 Hash,再对 Hash 签名。但 ML-DSA 的底层数学机制非常复杂,它要求对 H(H(pubkey) || 0×00 || context || message) 进行极度严苛的处理。

Go 团队没有去破坏原有的 crypto.Signer 接口,而是极其巧妙地发明了一个“虚拟的占位符”:crypto.MLDSAMu。

这个常量虽然属于 Hash 类型,但它不支持被实例化,调用 New() 会直接引发 Panic。它仅仅作为一个“信号标记”传递给 SignerOpts,优雅地实现了向下兼容。

为什么我们还不能在 X.509 证书里用它?

看到这里,很多着急的开发者(尤其是一些政企、军工背景的开发团队,正面临 CNSA 2.0 强制要求在 2025 年升级 PQC 的死命令)在 Issue 里疯狂催问:

“API 都做好了,为什么不顺手把它集成进 crypto/x509 证书解析里?为什么还不让在 TLS 中直接使用 ML-DSA 证书?”

Filippo 的回答,直接揭露了目前后量子时代最尴尬的一个物理瓶颈,也展现了他作为世界级密码学家的极致架构克制

“如果我们现在就把 ML-DSA-87 塞进 TLS,你知道一个 TLS 握手包会变得多大吗?足足 19KB!

大家要知道,传统的 RSA 签名不过几百字节,ECC 签名更是只有几十个字节。我们过去 30 年的互联网协议(如 TCP/IP、TLS),都是建立在“签名数据极小、传输成本几乎为零”的物理假设上的。

如果你用 ML-DSA 给证书签名,证书链上一叠加,一次最普通的 HTTPS 握手,瞬间需要传输几十 KB 的数据。在移动网络弱网环境下,这会导致大规模的丢包、延迟飙升,甚至是全球互联网的“大塞车”。

为了通过安全审计而罔顾物理性能,这不是高级软件工程,这是在耍流氓。

Go 团队的判断是:我们有时间去设计更好的协议(比如使用 Merkle Tree 证书),而不是现在急功近利地把数万字节的“肥胖签名”强塞进原本轻巧的 TLS 隧道里。

这种“不将就”的架构底线,正是 Go 语言最迷人的地方。

小结:在不确定的未来中,拥抱底层逻辑

图灵奖颁给量子密码学,不仅是对 Bennett 和 Brassard 两位科学先驱的最高致敬,更吹响了全球软件工程界系统升级的冲锋号。

从优先落地对抗 SNDL 攻击的 ML-KEM,到极度克制、优雅设计的 crypto/mldsa,再到坚决抵制“19KB 肥胖握手包”的底线坚守。我们看到的是 Go 语言团队对工程效率、安全性与网络物理特性的深度掌控。

资料链接:

  • https://awards.acm.org/about/2025-turing
  • https://github.com/golang/go/issues/64537
  • https://github.com/golang/go/issues/77626

今日互动探讨

如果在未来两年,为了抗击量子计算机,我们所有的 HTTPS 请求都要变慢 200 毫秒,甚至服务器内存消耗要翻倍,你觉得这个代价值得吗?在你的业务线里,有面临密码学升级的强制合规要求吗?

欢迎在评论区分享你的看法!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

拉个 JSON 居然要装 5 个第三方库?终于明白 Go 的标准库到底有多“霸道”

本文永久链接 – https://tonybai.com/2026/03/11/standard-library-is-part-of-the-go-success

大家好,我是Tony Bai。

在现代软件开发中,我们似乎已经患上了一种名为“依赖上瘾”的绝症。

新建一个项目,你敲下的第一行命令大概率不是写业务逻辑,而是 npm install、cargo add 或者 pip install。我们潜意识里已经默认:语言本身只提供最基础的砖块,稍微高级一点的功能(比如发起个网络请求、解析个 JSON),都必须去浩如烟海的开源社区里“淘金”。

但这种习以为常的生态繁荣,真的是一件好事吗?

近日,在 Reddit 的 r/golang 社区,一个题为《标准库是 Go 成功的一部分吗?》的帖子,像一颗深水炸弹,炸出了无数程序员对于“依赖地狱(Dependency Hell)”的疯狂吐槽。

发帖人分享了一个极其真实且让人啼笑皆非的日常小故事:

他想写一个微型应用,目的非常单纯——从家里的太阳能光伏电池 Web 服务器上抓取一个 JSON 文件,解析出来,然后把能源数据显示在屏幕上。

他首先用 Go 语言写了一版。极其丝滑,仅靠自带的标准库就搞定了网络请求和 JSON 解析,编译出一个干干净净的二进制文件,直接跑通。

几天后,他闲来无事,想测试一下其他编译型语言:

  • 他尝试了 D 语言,发现在不依赖第三方库的情况下,D 语言根本无法在三大主流操作系统上顺利完成“下载并解析 JSON”这个基础任务。
  • 他转头去折腾目前红得发紫的 Rust,结果发现,如果不借助 reqwest(处理 HTTP)和 serde(处理 JSON)这两个庞大的第三方 Crates,面对这个简单的需求,他同样寸步难行。
  • 一圈折腾下来,只有 Nim 勉强做到了原生支持。

这个看似不起眼的小实验,无意间撕开了现代软件工程一块遮羞布,也揭示了 Go 语言在后端开发中一个极其“霸道”、却常被新手低估的绝对优势:降维打击般的标准库(Standard Library)。

今天,我们就来深度剖析一下,为什么大量工程师越来越偏爱 Go 这种“零依赖”的极简哲学。

你以为你在写代码,其实你在做“库的选品”

在很多主打“生态繁荣”的编程语言中,标准库被视为一种“最小公集”。语言的设计者把高级特性推给社区,美其名曰“保持语言的核心轻量”。

这听起来很美好,但在实际的商业工程中,它带来了一个极其消耗心智的隐性成本:决策疲劳(Decision Fatigue)

想象一下,当你用 Node.js 或者 Rust 仅仅需要发起一个异步 HTTP 请求时,你需要经历怎样痛苦的内心戏?

  1. 打开包管理网站,搜索 “http client”。
  2. 面对排名前 5 的主流库,你开始像个电商买手一样比对:A 库的 Star 数最高但半年没更新了;B 库的 API 最优雅但是性能测试差点意思;C 库支持最新的异步模型但文档写得像天书。
  3. 你甚至还要去翻看它们的 GitHub Issues,看看有没有致命的内存泄漏。
  4. 纠结了一下午,终于选定了一个库,引入依赖,然后开始痛苦地学习它那套独创的 API 调用法则。

而在 Go 中,这一切内耗根本不存在。

正如 Reddit 帖子评论区一位资深 Gopher 一针见血指出的:

“Go 的成功不仅在于它轻量、简单、易学,还在于它自带了一个庞大且极其优秀的标准库。因此,在开始处理每个微小的子任务之前,你不需要去评估一堆第三方库。”

Go 的哲学是“开箱即用”。net/http 就在那里,encoding/json(以及json/v2) 就在那里。它直接消灭了你在技术选型上的无意义内耗,让你可以把 100% 的脑力,全部砸在能给公司赚钱的业务逻辑上。

不是所有的标准库,都敢叫“生产级”

看到这里,Python 开发者可能会不服气:“Python 也有非常丰富的标准库啊,我们叫 Batteries included(自带电池)!”

没错,Python 的标准库确实庞大,但问题在于:它好用吗?它能直接扛高并发吗?

Python 自带的 urllib API 设计得极其反人类,导致全网的 Python 教程都在教你第一时间去 pip install requests。

如果你提供的标准库只是一个“能跑就行”的玩具,开发者迟早还是要逃向第三方库的怀抱。其他语言的标准库,大多只敢称自己是“开发级(Dev-level)”的替代品。

但 Go 的标准库,是真正意义上的“生产级(Production-ready)”。

以 Go 的 net/http 为例。它不仅仅是能发个请求那么简单,它底层直接内置了工业级的连接池、自动支持 HTTP/2、拥有极其精细的超时控制,并且在骨子里完美契合了 Go 的 Goroutine 并发模型。

在这个世界上,有无数估值数十亿美元的独角兽公司,他们的高并发微服务底层,没有套 Nginx,没有套 Tomcat 或 Gunicorn,而是直接裸跑在 Go 标准库的 net/http.Server 之上! 这在其他语言的生态里,简直是不可想象的。

同样,Go 的 crypto 包也不是随便拼凑的开源算法,它是由谷歌著名的密码学家亲自操刀设计和维护的。它被全球安全界公认为是业界最安全、最难被开发者“误用”的密码学实现之一。

每一次引入第三方库,都是在给系统埋雷

在现代软件工程中,有一句极其沉重的话:“依赖即债务”

你想要一个香蕉,但开源社区给你的是一只拿着香蕉的大猩猩,以及大猩猩背后的一整片热带雨林。你敲下的每一个 npm install,都在把公司的核心系统暴露给未知的风险。

前几年的 Java Log4j 史诗级漏洞事件,以及三天两头上头条的 NPM 恶意投毒、删库跑路事件(比如著名的 left-pad 事件),给全行业上了血淋淋的一课。当你引入一个计算日期的第三方包时,它可能又间接依赖了 50 个你闻所未闻的子依赖,其中哪怕有一个包的作者被黑客盗了号,你的服务器底裤就被看穿了。

发帖的楼主深刻地探讨了这一点:

“保持项目没有外部依赖,让维护变得更加容易。开发者经常忘记,向项目中添加一个依赖,就增加了一份审查恶意代码的责任。”

Go 强大的标准库,为你提供了一道天然的“供应链安全护城河”。

像前面提到的“拉取光伏面板 JSON 并解析”这样的任务,在 Go 中是零外部依赖的。

零外部依赖,就意味着零第三方供应链风险。这种“自给自足”的底气,在如今极度苛求数据安全、合规性审计的企业级开发中,绝对是降维打击般的加分项。

被忽视的跨平台与 Unicode 魔法

除了宏观的网络和并发处理,Go 的标准库在极其底层、却又极其折磨人的领域,展现出了极其深厚的内功。

熟悉 C/C++ 的老兵一定懂得,在底层处理多语言编码(locales)和宽字符(wide chars)是一场怎样的噩梦。而 Go 的标准库原生且完美地接纳了 UTF-8。从 strings 包到 unicode/utf8,再到字符串底层极其优雅的字节切片(Byte Slice)设计,让多语言文本处理变得如同呼吸一般自然。

更不用提 Go 那近乎魔法的跨平台交叉编译

Go 的标准库(如 os、path/filepath)对底层操作系统的 API 差异进行了极致的抽象。作为开发者,你可以在一台舒舒服服的 Mac 上写代码,只需加一个环境变量 GOOS=linux,就能瞬间利用标准库编译出一个毫无平台依赖的静态二进制文件,直接扔到 Ubuntu 服务器上完美运行。

这种抽象能力,让一切第三方跨平台打包工具都显得极其多余。

Go 1 的承诺,十年前的代码今天依然能跑

最后,Go 的标准库之所以被几百万开发者绝对信任,离不开 Go 团队当年立下的一个近乎严苛的誓言:Go 1 兼容性保证(Go 1 Compatibility Guarantee)

这意味着什么?这意味着你在 2012 年基于 Go 1.0 标准库写下的一段处理 HTTP 的代码,在今天最新的 Go 1.26 编译器下,不仅能一字不改地编译通过,而且运行行为保持绝对一致!

在任何其他语言的开源生态中,很多曾经辉煌一时的第三方霸主库,都会因为作者的精力衰退、兴趣转移或资金断裂,最终走向被废弃(Deprecated)的命运。当你依赖的库停止维护时,你的整个项目组都要被迫进行痛苦的代码大重构。

开源世界充满了不确定性,而 Go 的标准库,背后站着的是谷歌顶级的工程团队,拥有与这门语言同等漫长的寿命周期。

这种确定性的安全感,是任何高星的第三方库都无法给予你的。

写在最后:最好的工具,就是让你感受不到它的存在

我们常说,Go 是一门为“大规模软件工程”而生的语言。

这种工程基因,不仅仅体现在它的极速编译和极简语法上,更深深地烙印在它那套“霸道”的标准库里。

它逼着你放下对“奇技淫巧”的追求,逼着你放弃花里胡哨的第三方依赖,回归到用最稳固的基石,构建最健壮的系统的正道上来。

当然,Go 的标准库并不完美,比如千呼万唤始出来的官方 UUID 至今仍让社区望眼欲穿。但在构建现代云原生应用、微服务 API 和数据网关时,它依然交出了一份近乎满分的答卷。

它告诉了所有高级架构师一个硬道理:最好的工具,是让你感受不到工具存在的工具;最强大的库,是让你根本不用去寻找库的库。


今日互动吐槽

你在平时的开发中,被哪个第三方库(依赖地狱)狠狠坑过?或者你觉得 Go 的标准库里,现在最缺哪个核心功能?

欢迎在评论区开喷吐槽!


认知跃迁:读懂底层骨架,才能驾驭“降维打击”

很多写了几年 CRUD 的朋友问我:“Tony 老师,既然 Go 的标准库这么牛,那我只要背熟标准库的 API 是不是就能进大厂了?”

大错特错。会调 API 只是技工,看懂底层设计才是架构师。

Go 语言“少即是多”的工程美学,其精髓并不在于它提供了什么函数,而在于它是如何用极简的代码,实现千万级并发与跨平台抽象的。比如 net/http 背后那精妙的 Goroutine 调度模型,比如 context 是如何控制全局超时的。

如果你渴望突破技术瓶颈,不再满足于做一个“只会调包的熟练工”,而是想从骨子里吃透 Go 的系统级设计思维——

我的全新极客时间专栏 《Go语言进阶课》正是为你量身打造。

在这 30+ 讲硬核内容中,我将带你剥开语法糖,深入标准库与并发模型的底层骨架,锻造你编写高可用、生产级微服务的顶级工程实践能力。

目标只有一个:助你完成从“Go 熟练工”到“能做架构决策的 Go 专家”的蜕变!

扫描下方二维码,加入专栏,让我们一起深挖这门语言背后的“降维打击”之力。


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!

我们致力于打造一个高品质的 Go 语言深度学习AI 应用探索 平台。在这里,你将获得:

  • 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。
  • 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等,掌握 AI 时代新技能。
  • 星主 Tony Bai 亲自答疑: 遇到难题?星主第一时间为你深度解析,扫清学习障碍。
  • 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术,碰撞思想火花。
  • 独家资源与内容首发: 技术文章、课程更新、精选资源,第一时间触达。

衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

img{512x368}


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

拒绝无效告警!用 Govulncheck 构建高信噪比的 Go 安全扫描工作流

本文永久链接 – https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow

大家好,我是Tony Bai。

在当今的软件开发流程中,持续集成/持续部署(CI/CD)和自动化的安全左移(Shift Left)已经成为行业共识。在这个大背景下,诸如 GitHub Dependabot 这样的自动化依赖更新工具应运而生,并迅速占据了几乎每一个开源项目和商业级代码库的 Repository 设置。它们不知疲倦地扫描 go.mod,一旦发现有依赖项爆出 CVE 漏洞,就会自动生成一个拉取请求(Pull Request, PR),仿佛是在告诉你:“别担心,我已经帮你修好了。”

然而,事实真的如此美好吗?

近日,密码学领域的权威专家、前 Google Go 安全团队负责人 Filippo Valsorda 在其个人博客上发表了一篇极具冲击力的文章,标题直截了当:“TURN DEPENDABOT OFF”(关掉 Dependabot)。他毫不客气地指出,这款被无数开发者信赖的工具,实际上是一个“噪音制造机”(Noise Machine)。它不仅浪费了开发者的宝贵精力,更在无形中损害了整个 Go 生态系统的安全根基。

作为 Go 开发者,我们该如何审视这种看似“政治正确”的安全自动化工具?如果不使用 Dependabot,我们又该如何保卫代码库的安全?本文将深度剖析 Filippo 的核心观点,揭示传统版本比对扫描的致命缺陷,并手把手教你如何利用官方推荐的 govulncheck 构建真正高效、高信噪比的现代化 Go 安全扫描工作流。

安全自动化的幻象与“告警疲劳”

为了理解 Filippo 为什么如此强烈地反对 Dependabot 这种类型的扫描工具,我们需要先剖析软件工程心理学中的一个经典问题:告警疲劳(Alert Fatigue)

什么是告警疲劳?

告警疲劳是指操作人员或开发人员在长时间暴露于频繁且大量低价值(即假阳性、False Positives)的系统警告下,逐渐变得对这些警告麻木、脱敏的现象。

在医疗领域,如果重症监护室的心电监护仪总是因为轻微干扰而发出刺耳的警报声,护士最终可能会忽略真正的病危信号;在网络安全领域,如果防火墙每天产生一万条拦截记录,安全分析师就不可能从中挑出那一条真正的 APT 高级持续性威胁。


图:Dependabot alerts

在软件开发中,Dependabot 完美地扮演了那个“总是狼来了”的角色。它带来的不是安全感,而是一种虚假的工作充实感。正如 Filippo 所言:“它让你感觉自己好像在做有用的工作,但实际上你是在阻碍真正有用的工作。”

传统版本扫描的致命缺陷:一刀切的模块级匹配

Dependabot 和大多数传统的软件成分分析(SCA)工具一样,其工作原理极其简单粗暴,可以概括为基于版本的字符串比对

以 Go 语言为例,它们的逻辑是这样的:
1. 解析你的 go.mod 和 go.sum 文件,列出你所使用的所有依赖模块(Module)及其版本(如 github.com/foo/bar v1.0.0)。
2. 查询公共漏洞数据库(如 NVD)。
3. 如果数据库显示 github.com/foo/bar 在 < v1.2.0 时存在某个漏洞,且你的版本在这个范围内,立刻生成一个高危告警,并创建一个将版本升级到 v1.2.0 的 PR。

在某些动态类型语言(如 Ruby 或早期 JavaScript)生态中,这种方法或许是唯一可行的。但在 Go 语言这样强调静态类型、拥有明确抽象边界和包级结构的生态中,这种“模块级”的一刀切匹配就显得极其愚蠢和低效。

真实案例分析:edwards25519 漏洞风波

为了让这个问题更加具象化,Filippo 在文章中分享了一个他亲身经历的“案发现场”。

不久前,Filippo 为他维护的密码学基础库 filippo.io/edwards25519 发布了一个安全修复版本(v1.1.1)。这个库在 Go 生态中举足轻重,被数十万个开源项目间接依赖。然而,这个漏洞的触发条件极其苛刻:

漏洞仅存在于 (*Point).MultiScalarMult 这个非常高级且罕用的 API 方法中,且只有当该方法的接收者(Receiver)不是初始的 identity point 时才会产生未定义的行为。

现实情况是:在整个 Go 生态系统中,几乎没有任何项目实际调用了这个存在缺陷的特定方法。 大多数依赖该库的项目(比如著名的 github.com/go-sql-driver/mysql 库,拥有 22.8 万以上的依赖者)仅仅是导入了该库的其他基础功能,与有漏洞的代码路径八竿子打不着。

Dependabot 的反应是什么?

灾难性的噪音。Dependabot 不分青红皂白,仅仅因为版本号低于 v1.1.1,就向 GitHub 上的数千个甚至根本不受影响的 Repository 发送了疯狂的更新 PR。更糟糕的是,这些 PR 附带了由算法自动生成的、耸人听闻的、根本不合逻辑的 CVSS v4 漏洞评分,以及所谓的“73% 兼容性风险警告”。

结果就是,无数个深夜,开源项目的维护者们收到了刺耳的安全警报,被迫中断手中的工作,去 review 一个修改了一行他们压根用不到的代码的依赖升级 PR。如果他们不合并,项目上就会一直挂着一个红色的“安全风险”标签;如果他们机械地合并了,这就成了“告警疲劳”的典型发作。

Filippo 一针见血地指出这种行为的荒谬性:

“由于扫描器未能过滤掉无关的漏洞,这种额外的劳作被硬生生地扔到了开源维护者的脚下,这是不可持续的。维护者的责任是确保项目不受安全漏洞影响;而扫描工具的责任是确保它们不会用假阳性告警去打扰用户。

当升级依赖(Dependency bump)成为一种应付扫描工具的机械动作,而不是基于对漏洞影响的真实评估(如是否需要轮换生产环境的密钥、是否需要通知受影响的用户),我们距离真正的安全就已经越来越远了。

拥抱静态分析,Govulncheck 的降维打击

既然基于版本的 Dependabot 如此不堪,我们应该如何科学地防范软件供应链安全风险?

答案是:抛弃盲目的版本匹配,使用严肃的、基于静态代码分析的漏洞扫描器。 计算机完全有能力为你完成过滤无用噪音的工作。在 Go 语言生态中,这个“杀手级”的工具就是官方出品的 govulncheck

丰富的 Go 官方漏洞数据库

要实现精准的扫描,首先需要高质量的数据源。这正是 Filippo 在 2020 年至 2021 年领导 Go 安全团队时极力推动的战略——投入大量资源建设 Go 官方漏洞数据库(Go Vulnerability Database)

与一般只记录模块版本和一段文字描述的 CVE 库不同,Go 漏洞数据库包含了极其丰富的、机器可读的元数据。它严格遵循标准的 OSV (Open Source Vulnerability) 格式。

让我们看看前面提到的 edwards25519 漏洞(GO-2026-4503)在数据库中的记录:

modules:
  - module: filippo.io/edwards25519
    versions:
      - fixed: 1.1.1
    vulnerable_at: 1.1.0
    packages:
      - package: filippo.io/edwards25519
        symbols:
          - Point.MultiScalarMult   # 关键所在:精确到了有漏洞的具体方法!

请注意最底部的 symbols 字段。Go 安全团队并没有笼统地标记整个模块不安全,而是像外科手术刀一样,精准定位到了那个有缺陷的方法 Point.MultiScalarMult。这就为后续的精准静态分析提供了弹药。

Govulncheck 的核心优势:基于可达性分析

有了精确到“符号(函数/方法)”级别的数据源,govulncheck 就可以对你的代码库施展“降维打击”了。相比于 Dependabot,它具有两大碾压级的优势:

优势一:包级别的过滤

Go 语言的模块通常由多个子包(Packages)组成,这是良好的代码组织习惯。如果一个漏洞发生在模块的 pkgA 中,而你的代码只导入了 pkgB,你显然是安全的。

任何合格的漏洞扫描器至少应该做到这一层过滤。实际上,这只需要执行一次简单的 go list -deps ./… 命令即可分析出包依赖关系。Dependabot 甚至连这基本的一步都没有做到,导致了大量的假阳性。

优势二:基于调用图的符号可达性分析

这是 govulncheck 引以为傲的黑科技。它不仅知道你引入了哪些包,它还会像编译器一样分析你的代码,构建出一棵完整的函数调用图(Call Graph)

当扫描器运行时,它会沿着调用链路一路追溯:从你的 main 函数或测试入口开始,顺着你的业务逻辑,追踪到你调用的第三方库,再追踪到第三方库调用的更底层的库……

如果 govulncheck 发现,存在漏洞的那个特定函数(比如 Point.MultiScalarMult),在这棵庞大的调用树中根本不可达(即没有任何一条代码执行路径会调用到它),那么它就会保持沉默。

让我们看看实际的运行效果。如果你的项目只使用了 go-sql-driver/mysql,并且运行 govulncheck:

$ govulncheck ./...
=== Symbol Results ===
No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 2
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.

看,结果多么清爽!

govulncheck 明确地告诉你:“我看到了你的依赖树里有一个有漏洞的模块,但是不用慌,你的代码逻辑根本没有触碰到那个雷区,你是安全的。”

这种极高的信噪比,是 Dependabot 永远无法企及的。它把安全专家的宝贵时间,留给了真正需要紧急响应的致命漏洞,而不是在日常的升级杂务中消耗殆尽。

重塑现代 Go 项目的 CI/CD 工作流

如果你被 Filippo 的观点说服,决定彻底关闭 Dependabot 的安全警报,那么你必须建立一套更为科学的自动化机制来接管依赖管理和漏洞检测的工作。

Filippo 给出了非常具体的行动指南:用两个定时执行的 GitHub Actions 替换 Dependabot。

行动一:部署独立的 Govulncheck 定时扫描任务

你应该每天定时运行一次 govulncheck。它的作用是充当真正有价值的安全哨兵。

name: Govulncheck Scan
on:
  push:
    branches: [ "main" ]
  pull_request:
  schedule:
    # 每天 UTC 时间 10:22 执行
    - cron: '22 10 * * *'
  workflow_dispatch:

permissions:
  contents: read

jobs:
  govulncheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          persist-credentials: false

      - uses: actions/setup-go@v6
        with:
          go-version-file: go.mod

      - name: Run govulncheck
        run: |
          go run golang.org/x/vuln/cmd/govulncheck@latest ./...

为什么这个 Action 不会自动开 PR?

这是深思熟虑后的设计。如果 govulncheck 报警并导致 CI 失败,这意味着:你的代码明确且切实地调用了一个有已知漏洞的函数。

此时,情况已经相当严重了。你不能仅仅是指望像机器人一样点击“Merge”升级一个版本就万事大吉。你需要人类工程师介入:

  1. 评估该漏洞在你的特定业务上下文中是否可被利用。
  2. 检查是否有数据泄露。
  3. 评估是否需要紧急轮换生产环境的数据库凭证、API 密钥或 JWT 签名密钥。
  4. 手动更新依赖,运行详尽的回归测试,然后再部署上线。

把安全审计权交还给人类大脑,这才是对工程负责的态度。

行动二:测试最新的依赖项,而不是盲目更新

有人会反驳:可是 Dependabot 除了报安全漏洞,还能帮我们保持依赖常新,避免未来积累过多的技术债啊!

Filippo 认为,这种做法同样陷入了误区。

依赖的更新节奏,应当服从于你自身项目的开发周期和发布节奏,而不是被你的上游库作者的发布频率牵着鼻子走。例如,你应该在决定发布下一个主要版本时,集中精力进行一次依赖升级和全面测试,而不是天天被各种次要版本的更新 PR 打扰。

但是,保持对上游变化的敏感度同样重要。如果我们不天天更新,等真正需要安全更新时,可能会因为版本跨度太大而遭遇严重的 API 不兼容(Patch Delta 过大)。

Filippo 提出的巧妙解法是:每天在 CI 中,使用你所有依赖的最前沿版本运行一次你的测试套件。

name: Go Nightly Tests against Latest Dependencies
on:
  schedule:
    # 每天运行
    - cron: '22 10 * * *'

# ... 省略部分环境配置 ...

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        go:
          - { go-version: stable }
          - { go-version-file: go.mod }
        deps:
          - locked  # 针对锁定版本的 go.mod 运行测试
          - latest  # 针对最新版本依赖运行测试
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-go@v6
        with:
          go-version: ${{ matrix.go.go-version }}

      - name: Run tests with sandboxed CI environment
        uses: geomys/sandboxed-step@v1.2.1
        with:
          run: |
            if [ "${{ matrix.deps }}" = "latest" ]; then
              # 关键指令:将所有依赖临时拉取到最新版本,但不修改 go.mod
              go get -u -t ./...
            fi
            go test -v ./...

这种策略的双赢之处:

  1. 零打断的早期预警:你的测试套件每天都在与最前沿的第三方代码搏斗。一旦某个上游库发布了一个引发不兼容的改动,你的每日 CI 就会立刻失败并向你报警,你可以在闲暇时从容应对,而不需要在某个紧急修复的当口被卡住。
  2. 极简的代码库:只要测试通过,你根本不需要去修改 go.mod 提交没必要的版本跳跃。你的仓库历史依然干净。

进阶安全提示:防范 CI 投毒

当你在 CI 中运行 go get -u 时,你实际上是在无审查的情况下执行可能包含了恶意代码的第三方库(尤其是在执行测试时)。为了缓解供应链攻击带来的风险,Filippo 强烈推荐在执行此类测试时引入安全沙箱机制。在上述配置中,geomys/sandboxed-step 是一个基于 gVisor 的沙盒工具,它收回了工作流脚本对 GitHub 环境变量、机密信息以及不必要网络的访问权,确保即使拉取到了恶意的依赖包,它也无法窃取凭证或进行横向移动。这种防御深度,展现了前 Google 安全专家一贯的严谨。

小结:让工具回归辅助的本位

从盲目轻信机器人的批量 PR,到利用编译原理和图论(可达性分析)进行精准手术刀式的漏洞定位,Filippo Valsorda 给 Go 社区上了一堂生动的工程哲学课。

自动化绝不是推卸责任的借口。作为一个成熟的软件开发团队,我们应当停止对“警报数量”的崇拜,转而追求“警报质量”。关闭那些让你产生疲劳的噪音机器,配置好你的 govulncheck,把精力集中在真正需要人类智慧去解决的架构演进和安全设计上。

这不仅是 Go 语言最佳实践的一次更迭,更是我们在面对日益复杂的软件供应链时,应有的冷静与定力。

资料链接:https://words.filippo.io/dependabot/


你被 Dependabot “骚扰”过吗?

自动生成的 PR 虽然方便,但也可能成为开发者的负担。在你的项目中,你是选择一键合并所有的安全更新,还是会仔细评估漏洞的真实影响?你会考虑关掉 Dependabot 的警报,转而投奔 Govulncheck 吗?

欢迎在评论区分享你的安全治理心得!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

别再轻信 GitHub 上的源码:为何我们需要全新的 Go 模块审查机制?

本文永久链接 – https://tonybai.com/2026/02/20/why-we-need-new-go-module-review-mechanism

大家好,我是Tony Bai。

你以为你在 GitHub 上看到的代码,就是你的 Go 程序编译时使用的代码吗?答案可能令你背脊发凉。

在 Go 语言的生态系统中,我们一直引以为傲的是其卓越的包管理和安全性。Go Checksum Database(校验和数据库)被公认为现代编程语言中最强大的完整性保障机制之一。然而,前 Go 安全团队负责人、著名的密码学家 Filippo Valsorda 在最近的一篇文章中揭示了一个令人不安的真相:虽然 Go 的工具链是安全的,但我们人类审查代码的方式却存在巨大的安全漏洞。

本文将深入探讨这一安全隐患的成因,剖析著名的“虚假 BoltDB”攻击案例,并介绍 Filippo 及其团队 Geomys 推出的解决方案——pkg.geomys.dev,一个致力于填补这一信任缺口的源码查看服务。

Go 的安全基石:坚不可摧的 SumDB

在深入探讨漏洞之前,我们有必要先回顾一下 Go 语言为何被誉为拥有“无可争议的最佳包完整性故事”。这主要归功于 Go Checksum Database (SumDB)。

Go 模块的获取本质上是去中心化的。你可以直接从 GitHub、GitLab 或任何 Git 托管服务上拉取代码。例如,当你运行 go get github.com/example/mod@v1.2.3 时,Go 工具链(在 GOPROXY=direct 模式下)会直接克隆对应的 Git 仓库并检出 v1.2.3 标签。

这种去中心化虽然灵活,但带来了巨大的安全风险:如果代码托管方(如 GitHub)被入侵,或者作者遭受胁迫修改了代码,亦或是作者恶意 Force-push(强制推送)覆盖了标签,下游用户该如何察觉?

SumDB 应运而生。它的工作原理如下:

  1. 首次记录:当某个模块版本第一次被 Go 生态系统中的任何人请求时,Go 代理(Proxy)会下载该模块,计算其内容的加密哈希值,并将其永久记录在 SumDB 中。
  2. 永久锁定:SumDB 是一个透明日志(Transparency Log),类似于区块链的 Merkle Tree 结构。这意味着记录一旦写入,就无法被篡改或删除(即使是 Google 也做不到)。
  3. 全网一致:此后,世界上任何一台机器下载该版本的模块时,Go 工具链都会计算本地下载内容的哈希,并与 SumDB 中的记录比对。如果 GitHub 上的标签被篡改导致哈希不匹配,构建将直接失败。

这种机制比传统的 PGP 签名或作者管理私钥要实用得多,同时提供了极高的安全性保障。

信任链的断裂:人类的“弱点”

既然 SumDB 如此完美,漏洞从何而来?

Filippo 指出,漏洞不在于机器,而在于人。

每当我们直接在代码托管平台(如 GitHub)上阅读代码时,我们就引入了一个薄弱环节。

Go 工具链验证的是下载到本地缓存中的 ZIP 包的哈希值。而我们在浏览器中打开 https://github.com/example/mod/blob/v1.2.3/exp.go 时,看到的是 GitHub 当前展示的 v1.2.3 标签对应的内容。

关键问题在于:Git 标签是可变的(Mutable)。GitHub 允许维护者强制推送标签。一个恶意的维护者(或攻击者)可以这样做:

  1. 发布一个包含恶意代码的 v1.2.3 版本。
  2. 诱导受害者(或通过自动化的 Go Proxy)下载该版本,使其恶意哈希被记录在 SumDB 中。
  3. 立即 Force-push 一个“干净”的 v1.2.3 版本覆盖原标签。
  4. 当安全研究员或用户去 GitHub 审查代码时,他们看到的是干净的代码,认为一切正常。
  5. 但受害者的 go.sum 中已经锁定了那个恶意的哈希,他们的构建使用的是恶意代码。

这种“狸猫换太子”的攻击方式,利用了 Web 界面(GitHub)与构建工具(Go Toolchain)之间的数据源不一致。

真实案例回顾:虚假 BoltDB 投毒事件

这并非理论上的恐慌,而是已经发生的现实。

去年,Go 生态系统遭受了一次经典的域名抢注(Typosquatting)攻击。攻击者发布了一个名为“BoltDB”的虚假模块(利用了大小写或相似名称的混淆)。为了掩人耳目,攻击者利用了上述机制:

  • 恶意代码被发布并被 Go Proxy 缓存。
  • 随后,攻击者向 GitHub 强制推送了无害的代码。
  • 当社区发现有可疑模块并试图去 GitHub 审查时,看到的只有人畜无害的代码逻辑。

当时,一些评论员错误地将此归咎于 Go Module Mirror 的缓存机制。但 Filippo 一针见血地指出:这本质上是利用了 GitHub Web 界面天然缺乏验证机制的漏洞。GitHub 展示的代码,并不是 Go 工具链正在使用的、经过 SumDB 验证的“真实”代码。

如何正确地审查 Go 模块?

既然 GitHub 不可信,作为开发者,我们该如何确保自己在审查“正确”的代码?

方案 A:本地硬核审查(CLI)

最安全的方法是将 Go 工具链实际使用的代码下载到本地进行审查。Filippo 给出了一个基于命令行的解决方案:

cd $(go mod download -json filippo.io/age@v1.3.1 | jq -r .Dir)

这条命令做了三件事:

  1. go mod download:通过 Go 代理下载指定版本的模块,并自动进行 SumDB 校验。
  2. -json:输出模块的元数据,包括其在本地缓存中的解压路径。
  3. cd:直接进入该目录。

在这个目录中看到的代码,才是绝对真实、不可抵赖的代码。此外,Go 团队也正在开发 go mod verify -tag 命令(预计将在Go 1.27版本落地),用于验证本地 Git 仓库的内容是否与 SumDB 匹配,这将进一步简化本地审查流程。

方案 B:全新的在线审查工具——pkg.geomys.dev

虽然本地审查最安全,但不得不承认,在浏览器中点击 pkg.go.dev 的链接查看源码实在是太方便了。为了在“便利性”和“安全性”之间取得平衡,Filippo Valsorda 开发了一个全新的服务:pkg.geomys.dev

这是一个类似于 go-mod-viewer 的源码查看器,但它在设计上完全针对安全性与现代体验进行了优化。它的核心价值在于:展示经 Go Proxy 和 SumDB 确认的、真实的 ZIP 包内容,而非 GitHub 上的 Git 仓库内容。

其核心特性包括:

  1. 真实源头:它不克隆 Git 仓库,而是直接处理 Go 模块的 ZIP 归档文件。这确保了你看到的代码与 go get 下载的代码完全一致。
  2. 优秀的阅读体验:支持语法高亮、行/多行链接、多种字体选择、自动暗色模式,以及完整的文件树和版本浏览器。
  3. 浏览器插件支持:Filippo 提供了 Chrome 和 Firefox 插件。安装后,当你在官方的 pkg.go.dev 上点击源码链接时,它会自动将原本指向 GitHub 的链接重定向到 pkg.geomys.dev,实现无缝的安全升级。

它是如何工作的呢?

这个服务的实现非常精妙,充分利用了现代 Web 技术:

  • HTTP Range 请求:它不需要下载整个模块的 ZIP 包。通过 HTTP Range 请求,它只获取 ZIP 文件的目录结构和特定文件的压缩数据。
  • 浏览器端解压:解压缩过程直接在用户的浏览器中完成。这不仅减轻了服务器压力,也提高了响应速度。
  • 未来的去中心化:目前的版本信任 Google 的 Module Proxy 提供的 ZIP 文件。Filippo 计划在未来(待 proxy.golang.org 修复 CORS 配置后)引入透明日志证明检查。届时,浏览器将能独立计算目录哈希(Dirhash),并与 SumDB 进行比对,甚至通过第三方八卦协议(Gossip)验证 SumDB 的一致性,从而实现真正的“零信任”安全查看。

对 Go 生态系统的启示

Filippo 的这项工作(以及背后的 Geomys 组织)不仅仅是造了一个轮子,它向整个软件供应链安全领域提出了一个严肃的问题:我们所依赖的基础设施,是否能够支撑“代码即法律”的信任?

长期以来,我们将 GitHub 视为代码的“真理之源”。但在现代包管理机制下,真理之源已经转移到了不可篡改的构件(Artifacts)和透明日志上。Go 语言通过 SumDB 先行一步,而工具链的配套设施(如 IDE、代码浏览器)也必须跟上这一步伐。

此外,Geomys 组织的运作模式也值得关注。它是由 Ava Labs、Teleport、Tailscale 和 Sentry 等知名科技公司资助的专业维护者组织。这种通过商业公司联合资助关键开源基础设施维护者的模式,或许是解决开源可持续性问题的一条新出路。

小结:与行动建议

作为一名负责任的 Go 开发者,我们应当意识到“便利”背后的代价。为了防止下一个“虚假 BoltDB”事件发生在你的项目中,我们建议:

  1. 改变习惯:在进行安全性要求较高的代码审查(Security Review)时,不要盲目信任 GitHub 的 Web 界面
  2. 尝试新工具:安装 pkg.geomys.dev 的浏览器插件,将你的默认源码查看器切换到更安全的模式。这不仅是为了安全,也是为了获得比 GitHub 更纯粹的阅读体验。
  3. 理解机制:深入理解 go.sum 和 SumDB 的工作原理。它们不是为了给 Git 仓库做备份,而是为了构建一个独立于代码托管商之外的信任锚点。

安全,往往隐藏在这些看似微不足道的细节之中。


参考链接:


你会怎么审代码?

习惯了在网页上“指点江山”的我们,可能都忽略了 ZIP 归档才是唯一的真理。在你的开发流程中,是否也曾遇到过 GitHub 源码与本地代码不一致的“灵异事件”?你会为了安全而安装那个将链接重定向到 pkg.geomys.dev 的插件吗?

欢迎在评论区分享你的安全观!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

Linux 桌面系统故障排查指南(一) - 系统启动与安全框架

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。

前言

本文将简要介绍 Linux 桌面系统的启动机制,从 UEFI 引导到内核加载,从 initramfs 到 systemd 服务启动,再到桌面环境加载。同时还会探讨系统的安全框架,了解 PAM、PolicyKit 等组件如何保护系统安全。


1. 系统启动流程

启动的四个关键阶段

Linux 桌面系统的启动过程可以分为以下几个主要阶段:

  1. 固件阶段:UEFI 固件初始化硬件
  2. 引导加载器阶段:加载内核和 initramfs
  3. 内核阶段:硬件探测和驱动加载
  4. initramfs 阶段:准备根文件系统
  5. 用户空间阶段:systemd 接管系统管理

UEFI:系统启动的起点

现代系统普遍使用 UEFI 固件 代替 BIOS。UEFI 初始化硬件后,从 EFI System Partition (ESP) 中加载启动管理器。

NixOS 在 UEFI 系统上支持多种引导加载器。默认使用 GRUB;启用 Secure Boot 时通常使用systemd-boot 配合 lanzaboote

systemd-boot 的全局配置是 /boot/loader/loader.conf,具体的启动项配置需要分类讨论:

  • Type 1:手动配置 (Boot Loader Specification Type #1

    • 配置方式:/loader/entries/*.conf,位于 EFI 系统分区(ESP)或 Extended Boot Loader Partition(XBOOTLDR)下

    • 特点:

      • 可自定义启动项名称、内核参数、initrd 等
      • 描述 Linux 内核及其 initrd,也可以描述任意 EFI 可执行文件
      • 包括 fallback / rescue 内核
    • 示例:

      title   NixOS Linux
      linux   /vmlinuz-linux
      initrd  /initrd-linux.img
      options root=UUID=xxxx rw
  • Type 2:统一内核镜像 (Boot Loader Specification Type #2

    • 配置方式:将 EFI 格式的 UKI 镜像放在 ESP 分区的 /EFI/Linux/ 下即可
    • 工作原理:
      1. systemd-boot 在启动时扫描 ESP 的 /EFI/Linux/ 目录
      2. systemd-boot 会自动将扫描到的内核镜像添加到启动菜单,无需单独的 .conf 文件
    • 特点:
      • 免配置,自动出现在启动菜单中
      • vmlinuz-linux, initrd 跟 cmdline 等信息被统一打包成一个 EFI 镜像,一个镜像就包含了系统启动需要的所有数据,更方面简洁。
  • 其他自动识别的启动项

    • Microsoft Windows EFI boot manager(如果已安装)
    • Apple macOS boot manager(如果已安装)
    • EFI Shell 可执行文件(如果已安装)
    • 「Reboot Into Firmware Interface」选项(如果 UEFI 固件支持)
    • Secure Boot 变量注册(如果固件处于 setup 模式,且 ESP 提供了相关文件)

常用命令

  • efibootmgr -v:查看 / 修改固件启动顺序
  • bootctl status:检查 systemd-boot 安装与 ESP 状态
  • bootctl list:列出启动条目
  • ukify inspect /boot/EFI/Linux/nixos-xxx.efi: 查看 efi 镜像中包含的信息

示例:

# 查看固件启动顺序
$ nix run nixpkgs#efibootmgr -v

BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 0000,0004
Boot0000* NixOS HD(1,GPT,34286f3b-d4df-456d-bf7a-eb67f2bf1a72,0x1000,0x12b000)/EFI\BOOT\BOOTX64.EFI
...
Boot0004* Windows Boot Manager  HD(1,GPT,34286f3b-d4df-456d-bf7a-eb67f2bf1a72,0x1000,0x12b000)/\EFI\Microsoft\Boot\bootmgfw.efi0000424f

# 检查 systemd-boot 安装与 ESP 状态
$ bootctl status

System:
      Firmware: UEFI 2.80 (American Megatrends 5.27)
 Firmware Arch: x64
   Secure Boot: enabled (user)
  TPM2 Support: yes
  Measured UKI: yes
  Boot into FW: supported

Current Boot Loader:
      Product: systemd-boot 257.7
     Features: ✓ Boot counting
               ✓ Menu timeout control
               ✓ One-shot menu timeout control
               ✓ Default entry control
               ✓ One-shot entry control
               ✓ Support for XBOOTLDR partition
               ✓ Support for passing random seed to OS
               ✓ Load drop-in drivers
               ✓ Support Type #1 sort-key field
               ✓ Support @saved pseudo-entry
               ✓ Support Type #1 devicetree field
               ✓ Enroll SecureBoot keys
               ✓ Retain SHIM protocols
               ✓ Menu can be disabled
               ✓ Multi-Profile UKIs are supported
               ✓ Boot loader set partition information
    Partition: /dev/disk/by-partuuid/34286f3b-d4df-456d-bf7a-eb67f2bf1a72
       Loader: └─EFI/BOOT/BOOTX64.EFI
Current Entry: nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
...
Available Boot Loaders on ESP:
          ESP: /boot (/dev/disk/by-partuuid/34286f3b-d4df-456d-bf7a-eb67f2bf1a72)
         File: ├─/EFI/systemd/systemd-bootx64.efi (systemd-boot 257.7)
               └─/EFI/BOOT/BOOTX64.EFI (systemd-boot 257.7)
...
Default Boot Loader Entry:
         type: Boot Loader Specification Type #2 (.efi)
        title: NixOS Xantusia 25.11.20250830.d7600c7 (Linux 6.16.4) (Generation 848, 2025-09-01)
           id: nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
       source: /boot//EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi (on the EFI System Partition)
     sort-key: lanza
      version: Generation 848, 2025-09-01
        linux: /boot//EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
      options: init=/nix/store/gaj3sp3hrzjhp59bvyxhc8flg5s6iimg-nixos-system-ai-25.11.20250830.d7600c7/init nvidia-drm.fbdev=1 root=fstab loglevel=4 lsm=landlock,yama,bpf nvidia-drm.modeset=1 nvidia-drm.fbdev=1 nvidia.NVreg_PreserveVideoMemoryAllocations=1 nvidia.NVreg_OpenRmEnableUnsupportedGpus=1

# 查看上述启动项中 uki efi 文件的内容
$ nix shell nixpkgs#systemdUkify
$ ukify inspect /boot/EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi
.osrel:
  size: 141 bytes
  sha256: e486dea4910eb9262efc47464f533f96093293d37c3d25feb954c098865a4be6
  text:
    ID=lanza
    PRETTY_NAME=NixOS Xantusia 25.11.20250830.d7600c7 (Linux 6.16.4) (Generation 848, 2025-09-01)
    VERSION_ID=Generation 848, 2025-09-01
# 启动内核时使用的内核命令行参数
.cmdline:
  size: 284 bytes
  sha256: 7f94ffed08359eb1d2749176eba57e085113f46208702a8c0251376d734f19ce
  text:
    init=/nix/store/gaj3sp3hrzjhp59bvyxhc8flg5s6iimg-nixos-system-ai-25.11.20250830.d7600c7/init nvidia-drm.fbdev=1 root=fstab loglevel=4 lsm=landlock,yama,bpf nvidia-drm.modeset=1 nvidia-drm.fbdev=1 nvidia.NVreg_PreserveVideoMemoryAllocations=1 nvidia.NVreg_OpenRmEnableUnsupportedGpus=1
# initramfs 内容的引用,实际镜像位于 ESP 的 /EFI/nixos/initrd-*.efi
.initrd:
  size: 81 bytes
  sha256: 26d9b1f52806c48c6287272cb26b8a640b62d55f09149abf3415c76c38e0b56e
# 内核映像(vmlinuz)的引用,实际镜像位于 ESP 的 /EFI/nixos/kernel-*.efi
.linux:
  size: 81 bytes
  sha256: 41ff83e4cae160fb9ce55392943e6d06dbf9f37b710bf719f7fe2c28ec312be5

内核启动后,会探测 CPU、内存、PCI、USB、ACPI 等硬件,加载关键驱动,然后挂载 initramfs 并执行 option 中指定的 init 程序。

观察方法

# 查看内核早期日志
sudo dmesg --level=err,warn,info | less

# 查看本次启动的完整日志
journalctl -b

initramfs 阶段

initramfs(Initial RAM File System)是一个临时的根文件系统,在真正的根文件系统挂载之前提供必要的功能。它在启动阶段被加载到 RAM 中并被挂载为根目录。

initramfs 阶段的主要职责

  1. 硬件检测与驱动加载

    • 检测存储设备(SATA、NVMe、USB 等)
    • 加载必要的存储驱动模块
    • 识别网络设备(如果需要网络启动)
  2. 存储设备准备

    • 解密 LUKS 加密分区
    • 激活 LVM 逻辑卷
    • 处理 RAID 阵列
    • 挂载临时文件系统
  3. 根文件系统挂载

    • 根据内核参数 root= 找到根分区
    • 挂载根文件系统到 /new_root
    • 执行 switch_root 切换到真正的根文件系统
  4. 启动用户空间

    • 执行 /sbin/init(通常是 systemd)
    • 在 NixOS 中,init 程序是 /nix/store 中的一个 Shell 脚本,它首先完成一些必要的初始化工作,之后才启动 systemd.

常见故障与排查

  • 找不到根分区:检查 cat /proc/cmdlineroot= 参数与 blkid 输出是否一致
  • 缺少驱动模块:确保 NixOS 配置包含所需模块:boot.initrd.kernelModules = [ "nvme" "dm_mod" ];
  • LUKS 解密失败:检查密码输入或密钥文件配置
  • LVM 激活失败:确认 LVM 配置和卷组状态

排查步骤

  1. 编辑内核 cmdline,添加 init=/bin/shbreak=mount 进入 initramfs shell
  2. 运行 lsblkblkid 确认设备
  3. 查看 dmesg 中的磁盘或 LVM 错误
  4. 检查 /proc/cmdline 中的启动参数

2. 启动故障排查

启动故障排查流程

flowchart LR
    A[系统无法启动] --> B{能否进入 UEFI/BIOS?}
    B -->|否| C[硬件问题
检查电源、内存、CPU] B -->|是| D{能否看到启动菜单?} D -->|否| E[引导加载器问题
检查 ESP 分区和启动项] D -->|是| F{能否选择启动项?} F -->|否| G[启动项配置错误
检查 bootctl 配置] F -->|是| H{内核能否加载?} H -->|否| I[内核或 initramfs 问题
检查内核参数和驱动] H -->|是| J{能否进入 initramfs?} J -->|否| K[initramfs 问题
检查根分区和文件系统] J -->|是| L{能否挂载根分区?} L -->|否| M[文件系统或加密问题
检查 LUKS 和 LVM] L -->|是| N{systemd 能否启动?} N -->|否| O[用户空间问题
检查 systemd 服务] N -->|是| P[启动成功]

常见启动问题:症状与解决方案

在系统启动过程中,可能会遇到各种问题。以下是按启动阶段分类的常见问题及排查方法:

2.2.1 固件和引导加载器问题

问题症状

  • 系统无法启动,停留在固件界面
  • 显示 “No bootable device” 错误
  • 启动菜单不显示或显示异常

排查步骤

使用 USB 启动盘进入 LiveOS, 进行如下检查:

# 检查 UEFI 设置
efibootmgr -v

# 检查 ESP 分区状态
bootctl status

# 验证启动项配置
bootctl list

2.2.2 内核和 initramfs 问题

问题症状

  • 内核 panic 或无法加载
  • initramfs 阶段卡住
  • 找不到根分区

排查步骤

# 进入 initramfs shell 进行调试
# 在内核参数中添加:init=/bin/sh 或 break=mount

# 检查设备识别
lsblk
blkid

# 查看内核日志
dmesg | grep -i error

# 检查文件系统完整性
fsck /dev/sdX

启动性能优化

2.3.1 启动时间分析

# 使用 systemd-analyze 分析启动时间
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain

# 生成启动时间报告
systemd-analyze plot > boot-time.svg

这些工具可以帮助你分析系统启动性能:

  • systemd-analyze 显示总启动时间,包括内核和用户空间的启动耗时
  • systemd-analyze blame 按耗时排序显示各服务启动时间,找出最耗时的服务
  • systemd-analyze critical-chain 显示关键路径分析,找出阻塞启动的服务链
  • systemd-analyze plot 生成启动时间图表,可视化各服务的启动顺序和耗时

识别到启动阶段的性能瓶颈后,就能据此优化服务依赖关系,加快启动速度。

2.3.2 启动优化策略

优化启动速度可以从多个层面入手:

硬件层面

使用 SSD 存储是最直接有效的优化方法。固态硬盘的随机读写性能远超机械硬盘,能显著减少文件系统访问延迟。启动时间通常可减少 50-80%,特别是对于大量小文件读取的场景。适用于所有系统,特别是启动时间较长的系统。

内核层面

启用内核并行初始化可以提升启动速度。现代内核支持并行初始化硬件设备,减少串行等待时间。通过内核参数如 initcall_debugacpi=noirq 等可以优化启动流程,减少硬件初始化时间。

服务层面

优化 systemd 服务依赖关系可以减少启动延迟。减少不必要的服务依赖,避免串行启动造成的延迟。使用 systemctl list-dependencies 分析依赖关系,移除不必要的依赖,减少服务启动等待时间, 提升并行启动效率。

启动流程

使用 UKI(统一内核镜像)可以减少启动步骤。将内核、initramfs、cmdline 打包成单个 EFI 文件, 减少启动步骤和文件系统访问。减少文件系统挂载次数,简化启动流程。在 NixOS 中通过boot.loader.systemd-boot.enableboot.loader.efi.canTouchEfiVariables 启用。

3. 系统安全框架:认证、授权与密钥管理

现代 Linux 桌面系统的安全架构由多个相互协作的组件构成,包括 PAM(认证)、PolicyKit(授权)、以及桌面环境提供的密钥管理服务。这些组件共同构建了一个多层次的安全防护体系,既保证了系统的安全性,又提供了良好的用户体验。

NOTE: 注意 PAM 与 PolicyKit 的设计目的都是为普通用户提供权限提升手段。对 root 用户而言,这些框架的限制很少或几乎不存在。如果你希望限制整个系统全局的权限(包括 root 用户), 应该考虑 SELinux/AppArmor 等强制访问控制框架。


3.1 PAM - 可插拔认证模块

PAM(Pluggable Authentication Modules) 是 Linux 的统一认证框架,为系统中的各种程序 (如 loginsudosshdgdm 等)提供标准化的认证接口。借助 PAM,系统管理员可以通过配置文件灵活控制认证策略,而无需修改应用程序本身。它支持多种认证方式(密码、指纹、智能卡、双因子验证等),是现代 Linux 安全体系的核心组件之一。


3.1.1 工作原理与配置结构

PAM 采用模块化设计,将认证流程分为四个阶段。应用程序通过调用相应的 PAM 接口触发这些阶段, 系统根据 /etc/pam.d/ 下的配置文件执行相应的模块(.so 文件)。

(1)配置文件语法

https://linux.die.net/man/5/pam.d

每行的基本格式如下:

<type>  <control>  <module>  [arguments]
  • type:表示阶段类型
  • control:定义该模块的执行策略
  • module:具体的 PAM 模块路径(或名称)
  • arguments:传递给模块的参数
(2)四个认证阶段
阶段类型 调用函数 主要作用
auth pam_authenticate() 验证用户身份,通常会提示用户输出密码或指纹以完成验证
account pam_acct_mgmt() 检查账户状态(过期、锁定等)
password pam_chauthtok() 处理密码修改
session pam_open_session() / pam_close_session() 建立和清理用户会话
(3)控制标志(control)
标志 含义 行为说明
required 必须成功,失败不会立即终止,但最终结果会失败 无论成功失败,都会继续执行后续模块。最终只要有一个 required 失败,整个认证就失败。
requisite 必须成功,失败立即终止并返回失败 失败立即返回,不再执行后续模块。
sufficient 成功则立即通过认证(跳过所有后续模块);失败则继续由后续模块进行认证 若前面没有 required 失败,则成功直接通过;否则失败不影响后续。
optional 可选模块,结果通常被忽略 无论成功失败,对最终结果无直接影响,除非是栈中唯一的模块。
include 包含另一个文件的配置 将指定文件的配置内容包含进来,通常用于复用通用配置(如 system-auth)。
substack 调用子栈 类似 include,但子栈的失败不影响主栈(即子栈只能跳过其自身的后续步骤),除非主栈中另有设置。
(4)常用模块示例
pam_unix.so                 # 基于 /etc/passwd 与 /etc/shadow 的标准密码认证
pam_google_authenticator.so # 双因子认证(TOTP)
pam_fprintd.so              # 指纹认证
pam_ldap.so                 # LDAP 集中式认证
pam_gnome_keyring.so        # GNOME 密钥环集成
pam_limits.so               # 用户资源限制
pam_deny.so                 # 拒绝所有认证请求

3.1.2 执行流程示例

/etc/pam.d/sudo 为例:

#%PAM-1.0
auth       sufficient   pam_rootok.so
auth       sufficient   pam_timestamp.so
auth       required     pam_wheel.so use_uid
auth       required     pam_unix.so nullok try_first_pass

各模块的执行顺序如下:

  1. 执行 pam_rootok.so (sufficient)
    • 检查当前用户是否为 root。
    • 如果成功:PAM 认证流程立即成功,并跳过后续所有 auth 模块。用户直接获得 sudo 权限。
    • 如果失败:继续执行下一个模块。
  2. 执行 pam_timestamp.so (sufficient)
    • 检查是否存在有效的时间戳文件(默认为 5 分钟内)。
    • 如果成功:PAM 认证流程立即成功,并跳过后续所有 auth 模块。用户免密码获得 sudo 权限。
    • 如果失败:继续执行下一个模块。
  3. 执行 pam_wheel.so (required)
    • 检查当前用户是否在 wheel 组(或 sudo 组,取决于配置)中。
    • 无论成功还是失败,都必须继续执行下一个 required 模块。但其结果会被记录下来。
  4. 执行 pam_unix.so (required)
    • 使用 nulloktry_first_pass 参数进行密码验证。
    • nullok:允许空密码账户登录。
    • try_first_pass:尝试使用前面模块(如果有的话)提供的密码。对于 sudo,这通常指之前 sudo 成功时缓存的密码。
    • 如果密码正确:此模块成功。
    • 如果密码错误:此模块失败。
  5. 最终结果判定
    • 在所有 required 模块执行完毕后,PAM 会检查它们的结果。
    • 如果任何一个 required 模块(pam_wheel.sopam_unix.so)失败,整个认证流程失败。
    • 只有当所有 required 模块都成功时,认证才最终成功。

常用模块及其参数说明

  1. pam_unix.so 参数 (用于密码验证) 这是最核心的密码认证模块,常见于 authpassword 类型。
    • nullok:允许空密码账户通过认证。如果不加此参数,空密码账户将无法登录。
    • try_first_pass:在提示用户输入密码前,先尝试使用之前栈中已缓存的密码(例如,由pam_timestamp.sopam_kwallet.so 提供的)。
    • use_authtok强制使用之前栈中已缓存的密码,如果不存在缓存密码,则直接失败。它比try_first_pass 更严格,通常用在修改密码的 password 模块栈中,以确保用户输入的是旧密码。
    • shadow:使用 /etc/shadow 文件进行密码验证(现代系统默认启用)。
  2. pam_timestamp.so 参数 (用于时间戳认证) 常用于 sudo,实现免密码操作。
    • timestamp_timeout=600:设置时间戳的有效期,单位为秒。默认是 300 (5分钟)。
  3. pam_wheel.so 参数 (用于组成员资格检查) 用于限制只有特定组的用户才能使用 susudo
    • use_uid:检查发起请求的原始用户 ID,而不是当前用户 ID(在 sudo 场景下很重要)。
    • group=admins:指定检查的组名,默认是 wheel
  4. pam_gnome_keyring.so 参数 (用于会话管理) 这个模块与 sudo 的认证流程无关,主要用于用户登录时解锁密钥环。
    • auto_start:在会话启动时,如果用户密码与密钥环密码相同,则自动解锁密钥环。
    • 典型应用场景:在 /etc/pam.d/gdm-password/etc/pam.d/loginauthsession 部分。
      # 在 /etc/pam.d/gdm-password 中
      auth       optional    pam_gnome_keyring.so
      session    optional    pam_gnome_keyring.so auto_start

3.1.3 应用程序与 PAM 的交互

程序通过 pam_start() 指定服务名,系统据此加载对应的配置文件。

程序 服务名 配置文件 功能
login "login" /etc/pam.d/login 控制台登录
gdm "gdm" /etc/pam.d/gdm GNOME 登录界面
sudo "sudo" /etc/pam.d/sudo 提权命令
sshd "sshd" /etc/pam.d/sshd SSH 登录
greetd "greetd" /etc/pam.d/greetd 轻量显示管理器

一个典型的调用顺序如下(以 sudo 为例):

pam_start("sudo", user, &conv, &pamh);     // 初始化 PAM
pam_authenticate(pamh, 0);                 // 身份验证
pam_acct_mgmt(pamh, 0);                    // 账户检查
pam_open_session(pamh, 0);                 // 打开会话
// 执行用户命令
pam_close_session(pamh, 0);                // 关闭会话
pam_end(pamh, PAM_SUCCESS);                // 释放资源

如下是一个用户登录流程的 PAM 调用示例:

#include <stdio.h>
#include <stdlib.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>

static void log_result(pam_handle_t *pamh, int ret, const char *step)
{
    if (ret == PAM_SUCCESS) {
        printf("[✓] %s 成功\n", step);
    } else {
        fprintf(stderr, "[✗] %s 失败: %s(返回码 %d)\n",
                step, pam_strerror(pamh, ret), ret);
    }
}

int main(int argc, char *argv[])
{
    pam_handle_t *pamh = NULL;
    struct pam_conv conv = { misc_conv, NULL };
    const char *user;
    int ret;
    if (argc != 2) {
        fprintf(stderr, "用法: %s 用户名\n", argv[0]);
        return 1;
    }
    user = argv[1];
    /* 1. 初始化 */
    ret = pam_start("login", user, &conv, &pamh);
    if (ret != PAM_SUCCESS) {
        log_result(pamh, ret, "pam_start");
        return 1;
    }
    /* 2. 认证 */
    ret = pam_authenticate(pamh, 0);
    log_result(pamh, ret, "pam_authenticate");
    if (ret != PAM_SUCCESS) {
        pam_end(pamh, ret);
        return 1;
    }
    /* 3. 帐户检查 */
    ret = pam_acct_mgmt(pamh, 0);
    log_result(pamh, ret, "pam_acct_mgmt");
    if (ret != PAM_SUCCESS) {
        pam_end(pamh, ret);
        return 1;
    }
    /* 4. 打开会话 */
    ret = pam_open_session(pamh, 0);
    log_result(pamh, ret, "pam_open_session");
    if (ret != PAM_SUCCESS) {
        /* 常见原因提示 */
        fprintf(stderr,
                "\n提示:\n"
                "  1. 若您以普通用户运行,失败通常是权限不足(写 /var/run/utmp 等)。\n"
                "  2. 以 root 再次运行即可验证会话模块能否通过:sudo %s %s\n",
                argv[0], user);
        pam_end(pamh, ret);
        return 1;
    }
    printf("\n全部 PAM 阶段通过!\n");
    /* 5. 关闭会话并清理 */
    pam_close_session(pamh, 0);
    pam_end(pamh, PAM_SUCCESS);
    return 0;
}

将上述配置保存为 pam_test.c, 再创建一个 shell.nix 内容如下:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = with pkgs; [
    pam
    gcc
  ];
}

最后编译运行:

# 进入引入了 pam 链接库的环境
nix-shell
# 编译
gcc pam_test.c -o pam_test -lpam -lpam_misc
# 测试
./pam_test ryan

3.1.4 模块间的数据传递

PAM 模块可通过 pam_set_data()pam_get_data() 共享状态。例如:

pam_set_data(pamh, "authenticated", "true", NULL);
const char *ok;
pam_get_data(pamh, "authenticated", (const void **)&ok);

这使多个模块在同一认证过程中共享信息。


3.1.5 调试与故障排查

PAM 的问题通常来源于配置错误或模块加载失败,可按以下思路排查:

(1)测试与验证
nix shell nixpkgs#pamtester
# 模拟特定服务的认证流程
pamtester sudo $USER authenticate
pamtester login $USER open_session
(2)检查模块与依赖
# 验证模块存在与架构匹配
ls /run/current-system/sw/lib/security/pam_unix.so
ldd /run/current-system/sw/lib/security/pam_unix.so
(3)查看系统日志
journalctl -b | grep -i pam
(4)跟踪调用行为
strace -f -e trace=openat,read,write -o sudo_trace.log sudo true
grep pam sudo_trace.log
(5)常见问题
问题 可能原因
模块加载失败 模块路径错误或权限不足
认证成功但无法建立会话 会话模块执行失败(如无法写入 /var/run/utmp
GNOME Keyring 不自动解锁 pam_gnome_keyring.so 未启用或未配置 auto_start
PAM 配置无效 程序服务名与配置文件不匹配,默认使用 other

3.2 PolicyKit - 细粒度的系统权限管理

PolicyKit(现称 polkit)是一个用于控制系统级权限的框架,它提供了一种比传统 Unix 权限更细粒度的授权机制。在现代 Linux 桌面系统中,PolicyKit 允许非特权用户执行某些需要特权的系统操作 (如关机、重启、挂载设备、修改系统时间等),而无需获取完整的 root 权限。

3.2.1 PolicyKit 的核心概念

配置文件路径

  • /etc/polkit-1/:NixOS 声明式配置中定义的自定义规则(优先级最高)
  • /run/current-system/sw/share/polkit-1/(NixOS)或 /usr/share/polkit-1/(传统发行版):软件包提供的默认规则

上述文件夹中又包含两类配置:

  • 动作(Actions)
    • 定义在配置文件夹的 actions 目录中的 XML 文件(如 /etc/polkit-1/actions/),描述可授权的操作。每个动作都有唯一的标识符,如 org.freedesktop.login1.power-off 表示关机操作。
  • 规则(Rules)
    • JavaScript 文件,定义授权决策逻辑,位于上述配置文件夹的 rules.d/ 目录中(如/etc/polkit-1/rules.d/)。规则决定了在特定条件下是否授权某个操作。在 NixOS 中,推荐使用声明式配置而非直接修改 /etc 目录。

身份认证代理(Authentication Agents):桌面环境提供的图形界面组件,用于在用户需要身份验证时弹出认证对话框。例如,当普通用户尝试关机时,认证代理会提示输入管理员密码。

举例来说,我使用的是 Niri 窗口管理器,它的 Nix Flake 启用了 pokit-kde-agent-1 作为其 Authentication Agent, 配置参见sodiboo/niri-flake.

3.2.2 PolicyKit 的工作原理

当应用程序请求执行需要特权的操作时,系统服务会询问 PolicyKit 是否授权。PolicyKit 的评估过程如下:

  1. 身份识别:确定请求者的身份(用户、组、会话等)
  2. 规则匹配:检查是否有适用的规则文件
  3. 权限评估:根据规则返回以下结果之一:
    • yes:直接允许,无需认证
    • no:直接拒绝
    • auth_self:需要用户自己认证(输入当前用户密码)
    • auth_admin:需要管理员认证(输入 root 密码)
    • auth_self_keep/auth_admin_keep:认证后在一段时间内保持授权

3.2.3 PolicyKit 的配置示例

在传统的 Linux 发行版中,管理员可以通过创建自定义规则来修改默认行为。例如,允许 wheel 组的用户无需密码即可关机:

// /etc/polkit-1/rules.d/10-shutdown.rules
polkit.addRule(function (action, subject) {
  if (action.id == "org.freedesktop.login1.power-off" && subject.isInGroup("wheel")) {
    return polkit.Result.YES
  }
})

NixOS 中的配置方法:在 NixOS 中,推荐使用声明式配置而非直接修改 /etc 目录。可以通过security.polkit 配置项来管理 PolicyKit 规则:

# configuration.nix
{
  security.polkit.enable = true;

  # 添加自定义规则
  security.polkit.extraConfig = ''
    polkit.addRule(function(action, subject) {
      if (action.id == "org.freedesktop.login1.power-off" &&
          subject.isInGroup("wheel")) {
        return polkit.Result.YES;
      }
    });
  '';
}

3.2.4 PolicyKit 与 D-Bus 的集成

PolicyKit 与 D-Bus 深度集成,为 D-Bus 服务提供动态授权机制。许多系统服务(如 systemd、NetworkManager、udisks 等)都使用 PolicyKit 来控制对其 D-Bus 接口的访问。当客户端通过 D-Bus 调用需要特权的方法时,服务会调用 PolicyKit 进行授权检查。

PolicyKit 调试主要涉及服务状态检查、权限测试和规则验证。常用的调试方法包括:

  • 服务状态检查:验证 PolicyKit 守护进程的运行状态
  • 权限测试:使用 pkcheck 工具测试特定操作的授权情况
  • 日志分析:查看 PolicyKit 的授权决策日志
  • 规则验证:检查当前生效的 PolicyKit 规则配置

具体的调试命令请参考 3.5.3 故障排查 章节。

3.3 桌面密钥管理

现代 Linux 桌面环境提供了统一的密钥管理服务,用于安全存储用户的密码、证书、密钥等敏感信息。

GNOME Keyring 和 KDE Wallet 分别是 GNOME 和 KDE 桌面环境的密钥管理解决方案,它们通过加密存储和自动解锁机制,为用户提供了便捷而安全的密码管理体验。

GNOME Keyring 和 KDE Wallet 都实现了标准的Secrets API, 可以根据需要任选一个使用。不过据我观察大部分窗口管理器的用户都是用的 GNOME Keyring.

3.3.1 密钥管理系统架构

GNOME Keyring 架构

  • 密钥环(Keyring):加密的存储容器,每个密钥环有独立的密码
  • 密钥环守护进程(gnome-keyring-daemon):管理密钥环的生命周期和访问控制
  • API:Gnome 原生支持 org.freedesktop.secrets DBus API, 目前流行的 secrets 客户端库 libsecret 也是 gnome 开发的。
  • PAM 集成:通过 pam_gnome_keyring.so 实现登录时自动解锁

KDE Wallet 架构

  • KWalletManager:图形界面管理工具
  • kwalletd:钱包守护进程
  • API:KDE Wallet 从 5.97.0 (2022 年 8 月)开始支持org.freedesktop.secrets DBus API, 因此可以直接通过 libsecret 往 KDE Wallet 中存取 passwords 等 secret.
  • PAM 集成:通过 pam_kwallet.so 实现自动解锁

核心组件路径

# GNOME Keyring 组件(NixOS 中位于 nix store)
/run/current-system/sw/bin/gnome-keyring-daemon
/run/current-system/sw/lib/libsecret-1.so
/run/current-system/sw/lib/security/pam_gnome_keyring.so

# KDE Wallet 组件(NixOS 中位于 nix store)
/run/current-system/sw/bin/kwalletd5
/run/current-system/sw/bin/kwalletmanager5
/run/current-system/sw/lib/security/pam_kwallet.so

# 配置文件位置
~/.local/share/keyrings/     # GNOME 密钥环存储目录
~/.local/share/kwalletd/     # KDE 钱包文件存储目录
~/.config/kwalletrc          # KDE 钱包配置文件

3.3.2 密钥环类型与用途

密钥环类型 用途 解锁时机
login 登录密钥环,存储用户密码 用户登录时自动解锁
default 默认密钥环,存储应用密码 首次访问时解锁
session 会话密钥环,临时存储 会话开始时创建
crypto 加密密钥环,存储证书和私钥 按需解锁

3.3.3 钱包创建与管理

图形界面管理

# GNOME 密钥环管理器
seahorse

# KDE 钱包管理器
kwalletmanager5

通过图形界面可以:

  • 创建新的密钥环/钱包
  • 设置密码和加密算法
  • 管理存储的密码和证书
  • 配置自动解锁策略
  • 备份和恢复密钥环

基本命令行操作

# 使用 secret-tool 管理 GNOME Keyring
secret-tool store --label="My Password" application myapp
secret-tool lookup application myapp

# 使用 kwallet-query 管理 KDE Wallet
kwallet-query --write password "MyApp" "username" "password"
kwallet-query --read password "MyApp" "username"

3.3.4 应用程序集成

常见应用程序集成

VSCode

  • 自动集成系统密钥管理服务
  • 存储 Git 凭据、扩展设置等敏感信息
  • 通过 git credential.helper 配置自动使用

GitHub CLI

# 配置 GitHub CLI 使用系统密钥管理
gh auth login --web
# 凭据会自动存储到系统密钥环中

浏览器集成

  • Firefox、Chrome 等现代浏览器支持系统密钥管理
  • 网站密码自动保存到密钥环/钱包中
  • 跨设备同步(如果启用)

API 集成示例

3.3.5 配置与优化

NixOS 配置示例

# configuration.nix
# 启用 GNOME Keyring
services.gnome.gnome-keyring.enable = true;
# GNOME Keyring GUI 客户端
programs.seahorse.enable = true;
# 启用 PAM 集成
security.pam.services.login.enableGnomeKeyring = true;

3.4 安全故障排查

3.4.1 认证问题排查

常见认证失败场景

  1. 用户无法登录

    • 检查 PAM 配置是否正确
    • 查看认证日志中的错误信息
    • 验证用户账户状态和密码
  2. sudo 权限问题

    • 确认用户在正确的用户组中
    • 检查 sudoers 配置
    • 验证 PAM 认证流程
  3. SSH 登录失败

    • 检查 SSH 服务状态
    • 查看 SSH 认证日志
    • 验证网络连接和防火墙设置

3.4.2 权限管理问题排查

PolicyKit 权限问题

  • 无法关机/重启:检查 PolicyKit 规则配置和用户组权限
  • 无法挂载设备:检查 udisks2 服务和 PolicyKit 集成
  • 无法修改系统时间:检查时间同步服务权限和用户组设置

3.4.3 密钥管理问题排查

GNOME Keyring 问题

  • 检查密钥环守护进程是否正常运行
  • 验证 PAM 集成是否正确配置
  • 查看密钥环状态和自动解锁设置

KDE Wallet 问题

  • 检查钱包守护进程状态
  • 验证钱包配置和访问权限
  • 测试钱包的读写功能

具体的调试命令和排查步骤请参考 3.5.3 故障排查 章节。

3.5 安全组件集成与最佳实践

3.5.1 组件协作流程

现代 Linux 桌面的安全组件协作流程:

  1. 用户登录:PAM 验证用户身份
  2. 密钥环解锁:PAM 模块自动解锁用户密钥环/钱包
  3. 应用启动:应用程序通过 libsecret/KWallet API 访问存储的密码
  4. 特权操作:PolicyKit 控制需要特权的系统操作
  5. 会话结束:密钥环/钱包自动锁定

3.5.2 安全最佳实践

密钥管理

  • 使用强密码保护密钥环/钱包
  • 定期备份密钥环文件
  • 避免在脚本中硬编码密码
  • 使用应用程序专用的密钥环

认证配置

# 启用双因子认证
auth required pam_google_authenticator.so
auth required pam_unix.so

# 配置密码策略
password required pam_cracklib.so retry=3 minlen=8 difok=3
password required pam_unix.so use_authtok

权限管理

// PolicyKit 规则示例:限制特定操作
polkit.addRule(function (action, subject) {
  if (action.id == "org.freedesktop.login1.power-off" && subject.user == "guest") {
    return polkit.Result.NO
  }
})

3.5.3 故障排查

PAM 认证调试

nix shell nixpkgs#pamtester

# 测试 PAM 配置
pamtester login $USER authenticate
pamtester sudo $USER authenticate

# 查看 PAM 配置
cat /etc/pam.d/login
cat /etc/pam.d/greetd
cat /etc/pam.d/sudo

# 检查 PAM 模块
ldd /run/current-system/sw/lib/security/pam_unix.so
ldd /run/current-system/sw/lib/security/pam_gnome_keyring.so

# 查看认证日志
journalctl -t login -f
journalctl -t greetd -f
journalctl -t sshd -f
journalctl -t sudo

# 验证程序与配置的对应关系
strace -e trace=pam_start login 2>&1 | grep pam_start
strace -e trace=openat login 2>&1 | grep pam.d

PolicyKit 权限调试

# 检查 PolicyKit 服务状态
systemctl status polkit

# 测试特定权限
pkcheck --action-id org.freedesktop.login1.power-off --process $$ --allow-user-interaction

# 查看 PolicyKit 日志
journalctl -u polkit -f

# 查看 PolicyKit 动作定义
ls -la /run/current-system/sw/share/polkit-1/actions/

# 查看当前生效的 PolicyKit 规则
ls -la /etc/polkit-1/rules.d/

密钥管理调试

# GNOME Keyring 检查
ps aux | grep gnome-keyring
seahorse  # GNOME Keyring GUI

# KDE Wallet 检查
ps aux | grep kwalletd
kwalletmanager5  # KDE Wallet GUI
kwallet-query kdewallet --list-entries

# 系统日志检查
sudo journalctl -u systemd-logind

调试技巧

  • 使用 strace 跟踪应用程序的密钥访问
  • 通过 journalctl 查看认证和授权日志
  • 使用 pamtester 测试 PAM 配置
  • 通过 pkcheck 测试 PolicyKit 权限

通过理解这些安全组件的协作机制,用户可以更好地配置和管理 Linux 桌面的安全策略,在保证安全性的同时提供良好的用户体验。

总结

从 UEFI 到 systemd,从 PAM 到 PolicyKit,本文详细介绍了 Linux 桌面系统启动与安全框架的核心组件。

下一篇文章将深入探讨 systemd 全家桶与服务管理,包括 D-Bus 系统总线、日志系统和设备管理等核心功能,这些组件为桌面环境提供了强大的基础设施支持。

快速参考

常用启动排查命令

# 启动时间分析
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain

# 引导加载器检查
bootctl status
bootctl list
efibootmgr -v

# 内核和硬件信息
dmesg | grep -i error
lspci -k
lsusb
lsblk

# 进入救援模式
# 在内核参数中添加:init=/bin/sh 或 break=mount

常用安全排查命令

安全相关的调试命令请参考 3.5.3 故障排查 章节,该章节提供了完整的 PAM、PolicyKit 和密钥管理调试命令。

重要配置文件位置

# 启动相关
/boot/loader/loader.conf          # systemd-boot 全局配置
/boot/EFI/Linux/                  # UKI 镜像位置
/etc/pam.d/                       # PAM 配置文件
/etc/polkit-1/                    # PolicyKit 配置

# 密钥管理
~/.local/share/keyrings/          # GNOME Keyring 存储
~/.local/share/kwalletd/          # KDE Wallet 存储
~/.config/kwalletrc               # KDE Wallet 配置

🔲 ☆

如何透過批次檔批次將 PFX 憑證的私密金鑰匯出並重新打包新的 PFX 檔案

今天有客戶希望我們重新提供給他們一次 PFX 憑證檔案,所以我寫了一支批次檔專門用來匯出先前的 PFX 金鑰,並且再重新將 TWCA 簽發的 server.cer 憑證搭配原始金鑰再匯出一份新的 PFX 金鑰與設定強式密碼給他們。這篇文章我就來分享今天撰寫的腳本。

... 繼續閱讀 ...

🔲 ☆

安装 Ubuntu 24.04 (LTS), Webmin, Nginx, MariaDB, PHP8.3-FPM,Perl-Fastcgi 到 DigitalOcean 的 VPS(4)

安装 Ubuntu 24.04 LTS, Webmin, Nginx, MariaDB, PHP8.1-FPM,Perl-Fastcgi 到 DigitalOcean 的 VPS上。

DavidYin 介绍了如何在 DigitalOcean 创建新 VPS。并且完成基本的 Ubuntu 24.04 LTS的系统。然后介绍如何安装 Webmin 主机控制面板,时区设置和 SSH 的安全设置。再之后说明一下如何用之前的新添加的用户来安装 Nginx Web 服务器和 MairaDB 数据库服务器。

现在就是很重要的语言支持部分了。因为我用的最多的就是 php 以及 perl 语言。所以这两部分就是重点了。

第四部分
安装 php8.3-fpm

Ubuntu 24.04 LTS 仓库所包含的是 php8.3.6,目前 php 官方支持的版本是 8.1,8.2,8.3 这三个系列。所以直接使用 Ubuntu 的就已经是很新的版本了。

sudo apt install php8.3 php8.3-fpm php8.3-cli php8.3-common php8.3-mbstring php8.3-gd php8.3-intl php8.3-xml php8.3-mysql php8.3-zip php8.3-curl

安装完成后,执行 php -v 命令,可以看到版本信息。

davidyin@walnut:~$ php -v
PHP 8.3.6 (cli) (built: Sep 30 2024 15:17:17) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.6, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies
davidyin@walnut:~$

再看一下 php8.3-fpm 是否已经运行。

walnut-php-fpm.jpg

设置虚拟主机

说明:下面这些都是用来举例说明所用的,在实际使用中请用真实的数据。
IP: 143.110.227.68
Domain: u24.webexample.win
username: davidyin

接下来我要设置一个 vhost,就是一个虚拟主机,我用的域名是 u24.webexample.win,此为举例而已。 到域名服务商的网站,专门设置域名记录的地方,把 u24.webexample.win 的 A 记录指向此 VPS 的 IP 地址,生效可能需要十分钟或更久,为快捷,可以在所操作的Windows hosts 文件添加纪录使之在本地立即可用。 新建一个主机配置文件,u24.conf,如下。

sudo nano /etc/nginx/conf.d/u24.conf

这里我会定义 log 文件的格式,以及它的储存位置。

log_format   main '$remote_addr - $remote_user [$time_local]  $status '
    '"$request" $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

server {
    listen       80;
    server_name  u24.webexample.win;
    access_log  /var/log/nginx/host.access.log  main;

    root   /home/davidyin/u24.webexample.win;
    index  index.php index.html index.htm;
    
    location / {
        try_files $uri $uri/ = 404;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
      fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;     } # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # location ~ /\.ht { deny all; } }

保存退出,然后执行 sudo nginx -t 命令看看,是否配置文件正确。若正确,就重启 Nginx 服务,使配置生效。

sudo service nginx restart

在/home/davidyin/u24.webexample.win/下新建一个文件 info.php

输入如下内容:

<?php
phpinfo();

回到桌面浏览器中,输入网址 http://u24.webexample.win/info.php,如果看到下面这些内容,就表示 php 安装正确, nginx 也运行正确。

我一般会把 www-data 用户添加到 当前用户的组内,比如我这里用的 davidyin 用户就在同名的 davidyin 组内。

有时候会出现奇怪的问题,找不到文件啊,没有权限啊。这是最好的解决方法,就是重启服务器。有时候是配置未生效。

walnut-phpinfo.jpg


如果出现问题,或者是页面错误,可以查看这两个日志文件。

/var/log/nginx/host.access.log
/var/log/nginx/error.log

至此,php8.3 已经安装完毕,并且虚拟主机也可以使用 php 的语言了。

SSL 证书的签发

如果是商用,或者愿意购买一年期的证书,DavidYin建议到 Gogetssl 购买,这里价格最优,无限重签,不限服务器。目前的证书可以购买5年的,但是实际签发的证书都是一年的,每年重新签发一次,直到购买的年份用完为止。买多年的会便宜一点。

  • Sectigo Essential SSL 一年的证书,$15.84;两年证书,$27.72;五年证书 $63.36。
  • Sectigo PositiveSSL 一年的证书,$7.70;两年证书,$13.48;五年证书 $30.80。
  • GoGetSSL Domain SSL 一年的证书,$4.50;两年证书,$7.88;五年证书 $18.00。
  • 还有 Thawte, DigiCert,GeoTrust,RepidSSL 的证书可以选择。
  • 目前我用下来还是 GoGetSSL 自己的 DV 证书最便宜。

Gogetssl 证书的好处是你买了一个证书给域名 A 用,如果这个域名不用了,还可以签发给域名 B 使用剩下的时间。

免费证书这里我采用 Zerossl 的 SSL 证书。使用了 Neilpang 的 ACME.SH 来安装。每月自动续签。

先安装工具。

curl https://get.acme.sh | sh -s email=seo@g2soft.net

接下来,重新登入SSH,相当于重新载入 BASH 环境,因为上面的安装已经把路径配置到 Bash 中了,并且自动创建了一个 bash 的别名,方便使用,直接输入 acme.sh 命令就可以了。另外还自动创建了一个 cronjob,每天零点自动检测所有的证书,如果快过期,就会自动更新。

验证域名的方式有两种,DNS 和 http,这次我用了 http 方式来验证。

davidyin@walnut:~$ acme.sh --issue -d u24.webexample.win --webroot /home/davidyin/u24.webexample.win
[Mon Oct  7 16:37:46 PDT 2024] Using CA: https://acme.zerossl.com/v2/DV90
[Mon Oct  7 16:37:46 PDT 2024] Single domain='u24.webexample.win'
[Mon Oct  7 16:37:48 PDT 2024] Getting webroot for domain='u24.webexample.win'
[Mon Oct  7 16:37:48 PDT 2024] Verifying: u24.webexample.win
[Mon Oct  7 16:37:49 PDT 2024] Processing. The CA is processing your order, please wait. (1/30)
[Mon Oct  7 16:37:52 PDT 2024] Success
[Mon Oct  7 16:37:52 PDT 2024] Verification finished, beginning signing.
[Mon Oct  7 16:37:52 PDT 2024] Let's finalize the order.
[Mon Oct  7 16:37:52 PDT 2024] Le_OrderFinalize='https://acme.zerossl.com/v2/DV90/order/-0HtI52SzVp9B1iWfXvHrw/finalize'
[Mon Oct  7 16:37:53 PDT 2024] Order status is 'processing', let's sleep and retry.
[Mon Oct  7 16:37:53 PDT 2024] Sleeping for 15 seconds then retrying
[Mon Oct  7 16:38:09 PDT 2024] Polling order status: https://acme.zerossl.com/v2/DV90/order/-0HtI52SzVp9B1iWfXvHrw
[Mon Oct  7 16:38:09 PDT 2024] Downloading cert.
[Mon Oct  7 16:38:09 PDT 2024] Le_LinkCert='https://acme.zerossl.com/v2/DV90/cert/65Q_RSKwu-urE1DZVXE7FA'
[Mon Oct  7 16:38:10 PDT 2024] Cert success.
-----BEGIN CERTIFICATE-----
MIIEBzCCA4ygAwIBAgIRANWHTHkkfhcpadmh96AqH5IwCgYIKoZIzj0EAwMwSzEL
MAkGA1UEBhMCQVQxEDAOBgNVBAoTB1plcm9TU0wxKjAoBgNVBAMTIVplcm9TU0wg
中间省略
2u8271N/ejTHa2yuKuF4KiMP+BywmEifAjEAm/U9GoOqf7u/4yiVAAp6Neo5Nt5Q
Xm/X1Y3+KB0c636aAkftFce8fXep9o5RXpB2
-----END CERTIFICATE-----
[Mon Oct  7 16:38:10 PDT 2024] Your cert is in: /home/davidyin/.acme.sh/u24.webexample.win_ecc/u24.webexample.win.cer
[Mon Oct  7 16:38:10 PDT 2024] Your cert key is in: /home/davidyin/.acme.sh/u24.webexample.win_ecc/u24.webexample.win.key
[Mon Oct  7 16:38:10 PDT 2024] The intermediate CA cert is in: /home/davidyin/.acme.sh/u24.webexample.win_ecc/ca.cer
[Mon Oct  7 16:38:10 PDT 2024] And the full-chain cert is in: /home/davidyin/.acme.sh/u24.webexample.win_ecc/fullchain.cer

验证正确,就会自动签发证书,证书会临时先存放在一个工作目录,现在我要指定一个目录存放: /home/davidyin/ssl/。 之后就是安装证书到该目录。

acme.sh --install-cert -d u24.webexample.win \
--key-file       /home/davidyin/ssl/key  \
--fullchain-file /home/davidyin/ssl/cert \
--reloadcmd     "service nginx force-reload" 

就这样,证书也签发好了,也安装到指定位置,接下来会介绍如何在 nginx 的配置文件中,设置证书路径,设置 https,设置重定向,还有 perl-fastcgi等等。

🔲 ☆

如何對 PowerShell 腳本檔案進行數位簽章

我個人寫過的 PowerShell 腳本可能有數百到上千支,數不清了,由於大部分的腳本都是自己個人使用為主,所以大多都不會特別對這些腳本進行數位簽章。但是對於金融業這種高度管制的企業或組織來說,其實 PowerShell 腳本是被嚴格禁止的,此時對你的 PowerShell 腳本進行數位簽章就顯的十分重要。除此之外,若你要發佈腳本給其他人使用,對腳本進行數位簽章也是一個很好的選擇,不但可以增加可信度,也可以確保腳本的完整性,不會被惡意竄改後重新散佈有問題的版本。這篇文章我將介紹如何對 PowerShell 腳本檔案進行數位簽章。

... 繼續閱讀 ...

🔲 ☆

在外置硬盘上,加密安装 ubuntu

需求:

  1. 在便携硬盘盒(M.2 SATA/NVME)安装 Linux(Ubuntu/Zorin),以便在不同的电脑上都可以启动使用。
  2. root 级别的系统分区加密(使用 LUKS & LVM)。
  3. 不要把整块硬盘都加密,而是在硬盘上保留一个未加密分区。这样也可以作为普通的移动硬盘使用。

——这篇攻略和是否外置硬盘盒,没多大关系。普通内置硬盘也可以这样加密安装。

最新的 Ubuntu 22.04 之后的版本,在安装界面里自带了 LVM 全盘加密安装的选项。但是并不能满足第 3 条需求。所以还需要一些复杂的手动操作。

安装过程尽量围绕 ubuntu 的图形安装界面,对新人友好。参考并验证了这篇教程。但原文连同 /boot 引导分区也一起加密了,于是在配置上略显繁琐。我觉得加密 /boot 并不是很有必要,做了一些改动。最终的硬盘分区结构为(以 512GB 硬盘为例):

  • 大约 800MB,EFI 引导分区
  • 大约 300GB,LUKS 加密分区。在其中配置 LVM 逻辑分区:
    • 2GB,swap 交换分区
    • 大约 300GB,Ubuntu 系统分区 root /
  • 大约 200GB,普通移动硬盘分区

操作步骤:

下载 Ubuntu,制作 USB 安装盘(过程略)。——然后,强烈建议在整个安装过程之前,在电脑的 BIOS 里,把内置的其它硬盘暂时卸载。

插上移动硬盘和 USB 启动盘。从 U 盘启动电脑,选择 Try Ubuntu。最新的 Ubuntu 22.04 安装程序里,已经内置了所需的 cryptsetup 和 cryptsetup-initramfs 软件包。因此,整个安装过程中,应该不需要连接互联网。

首先,把硬盘预分区。分区软件有很多种,可以用原文的 sgdidk,也可以直接用图形界面下的 Disk 或者 Gparted。在硬盘上创建 GPT 分区表,然后分成:

  • 大约 800MB,EFI 引导分区
  • 大约 300GB,要加密的系统分区
  • 余下的约 200GB 移动硬盘

这些分区都先不用格式化。记住第二个分区的名字,本文假定为 /dev/sda2。

分区成功后,关闭分区软件,打开 Terminal 命令界面,执行 root 权限

sudo -i

将系统分区加密。按提示输入密码,——这个密码,就是以后每次启动时,挂在硬盘用的密码。和安装 Ubuntu 时的用户密码,并不是一回事。

cryptsetup luksFormat --type=luks1 /dev/sda2

解锁刚刚加密的分区:

cryptsetup open /dev/sda2 hd2_crypt

创建逻辑卷组(LVM),然后在其中创建 2GB 的 swap 交换分区,再把剩余的空间创建为系统分区(这两个分区的大小,大家自行调整):

pvcreate /dev/mapper/hd2_crypt

vgcreate ubuntu--vg /dev/mapper/hd2_crypt

lvcreate -L 2G -n swap_1 ubuntu--vg

lvcreate -l 100%FREE -n root ubuntu--vg

然后,运行桌面上的 Ubuntu 安装程序(Terminal 先不要关),在磁盘分区页面,选择 Something else,进行手动分区。

  • 把 /dev/mapper/ubuntu—-vg-root 格式化成 ext4,挂载为系统根目录 /
  • 把 /dev/mapper/ubuntu—-vg-swap_1 设为 swap 交换分区
  • 把 /dev/sda1 设为 EFI 引导分区

点击 Install Now,确认对分区的设置。注意,到了下一步创建用户的界面时,先不要继续。切换回 Terminal 命令行界面,正式安装前,在 GRUB 中启用加密(能看懂下面这些命令的话,也可以直接去编辑相应的文件):

while [ ! -d /target/etc/default/grub.d ]; do sleep 1; done; echo "GRUB_ENABLE_CRYPTODISK=y" > /target/etc/default/grub.d/local.cfg

然后回到创建用户的页面,点击继续,开始安装系统。安装结束后,先不要 restart。而是点击 Continue Testing。

回到 Terminal 命令行界面,chroot 到新装的系统:

mount /dev/mapper/ubuntu----vg-root /target

for n in proc sys dev etc/resolv.conf; do mount --rbind /$n /target/$n; done

chroot /target

mount -a

原文说此时需要(联网)安装 apt install cryptsetup-initramfs;但我用的 ubuntu 安装程序已经自带了,并不需要联网安装软件包。

添加密钥文件相关设置:

echo "KEYFILE_PATTERN=/etc/luks/*.keyfile" >> /etc/cryptsetup-initramfs/conf-hook

echo "UMASK=0077" >> /etc/initramfs-tools/initramfs.conf

创建密钥文件并将其添加到 LUKS

mkdir /etc/luks

dd if=/dev/urandom of=/etc/luks/boot_os.keyfile bs=512 count=1

chmod u=rx,go-rwx /etc/luks

chmod u=r,go-rwx /etc/luks/boot_os.keyfile

将密钥添加到 boot_os.file 和 Crypttab

cryptsetup luksAddKey /dev/sda2 /etc/luks/boot_os.keyfile

echo "hd2_crypt UUID=$(blkid -s UUID -o value /dev/sda2) /etc/luks/boot_os.keyfile luks,discard" >> /etc/crypttab

更新 Initialramfs 内核映像

update-initramfs -u -k all

此时全部结束。可以重启系统啦。


关于这个硬盘密码:

  • 是用来防止,别人拿到这块硬盘时,无法查看硬盘的文件;
  • 并不能防止,当你登入系统后,因为系统漏洞或操作失误,而造成的入侵;
  • 这个密码,如果忘记了,硬盘里的文件,就再也无法看到了!!(有添加 recovery 的操作,但我觉得没必要);
  • 每次开机启动时,都要输入一次这个密码。所以,虽然密码需要足够复杂,但最好选一个,自己能方便记住,日常使用的方式。

如何修改硬盘密码:

最简单的方式,是在已经启动的移动硬盘系统里,先通过 disk 等分区软件,确认加密分区的名字(这里假设仍然是 /dev/sda2,但实际上不一定了),打开 Terminal 界面,

sudo -i

cryptsetup luksChangeKey /dev/sda2

按照提示,输入旧密码,再输入两遍新密码。最后,更新 initramfs,

update-initramfs -u -k all

就可以了。

❌