Ed25519, TOFU pinning and offline verification: how monsys evidence packs work
Dashboards are for operators. Auditors want an artifact they can verify offline, without trusting your system. How the monsys signing chain works.
An auditor asks for proof that your server had no unauthorised changes on March 14. You send a dashboard screenshot. The auditor asks: how do I know this wasn't edited?
That's the core problem of any monitoring tool that wants to deliver compliance evidence. Dashboards are great for operators. Auditors want something else: an artifact they can verify offline, without having to trust your system.
This is how the monsys signing chain works.
The architecture in one picture
Every payload the agent sends to the hub is signed with an Ed25519 keypair. The keypair is unique per agent — the private key is generated on the host at install time and never leaves it. The hub only knows the public key.
Agent (host) Hub (monsys.ai BE)
───────────────── ──────────────────────────────
Ed25519 private key Ed25519 public key (TOFU pinned)
│ │
▼ ▼
payload → sign(payload, privkey) → verify(sig, pubkey) → store
The hub pins the public key on first connection (TOFU). Every payload after that is verified. An attacker who steals the hub token can't inject fake telemetry — they would need the private key on the host to produce a valid signature.
How an evidence pack is built
When you click "Generate evidence pack" for a period (e.g. April 2026):
Step 1: Query The hub fetches every relevant event for your tenant in that period:
- Agent inventory snapshots
- Alert events (open, closed, acknowledged)
- Detection events
- Compliance control evaluations
- Audit log entries (who did what, when)
- AI observability traces (if active)
Step 2: Normalisation Each event row is serialised to canonical JSON (sorted keys, no whitespace variation). This matters: two identical datasets must produce identical bytes, otherwise the signature won't match.
Step 3: Manifest The manifest is a JSON file that lists every file in the tarball with its SHA256 hash:
{
"tenant_id": "acme-bv",
"period_start": "2026-04-01T00:00:00Z",
"period_end": "2026-04-30T23:59:59Z",
"generated_at": "2026-05-01T08:03:14Z",
"signing_key_id": "sk_2024_q1",
"files": [
{
"path": "agents/web-edge-01/inventory_snapshots.ndjson",
"sha256": "b4c2f1a3...",
"record_count": 2880
},
{
"path": "alerts/april_2026.ndjson",
"sha256": "e7d9c2b1...",
"record_count": 47
}
],
"inputs_hash": "sha256:a1b2c3d4..."
}
The inputs_hash is the SHA256 of all sha256 values concatenated — a fingerprint of the whole bundle.
Step 4: Signing The hub signs the manifest with its tenant Ed25519 signing key:
signature = Ed25519.sign(manifest_bytes, tenant_signing_privkey)
The signature and the public key both go into the tarball.
Step 5: Packaging
evidence_acme-bv_april-2026.tar.gz
├── manifest.json
├── manifest.sig ← Ed25519 signature (hex)
├── signing_key.pub ← public key (PEM)
├── verify.py ← standalone verification script
└── data/
├── agents/
├── alerts/
├── detections/
├── compliance/
└── audit_log/
Offline verification: verify.py
The verify.py script inside the tarball has no external dependencies beyond the Python standard library (3.8+) and cryptography (pip installable). An auditor who's never heard of monsys can run it:
pip install cryptography
python verify.py evidence_acme-bv_april-2026.tar.gz
Output on success:
✓ Signature valid (Ed25519)
✓ Signing key matches expected fingerprint: 4a:b3:c2:...
✓ All 12 files intact (SHA256 match)
✓ inputs_hash consistent
✓ Period: 2026-04-01 → 2026-04-30
✓ Tenant: acme-bv
exit 0
Output on tampering:
✗ File tampered: data/alerts/april_2026.ndjson
Expected SHA256: e7d9c2b1...
Actual SHA256: f8e0d3c2...
exit 1
The auditor doesn't need a monsys account. The script downloads nothing from the internet. Verification is fully offline.
Key rotation: the signing-key lineage
Signing keys are rotated periodically (recommended: yearly, or on suspicion of compromise). But what happens to evidence packs signed under an old key?
monsys maintains a key lineage. Each key has:
key_id: unique identifier (e.g.sk_2024_q1)public_key: PEM formatactive_from/active_untilsuperseded_by: pointer to the successor key
On verification, verify.py checks not only whether the signature is valid, but also whether the key used was active at generation time (generated_at in the manifest). A key rotated on January 1 2025 is still valid for evidence packs generated before that date.
# Simplified from verify.py
def verify_key_was_active(key_id: str, generated_at: datetime, lineage: list) -> bool:
for key in lineage:
if key["key_id"] == key_id:
active_from = datetime.fromisoformat(key["active_from"])
active_until = datetime.fromisoformat(key["active_until"]) if key.get("active_until") else datetime.max
return active_from <= generated_at <= active_until
return False
The lineage snapshot is in the tarball too, so verification works without network access.
Bearer-token theft: what it can and can't do
A frequent question: if an attacker steals the agent token (the token the agent uses to push telemetry to the hub), what can they do?
Can the attacker:
- Send fake telemetry as that agent → the hub accepts the request
- But: that telemetry lacks the Ed25519 signature from the agent's private key
- The hub stores unsigned telemetry with a
signature_missingflag - Evidence packs for that period are marked with
integrity_warning
Cannot the attacker:
- Send a validly signed payload without the private key on the host
- Retroactively edit existing evidence packs (the signature won't match anymore)
- Rotate the signing key at the hub (that requires TOTP + admin rights)
This is why periodic token rotation is still recommended — but the signing chain guarantees that stolen tokens can't poison historical evidence.
mTLS: the second security layer
On top of Ed25519 signing, the hub-agent connection uses mTLS. The agent receives a client certificate at first connection that is then pinned. This means:
- The hub knows for certain the connection comes from the registered agent (not from someone with the token alone)
- A man-in-the-middle cannot simulate a fake agent
The combination: mTLS authenticates who makes the connection; Ed25519 authenticates the contents of each payload.
NIS2 and AI Act: what this concretely means
Article 21 of NIS2 requires "appropriate technical measures" for logging and incident response. "Appropriate" is vague, but a signed, offline-verifiable audit trail scores noticeably better with a regulator than screenshots or CSV exports.
For AI Act Article 12 (logging of AI-system events): the AI observability module uses the same signing chain as the rest of monsys. Every trace — prompt, completion, tokens, PII hits — is in the tarball, signed, verifiable by a DPA inspector without a monsys account.
DPA inspector: "Prove that your AI system did not pass any IBAN
numbers to OpenAI on April 3."
Operator: python verify.py evidence_april-2026.tar.gz
✓ Signature valid
grep -r "pii_hits_count" data/ai_traces/ | grep "2026-04-03"
→ 0 results for IBAN hits on that date
Verifiable. Reproducible. Without asking for your trust.
The signing chain is documented at docs.monsys.ai/en/security/agent-signing and docs.monsys.ai/en/security/signing-keys-rotation. First five servers free: monsys.ai/en/signup.