インフラ・DevOps
NixOSにおけるVEXの不合理な有効性
The Unreasonable Effectiveness of Vex in NixOS (blog.kammel.dev)
要約
LLMによる脆弱性報告の増加に伴い、開発者だけでなくシステム管理者も脆弱性管理に苦慮している。VEX(Vulnerability Exploitability eXchange)は、特定のソフトウェア製品が既知の脆弱性に対して実際に影響を受けるかどうかを示す機械可読なセキュリティ勧告だが、その自動生成は課題となっていた。本記事では、NixOSの宣言的な設定と静的解析能力を活用することで、VEXステートメントを条件付きで正確に生成し、脆弱性管理の効率を大幅に向上させる方法を解説する。
全文翻訳
NixOSにおけるVEXの不合理な有効性
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"))
}
v0.38.0のgolang.org/x/textライブラリを使用しています。
module maybe-vulnerable
go 1.26.8
require golang.org/x/text v0.38.0
プログラムを脆弱性スキャンする一般的なアプローチは以下の通りです。
1. ソフトウェア部品表(SBOM)を生成する
2. 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と述べる2つのVEXステートメントを作成できますが、これには2つの問題があります。
1. この手動プロセスは、コードベースのサイズと依存関係の数に応じてスケールします。
2. コードを変更するたびに、脆弱な関数を使用し始める可能性があるため、完全な分析を繰り返す必要があります。
到達可能性分析
幸いなことに、プログラミング言語は高度に構造化されているため、(少なくとも機械にとっては)推論が容易です。それに加えて、Goチームはその作業に非常に熱心であり、新しい脆弱性が文書化される際に、ecosystem_specificフィールドを使用して、影響を受けるすべてのインポートパスとシンボルを記録しています。これがgovulncheckの基盤となっています。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は有効ですが、keyboard-interactive認証は無効になっており、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ベースのOSの例に非常に近いこのシステムを考えてみましょう。
{
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