# Audit log

> What Team Cloud's audit log records, how it's chained and checked by admins' Macs, how to filter and export it, and how to verify an export offline with cloakctl.

Source: https://docs.subcloak.com/cloak-proxy/team/audit-log/

Added in Cloak Proxy 0.7.0.

{/* L9 draft. Sources: PLAN 7.15, 9.5 page 12, 9.2 (step-up); T11. Re-check labels and the verify command's output against L2/L6/L7. */}

Team Cloud keeps a log of who did what in the team for 365 days. It holds **metadata only**: who acted, what kind of action, when, from which country, and on which item by id. It never holds names, comment text, secrets or any other content, which stay end-to-end encrypted.

## What it records

{/* PLAN 7.15 events. R4D-M4: only kinds the server writes (server/cloud/src: team/vault.ts, hosted/durable.ts,
   identity/*, console-ops.ts); scripts/check-claims.sh checks this table against the code both ways. */}

| Area | Events |
|---|---|
| Team | `team.created`, `team.renamed`, `console.team_delete` (the owner asked to delete the team, with when it happens), `console.team_delete_cancelled` |
| Members | `member.invited` (also when an invitation is withdrawn), `member.joined`, `member.removed`, `member.role_changed`, `member.removal_needs_kit` (a removal that only the Team Recovery Kit can finish) |
| Macs | `device.requested`, `device.approved` (with how: code comparison, automatic after SSO, or the kit), `device.confirmed`, `device.denied`, `device.removed` |
| Keys | `roster.epoch`, `roster.options`, `kit.stored` (an admin confirmed the current Team Recovery Kit is stored), `kit.used`, `kit.rotated` |
| Spaces | `space.created`, `space.deleted`, `space.acl_changed`, `space.write` (grouped per Mac per space per 10 minutes, with a count), `vault.read` (per Mac per folder per hour) |
| Reviews | `proposal.created`, `proposal.approved` |
| CI | `ci.created`, `ci.revoked`, `ci.run` |
| Pairing | `pair.started`, `pair.joined`, `pair.ended` |
| Monitors\* | `monitor.created`, `monitor.updated`, `monitor.paused`, `monitor.resumed`, `monitor.deleted`, `monitor.failing`, `monitor.recovered` |
| Hosted mocks\* | `mock.published`, `mock.republished`, `mock.unpublished`, `mock.taken_down`, `mock.restored` |
| Alert routes | `integration.created`, `integration.updated`, `integration.deleted`, `integration.tested` |
| Settings | `policy.changed`, `sso.changed`, `sso.tested`, `scim.token_rotated` |
| SCIM | `scim.user_created`, `scim.user_updated`, `scim.user_deactivated`, `scim.user_deleted` |
| Console | `console.signin`, `console.stepup`, `console.user_added`, `console.user_removed` (a billing manager added or removed) |
| The log itself | `audit.budget_exceeded`: a limit below was reached; it names whose entries were left out (a member's, or the whole team's for the shared limit) and which kind |

### Limits on busy days

So one member can't flood the log, some entries have daily limits (counted in UTC days). Past a limit, one
`audit.budget_exceeded` entry says so and the rest of that day's entries of that kind are left out:

- Each Mac: 200 `space.acl_changed`, 200 `proposal.created`, 200 `proposal.approved` and 200 `pair.started` a day.
  `space.write` entries past 300 ten-minute groups a day are merged into one entry for the rest of that day.
- Each member: 1,000 a day of `space.write`, `proposal.created`, `space.created` and `pair.joined` together. Only that
  member's entries stop, and the marker names them. `vault.read` is hourly per Mac and folder for a member's first 2,000
  entries a day; past that it's recorded once per member, folder and day, so every folder read still shows.
- The whole team: those kinds plus `pair.ended`, 5,000 a day or 150 for each member, whichever is more.

`vault.read` is never cut by another member's activity or the team limit. `space.acl_changed`, `proposal.approved`, and
everything about members, Macs, keys, CI, settings, SCIM, the console and billing are never left out by the member or
team limits.

Billing changes (seats, the billing interval, payments and cancellation) aren't in the audit log; Polar's receipts and customer portal show them.

Each entry names its **actor**: a member's Mac, a console user (by email), a CI token (by label), the Team Recovery Kit, SCIM or Subcloak's server. The country comes from the request's network.

`vault.read` means a Mac **downloaded** a vault folder's items, not that anyone used a value. Use can't be seen, because the vault is end-to-end encrypted.

## How it's protected

{/* T11 */}

- Each entry includes a hash of the one before, so the log is a chain.
- Admins' Macs check the chain as they sync and sign a checkpoint at least once a day.
- If entries before a checkpoint were changed or removed, admins' Macs and `cloakctl team audit verify` on an export detect it. The console's line at the top of the page, for example "Chain verified to entry 18,204, signed by Priya's Mac 3 hours ago", is Subcloak's server checking its own log: the independent check is an admin's Mac or an export.
- After 365 days entries are deleted, and the log keeps an anchor so what remains still verifies.

Subcloak's server writes the entries. The checkpoints mean it can't quietly rewrite the history afterwards; they don't stop it from leaving out an event when it happens.

## View it

{/* PLAN 7.15 viewing, 9.5 page 12 */}

**In the console** (owners and admins): **Audit log**, with filters for actor, kind and date. Columns: **Time**, **Actor** (an email, or a Mac as its platform and a short id), **Action**, **Target** (an id) and **Country**.

**In the app** (admins): **Settings › Team › Activity**. Your Mac replaces ids with names where it can decrypt them.

## Export and verify it

{/* PLAN 7.15, 9.2 */}

**Export…** on the console's **Audit log** page asks you to confirm with a passkey or a fresh email code, then downloads:

- **JSONL**, with the hashes, the checkpoints and the team's signed member list, for offline verification;
- **CSV**, for spreadsheets. Values that a spreadsheet would treat as formulas are quoted.

Verify a JSONL export offline with `cloakctl`:

```bash title="Terminal"
cloakctl team audit verify export.jsonl --security-code "1234 5678 9012 3456 7890" --at-change 42
```

Take the **Team Security code** and the change number beside it from **Settings › Team › Security** on an admin's Mac. The export carries its own copy of the team's member list, so the code is what ties it to your real team: `cloakctl` checks that the export's member list gives the same code at that change, then checks the chain and the checkpoints admins' Macs signed against that list.

Without a code, `cloakctl` stops and shows how to give one. With `--unpinned` it checks only that the export agrees with itself, and says so: that doesn't show the export is your team's. The check an admin's Mac runs in the app, against the member list it has verified all along, is the authoritative one.

## Related

  - [What Subcloak can and can't see](https://docs.subcloak.com/cloak-proxy/team/what-subcloak-can-see/): The audit log among everything else our servers hold.
  - [Version history](https://docs.subcloak.com/cloak-proxy/team/version-history/): Earlier versions of team items.
  - [Policies](https://docs.subcloak.com/cloak-proxy/team/policies/): Policy changes are logged.
