nixos-vex: configuration-aware VEX for vulnerability triage
nixos-vex: configuration-aware VEX for vulnerability triage
SBOM scanners are blunt. They see package names and versions, not whether your OpenSSH build ever hands a login prompt to PAM, whether OPIE is in the closure, or whether keyboard-interactive authentication is off. The result is a flood of CVEs that do not apply to this machine. VEX (Vulnerability Exploitability eXchange) is the machine-readable answer: a statement that a product is not_affected (or still affected) for a named vulnerability, with a justification scanners can honor.
NixOS makes that statement cheap to re-check. The system configuration is data. A module can evaluate predicates against config.* and emit an OpenVEX document that only keeps statements still true for your host. nixos-vex packages that idea: a shared feed of unconditional and conditional triage decisions, plus a NixOS module that builds system.build.vexFeed for your configuration.
This chapter teaches the VEX mental model, how NixOS turns config into exploitability predicates, how to wire nixos-vex + sbomnix + grype, and where the trust boundaries are. Versions: NixOS 26.05 (stable “Yarara” as of 2026-10) / nixos-unstable for the feed’s current baseline; OpenVEX v0.2.0 context. No local NixOS rebuild was run on this box—module shapes and commands follow the project’s README and the October 2026 walkthrough that introduced the technique.
Learning goals
By the end of this chapter you can:
- Explain what an OpenVEX statement claims (
status,justification, product@id) - Contrast SBOM scanning with config-aware triage on NixOS
- Import
nixos-vex.nixosModules.defaultand buildsystem.build.vexFeed - Pass the feed to
grype --vexnext to an SBOM - State the project’s disclaimer: zero untriaged is not zero vulnerabilities
Thesis
On NixOS, exploitability is often a property of the module graph, not only of the package version. Codify the triage once as a conditional OpenVEX statement; re-evaluate it on every rebuild so scanners stop re-asking questions the config already answered.
Mental model
SBOM (sbomnix / syft) OpenVEX feed (nixos-vex)
┌──────────────────┐ ┌─────────────────────────────┐
│ packages in │ │ unconditional: always true │
│ system closure │──grype──▶ │ conditional: config.services│
│ │ + VEX │ .openssh.settings.… │
└──────────────────┘ │ only true statements kept │
└─────────────────────────────┘
| Idea | Meaning |
|---|---|
| SBOM | Inventory of components (what is installed) |
| VEX | Asserted exploitability status for a CVE × product |
| Unconditional statement | True for every NixOS system in the feed’s scope (bad upstream data, wrong distro patch, …) |
| Conditional statement | True only if predicates on your config hold; dropped (with a warning) when they fail |
system.build.vexFeed |
Derivation output: OpenVEX JSON tailored to this configuration |
OpenVEX statuses you will see most often: not_affected with justifications such as vulnerable_code_not_present, vulnerable_code_not_in_execute_path, or component_not_present (see the OpenVEX specification).
Case 1 — Why SBOMs alone drown ops
A typical pipeline:
- Generate a CycloneDX SBOM for a system or container image.
- Run Grype (or another scanner) against that SBOM.
- Triage every hit by hand.
For language ecosystems with strong reachability tools (Go’s govulncheck), many hits evaporate. For an OS image, the same CVE may depend on PAM modules you never install, filesystems you never mount, or SSH options you already disabled. Repeating that analysis on every config change does not scale.
NixOS’s advantage is not “fewer CVEs in nixpkgs.” It is that the same evaluation that builds the system can answer “does this knob still hold?”
Case 2 — Minimal conditional VEX from a NixOS module
The technique (illustrated in Fabian Kammel’s October 2026 post, and the shape nixos-vex automates) looks like this: a boolean derived from config and pkgs, then lib.optional into an OpenVEX statements list.
# illustrative-module.nix — pattern only; prefer nixos-vex's feed in production
{
config,
lib,
pkgs,
...
}:
let
# CVE-2007-2768: username enumeration via OPIE through PAM / keyboard-interactive
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://example.invalid/vex/sshd";
author = "lab";
timestamp = "2026-10-16T00: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";
};
}
);
}Wire a host that enables SSH with keyboard-interactive off (NixOS 26.05+ system.stateVersion style):
# configuration.nix fragment
{
system.stateVersion = "26.05";
services.openssh.enable = true;
services.openssh.settings.KbdInteractiveAuthentication = false;
}When notAffected is true, Grype can ignore that CVE for the OpenSSH product id. When someone flips keyboard-interactive back on, the statement disappears from the document—the scanner noise returns, which is the correct failure mode.
nixos-vex’s author explicitly warns: treat the feed as a triage aid, not a high-trust attestation. Statements may be wrong; LLM-assisted drafts exist in the project history and are human-signed-off, not oracle-perfect. Conditions see configuration, not imperative runtime changes.
Case 3 — Using nixos-vex on a flake host
From the nixos-vex README:
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixos-vex.url = "github:datosh/nixos-vex";
};
outputs = { nixpkgs, nixos-vex, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
./configuration.nix
nixos-vex.nixosModules.default # adds system.build.vexFeed
];
};
};
}Build the feed and scan:
# SBOM for the system closure (sbomnix elsewhere in your CI)
# sbomnix .#nixosConfigurations.myhost.config.system.build.toplevel
nix build .#nixosConfigurations.myhost.config.system.build.vexFeed -o vex-feed.json
grype sbom:sbom.cdx.json --vex vex-feed.json| Artifact | Role |
|---|---|
| Baseline host in the repo | Defines which packages the shared feed has triaged |
| Unconditional statements | Apply to every consumer of the feed |
| Conditional statements | Filtered by the module against your config |
| Failed condition | Warning + triage handed back to you (status_notes may carry [nixos-vex:condition=<id>]) |
Demos in the repository (demo/desktop, demo/server) show a soft desktop profile (more failed conditions) versus a hardened server (strict mode when every statement holds).
The trap
- Equating “ignored by VEX” with “safe.” The feed’s goal is zero untriaged findings for packages it covers—not a clean bill of health.
- Assuming the feed covers packages you added. Only the reference baseline’s packages are triaged; your extra services are on you.
- Trusting conditions against a drifted runtime. Loading a kernel module by hand or starting a unit outside the config is invisible to
config.*predicates. - Shipping a hand-written VEX without CI. The whole point is re-evaluation on rebuild; a stale JSON file in git is worse than honest scanner noise.
The boring rule
Pair every SBOM scan of a NixOS closure with a config-evaluated OpenVEX document. Prefer a maintained feed module (nixos-vex or an in-house equivalent) over one-off JSON. Re-check failed conditions in review the same way you review services.* diffs.
Try this
- Add
nixos-vexto a lab flake and buildsystem.build.vexFeed. - Generate an SBOM with sbomnix for the same
toplevel. - Run Grype twice: once without
--vex, once with it. Diff the ignored count. - Flip one hardening knob that a conditional statement depends on; rebuild the feed; confirm the statement drops and the CVE reappears.
Sources
- nixos-vex — module, feed, demos, disclaimer
- The Unreasonable Effectiveness of VEX in NixOS (Fabian Kammel / DaTosh, 2026-10-11) — walkthrough motivating the tool
- OpenVEX / OpenVEX spec
- sbomnix, Grype VEX support
- NixOS 26.05 announcement — current stable channel context
Inspiration
- Topic signal: discussion of configuration-aware VEX on NixOS (HN / X threads around the Kammel post, October 2026). Chapter cites the blog and repository above, not social posts.
Verification
Docs-based against nixos-vex README and the Kammel post (retrieved 2026-10-16). No nixos-rebuild / grype run on this shared box (no NixOS host, no package installs for scanners). NixOS 26.05 confirmed current stable via nixos.org announcement; nixos-vex itself currently targets nixos-unstable x86_64 for its baseline feed—call that out in CI if you pin 26.05.