Heimdall

In Norse myth, Heimdall guards the Bifröst bridge and lets nothing into Asgard unchecked, especially not data.
What it is
Heimdall is a defensive sandbox for inspecting content. All content is untrusted until it is verified. That includes signed content: a digital signature is itself untrusted input until it has been checked, and the check must not be able to attack the checker. So Heimdall parses, verifies and scans in an isolated job:
- A compromised job can touch nothing but its own input and its own output folder.
- The caller never parses the untrusted bytes itself. It trusts only Heimdall's signed verdict, checked against a pinned key.
- Heimdall's tests exercise its own containment, never anything outside it.
Pure Rust, static binaries.
Users
- Current: Holger and Gunnar (integration in progress).
- Planned: tunnr Rusternetes (a seccomp profile per pod, then a
heimdallRuntimeClass).
Layers
Each job runs inside every layer below; an attacker must break all of them.
| Layer | What it does |
|---|---|
| Kernel | The node runs a kernel trimmed to what its programs use, so less kernel code can be attacked. |
| Container (youki) | Namespaces, read-only root, no network unless allowed, all capabilities dropped, no-new-privs, cgroup v2 limits, image checked at every start. |
| Seccomp allowlist | Only the system calls the job really uses, with fixed arguments where possible. Anything else kills the job. |
| Landlock | File access only to the job's input (read) and output (write). |
| Syscall broker | The few risky calls (open, socket, connect) are paused and checked by Heimdall, which opens the file or socket itself, so the job cannot race the check. |
| Wasm host (optional) | The parser runs as signed WebAssembly with no file or network access, under fuel, time, memory and stack limits. |
| Signed result | The verdict and the output hashes are signed with a per-boot Ed25519 key. |
Tuning: how the allowlists and the kernel are fitted
Nothing is guessed. Heimdall measures what a program does and allows only that.
- Build the program and the node kernel (Build Thing).
- Probe: run the program's full test suite and fuzz corpus on a probe kernel, in learning mode. Heimdall records every system call with its important arguments; the probe kernel records which kernel code ran.
- Generate: Heimdall merges the runs into the smallest seccomp profile per program, architecture and libc (a Rust table plus reviewable JSON). The kernel side lists kernel options no program used, explained through the Kconfig dependency graph.
- Review: a person checks the diff and commits it. Nothing is merged automatically.
- Enforce: production runs the checked-in profile; any call outside it kills the job. Kernel options are removed one at a time, each with a full test rerun.
- Drift check: every CI run probes again and fails if a program makes a call its profile does not have, naming the call and the test that made it.
Paths a test never reached are missing from the profile, so they fail closed. The probe therefore runs error paths, failover and fuzzing, not only normal use.
Details: docs/API.md (the job API: the JobRunner trait, v0.1.0, and the interim heimdall-plain runner, an ordinary youki container stated as such), docs/SPEC.md (the specification) and docs/BUILD-THING.md (how Build Thing drives the tuning).
Usage
heimdall run --job wasm --module <guest.wasm> --trust-root <key> --in <dir> --out <dir> [--policy <file>]
heimdall verify-result <out>/result.signed --key <pinned key> --files
See docs/CLI.md, docs/POLICY.md and docs/RUN.md.
Standards and specifications
| Standard | Used for | Status |
|---|---|---|
| RFC 8032 (Ed25519) | Signed results, strict verification | Implemented |
| FIPS 180-4 (SHA-256) | Output hashes, key ids | Implemented |
| BLAKE3 | Guest module pins | Implemented |
| WebAssembly Component Model, WIT | The guest interface; WASI is not linked | Implemented |
| DSSE (in-toto envelope) | Bundle verification before payload parsing | In progress |
| RFC 5280 (X.509) | Bundle signatures | In progress |
| OCI Runtime Specification | The youki container (heimdall-plain) | Implemented |
| Linux seccomp-bpf | A fixed default profile (heimdall-plain) today; the generated per-program allowlist is planned | Implemented (fixed profile) |
| seccomp USER_NOTIF | The syscall broker | Planned |
| Landlock LSM (ABI 1–4) | File and network access rules | Planned |
| Linux cgroup v2, user namespaces, no_new_privs | Resource limits and privilege drop | Implemented |
| NIST SP 800-190 (Application Container Security Guide) | Container hardening guidance | Followed in design |
| SLSA v1.0 Build Level 3 | Release provenance through the same pipeline Holger and Gunnar already release with (dwarves-slsa, hardware-key signed) | Planned for Heimdall's first release (v0.1.0 is not attested yet) |
"Implemented" means it is on main with tests; nothing in this table is a certification.
Get the code
git clone ssh://anonymous@git.gunnar.rs:2222/nordisk/heimdall.git
cd heimdall
cargo build --release
Contributing, security and changes
- CONTRIBUTING.md: how to get the code, report a bug and propose a change.
- CLA.md: the Contributor License Agreement (version 1.1) that every outside contribution needs; accept it with one line in CLA-SIGNATURES.md.
- SECURITY.md: how to report a vulnerability privately (gunnar@gunnar.rs, subject
SECURITY:). - CHANGELOG.md: what changed in each release, and the vulnerabilities each one fixed.
Licence
AGPL-3.0-only, or a commercial licence from the joint owners Ignalina ApS and Vetra Automation AB (see COMMERCIAL-LICENSING.md). Contributions are accepted under the CLA.
Fuzzing results
Measured 2026-10-30 on main after v0.1.0: every cargo-fuzz target ran 30 minutes (the first six twice), about 1.9 billion inputs in total, with no crash. Run it yourself with scripts/fuzz.sh.
| Target | What it feeds | Inputs tried | Crashes |
|---|---|---|---|
output_name | Output file names | 466 million | 0 |
pinned_key_text | The pinned key line | 287 million | 0 |
result_encoding | The canonical result encoding | 209 million | 0 |
policy_file | The TOML policy file | 14.4 million | 0 |
verify_result_file | A result file given to verify-result | 10.7 million | 0 |
signed_result_wire | The signed-result wire format | 10.6 million | 0 |
wasm_emit_file | Guest file names, sizes, traps partway through | 4.6 million | 0 |
wasm_guest_load | Random bytes through pin, signature and compile | 186,000 | 0 |
wasm_guest_load is slow by design: it signs and compiles every input.