ROOSTby Axowl

Docs

Auditor, log export, time and attestation

axowl.com/dashboard/acme/roost/records/reports
AxowlacmeRoostM

Evidence reports

ReportScopePeriodSigned by
Event exportall partitions2026 Q3ROOST-26-0001 identity keyDownload
Key attribute reportprod · paymentsas of todayROOST-26-0001 identity keyDownload
Access review matrixallas of todayROOST-26-0001 identity keyDownload
Chain verificationalllast 90 daysverified by M. Lee (auditor)0 gaps
Each report is produced by the appliance and signed; auditors verify the signature with the published identity key.
Evidence reports: produced and signed by the appliance, ready for an audit.

Four controls that bank HSM tenders ask for, mapped onto Axowl. They sit on the device ledger above: a box is addressed by its ledger serial, and the box's own requests are signed with the attestation key the factory recorded in the ledger.

Roles: who may touch the audit log

RoleHow it is heldAudit log (read, export, verify)Audit log (delete / retention purge)Export targets (syslog / SNMP)Time policyAppoint / remove an Auditor
AuditorRoost role Auditor , appointed by Owner quorum✅✅ (passkey seal)✅read—
OwnerOrganization owner✅ until an Auditor exists, then ❌ AUDITOR_REQUIRED✅ until an Auditor exists, then ❌✅ until an Auditor exists, then ❌✅✅ seals (majority of active Owners)
AdminOrganization admin✅ until an Auditor exists, then ❌❌❌read❌
MemberAny other memberown records only (as before), ❌ once an Auditor exists❌readread❌
Security Officer · Operator · UserRoost roles on the box (ledger)no effect on the audit log————

The rule is one switch with no separate flag: as soon as at least one Auditor is active, the organization audit log is Auditor-only. The rule applies to /api/v1/admin/audit query, query/detail, compact/urls, presign, content, verify/{logId}, stats/forensic and retention/purge-now. Aggregated dashboards (/stats, /analytics) are not log reads and stay as they were. Nobody — not the Owner, not Axowl operators — has an API that edits a sealed audit record; the only deletion is the retention purge, and once an Auditor exists it needs the Auditor's passkey seal.

An Auditor cannot be an Owner, a service account or a guest. The role is not assignable through the normal role or SCIM endpoints; only the quorum flow below writes it.

Auditor quorum — /api/org/{slug}/roost/auditors

Method · RoutePurpose
GET /auditors{ auditors[], candidates[], separationActive, ownerCount, requiredSeals, callerIsOwner, callerIsAuditor, pending[] } — any member. candidates[] and auditor emails are filled only for an Owner; other members see auditor names.
POST /auditors/{connectedId}/assignOne Owner seal towards appointing. Body { stepUpAssertion } with purpose roost.auditor.change, subject RoostAuditorSubject(slug, "assign", connectedId)
POST /auditors/{connectedId}/revokeOne Owner seal towards removing. Same body, action revoke
POST /auditors/actions/{id}/cancelCancel a pending action (Owner)

requiredSeals = activeOwners / 2 + 1 (one Owner → one seal). Each Owner counts once; a pending action expires after 7 days. Response { actionId, sealCount, requiredSeals, completed }. Errors: 403 OWNER_REQUIRED, 400 INVALID_AUDITOR_TARGET, 409 ALREADY_AUDITOR / NOT_AUDITOR, 401 STEPUP_REQUIRED / STEPUP_FAILED.

Audit events: roost.auditor.seal_recorded (RST050, one per seal, carries the seal proof), roost.auditor.assigned (RST051), roost.auditor.revoked (RST052).

Audit-log export — /api/org/{slug}/roost/log-export

Method · RoutePurpose
GET /log-exportCurrent targets; secrets come back as authKeySet / privKeySet / caPemSet booleans. Any member.
PUT /log-exportSet targets. Owner — or the Auditor once one is appointed. Hosts must be reachable on the public internet: private, loopback and link-local addresses are refused (400 LOG_EXPORT_HOST_NOT_ALLOWED for an IP address; a name that resolves to one fails when a message is sent).
POST /log-export/testBody { target: "all" | "syslog" | "snmp" } → { results: [{ target, ok, error }] }. Each attempt is roost.log_export.test_sent (RST054).
{
  "syslog": { "enabled": true, "host": "siem.bank.example", "port": 6514, "protocol": "tls",
              "serverCertSha256": "…64 hex…", "caPem": "-----BEGIN CERTIFICATE-----…", "facility": 10, "appName": "axowl-roost", "allOrgEvents": true },
  "snmp":   { "enabled": true, "host": "nms.bank.example", "port": 162, "engineId": "80004fb805617877", "userName": "axowl",
              "authProtocol": "sha256", "authKey": "…", "privProtocol": "aes128", "privKey": "…", "kinds": ["Tamper", "Zeroize", "Degraded"] }
}
  • protocol: tls = RFC 5425 (octet-counting frames over TLS 1.2+) · relp = RELP over TLS with per-message acknowledgement. There is no plain TCP or UDP.
  • Server trust: OS chain by default; caPem pins a private CA; serverCertSha256 pins one certificate (wins over the chain).
  • allOrgEvents: true sends every audit event of the organization, false only roost.* events.
  • SNMPv3 only (USM). authProtocol none|sha1|sha256, privProtocol none|aes128|aes256; privacy needs authentication. Keys are write-only and stored encrypted. kinds empty = every alarm.

