Eight bits of trust, one disposable microVM
Private keys of Indian digital certification authority eMudhra were compromised. We simulated a MITM attack with its keys in an isolated Slicer microVM.
One of the compromised private keys was the number 4. It begs the question of how this could have been deemed acceptable and responsible.
I came across Scott Helme's Eight Bits of Trust on Hacker News. Eight intermediate CA private keys, which should have been random 384-bit numbers, each fitted into a single byte.
Scott recovered all eight from their public certificates in less than a second. The incident disclosure contained the affected certificates and signatures proving that the reporter held the corresponding keys.
What I set out to test
This incident gave me a useful, bounded test for Slicer as a forensic environment. I wanted to see whether agents could take the public evidence, recover the affected keys, and simulate a MITM attack without gaining access to my workstation or LAN. I was also interested in refusals due to the nature of the work.
The walkthrough has four parts:
- I prepare the tools and pin the public inputs in an Internet-connected builder
- Then I shut down and commit that VM, then fork it with all egress blocked in the new VM
- Then I recover the keys and issue a local test certificate
- Finally I compare an old Ubuntu trust store with a refreshed bundle, then clean up the lab
For the models, I used a mixture of Daybreak Blue (codex) and GLM 5.3 Flash (opencode), with GLM running locally on our DGX Sparks. Neither model hit a classifier warning or refused any part of the task. We reproduced the public evidence inside an authorised, isolated environment fully within our own control.
Working with recovered CA private keys and minting certificates is exactly the kind of job I keep away from my workstation. The inputs may be public, but the tools, generated keys, test server, and mistakes made along the way can all stay inside a disposable microVM.
What's more - when testing "exploits" or "PoCs" from people we don't know on the Internet, there is always the latent risk that these artifacts themselves contain malware or exploits. We exercise the same care when evaluating local AI serving recipes for the DGX Sparks.
I used the same pattern of building a VM image with full Internet access, before cloning it and running it in an airgap. That's the same approach as in Build once, fork many. You'll see similar patterns in OpenAI's sandboxes and those used by GitHub Copilot for code-review.
The first VM downloaded everything needed and recorded the hashes. The clone had no egress, so it could not fetch different inputs, call home, or send generated material elsewhere.
Open builder, closed runner
I use this pattern a lot with agents:
+----------------- open builder -----------------+
| Download public evidence -> verify SHA-256 |
+-------------------------+----------------------+
|
shutdown + commit
|
v
+---------------- closed runner -----------------+
| no egress -> reproduce -> negative control |
+------------------------------------------------+
The builder can reach the Internet to download the public inputs and packages. I then shut it down, committed the disk, and forked a fresh child with all egress removed.
That gave the agent a hermetic environment for the forensic work: pinned inputs, no route to the Internet or my LAN, no access to the host, and a working disk I could delete afterwards.
The commands below are from my Linux and Firecracker run, where the firewall flags can be applied directly to the fork.
For our own code review bot, the pattern mirrors this, only we gather the Git checkout, the PR description, and instructions on the host, and then copy them directly into an airgapped VM.
Preparing the lab
Rather than muddying any existing Slicer installation we have, we'll start a new daemon and throw it away after.
so on a Linux machine with KVM and Slicer installed, create an isolated host group with no automatically managed VMs:
mkdir -p emudhra-lab
cd emudhra-lab
slicer new emudhra \
--count 0 \
--min \
--storage image \
--net isolated \
> slicer.yamlStart Slicer in one terminal:
sudo -E slicer up slicer.yamlThe generated group permits outbound traffic. That is useful whilst preparing the builder, but not for the actual reproduction.
In another terminal, give the run a unique ID and launch a persistent builder:
RUN_ID=$(date -u +%Y%m%d%H%M%S)
WORKFLOW=emudhra-ca-repro
BUILDER=$(slicer vm add emudhra \
--name "emudhra-builder-$RUN_ID" \
--persistent \
--tag "workflow=$WORKFLOW" \
--tag "run=$RUN_ID" \
--tag "phase=builder" \
--wait \
--json | jq -r '.hostname')--min uses the smaller Slicer image. On a fast host, its guest agent is usually ready in around 500ms. The first image download and unpack is a separate, one-off step.
Bringing in the public evidence
I pinned the disclosure repository to commit bb7c2a8, then recorded a digest for every input:
SOURCE=https://raw.githubusercontent.com/galadran/we-have-a-problem.com/bb7c2a87c5f9157ad83dd8ac32ae90a7e3205274
slicer vm exec "$BUILDER" -- "
mkdir -p /home/ubuntu/emudhra &&
cd /home/ubuntu/emudhra &&
curl -fsSLO $SOURCE/proof.zip &&
curl -fsSLO $SOURCE/chains.zip &&
curl -fsSLO $SOURCE/impacted-intermediates.json &&
curl -fsSLo cacert.pem https://curl.se/ca/cacert.pem &&
sha256sum proof.zip chains.zip impacted-intermediates.json cacert.pem
"These were the files used for my run:
2beef4e528266c0df40f3571e5f1c428c6a28b1f22c313b12b62f8e4125a699f proof.zip
8a2edc81b6c51e3ad27d45ca1e441ec84c961e2dee0fc547f2a02c0f74315f95 chains.zip
f67b3abe9b4879f4e91874ed041beccd71257f1db41c588c724ccf12aa90c59e impacted-intermediates.json
796f1389bfea13c0ed78f663b765baaff54a508e5483786bd6ece73c5e3e3cbe cacert.pemThe last file came from curl's Mozilla CA extract. It was fetched on 21 October and had a Last-Modified date of 20 October 2026. That URL changes over time, which is why the digest is important.
Cutting off the Internet
With all the inputs on disk, shut down the builder and commit it:
slicer vm shutdown "$BUILDER"
COMMIT=$(slicer vm commit "$BUILDER" \
--cache-key "emudhra-ca-$RUN_ID" \
--json | jq -r '.commit_id')Now fork a disposable child, clear the inherited allow list, and drop everything else:
RUNNER=$(slicer vm fork "$COMMIT" \
--name "emudhra-runner-$RUN_ID" \
--rm \
--no-allow \
--drop 0.0.0.0/0 \
--tag "workflow=$WORKFLOW" \
--tag "run=$RUN_ID" \
--tag "phase=locked" \
--json | jq -r '.hostname')The drop is enforced in the VM's network namespace on the host. Root inside the guest cannot run iptables -F and make it go away.
To prove the drop, I could not use ports 80 or 443 because Slicer's guest helper handles them. Instead, I tried port 22, which is not redirected:
slicer vm exec "$RUNNER" -- \
"if timeout 3 bash -c '</dev/tcp/1.1.1.1/22'; then
echo 'unexpected outbound connection'
exit 1
else
echo 'outbound connection blocked'
fi"The connection timed out. From then on, the agent only had the files already on its disk.
What happened?
The result matched Scott's post:
| Check | Result |
|---|---|
| Weak intermediate keys recovered | 8 of 8 |
| Match with Scott's published values | Exact match |
| Published proof-of-possession signatures | Verified for all eight keys |
| Total key-recovery time | About 0.4 seconds |
| Locally issued leaf with the old Ubuntu bundle | Accepted |
| The same leaf with the refreshed CA bundle | Rejected |
Instead of using the name of a real company, the agent issued the local leaf for demo.local, mapped it to 127.0.0.1, and bound the test server to the guest's loopback adapter.
The old Ubuntu 22.04 trust store said:
subject: CN=demo.local
issuer: C=NL; O=eMudhra BV; CN=EM DV TLS CA - G3B
SSL certificate verify ok.The key behind that chain took 63 milliseconds to recover.
The negative control caught something I could easily have missed. Giving curl a fresh bundle with --cacert did not disable OpenSSL's default CApath. OpenSSL could still build the chain through the stale root under /etc/ssl/certs.
I repeated the request with a Python SSL context configured to use only the refreshed bundle. It rejected the certificate.
Pointing one command at a newer file is not enough when another trust path remains active. Update the complete trust store, rebuild images which carry their own bundle, and restart the affected workloads.
This reproduced the TLS failure in a lab. A real interception also needs a client which still trusts the affected chain and an attacker able to get onto its network path.
Cleaning up
I stopped the local server, then removed the child, commit, and builder:
slicer vm delete "$RUNNER"
slicer vm commit delete "$COMMIT"
slicer vm delete "$BUILDER"Afterwards, I checked for any VMs carrying the run tag:
slicer vm list --json | jq -r --arg run "run=$RUN_ID" '
.[] |
select(any(.tags[]?; . == $run)) |
.hostname'The query returned no results. I also checked for the commit, Firecracker process, network namespace, and working disks before calling the run complete.
A useful pattern for forensic work
I don't want recovered signing keys, generated certificates, or whatever today's Hacker News curiosity happens to be scattered across my workstation.
An ordinary VM protects the host, but still has an open door to the Internet. Cold forking gave me a clean point at which to close it: download and hash the evidence, shut down, commit, then fork the forensic environment without egress.
I'd use the same sequence for malware inspection, an untrusted build script, a questionable dependency, or agent-generated code. Give the agent a full Linux computer, copy out the evidence you want to retain, then delete it.
Appendix I: Further reading
- Scott Helme: Eight Bits of Trust
- The public incident evidence and remediation table
- Build once, fork many
- Mozilla's dev-security-policy announcement
- Google's Chrome and Android response
Appendix II: The same workflow on a Mac
Slicer for Mac can use the same workflow on Apple Silicon. It runs full Linux microVMs with Apple's Virtualization Framework, and uses APFS copy-on-write clones for slicer vm commit and slicer vm fork on the sbox host group.
Networking differs on Mac. With enforcement enabled, Slicer drops all traffic for the sbox host group. Each VM must opt into Slicer Proxy on macOS with its own client token and allowlist. That allowlist can be *, or restricted to the sites needed by the builder. For this workflow, grant the builder access to the required download sites, then leave the fork without an egress policy for the closed run. See the installation, sandbox, and cold-forking pages for the Mac-specific steps.