sign_ok · refuse_double_vote
Policy-enforcing trust boundary
for PoS validators.
nklave sits between your validator client and your signing keys. It enforces slashing-prevention rules and configurable policies before any signature is produced — so slashable signing is impossible by construction, even if the host is compromised.
Untrusted
Validator client
Lighthouse, Teku, Prysm, Lodestar. Speaks the Web3Signer HTTP signing protocol.
POST /api/v1/eth2/sign/:pubkey
Trust boundary
nklave
Policy engine evaluates the request. Slashing-protection DB is consulted. Decision is logged. Only then is the key touched.
policy.evaluate(ctx) → Allow | Refuse
Custody
Signing keys
Local keystore, YubiHSM 2, AWS CloudHSM, GCP KMS. Refused requests never reach the HSM.
BLS (ETH2) · Ed25519 (Cosmos)
What the trust boundary enforces
Four built-in policies. All evaluate before the key is consulted.
Each refusal is deterministic, has a reason code, and is committed to the append-only log atomically with the slashing-protection DB update.
slashing-protection-attestation
Double-vote & surround-vote
Refuses any attestation whose target_epoch matches a previously-signed one, or whose (source, target) would surround / be surrounded by a prior signature.
tracked: min/max source · min/max target epoch · per pubkey
slashing-protection-block
Double block proposal
Refuses a beacon-block proposal if a block has already been signed for the same slot. For Cosmos / CometBFT validators, the equivalent rule applies on (height, round).
tracked: min/max proposal slot · last height/round
fork-allowlist
Allowlisted fork versions
Refuses any signing root whose fork version is not in the operator's allowlist — a defense against accidentally signing on testnet or an unintended fork.
allowed_forks = ["0x05000000", "0x06000000"]
rate-limit
Per-validator rate cap
Refuses signing requests when the validator's signing rate exceeds max_signs_per_hour. A backstop against runaway validator clients.
max_signs_per_hour = 240 # default ~4/min
Custom policies are first-class: implement the Policy trait, register at startup, and your rule runs in the chain ahead of the key. Policies evaluate in registration order; the first refusal short-circuits.
Integrations
Keep your validator client. Keep your HSM. Drop nklave in between.
Validator clients
nklave speaks the Web3Signer HTTP signing protocol. Anything that supports a remote signer URL works.
- · Lighthouse — --use-remote-signer
- · Teku — validators-external-signer-url
- · Prysm — --web3signer-url
- · Lodestar
Key custody backends
Signing keys live in dedicated custody. nklave only requests signatures — and only for requests the policy chain allowed.
- · Local keystore — dev / single-host
- · YubiHSM 2
- · AWS CloudHSM
- · GCP KMS
Slashing-protection DB
Embedded or shared. The HA pattern is two nklave instances sharing a Postgres DB serialized by row-level locks.
- · RocksDB — default, single host
- · Postgres — leader/follower HA
- · EIP-3076 import/export
Threat model — v1
What nklave defends against. And what it doesn't.
We publish the threat model in full because the security properties of a signer are only as good as the assumptions behind them. Summary below; the authoritative version is in the docs.
v1 security goals
- guaranteed Signing keys are not readable by the host.
- guaranteed Slashable requests are refused, even from compromised software.
- guaranteed Safety state is monotonic — cannot be rolled back by host tampering.
- guaranteed Every decision is logged with a deterministic reason code.
Out of scope (v1)
- not_covered Physical access attacks (cold boot, DMA, evil maid).
- not_covered Advanced side-channel attacks on enclave execution.
- not_covered Compromise of the enclave implementation itself.
- not_covered Global anti-slash across multiple independent enclaves without explicit coordination.
Residual risks we name explicitly: a compromised host can still cause denial of service by blocking requests; strict enforcement can reduce availability if state is corrupted; supply-chain vulnerabilities in the build pipeline matter.
Append-only log
Every decision. Allow or refuse. Forever.
Every evaluation lands in a newline-delimited JSON log. Entries are sealed into Merkle-root checkpoints every 60 seconds and signed by a separate operator key. Tampering is detectable by re-walking the chain.
{"ts": 1742054400, "validator": "0xabc…", "type": "ATTESTATION", "decision": "allow", "signing_root": "0xdef…"}
{"ts": 1742054401, "validator": "0xabc…", "type": "ATTESTATION", "decision": "refuse",
"policy": "slashing-protection-attestation",
"reason": "double vote at target_epoch=12345"} verify chain
nklave log verify --from 0 --to latest re-walks every checkpoint, recomputes each Merkle root, and confirms the operator-key signatures. Non-zero exit means the log was edited.
operator key custody
Checkpoint-signing key is separate from any validator key. Lives in its own keystore, a YubiHSM slot, or an AWS KMS key — operator's choice.
retention
Logs rotate at 1 GB / 365 days by default. Older logs seal; the checkpoint chain remains verifiable across the boundary into cold storage.
Getting started
Five minutes from zero to validator signing through nklave.
1. Run nklave
docker run -d --name nklave \
-p 9000:9000 \
-v nklave-data:/var/lib/nklave \
-v $(pwd)/keystores:/keystores:ro \
ghcr.io/cryptuon/nklave:latest \
--keystore-dir /keystores \
--data-dir /var/lib/nklave Or cargo install nklave-server if you build from source.
2. Point your validator at nklave
# Lighthouse
lighthouse vc \
--beacon-nodes http://localhost:5052 \
--validators-dir /opt/lighthouse/validators \
--use-remote-signer http://localhost:9000
# Teku
validators-external-signer-url: http://localhost:9000 Prysm: --web3signer-url=http://localhost:9000
3. Import your slashing history
nklave import \
--interchange-file ./slashing-protection.json \
--keystore-dir ./keystores Standard EIP-3076 interchange. nklave will refuse anything that would duplicate what your previous client recorded.
4. Verify
curl http://localhost:9000/livez
curl http://localhost:9000/readyz
curl http://localhost:9000/health Then watch <data-dir>/log/ for signing decisions as they happen.
Slashing should be a class of bug, not a class of incident.
nklave is MIT-licensed, written in Rust, and built to be the simplest correct thing between your validator client and your keys. Read the threat model. Run the import. Switch your signer URL.