SIEM JSON line (the syslog MSG; RFC 5424 header <PRI>1 TIMESTAMP HOST axowl-roost - RSTnnn [axowl@PEN org="…" event="…" code="…" serial="…"] MSG):

{"v":1,"ts":"2026-10-19T09:10:11.123Z","org":"<org guid>","event":"roost.alert.raised","code":"RST009","id":"<logId>",
 "actor":{"cid":"<connectedId or system sentinel>","type":"system"},"target":{"type":"external","id":"ROOST-26-0001"},
 "ip":null,"device":null,"data":{"Serial":"ROOST-26-0001","Kind":"Tamper","Severity":"Red","Summary":"mesh resistance out of band"}}

id is the audit logId — the same value GET /api/v1/admin/audit/verify/{logId} verifies, so a SIEM line can be tied back to the sealed record. Syslog severity: Red → 2, Yellow → 4, other Roost events → 5, everything else → 6.

SNMPv3 traps go only for alerts: roost.alert.raised (RST009) and roost.alert.cleared (RST010) from the alert evaluator (box status reports and box-sent alarms both pass through it), plus roost.box.attestation_mismatch (RST059, as key events Red) and roost.time.drift_exceeded (RST057, as key drift Red).

enterprise      = configured enterprise OID (configurable, default 1.3.6.1.4.1.99999)
trap OID        = <enterprise>.1.0.<key>     key (alert item): chipTemp 1 · boardTemp 2 · v12 3 · v5 4 · v3v3 5 · battery 6 ·
                                             mesh 7 · quorum 8 · fans 9 · psu 10 · link 11 · drift 12 · events 13 · other 99
varbinds        = <enterprise>.2.1 serial · .2.2 key · .2.3 level (Red | Yellow | Normal = cleared | Gray) · .2.4 detail · .2.5 orgId ·
                  .2.6 eventType · .2.7 auditLogId · .2.8 occurredAt (ISO 8601 UTC)

The sender is the authoritative engine (engineId as configured, boots 1, engine time = process uptime); register user, keys and engine ID on the receiver.

Failures: the audit record is written first and is never held back by a slow or dead target. A failed delivery is logged, recorded as roost.log_export.delivery_failed (RST055, at most once per target per 10 minutes) and raises a SecurityAlert notification to every Owner. Target changes are roost.log_export.target_changed (RST053) without secrets.

Time policy — /api/org/{slug}/roost/time-policy

Method · RoutePurpose
GET /time-policy{ policy: { servers: [{ host, mode: "nts" | "ntp" }], maxDriftMs, refuseSigningOnDrift, staleAfterSeconds, updatedAt }, callerCanEdit } — any member
PUT /time-policyOwner. maxDriftMs 10–60000 (default 500), staleAfterSeconds 60–604800 (default 3600), up to 8 servers. Audit roost.time.policy_changed (RST056).
GET /{serial}/status{ serial, lifecycleState, lastReportAt, time: { synced, driftMs, rtcOk, withinPolicy, stale, signingAllowed, maxDriftMs, refuseSigningOnDrift } }

mode: "nts" is Network Time Security (RFC 8915, authenticated); plain ntp is accepted but the dashboard warns. The verdict is computed from the box's latest status report (ntpOk, driftMs, reportedAt in the ledger): synced = ntpOk && !stale, withinPolicy = |driftMs| ≤ maxDriftMs, signingAllowed = !refuseSigningOnDrift || (synced && withinPolicy). The same function answers the box (below), so the dashboard and the box never disagree.

Device identity and attestation — /api/org/{slug}/roost/{serial}/…

