Gunnar

nordisk/gunnar

.nornir/account-audit-operator-privacy.md

# The account's audit log, the operator's aggregates, the name filter

Owner's enterprise list, 2026-10-05. Nothing here is bought: every account has it.
The account model it sits on (owner, 2026-10-03): one account type; the person who
starts it is its admin; employees are NEW users the admin enrolls; private by default;
only the account admin makes things public.

## 1. The account admin's audit log

* **What is in it** — every security-relevant event in the account: logins (passkey;
  company SSO when that line lands) and refused logins, passkeys and SSH keys added,
  renamed and removed, invites made and withdrawn, enrolments completed, members added
  (and seats), removed and renamed, grants given and taken, the account gated, ungated
  or purged, repositories created, deleted, undeleted and purged, namespaces made and
  deleted, public/private changes (and a member's refused attempt), the namespace's
  settings, branch protection and ref-name policy, billing (an order, renewal or
  cancellation started at the console; the plan bought, prolonged or upgraded when
  monetize's fact lands), the operator's looks (asked, approved, refused, ended),
  names the filter refused or held and the admin's decision, and every export.
* **Each entry** carries who did it and what they are to the account — `admin`,
  `member`, `operator`, `machine`, `outsider`, `system` — decided by gunnar, not by
  whoever recorded it.
* **Append-only and tamper-evident**: one hash chain per account
  (`gunnar_core::account_audit`, `SHA-256("gunnar-account-audit-v1" ‖ account ‖ prev ‖
  seq ‖ …)`, every field length-prefixed). An edited, removed or reordered entry breaks
  the chain at that entry; an entry copied from another account does not verify. The
  page shows whether the chain holds and its tip; an admin who writes the tip down can
  later tell a cut tail too.
* **Kept a year** by default. The append that crosses the line, and a sweep every six
  hours, drop the days past it and keep the last dropped entry as the anchor, so a
  pruned log still verifies end to end.
* **Stored** as one control document per account (`refs/gunnar/account-audit/<account>`),
  so the twin holds it and a restore brings it back with everything else.
* **Read ONLY by the account's admins** — its own person, or a principal its admin made
  Admin over the namespace — filtered (kinds, who, time, text), paged, exported as CSV
  (formulas defused) or JSON: `Enterprise.AuditLog`, `Enterprise.ExportAuditLog`;
  console: Administration → Audit log (or Account → Audit log). A server-scope grant is
  not an admin of any account: the operator is refused, approved look or not.

## 2. The operator sees aggregates only

* **The rule is in gunnar's ACL** (`AuthTable::may`, `may_namespace`): inside a
  namespace whose first component is a PERSON's account, a PERSON's server-scope grant
  — held directly or through a group — counts for nothing. Every door asks that
  one function, so SSH, smart-HTTP and every gRPC verb refuse the operator alike: no
  clone, no `Repos.Get`, no `ListEmployees`, no making anything public, no repository
  created inside. The account's own person, its employees, a grant its admin made on
  its namespace or a repository, and a PUBLIC repository still reach as before.
  Namespaces no person owns (the operator's own forge, `nordisk`), the operator's own
  account, and the namespace of the box's FIRST identity — the one bootstrap enrolled,
  the installer's admin key, which is the estate itself as a machine is — are the
  operator's as before.
* **Listings are narrowed by the same rule**: `Repos.List`, `ListNamespaces`,
  `ListDeleted`, `ListOrphans`, `Usage.ReadAll` (a nested namespace's row), `Events.Watch`
  for a server reader, and the identity listings — an account's MEMBERS (its employees)
  are not listed, their keys, grants and invites are not shown, and their passkeys are
  shown by opaque id alone (the console's login offers them, nothing names them).
* **The totals** — `Enterprise.Aggregates`, server `Read`; console: Administration →
  Accounts: accounts, accounts with employees, members, repositories (public), storage
  used and sold, paid accounts, the looks, health.
* **A look, only when the account admin grants it**: the operator asks
  (`Enterprise.RequestOperatorAccess`: an account, a reason, minutes — at most a week);
  nothing changes until the ACCOUNT'S admin approves it with a fresh, user-verified
  step-up of their OWN passkey and the request's code (the machine enrolment's Approve
  flow), or refuses it. An approved look READS — never writes — until it ends on its own
  or the admin ends it. Every step is in the account's audit log. A request nobody
  answers lapses after three days.

### What still reaches the operator — said plainly

"We don't look, and the system doesn't show us." It is not secrecy from the people
who run the box. What remains:

1. **Names in clone URLs.** `https://git.gunnar.rs/<account>/<repo>.git` and the SSH
   path pass through the operator's front and gunnar's own process; the account and
   repository names are in those requests and in gunnar's own log lines (the journal,
   `/serialdat`) that whoever holds the box can read. The domain itself says whose
   forge it is.
2. **The box's own logs and trails.** gunnar's `tracing` records name repositories and
   principals on transfers and decisions. The control plane's server-wide audit trail
   (GUN-044) is served over the API as its shape only (`Admin.AuditTrail` carries no
   entry), but the file is on the operator's disk.
3. **Timing and size.** When an account is active, how long its requests take, how much
   it moves.
4. **Totals.** The account's share of the aggregate figures, its own size and room
   (`Usage.ReadAll`'s row for the account itself), and what it bought (the entitlement
   facts monetize pushes are the operator's to bill by).
5. **The account's own name.** An account is addressed by its id — its namespace and
   its owner's principal — to gate, bill or recover it. Its members' names are not shown.
6. **Root on the box.** The control store holds every repository, every name and every
   audit entry — pruned days included, in old document versions, until the store is
   compacted — and the twin and the backups hold copies. True secrecy from the operator
   needs keys the customer holds (the encrypted-repos line), not an API rule.
7. **The identity verbs are still the operator's.** A server admin can add a passkey,
   an SSH key or an invite to an account's person (account recovery). Each such act is
   written into THAT account's audit log as done by an `operator`, which the operator
   cannot read and cannot rewrite without breaking the chain the admin sees.
8. **Machine keys, and the box's first identity.** The shield binds people. A MACHINE principal's server grant — the
   replicator that pushes a tenant's data to its standby, the console's relay, an ops
   kit approved as a machine — keeps its reach, because a standby that refused a
   lapsed tenant's push would diverge from its primary. The same holds for the box's
   first identity (the installer's admin key bootstrap enrolled). Whoever holds such a key
   (the operator does, on its own boxes) can use it; the console's break-glass admin
   token signs with the console's own machine key. The operator's PASSKEY session in
   the console is a person and is shielded.
9. **monetize's narrowed principal** calls its listed verbs (`Entitlement.*`,
   `Accounts.EnrollEmployee` with an order, `Usage.ReadAll`, `Accounts.Purge`) without
   consulting grants, by design; a server-scope caller delivering a PAID order may enrol
   the new user the order names. Both land in the account's audit log.
10. **Public is public** — a repository or an account's events its admin made public are
   readable by the operator as by anybody.

## 3. The name filter

* Account, namespace, member and repository names are put to it where gunnar makes them:
  `AddPrincipal`, `RenamePrincipal`, `EnrollEmployee`, `CreateNamespace`, `Repos.Create`
  (`gunnar_core::name_filter`).
* Folding before matching: zero-width characters dropped, unicode confusables
  (Cyrillic, Greek, fullwidth, mathematical, circled, accented) folded, leetspeak
  (`0 1 3 4 5 6 7 8 9 @ $ !`, `ph`), separators removed, each letter stretchable
  (`fuuuck`). Words of four letters or fewer match a whole word only (the Scunthorpe
  rule), and an allow list names words that contain a listed one and are fine.
* **Blocked** → refused with a sentence that does not repeat the word; noted in the
  account's review queue and its audit log. **Reserved** (machine names `kit-`,
  `console-`, `twin-`…, brands — gunnar, vetra, ignalina, nordisk — and staff roles —
  admin, support, security, root…) → refused for a PERSON's name or an account's
  namespace; the operator (a person with server Admin) may still name the estate's own,
  and machines, groups and deploy keys are gunnar's machinery. **Borderline** → held
  for the ACCOUNT ADMIN's review (`Enterprise.ListNameReviews`, `DecideNameReview`;
  console: the Names panel of the Audit log), never the operator's; allowed, the same
  name passes in that account from then on. A borderline name for a NEW account (no
  admin yet to ask) is refused unless the operator names it.
* **The lists are gunnar's configuration** (`refs/gunnar/name-filter`,
  `Enterprise.GetNameLists` / `SetNameLists`, a server admin's), with gunnar's defaults
  until set — never hard-coded in the console.