NixOS 中 VEX 的惊人有效性
The Unreasonable Effectiveness of Vex in NixOS
面对今年 LLM 辅助漏洞报告激增,NixOS 展示了用 VEX(Vulnerability Exploitability eXchange)文档自动化漏洞分诊的思路。
- 2026年10月11日,星期日
- 7分钟阅读
今年,LLM辅助生成的漏洞报告大量涌现,由此我们听到了许多关于热门(开源)项目维护者应对这些报告的艰难处境。大多数项目已经放缓了功能开发,转而专注于漏洞分类处理;另一些项目仍在寻找应对这股洪流本身的方法。
虽然我们倾向于听到许多来自维护者的报告,但对于流程另一侧的人们,我们听到的较少:系统管理员、SRE和AppSec团队同样在应对这股洪流。新的(可能未修补的)CVE每天都在出现。每个CVE都需要评估,受影响的软件需要更新或手动打补丁。
如果你在这个领域待过一段时间,用不了多久就会听说VEX文档和声明。VEX代表Vulnerability Exploitability eXchange(漏洞可利用性交换):一种机器可读的安全公告,告诉你某个特定软件产品是否真的暴露于已知漏洞。
听起来很棒!但我们真的能以自动化方式生成这些声明,来支持可扩展的漏洞分类流程吗?答案一如既往:视情况而定。
基于SBOM的扫描
让我们来看看当今许多项目和组织中使用的安全流程。
给定一个简单的Go程序:
package main
import (
"fmt"
"golang.org/x/text/width"
)
func main() {
fmt.Println(width.Widen.String("hello, world"))
}
使用golang.org/x/text库的v0.38.0:
module maybe-vulnerable
go 1.26.8
require golang.org/x/text v0.38.0
扫描程序漏洞的常见方法是:
- 生成软件物料清单(SBOM)
- 扫描SBOM中记录的组件以查找已知漏洞
每个使用这种方法的团队都会看到以下漏洞(截至2026年10月11日):
$ syft scan . -o cyclonedx-json > sbom.cdx
$ grype sbom:sbom.cdx
NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK
golang.org/x/text v0.38.0 0.39.0 go-module GO-2026-5970 High 0.5% (39th) 0.4
golang.org/x/text v0.38.0 0.41.0 go-module GO-2026-6629 High 0.3% (24th) 0.3
但如果我们做足功课,就会发现这两个漏洞实际上都不影响我们的程序。
GO-2026-5970只存在于golang.org/x/text/unicode/norm包中,而GO-2026-6629只存在于golang.org/x/text/secure/precis包中。我们的程序都没有使用它们!
我们可以利用这些信息,编写两份声明vulnerable_code_not_present的VEX声明,但这存在两个问题:
- 这个手动流程的规模会随着代码库大小和依赖数量的增长而增长。
- 每次更改代码时都必须重复完整的分析,因为你可能会开始使用某个易受攻击的函数。
可达性分析
幸运的是,编程语言高度结构化,因此(对机器来说)很容易对其进行推理。
除此之外,Go团队工作非常勤勉,在记录新漏洞时,会使用ecosystem_specific字段记录每个受影响的导入路径和符号。
这正是govulncheck的动力来源。它利用对我们源代码的静态分析和Go漏洞数据库,将报告缩小到仅可能影响应用程序的那些。在我们的示例应用程序上运行它会显示:
$ govulncheck .
=== Symbol Results ===
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 14 vulnerabilities in
modules you require, but your code doesn't appear to call these vulnerabilities.
如果你拥有这种能力,可以想象关闭Dependabot,只在受影响时打补丁。
这在操作系统上就行不通了
让我们看看这在操作系统上会如何运作。我们使用这个Dockerfile:
FROM debian:trixie@sha256:913f6706df59a68922d1dd08f78c2476560a8d367897200a6005b00e5f67c2d5
RUN apt-get update \
&& apt-get install -y --no-install-recommends openssh-server \
&& mkdir -p /run/sshd \
&& rm -rf /var/lib/apt/lists/*
然后像之前一样扫描:
$ docker build -t opensshpam .
$ syft scan docker:opensshpam -o cyclonedx-json > sbom.cdx
$ grype sbom:sbom.cdx
✔ Scanned for vulnerabilities [284 vulnerability matches]
├── by severity: 3 critical, 65 high, 80 medium, 39 low, 97 negligible
└── by status: 0 fixed, 284 not-fixed, 0 ignored
NAME INSTALLED FIXED_IN TYPE VULNERABILITY SEVERITY EPSS RISK
[...]
openssh-server 1:10.0p1-7+deb13u4 deb CVE-2007-2768 Negligible 8.6% (94th) 0.4
[...]
在这个演示中,我们将专注于单个漏洞CVE-2007-2768的分类流程,其余283个留给读者作为练习。🫠
简而言之,如果 sshd 通过 PAM 使用 OPIE 一次性密码,攻击者就能从登录提示中判断某个用户名是否存在。这个问题自 2007 年以来在上游一直未修复。
手动检查我们的 sshd 配置,可以看到 PAM 已启用,但键盘交互式认证被禁用,而这是 sshd 将登录提示交给 PAM 的唯一方式。
$ docker run --rm opensshpam sh -c 'sshd -T | grep -E "^(usepam|kbdinteractiveauthentication) "'
usepam yes
kbdinteractiveauthentication no
此外,我们可以看到没有配置名为 opie 的模块。
$ docker run --rm opensshpam grep -E '^(@include|auth)' /etc/pam.d/sshd /etc/pam.d/common-auth
/etc/pam.d/sshd:@include common-auth
/etc/pam.d/sshd:@include common-account
/etc/pam.d/sshd:@include common-session
/etc/pam.d/sshd:@include common-password
/etc/pam.d/common-auth:auth [success=1 default=ignore] pam_unix.so nullok
/etc/pam.d/common-auth:auth requisite pam_deny.so
/etc/pam.d/common-auth:auth required pam_permit.so
$ docker run --rm opensshpam sh -c 'find / -xdev -name "pam_opie*" 2>/dev/null; dpkg -l | grep -i opie'
# nothing is printed
每次配置变更都要对另外 283 个漏洞做这件事,根本不可行!
NixOS 的非凡效力
那么,NixOS 有什么不同?让我们看看这个系统,它非常接近我们之前那个基于 Docker 的操作系统示例:
{
fileSystems."/" = {
device = "/dev/sda1";
fsType = "ext4";
};
boot.loader.grub.device = "nodev";
system.stateVersion = "26.11";
services.openssh.enable = true;
services.openssh.settings.KbdInteractiveAuthentication = false;
}
使用以下 flake:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ nixpkgs, ... }:
{
nixosConfigurations.sshd = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
];
};
};
}
将 syft 替换为 sbomnix,我们就可以运行与之前相同的过程:
$ sbomnix .#nixosConfigurations.sshd.config.system.build.toplevel
[...]
INFO Wrote: sbom.cdx.json
$ grype sbom:sbom.cdx.json
✔ Scanned for vulnerabilities [272 vulnerability matches]
├── by severity: 14 critical, 84 high, 152 medium, 22 low, 0 negligible
└── by status: 147 fixed, 125 not-fixed, 0 ignored
NAME INSTALLED FIXED_IN TYPE VULNERABILITY SEVERITY EPSS RISK
[...]
openssh 10.5p1 nix CVE-2007-2768 Medium 8.6% (94th) 4.0
[...]
终于,我们来到了收获的时刻。正如我们之前了解到的,CVE-2007-2768 只在某些条件成立时才会影响我们的系统,而 NixOS 让我们能够将这些条件代码化!
不仅如此,我们还可以有条件地生成 VEX 源,仅在条件仍然成立时保留 VEX 声明:
{
config,
lib,
pkgs,
...
}:
let
notAffected =
!(pkgs ? opie)
&& config.services.openssh.settings.KbdInteractiveAuthentication == false
&& !(lib.any (rule: rule.enable && lib.hasInfix "opie" rule.modulePath) (
lib.attrValues config.security.pam.services.sshd.rules.auth
));
in
{
system.build.vex = pkgs.writeText "vex.json" (
builtins.toJSON {
"@context" = "https://openvex.dev/ns/v0.2.0";
"@id" = "https://kammel.dev/vex/sshd";
author = "Fabian Kammel";
timestamp = "2026-10-11T00:00:00Z";
version = 1;
statements = lib.optional notAffected {
vulnerability.name = "CVE-2007-2768";
products = [ { "@id" = "pkg:nix/openssh"; } ];
status = "not_affected";
justification = "vulnerable_code_not_present";
};
}
);
}
将其集成到我们的流程中:
$ VEX=$(nix build --no-link --print-out-paths .#nixosConfigurations.sshd.config.system.build.vex)
$ grype --vex $VEX sbom:sbom.cdx.json
✔ Scanned for vulnerabilities [271 vulnerability matches]
├── by severity: 14 critical, 84 high, 152 medium, 22 low, 0 negligible
└── by status: 147 fixed, 125 not-fixed, 1 ignored
这个已分诊的 CVE 现在被成功忽略了。
nixos-vex
如你所见,构建这些条件需要大量工作,但回报是巨大的。一旦某个 CVE 被分诊并代码化,重新检查的成本几乎为零(只需一点点 CI 开销)。
我一直在为我的 NixOS 机器做这类分诊工作,并将其开源为 nixos-vex。
查看示例 桌面 和 服务器 配置的演示,它们还展示了 nixos-vex 内置的一些其他功能。
我还不建议将其用作高可信度的数据源!
我的目标是分享这一流程,邀请反馈,并希望将来能将其回馈给 Nix 生态系统。
来源:Hacker News · blog.kammel.dev