Method · RoutePurpose
GET /{serial}/identityIdentity payload + Axowl signature (below). Any member. 404 ROOST_DEVICE_NOT_FOUND.
POST /{serial}/attestation/challengeNew nonce (32 hex, 10 minutes). 409 ATTESTATION_KEY_NOT_PROVISIONED when the ledger has no key; 429 above 5 open challenges.
GET /{serial}/attestation?nonce=The answered attestation for that nonce (or the latest without nonce), including the box's signed bytes and signature.
GET /{serial}/attestationsLast 20 records.
GET /api/roost/identity-public-keyAnonymous. { alg: "ES256", publicKeyPem, fingerprint } of the Axowl key that signs identities.
GET /api/roost/reference-measurementsAnonymous. { components: [{ c, allowed: [sha256…] }] } — the published reference the server compares against.
{
  "identity": {
    "typ": "roost-identity", "v": 1, "org": "acme",
    "serial": "ROOST-26-0001", "model": "ROOST-3U", "attestationKid": "9f1c…", "deviceKeyFingerprint": "SHA256:…", "devicePublicKeyPem": "-----BEGIN PUBLIC KEY-----…",
    "lifecycleState": "Active",
    "measurements": [{ "c": "controller", "v": "1.1.0", "h": "…64 hex…" }, { "c": "voterkv260", "v": "bit-2026.3", "h": "…" }],
    "measurementsHash": "…", "measurementsSource": "attestation", "referenceResult": "match",
    "lastAttestationAt": "2026-10-19T08:00:00Z", "lastAttestationNonce": "…", "issuedAt": "2026-10-19T09:00:00Z"
  },
  "signed": true, "signature": "<base64url(payload)>.<base64url(r‖s)>", "signingKeyFingerprint": "SHA256:…", "alg": "ES256"
}

signature uses the re-arm ticket envelope: verify the P-256 signature over the base64url payload bytes with the identity public key; the decoded payload is the identity object. signed is false when no identity signing key is configured.

Measurements are { c: component, v: version, h: sha256 }, normalized (lower-case, sorted by component). referenceResult is match, mismatch or unverified (no reference published for a component). A mismatch records roost.box.attestation_mismatch (RST059) and raises alert key events Red through the alert evaluator (timeline, notification, SNMP trap); a later matching attestation clears it. Every answered attestation is roost.box.attestation_recorded (RST058) with the measurements hash.

Box-side API — /api/roost/boxes/{serial}/… (device-signed)

The box does not log in. It signs each request with the private half of the attestation key recorded in the ledger (written at the factory).

headers   X-Roost-Box: <serial>   X-Roost-Timestamp: <unix seconds>   X-Roost-Signature: base64url(r‖s)
signed    UTF-8 "ROOST-REQ-V1\n{METHOD}\n{PATH}\n{TIMESTAMP}\n{sha256hex(body)}"   (empty body → sha256 of "")
window    ±300 s; the same (serial, signature) is refused for 10 minutes (replay)
errors    401 BOX_HEADER_MISMATCH · BOX_NOT_REGISTERED · BOX_KEY_NOT_PROVISIONED · BOX_SIGNATURE_INVALID · BOX_REQUEST_REPLAYED · 403 BOX_RETIRED
Method · RoutePurpose
GET /policyTime policy, the current verdict (signingAllowed) and the URLs of the reference measurements and identity key. Fetch at boot and periodically.
POST /time-sync{ synced, driftMs, source, lastSyncUnix, stratum, authenticated, rtcOk } → updates the ledger's time fields; |driftMs| > maxDriftMs writes a TimeDrift event and roost.time.drift_exceeded (RST057, once per 10 min per box). Response time.signingAllowed is the instruction the box applies.
GET /attestation/pendingOldest open nonce, or 204.
POST /attestation{ payload: base64url(JSON { typ: "roost-attest", serial, nonce, iat, measurements[] }), signature: base64url(r‖s over the payload bytes) }. Compared with the previous verified attestation (or the enrollment ceremony values) and the published reference.
POST /alarms{ kind, severity?: "Red" | "Yellow" | "Info", summary?, payload?, ts? } with kind from the event list above. Alarm kinds map to alert keys (Tamper/Zeroize/Moved → events Red · PowerLoss/PsuFailed → psu · FanFailed → fans · BatteryLow → battery · TimeDrift → drift · Degraded → quorum · Recovered → clears events) and go through the alert evaluator: alert state, timeline event, roost.alert.raised / roost.alert.cleared, notifications, syslog and SNMP. Non-alarm kinds only add an Info timeline row.

The signing conventions are P-256, SHA-256, IEEE P1363 r‖s and base64url without padding, the same as re-arm tickets.