AuthBox documentation — User guide

Documentation embedded in this build.

The console, page by page

The administration console is server-rendered — no JavaScript framework, no build step, nothing but HTML your browser already knows how to draw. Every page is reachable under /ui, requires the certificate you enrolled with (there is no separate login), and shows only what your certificate's group memberships and mission bindings actually grant. A group you cannot see never appears in a listing and is never counted — it looks exactly like a group that does not exist.

This page walks every page the console currently has. If a term below is unfamiliar — what a project is, or how a mission differs from an obligation — the Glossary of resources is one paragraph per term, alphabetically. If your deployment turned a page off (ui.enabled) or made it read-only (ui.writable), you will still see it — a capability that exists and is switched off says so, rather than disappearing. Below the page list the sidebar grows a Companions section for each companion a deployment has wired. Each entry names its audience: ui.vault_url renders as Vault — operators ↗ and links onward to this deployment's AuthboxVault, whose own page links back here (vault.delegator.console_url); public.vault_claim_url renders as Claim page — invitees ↗; and when the box serves its own sales page (public.site, plan 096) it renders as About AuthBox — visitors ↗, pointing at the console's own root / — the same page the certificate-less listener serves at /site for a visitor — so the one setting that mounts the page is the one that wires the link, and a page the deployment does not serve is never offered. That link is a same-host subpath on purpose: every link the console emits is either a subpath of the host you reached it on or a subdomain the front door routes, never a listener's raw host:port (plan 097's rule), so a link works wherever the door does. The audience is in the label rather than behind a hover, because the two are not two spellings of one address: the vault is where an operator delegates, and the claim page is where an invitee, holding no certificate at all, turns an invitation token into one. A console used by operators keeps the first and hands out the second, which is why the claim link also appears beside every token the Invitations page mints. Both are plain anchors, absent when unconfigured, and never a call in either direction. The full neighbourhood — including the companions nobody has wired yet, and the setting that would wire them — is one click away on Services.

The mark is AuthBox's own, and it is served from the binary. The shield-and-lock in the sidebar's top corner, the icon in your browser's tab, and the one an iOS home screen saves are all the same artwork, cut to each size by a tool in the repository and carried inside the box (plan 163). The console, the portal, the object store, the front door's directory, the public site and every page of this documentation draw it, and not one of them fetches it from anywhere: on an enclave with no route out, a page that asked a content network for its own logo would be a page with a hole in it. The showcase applications are the deliberate exception — each of those stands in for somebody else's software and keeps a mark of its own.

The console wears AuthBox's own design system. Everything you see here — the type, the two themes, the cards, the tables, the state words, the notices — comes from one stylesheet (the Register, plan 165) that the object store, the portal and the front door's public directory wear too, so moving between them is moving around one product rather than between four. Three typefaces ship inside the box and are served from it: a serif for headings and names, a humanist sans for body and controls, a mono for identifiers, addresses and commands. Nothing on any page is fetched from outside the door. The page follows your system's light or dark setting and always has a Theme button at the foot of the sidebar; press it and the choice is remembered in that browser alone, and nothing is sent anywhere. If you had never pressed it, the console used to be light regardless — it now matches whatever your machine asks for, which for a machine set to dark is a visible change on first load and no change at all after you choose. Content fills the width it is given: tables, grids and listings run to the edge of the column beside the sidebar, and the only things held to a narrower measure are paragraphs of prose, which stop being readable past about eighty-six characters. The whole system is on one page at /ui/docs/design-system — every token, both themes, every component — rendered from the same stylesheet the page you are reading is wearing, so it cannot show you a look this build does not have.

A name is a link where you may follow it. Wherever a page names another record — a project, a mission, an entity, an authority, a tag — that name is a link to that record's own page, and the mission a group binds, the approver group a mission names, the person who decided a request, the subject of an invitation and the project behind a cached row are each one click from where they are mentioned. Where you may not see the target, the same name renders as plain text with nothing to follow: a page you would be told does not exist must not be reachable by an anchor either, since a link that resolved would tell you the record is there. So a dead-looking name is not a broken link — it is the visibility rule, said the only way it can be said without saying it.

Finding one thing: the search grammar

Every listing here — the directory, Projects, Missions, Invitations, the enrollment queue, the requests board, a group's member table, and the decision record's recent window — speaks one query grammar, and it lives entirely in the URL. ?q= is a case-insensitive substring search over exactly the columns that page draws for you; where a column is a DN it is matched the way DNs are matched, so a search for Smith John finds CN=Smith John,O=acme and SMITH JOHN finds it too. ?page= walks fifty rows at a time, clamps rather than errors when it is out of range, and leaves page 1 as the bare address. One filter parameter per page joins them where the page already has the concept — ?kind= on the directory, ?tag= on Missions, the requests board's existing ?state=open — and the search box, the pager and the filter chips all carry each other's parameters, so no link ever quietly widens what you are looking at. The directory, Projects and Missions also take ?authority=, which narrows a listing to the records one upstream owns; ?authority=local is the way to say "owned here", since a locally-owned record carries no authority name at all. Each of those three draws an Authority column beside its primary one, and the word in it is that link — so the fact the filter selects on is a fact the page already shows you, and "everything demo-org owns here" is reachable from any row that names it. Missions carries two filters and the directory carries three — a kind, an authority that owns a record, and ?asserted-by=, an attribute authority that vouches for a value on one — because those are facts about different things, and each renders its own chip with its own way out. An authority whose records this deployment holds none of, and one that asserts on nobody you can see, each answer exactly as a name nobody ever configured does — the same empty listing, the same words, the same bytes — because neither was ever a candidate. Two properties are worth stating plainly. The search runs after the visibility filter and only over text the page already rendered for you, so it can never find a group or an entity the page itself would refuse to show, and every number in the "42 of 1,207 you may see" line comes from that same filtered set. And Projects does not paginate: with ?q= set it switches from the tree to a flat list of matching groups — each row still printing the full DN, which names every ancestor the indentation was drawing — because pruning a tree by pages would draw parents without children and children without parents. Everything works with JavaScript off; the one small script this adds narrows the rows already on your screen as you type, and pressing Enter submits the same plain GET a scriptless browser sends. On a page you have already searched — one whose address carries ?q= — that script does nothing at all until you change what is in the box, because the rows on screen are already the server's answer to that query and the line above them ("1 of 23 entities you may see · matching auditor") counts the whole filtered set rather than the page. It reads that way because a browser was finally pointed at it: make console-browser (docs/plans/085) drives this walk with a real client certificate in Chromium and Firefox against a running make all-in-one, and it is the only test anywhere that executes this script — the first run found that a searched page's honest line was being overwritten with a count of one page and an invitation to press Enter for the search you had just run.

Find (/ui/find)

The box above the navigation, and the page it submits to. The sidebar answers which page; this answers the question it cannot — which page is the one with the thing called X on it. Type a name and press Enter (or press / from anywhere in the console and then type), and the answer is one list of places, grouped in a fixed order: the console's own pages, the settings in force, the services directory's rows, the embedded documentation, and the dictionary's declared attribute names. Every hit is a link to where the thing lives — a page's own route, the settings row's own anchor (a hit for ui.docs lands on that row, not at the top of a table of nearly four hundred), a services row, a document, the policy browser. Matching is case-insensitive; an exact or leading match on a name or a path ranks above a match in the prose beside it, so typing a path does not bury that path under the notes that cite it. Each group shows a dozen hits and then says how many more there are, because you are learning the shape of the answer rather than reading a wall. With no query at all the page says what it would search and how much of each there is.

Two things it does not do, both deliberate. It never returns a record — no entity, group, mission, invitation or request. What you may learn exists about a record depends on your standing on that record, every listing answers that under its own rule with its own ?q= (the section above), and one global search across all of them would be a second and softer way to ask the same question. And it never shows you anything you could not reach: a page you may not open is not in the list, the settings are indexed only for someone who may open /ui/settings, the services rows only for someone who may open /ui/services, and a withheld value is withheld here exactly as it is there — the path, the tier and the note, never the secret, which is also not among the text your query is matched against. Everything works with JavaScript off; the small script this adds only focuses the box on / and narrows the hits already on your screen as you type.

Home (/ui)

The landing page, ordered by consequence: what has already stopped (subsystems failing closed), what is about to (aging credentials, CRLs going stale), then what is waiting on a person (pending enrollments, requests, revocations), with who you are — read from the certificate on the connection, on every request — and this deployment's own facts beside them. The revocation detail, the resolved project-to-mission tag mapping, and any dataset warnings keep their cards further down. If no CRL is loaded and revocation checking matters to your deployment, this page says so loudly — every client certificate is denied until one is imported.

Projects and missions (/ui/projects)

Every group you can see, drawn as the tree it always was: subgroups indented under their parents, and the purely structural OU= nodes drawn as folders, which authorize nothing and are shown because a path with a gap in it is harder to read than one without. Each row still carries its full DN, so the hierarchy survives styles being off or a screen reader reading it — the name is what states the shape, not the indentation. The filter is unchanged and applies row by row: a group you cannot see is absent, and a visible group whose parent you cannot see stands at the top level rather than under a placeholder that would tell you something exists. Beside a row's DN you may also see its bang path — a project-first root!child!grandchild spelling of the same group, the form that is quick to type at authboxctl and quick to read here. It is a display alias and an input convenience, never a name: it is shown only when typing it back would resolve, against exactly the groups you can see, to the very group it sits beside, so two groups sharing a path show it for neither. The DN is always printed and is always the thing that travels. The Authority column beside the DN is the upstream that owns that project's record; one owned by this deployment says local, and ?authority=local lists those alone. Following the word narrows the page to that authority's projects; the authority lens gathers the whole owned set and says what stops working when that upstream goes quiet.

The listing also carries the missions this deployment declares that you may know about — its tags, validity state, and restrictions. Creating or deleting a group is CLI/API only; a form cannot safely stand in for a write that replaces a whole record at once. Membership, access lists, and mission bindings are editable per-group from the group's own detail page, and a group's bindings show only the missions your own classification reaches — the count beside them counts what you were shown.

The console governs itself with this same directory. Its features are the nine subgroups of the CN=authbox project — ui (the door), missions, invitations, enrollments, directory, requests, observe, settings, obligations — and each page requires the one that owns it. Each is also a bang path you can type (authbox!missions), and the same word is that desk's own -role preset at authboxctl invite create. Membership of CN=operators no longer opens the console at all: that group survives as the machine-caller group (the sync bundle, delegator scopes, the AuthZEN search surface), and the desks are what people hold. So the smallest grant anybody can be given is one desk plus the door, an administrator is simply a member of all nine, and a page you are not a member for refuses with a 403 naming the group you are missing rather than vanishing from the navigation. See plan 072, extended by plan 081 for the ninth.

One desk deliberately does not own its whole page, and it is the newest. CN=obligations authors standing compliance requirements and cannot enforce them: reading the obligations page is gated at CN=observe with the rest of the fleet reporting, the authoring forms on it declare CN=obligations, and materializing a clause onto a mission declares CN=missions, because that is an ordinary mission write and nothing about authoring an obligation should let somebody make one bite.

Missions (/ui/missions)

The missions you are cleared to know about, with a page of its own per mission (/ui/missions/{id}): its tags and restrictions, which groups bind it, and which operations require it. A subject holds a mission through a bound group or a direct grant — see How policy and missions fit together for how the two relate to a requirement's own clauses.

This page used to say it was not filtered, on the argument that a mission carried no access lists. It carries one now. Every mission has a classification, shown in its own column beside the name and on the mission's page, written in the same vocabulary the dictionary declares for people. A mission yours does not reach is not on this listing, is not counted in the line above it, is not named on any other page of this console — not in a group's bindings, not on somebody's holdings, not in a policy clause, not in a denial you read — and its own address answers exactly as an identifier nobody ever declared. Holding a desk does not exempt you: the deployment's own map of what it does is precisely the kind of thing a classification is for.

A mission that names no classification still has one. It is marked at whatever its restrictions already demand of the people who hold it, so nothing became visible to somebody who could not already have held it, and a mission whose restrictions say nothing about classification is marked with nothing and is as visible as it ever was.

The rows are grouped by tag, in the order the deployment declared its tag vocabulary, with each heading carrying that tag's one-sentence meaning and the untagged remainder last. ?tag= narrows to a single group; a tag nobody declared is simply an empty listing rather than an error naming it. An Owner column names who to ask, as a link where you may open that entity's record and as a plain statement that there is an owner you may not see otherwise.

Create, edit, and delete are ordinary forms behind the same write gate every other write uses, and the form carries the whole record: one control per classification attribute the dictionary declares (a list of levels for a ranked one, a checkbox per value for a compartment), an owner field with the entities you may open offered beside it, and a checkbox per declared tag with what each tag means. You cannot write a classification that excludes you — the write is refused, and it says so — because a mission you could not see the moment you had written it is a mission you could not fix. An owner sees and decides and does not edit; edits are this desk's.

The form says which of those values you could actually write. Every value the dictionary declares is still drawn, because a page that hid what it would not accept would teach you the deployment has a smaller vocabulary than it does. What is out of your own reach carries a note instead — a level above the one you hold says so and names what you hold, a compartment you do not carry says so, and a value on a release list says it would not release to you on its own — and on the first two it is also greyed out. A release value is deliberately NOT greyed out: that direction compares over the whole list at once, so data released to two places reaches somebody neither entry reaches alone, and greying one out would forbid a marking the desk accepts. A value the mission already carries stays selected and stays writable whatever your own reach, because this form replaces the whole marking and dropping it would declassify the record; the form says so where it happens. The greying is a courtesy and nothing rests on it — it is an attribute of a page, and a request built by hand never reads it. What decides is that the console asks the writer's own question, over the same chain, before it posts at all, and prints the writer's own sentence if the answer is no. That is not a second rule: it is the same function the desk calls, asked one step earlier so you are told before the round trip instead of after it. The floor rule — a mission is marked at least as high as what it asks of its holders — is untouched by any of this and stays the loader's, judged when the write arrives.

*After you create one, the mission's own page says who can see it — and the first honest answer is usually nobody. A write succeeds and the console lands you on the mission's page, where a Who can see this panel states the catalog rule's own facts about it: which groups bind it, whether any grant names it, whether it is discoverable and at what classification, who owns it, and whether you hold it right now. Under them is one sentence of consequence — for a mission just created, Nobody holds this mission yet and it is not discoverable: it will appear in no catalog, yours included, until a group binds it, a grant names somebody, or it is marked discoverable.* That is correct behaviour and not a delay: nothing is waiting for a bundle to travel, because the portal and the store ask this writer directly. A mission reaches somebody when they hold it, when it is discoverable and their classification reaches its marking, or when they own it, and creating one confers none of the three. Beside the sentence are the pages that change each fact — the group whose bindings put it in somebody's catalog, the requests board where assigning a request mints a grant, and the Discoverable checkbox on this page's own replace form. If the mission's identifier has no / in it, the panel also says it has no folder in the store: an organization-wide mission is a permission rather than a place, and a mission that should have storage is authored under a project in the portal's project workspace.

Deleting a mission that some group still binds is refused, and the refusal names the groups you may see, because a rule referring to a mission that no longer exists should not be something you can produce by clicking. The Authority column beside the identifier says who owns each mission's record — an upstream's name, or local for one this deployment owns: an adopted mission authorizes here exactly as a local one does, and stops authorizing entirely once that upstream is past its freshness budget.

Settings (/ui/settings)

What this process is actually running with — never what a configuration file says, which can drift from reality the moment a file is edited without a restart, several files are merged, or an indirection resolves to something its author did not expect. There is no editor here and there will not be one: showing a value you could edit back would mean either putting a secret in plaintext or letting you overwrite a reference you were never allowed to see resolved. Two things worth reading even if you skip the rest of the table: acknowledged departures from the safe default (every tier-2 setting this deployment turned on, since each one disables a protection) and, if this deployment delegates identity, any unscoped XPE delegator — one that may assert any subject as a chain head with nothing narrowing it.

Propose a configuration (/ui/settings/propose)

The other half of the page above, and the only page in this console that produces a configuration file. It shows a form prefilled from what is in force — the listeners, the root directory, the store backend, every registered capability with the setting that switches it — and turns your edits into a rendered authbox.yaml, a unified diff against the file this process was started from, each tier-2 departure with the acknowledgement the loader itself would demand, and config.Load's own verdict on whether the file would start.

Then it hands you the file. That is the only exit: this page does not save it, does not restart anything, and cannot apply it to the running server — the configuration is a reviewed security artifact and a session authorised by the configuration it is editing is the wrong actor to apply it (plan 017 §5). A file the loader would refuse, or one carrying a tier-2 choice you have not acknowledged, is not offered for download at all: the acknowledgement is shown before the file is downloaded rather than after it fails at the next restart.

The first configuration a deployment ever has is produced the same way, before this console exists, by authboxd -setup — see the first boot.

Status (/ui/status)

Aging warnings first, deliberately — crl_aging and cert_expiry, the drift that becomes an outage if nobody looks. Beneath that: the data store's backend and (for a SQL-backed writer) which instance currently holds the write lease; every upstream this deployment caches projects from, with its freshness budget and whether it has gone stale; replica, OCSP, ACME, and OIDC posture, each filtered to what you may see. If this build shipped a signed test evidence report (plan 038), its summary appears here too — never on the public landing page, only on this authenticated one.

A Cryptographic posture card sits immediately above that report and answers a different question from it (plan 133): the report is a fact about what was proven at release time, this is a fact about the process answering you right now. It names which of the two supported builds this binary is, the effective fips140 mode read by probe rather than from what the build intended, which module is linked and what that module's validation does and does not cover. The card is unconditional, because a posture has no "off" for an absent block to mean. When the two disagree — a binary running as the posture it was not built as — the card turns red and the line also appears on Home, in the "what has already stopped" group: such a process reports one cryptographic identity and performs another, which is the downgrade the startup check refuses, so seeing it at all means the process started outside that check's reach. authboxclient doctor is the same two facts for an endpoint, which no server surface can answer on its behalf.

Activity (/ui/activity)

What this deployment is doing, beside what Status says it is: a series of the decisions in a bounded window of the record, the counters this process has kept since it started, and the security events in between — named patterns over records the chain already holds, each with its count in the window, the subjects in it and a link into the audit page's own filter so you can read the records the number came from. ?issuer= narrows every one of them to one issuing authority, which is how a rehearsal population's activity is told apart from the deployment's own; ?bucket= widens the series to five minutes or an hour; and ?refresh=10 makes the page reload itself, which is what a person watching a rehearsal go past wants (plan 176 §3). The reload is opt in and off with no parameter — a whole number of seconds between 5 and 300, a meta refresh rather than a script, so it works with scripting off — and a page that is reloading says so above the chart and carries the link that stops it. A value outside those bounds reloads nothing and names the bounds rather than being rounded into range.

Nothing here is retained, scored or watched for you: the page is computed when you open it and holds no state between renders, and a handshake refused at the door is counted as a process total beside the series rather than drawn inside it, because such a refusal is deliberately never written to the record and so has no minute to be drawn in. It carries the same gate the decision record does — you see it only if you can see every group — for the same reason: a projection with holes in it is a partial audit. And it withholds what that page withholds: a front door's route is the record's own resource, so a route naming a mission you are not cleared for reads a mission you may not see while its counts stand unchanged.

A last card, Front doors' own decisions, is about chains that are not this deployment's (plan 122). A front door decides at the edge and records to its own hash-chained log; where a door is configured to hand its sealed segments up, this writer stores each one exactly as the door sealed it — inside the envelope the door signed, the same artefact a SIEM would get — and the card names the door, what it holds, how far behind the door says it is, and whether there is a gap. The two chains are never merged: a door's records are counted in the door's row and in no bucket of the series above, because a sequence number is per chain and adding them would produce a count of nothing. A deployment that keeps no spool says so and says what it would take (audit.doors_path, the door's anchor public half in audit.doors_keys[].public_key, and proxy.audit.ship at the door) rather than reading as a deployment with no doors, and authboxctl audit doors <dir> verifies the whole spool offline.

Streams (/ui/streams)

The authorized streams this deployment's front doors say they are carrying (plan 117). Per door: when it last reported, how often this writer has observed it fetching, and how many streams its bounded report left out. Per stream: the route, the stream id, the producer, every sink with the frames per second, bytes per second and refusals that door measured, the markings it saw, when the stream opened, its last batch receipt, and its state.

Nothing here is live, and nothing here was measured by this server. AuthBox carries no stream and decides on none — a labelled stream lives in the front door, which is already the component that forwards bytes it does not read. What reaches this page is a summary the door posts on the bundle fetch it was making anyway: at most 32 streams, at most 4 KiB, and a count of what it dropped. So every row is dated, and a rate you read here is the door's own measurement over the door's own interval.

A door that stops fetching keeps its last report, marked unreported since — dropping the row would make "this deployment stopped streaming" and "nobody can hear this door" look identical. A door that fetches and reports nothing is different again: it says so, because the proxy sends the summary on every fetch and an empty one means it is carrying nothing. And an ordinary replica never appears at all: it sends no summary, so it is a bundle consumer (see the topology page) rather than a front door with nothing to report.

The same gate as Status, and for the same reason: a stream summary names a producer by DN and the labels its frames carried, which is what the topology page already discloses at this desk — never a body, never a frame, and never a hash of one.

Deployment topology (/ui/topology)

The deployment drawn as a map: this writer, its listeners, its store, the replicas and proxies it can see, and the applications behind them. Every node says how it is known — configuration is drawn solid, anything merely OBSERVED is drawn dashed, and the page says plainly what it cannot see (a consumer that has been silent looks the same as one that was never there). Individual callers do not each get a box; they share one aggregate node whose details panel lists the busiest, and an identity earns a box of its own by traffic rather than by what kind of thing it is.

The map is laid out on the server and complete with no JavaScript at all, and it is worth being exact about which half is which. The server decides which band every card is in, which row, and which slot across it; the cards then sit on a grid that stretches to the width of your window, keeping their own type size rather than growing with it. What the server cannot know is where two cards land in pixels on your screen — so the LINES between them are drawn by the browser, from the link list the server hands it, with the strokes and labels the server chose. With scripting off you get no lines, and lose nothing: every card states its own links in words (→ store: memory, ← sigint — sync, → CN=replica-1,O=acme — serial 7, observed), capped to the first few with a count of the rest, and the panel beside the map lists all of them for every card. The lines are the enhancement over that text, not the other way round. Where a deployment has wired the seams, your browser also fills in what the vault and the proxy say about themselves, and a failure to reach either leaves the map standing with a note.

There is a third kind of edge in the legend beside solid and dashed: a fine dotted fronts line, drawn from a door to the authority whose published names that door carries (plan 100). It is not a third style for one of the other two meanings — it is a third kind of knowledge. Solid says this deployment talks to that box; dashed says that box talked to this deployment; dotted says neither, and means only that a name belonging to the box at the far end is answered for at the box at the near end. Its label names the fronting mode (fronts · passthrough, fronts · terminate) and how many names travel that way.

Two things follow from that, and both are visible on the map. This deployment's own door draws one from this deployment; a door somebody else runs draws one from that peer, learned from the path stamped onto a name when it was relayed here — so a chain reads as a path, this deployment → region → hq, with no line anywhere claiming a channel that does not exist. And the authority at the far end may be one this deployment has never contacted at all: it gets a box of its own that carries no freshness badge, because there is no budget being tracked for a box nobody syncs from, and a green marker there would be a reassurance nothing measured. Selecting it says so in words. A terminate edge is labelled with the mode alone; whether the owner accepts that door as a delegator is a column on the Front door status page, below, because that answer needs the door's own identity and this map does not hold it.

There is now a fourth kind, and it is the only one on the page that is somebody else's claim rather than this deployment's own knowledge (plan 168). A consumer that fetches its bundle over the network carries, on that same fetch, a small summary of what it can see: what kind of box it is, what it adopts from, which boxes fetch from it, and the origin marks it holds. Nothing was probed to obtain it — the page's oldest rule still holds — and nothing in that summary is dataset content. A box named only in such a summary is drawn with a widely dashed edge, a dashed border, and a line saying who reported it and how long ago; where the reporter has gone quiet the box is faded with its age beside it, never removed, because a box that vanished when its reporter fell silent would draw the chain changed and we stopped hearing as one picture. And where a report disagrees with something this deployment knows first-hand, what you see is this deployment's own reading, with the difference written on the box.

The network map (/ui/topology?lens=network)

The same page has three further layouts, reached from the Lens switch at the top of it, and the first of them is the shape of the chain rather than the state of it. Boxes carry an identifier and nothing else — the federation name or the host, with AuthBox's own mark on every box that is one — with this deployment in the middle, the authorities it adopts from above it, the boxes that adopt from it below, what its door fronts to the right, and what it runs on to the left; each edge is named by what travels on it. A box that was reported rather than seen, or that you stepped into, is drawn under whoever reported or answered for it, one step further in, so depth on the page is depth in the chain. Use the map to find your way, the diagram above to read how each link is doing, and the explorer below to walk the whole thing; open a box to see what it in turn is connected to, and select one to fill the pane beside it — the same pane, and the same selection, as the explorer's, so the Lens switch keeps your place and keeps any box you have stepped into.

The explorer, and the authority lens (/ui/topology?lens=authority)

The next layout opens with the explorer — the same deployment the map draws, walked instead of drawn.

It is two panes. On the left, a tree: this deployment first, then every other authority that carries anything here; under each, the boxes chained to it with the relationship written on the row, the sources it adopts from and the surfaces it serves outward, and the organisations it holds — and under an organisation its groups, under a group its members and the missions that group binds. On the right, whatever row you selected: what it is, the facts the model holds about it, its own children as a table, and the one link to the page that owns it. The explorer is a way of getting somewhere; it is never a second copy of the page you are getting to. Above both is the path you are on and a find box that searches the tree.

So there are three kinds of knowledge in the tree and on the map, and every row says which it is. Own is what this deployment holds itself — a link somebody configured, a consumer it watched fetch. Reported is a box further down the chain that one of those consumers told this deployment about, drawn with that reporter's name and the age of what it said — or, where the writer this box replicates publishes one, what the box above it said about its own view; an observed consumer that has told this deployment nothing is a leaf reading reports nothing, which is a different fact from a box with nothing under it. Reached is a box you stepped into yourself, and it is the third.

Stepping in

A reported box is what somebody chose to say, and it flows one way. Where a neighbouring box allows it, you can go and ask it yourself: select the box and follow Step into this box on its pane.

What happens then is worth knowing exactly, because it is the one thing on this console that causes a request to leave your deployment.

Being explorable is a declaration the far box makes, and a box that does not make it is invisible without having to say so. It binds the well-known mission authbox-topology to whoever may look. A fresh deployment already binds it to CN=everyone — everyone this authority holds, which given the pool and the dataset above is a much smaller set than it sounds — so a federation of boxes that already trust each other's certificates is explorable on the day it is stood up; a box that wants to be invisible removes that binding and sets topology.explore_as: off. In the admin posture the far box must ALSO register the exploring console as a delegator, which is why that is not the default: a delegator speaks for people, and no box should grant one to a neighbour by default. Optionally a box also publishes topology.reachable_at, the address it invites a neighbour to ask at, which travels in its own summary — and a console dials a disclosed address only when it independently holds that box's (issuer, subject) pair from a bundle fetch it served, pinning the peer to it; an address whose pair it has never verified is drawn and never dialled.

Everything the explorer does is an address. Selecting a row, choosing which of the places a row appears in, searching, and turning the page of a long branch are all query parameters on this one page, so any view of it can be sent to somebody else and every one of them works with JavaScript switched off — the disclosures are ordinary browser disclosures and every row is an ordinary link. With scripting on you also get the arrow keys, a type-ahead, and the "next" link at the foot of a long branch followed by scrolling; none of those is the only way to reach anything.

Two things about the tree are worth stating plainly, because a folder tree suggests otherwise in both cases.

And the tree shows what you may know exists, counts exactly what it shows, and never tells you how many rows it left out — the rule every listing in this console follows, on a page that happens to be shaped like a tree.

Beneath the explorer, the lens answers a different question: not who is this deployment connected to but what stops working if this peer goes quiet. It is one card per known peer — every configured upstream, and every bundle consumer this deployment has actually served — and each card carries four things:

A card also appears for an authority that records name but configuration does not — the upstream line was removed while its cached records remain — because nothing can ever refresh those records again, and that is the worst state this page has to show.

The lens is a layout of the topology page and not a page of its own: it is gated by the same ui.topology switch, requires the same desk, and needs no JavaScript. Each card's name links to that authority's own page, below, and the page links back to the card — the two agree because they read the same store fields. Nothing on this page is fetched from any peer; it is what this store already holds, projected.

The names lens (/ui/topology?lens=names)

The third layout of the same page, and the one that answers which of this federation's names does this box believe, and for how long. One block per authority: this deployment's own zone first — the one it publishes for other boxes to adopt — then each authority whose names it has adopted, sorted by name. Every name is listed with what it is for (console, vault, claim, app…), its type, and the address or alias target exactly as its authority published it. Nothing here is fetched from a peer and no name is resolved; it is what this store already holds, projected.

Five things each block carries:

Above the blocks are the two artifacts an operator actually copies out of band: the emitted zone (/admin/v1/names?format=zone) and the hosts snippet (/admin/v1/names?format=hosts), both on this same host, both plain text. Those are the merged answer with a stale authority's names withheld. Both are unsigned, and the page says so and points at where signing lives: DNSSEC belongs to the door that serves the answers rather than to the box that publishes the names, because a fronted name is rewritten at that door and this deployment's signature would not validate over the address a client is actually given (plan 101).

Names on this page are not links, and that is a rule rather than an omission: a DNS name is a published address for a door that decides for itself who may come in, not a record this console has a page for. The authority names beside them are, and every one of them links.

A deployment holding no zones says so, and says how one would get here — this box publishing its own (a names.yaml in its dataset, derived from the front door's route table and the services directory) or adopting an authority's bundle that carries one.

On the map itself, each configured upstream's edge now says what came over it: the card reads ← region — sync · 2 zones where two arrived along that link, and hovering the drawn line says the same. An upstream carrying none draws the plain sync it always drew.

An authority's page (/ui/authorities/{name})

The card above, promoted to a page — and the address every authority NAME in this console links to. Clicking demo-org on a Directory row, in a record's ownership banner, in the Status page's upstreams table or cached-projects rows, in the Services directory's upstream list, or on Home lands here; the filter glyph beside the name in an Authority column is the other question, and still narrows that listing to ?authority=<name>.

The page says what the authority is — a configured upstream or one only records name, its freshness and budget, and how its bundles are trusted — and then what this deployment holds from it, in four sections: its projects, its missions and its people, each row a link to that record's own page and each list footed with the same set on the Projects, Missions or Directory listing, where it can be searched and paged — and its names, the DNS zone or zones that authority publishes as this deployment holds them. The names section is the names lens's own block for this one peer, drawn by the same rendering, so the two surfaces cannot come to disagree about whether a zone is withdrawn; it is footed with the lens, where this authority's names are read beside every other's. An authority that publishes no zone here says None. and offers no link onward. The identity block is the lens card's own model and its own sentences rather than a second description of them, so the two surfaces cannot disagree about a peer's state, and every list withholds exactly what the lens and the listings withhold: a project your access lists do not reach is absent from all three, and no count on the page hints that it exists.

Who it vouches for. An authority that owns the values of one or more attributes gets a fifth section, People asserted, which answers the question the four above it cannot: not what this authority owns, but who it currently vouches for. One row per subject, sorted by DN, with a column for each attribute it owns — in the dictionary's own order, so two people are compared by looking down a column — the adoption that last changed that subject's assertions (the bundle's own serial and version, and when it landed), and whether the values are still conferring, in the same freshness sentence Status and Services print for that upstream. Above the table, one sentence: nobody types these values, and the computing authority is the only pen. The section foots with the directory narrowed to those subjects — the same link the lens card offers — and an entity page's asserted by … line links straight back to it.

The rows are a reading of what this store holds, not a second ledger. Adoption writes a bundle's assertions onto each subject's own record and clears the names the bundle stopped carrying, so an assertion withdrawn is simply not a row: absence is how a lapse is spelled. The Adoption column can be blank, and the footnote says why — the change may be older than the window this page reads back through, or older than the active audit segment, or in a bundle that moved more subjects than one record names, or the value may have been seeded into this deployment's dataset with no bundle behind it at all. A console that cannot read its decision record says that instead, because "the record does not reach that far" and "this console could not read it" have opposite remedies. And a subject you may not open is not a row and is not counted, which matters more here than it looks: a subject asserted on can be a group, and a group is the one kind of entity that carries access lists.

For local, and for an authority that owns no attribute name, the section is simply not there — never an empty table. That is deliberate: an authority asserting on nobody you can see has to look exactly like an authority asserting on nobody at all.

local is a name like any other here: put it in the path and you get the page for what this deployment owns itself — the same four sections, including the zone this box publishes under its own signature for the rest of the federation to adopt, and no freshness, budget or blast radius, because there is no peer to go quiet.

Absent, not forbidden. A name that is neither configured nor named by a record you may see answers with exactly the same 404, in the same words, as a name nobody ever configured — so the address bar is not a way to discover which authorities exist. And the page is gated where the lens is: with ui.topology off it explains itself rather than 404ing, and no name anywhere in the console links to it, while every Authority column still says who owns each row and every ?authority= filter still works.

The demonstration organization

When this deployment runs the federated topology (make all-in-one-federated), one of the authorities on the lens is demo-org — a full, deliberately fictional government-shaped organization of about a hundred people in the O=example,C=US namespace, adopted whole so you can explore a realistic directory at real scale without any of it touching this deployment's own records. It is the enclave you show someone who asks "what does this look like with an actual organization in it." Everything below is read through this console's own pages, filtered to what you are cleared and designated to see, exactly as your own directory is.

Who sees it. demo-org's projects are adopted records, and an adopted project is visible to exactly whom its owner names on its access lists — this deployment cannot add its own operators to a group it merely caches (plan 007; the cascade stops at the ownership boundary). That is a security property, not a gap: a consumer never grants itself sight over a partner's data. So the demonstration names, as readers on every root, the identities the showcase ships with — CN=admin, CN=operator and CN=auditor — and an identity you enrolled yourself sees its people but none of its projects (the authority lens shows the missions, which are not readership-gated, beside 0 projects). To see it as yourself, have the owner name you: the federated bring-up honours DEMO_ORG_READERS="CN=you,…" (several DNs separated by ;, since a DN itself carries commas) and stages demo-org's dataset with your DN on every root, exactly as a real partner would add you to theirs. The checked-in dataset never carries a deployment's own names.

The separation is structural, not a matter of discipline: O=example,C=US is disjoint from this deployment's own O=authbox, so nothing demo-org carries can ever be mistaken for a record this deployment owns. It is the whole demonstration in one place — searchable at scale, tree-shaped, honest about a suspension, reportable — and it never pollutes the feature fixtures beside it.

Policy (/ui/policy)

Who may do what, and why not. Every operation from the documents this server actually enforces from — never a second reading of the files — with its requirement rendered as clauses you can read: the mission, group, and attribute conditions, whether it is public, and the two cases a grammar cannot state about itself (an operation that declares nothing and therefore denies everybody, and one that declares a requirement naming nothing and therefore admits every authenticated subject). A mission clause is satisfied through a bound group or a direct grant, whichever a subject holds — see How policy and missions fit together if the relationship between this page and Missions above is not obvious from the clause alone. Pick an operation and its own page (/ui/policy/{operationId}) answers, from the same evaluator that decides real requests, which known subjects currently satisfy it and — per subject — which clause is the one they fail. That answer is computed fresh and not cached, because a slightly out-of-date answer about who may do what is worse than a slow one.

Nothing here edits a requirement, and nothing ever will. Requirements live in the shipped documents because that is what makes them reviewable code; a form that rewrote one from a browser would turn policy-as-reviewed-code into policy-as-whoever-clicked-last.

The same question, at the person's own door. The portal serves the question pages at can.<domain>: Can I, Can they, Who can and a matrix of up to eight subjects against eight policies, over this same evaluator, with each answer fenced to what the asker could already learn elsewhere on the deployment — see Can they?. This page is the operator's version of it and Who can is literally this page's own question travelling with the asker at the head of the chain; what the portal adds is that a person who is not an operator can ask about themselves, and that asking about somebody else is delegated and audited rather than gated on console standing.

Obligations (/ui/obligations)

The standing version of what every mission decides per request: who is out of compliance right now, before anybody bounces off a door. Each obligation is a set of clauses written in the mission restriction grammar verbatim, asked as a requirement with attributes and nothing else, through the same Reporter a real refusal goes through — which is what makes the fleet report structurally unable to disagree with enforcement. A row per obligation with its counts, a page per obligation (/ui/obligations/{id}) naming who is short and on which clause, and a drift verdict comparing the obligation against the mission that is supposed to enforce it, because the two are deliberately separate documents and the gap between them is the thing worth seeing. See How missions, obligations, and goals fit together for how this connects to Goals below.

It denies nothing, and cannot. Every operation on this page is a read; nothing here suspends, revokes or refuses anybody, and the consequence for standing non-compliance stays a human act with plan 073's signed suspension as its lever. Making a clause actually bite is a separate, ordinary mission write — materialize — which declares the missions desk rather than this one. Standing obligations is the whole story: writing one, reading the drift, and what this deliberately does not do.

Goals (/ui/goals)

The question an obligation cannot ask: is this org compliant with onboarding, where onboarding is several obligations at once and the org is a tree rather than a group. A goal names obligations and a project, and derives every number from the obligation reports beneath it — it holds no clause of its own, calls no evaluator, and reaches the engine only through the same per-member call each obligation's own page makes. So a goal's page cannot disagree with an obligation's page: there is no second computation to disagree with. See How missions, obligations, and goals fit together for the whole chain from a mission's restrictions to here.

A row per goal with its target, its percentage and whether the target is met — never "not met" on its own, because that does not say whether anybody still has time. A page per goal (/ui/goals/{id}) with the per-unit table, one row per group at or beneath the project, indented by the same tree the Projects page draws. Each unit links to its own page and each obligation to its own, and a shortfall row names the obligation and the clause, never a value and never a member: a goal adds nothing an obligation's page would not say, and who is short is that page's disclosure to make under its own gate.

Each row counts that group's own direct members, which is what an enforcing mission bound to that group would reach — so the rows sum to more than the header whenever somebody is a direct member of two groups in the tree. The header counts each member once, each row counts them once, and the page says so rather than leaving you to reconcile it. Two more readings the page draws rather than rounds away: a unit no obligation reaches says so instead of sitting at a reassuring 100%, and a percentage is never drawn without its denominator, so a tree with nobody in it cannot read as a fully compliant one.

A target enforces nothing. by and threshold let a goal say "everyone by the thirtieth"; passing the date changes what this page says and nothing at any door. The obligations beneath keep their own enforce_after, which is the only date anything in this system reads as a state change, and the one enforcement lever stays plan 073's signed suspension on the obligation's page. There is no write control on either goals page.

A unit you may not see is absent, not refused — plan 007's rule applied to arithmetic. The roll-up above it is drawn from what you can see and the page says so, in one sentence it shares with the API; how many units were withheld is deliberately not stated, because a count is the withheld information arriving one integer at a time. A goal over a project you cannot see at all is simply not there, and its page answers the same not-found an id nothing declares gets. Home carries a card for the goal closest to its date — absent, never empty, when there is none.

Progress is evidence or it is a dashboard. A goal that declares a snapshot_every cadence has its whole roll-up appended to the audit chain on that cadence, by the writer's own scheduler — the counts per unit, each obligation's drift verdict, the target as it stood, and the freshness of any adopted facts the numbers rest on. The goal's page draws that series and does not keep it: every row is a chain record with a serial, and "we were at 91% on Friday" is a fact somebody can check rather than a line this console drew about itself. A trend arrow beside each row compares the last two snapshots and has three states — up, down, and level, because a compliance number that has not moved for a fortnight is a finding rather than the absence of one. Fewer than two snapshots gets no arrow at all: "no progress" and "no evidence" are different things. A goal that declares no cadence says it is never snapshotted rather than drawing an empty chart, and a chain this console cannot read says that rather than reading as a goal with no snapshots.

The console reads a bounded window of the current audit segment and prints the bound on the page, so the left edge of a series is a property of the window rather than of the deployment. The unbounded reading is offline and in a second process: authboxctl goals history <goal> -audit <exports> -anchor-key <id>=<pem> reads the series off a signed audit export, verifying every envelope's signature, the segment ordinals' contiguity and the chain across every boundary before it prints a row — and refusing outright, naming the file, if one byte moved. authboxctl goals snapshot <goal> takes one now, which is recorded as your act with your DN; the scheduler's own records carry no subject, because nobody asked for those. A snapshot is written on the cadence whether or not the number moved: a series with the unchanged days left out has no Friday in it.

Browser keys (/ui/webauthn)

Your own page rather than an administrative one — the navigation groups it under You — and the only thing it does is register a browser key, which is a second signer over requests your certificate already authenticated. It is never a login: the certificate is still what authenticates, and the key only ever adds a signature the certificate key had to admit (plan 068). The badge beside it says none until you register one, and where the deployment declares no webauthn.rp_id the page says browser keys are switched off here rather than unavailable to you. Every ceremony that can use one — accepting a mission grant, suspending or reinstating an entity — keeps its verbatim authboxctl command on the page in all three states, so nothing on this page is load-bearing for getting work done. Signing has the registration walkthrough and what a signature actually covers.

Front door status (/ui/frontdoor)

Three pages here are about this deployment's doors and each answers a different question, which is why each one now opens by naming the other two: this page is what the door is doing right now, Services is what this deployment mounts and is wired to, and the front door's own public directory — a page on the certificate-less listener listing what is behind the door and how to reach each of it, for somebody arriving with nothing (plan 157) — is the one you hand to a newcomer, linked from all three where a deployment sets ui.directory_url and named as unconfigured where it does not.

What that page looks like. Its masthead carries the host — apps.<domain>, the name you typed, because a person arriving at a deployment they do not know is told first which one it is — with "the front door" as the note beside it. Then the deployment's own title: as the heading, one sentence saying that this page states what exists and decides nothing, and straight into the search box and the tabs, full width. (It opened with a header band until plan 167 §1: the band's two-column layout is built around an identity card, and this is the one page in the product with no caller to put in one, so half the first screen was empty.)

Every door and every application on it is a card in a grid — three across a reading column, four at 1440, five on a wide screen, one on a phone — carrying a line icon at its head, the name, the one-line description: the route declares, and the way in at its foot: the bare host, where a browser can open it, or the exact command, where it cannot. A card with an address is the link, so the whole box is the click target, and it opens in a new tab — you are working down a directory, and coming back should be coming back to the page.

The icon is declared, not guessed. A route says which one it wears (routes[].icon, a name from the product's own set — plan 167 §2), which is the operator saying what the thing is: logistics.<domain> is a truck. A route that says nothing keeps the icon its shape earns — an application, a door, a spliced host, a stream — and this deployment's own surfaces wear the AuthBox mark whatever they declare. Nothing on that page is guessed from a hostname, which is the rule the declared name exists to keep: an icon is a claim about what a thing is, and this page may not make one the configuration did not. A name the set does not carry is refused when the door loads its configuration, with the whole set printed.

Above the grid are a search box and tabs — All, Applications, Doors, This deployment's own, with a count on each. Both work with no script at all: the search is an ordinary GET form the door applies on the server (type words, every one of them has to match something — the same grammar the portal's Missions page uses), and the tabs are links to the panels, so the address bar alone drives them and a link to #doors lands on that tab. Neither is a filter on you: searching or choosing a tab narrows what you are looking at and never what this door declares — the all tab is always exactly the three groups put together, which is what a test holds. It wears the same chrome as the object store at store.<domain> and the portal at me.<domain>: one stylesheet, the console's colours, the product's three embedded faces. Nothing is fetched. The stylesheet is a file on this door at a path carrying its content hash — which is what lets this page forbid inline styles outright, where the two signed-in doors have to allow them — and the only other addresses the page names are the AuthBox mark and those faces, served from the same binary. There is no script on it and there will not be one.

The proxy's world, if this deployment runs one: its live route table (both the routes an operator declared and any it discovered), per-route decision counts, the age and serial of the dataset bundle it is deciding from, its acknowledged weakenings, and the identity of its receipt-signing key. Every one of those is read by YOUR browser on YOUR certificate — the console server never calls the proxy for anybody — so a deployment that has not wired that seam sees a page saying exactly that rather than an empty table pretending to be a proxy with no routes. Routes are not editable here either; what the page can do instead is help you author one correctly, and the change ships through review like any other configuration change.

A route that serves another authority's published name carries a Fronts column (plan 100): the authority whose name it is, the mode, and — for terminate — whether that owner accepts this door. That last value has three states and the page draws three, because they are three different facts. Accepted by region means the owner has published this door in the delegators it admits. Declared here, not accepted by region means the owner has not, and the request will be refused at the owner's door — shown here before anybody makes it, which is the whole reason the column exists. And silence — no acceptance drawn at all — means the owner publishes no such list, or its bundle has not arrived yet; that is emphatically not a refusal, and a page that rendered it as one would invent a decision nobody made. A passthrough route shows no acceptance either, and for a stronger reason: it asserts no chain, so there is nothing for an owner to accept. The column also carries a stale marker when the fronted authority is past its freshness budget, because its names are withdrawn from the emitted zone at that moment and the route is then carrying a name this deployment can no longer say anybody published.

The card also carries Zone signing (plan 101): signed, key tag 50494, signatures valid until … for a door that holds a DNSSEC key, or unsigned for one that does not — which is the default and reads as a fact rather than as a warning, since signing disables nothing and is absent from the weakenings list for that reason. A door that was keyed and could not use its key is drawn as a third state and not as "unsigned", because those have opposite remedies. The validity shown is the soonest expiration across the zones this door serves, which is routinely much shorter than the configured lifetime: a signature lives no longer than the owning authority's remaining freshness budget.

Mid-rollover the same row reads signing with key tag 50494; also publishing 21131 — a door that publishes several keys and signs with exactly one, which is the overlap a key rollover is made of. That sentence is the thing to watch during one: the anchor you distribute must hold every tag named there before the signing tag becomes the new one, because a validating resolver whose anchor no longer matches refuses every name this door serves rather than degrading to unsigned. authboxctl names rollover -plan prints the procedure and the waits it computes from this door's own TTLs.

Where a deployment runs the demonstration doors (plan 153), they are ordinary routes on this table and read as such: records.<domain> (Marked data), login.<domain> (Login with AuthBox), sso.<domain> (Login with AuthBox, for an application that speaks SAML), and ssh.<domain> — which is the one that looks different here, because it is a raw-TCP route rather than an HTTP one (nothing is parsed, nothing is rewritten, and the door splices the bytes to an sshd that reads a different credential over the same connection; Logging in to a machine). The Services page lists three of them as companions, with an address each and a link to its page; this page is where you see the routes that put them there. files.<domain> was a fifth until plan 167 §3 retired the upload showcase; the stream route it drove, upload.<domain>, is still on this table, and what it streams is filed by the object store (Filing a file).

Services (/ui/services)

The directory: everything this deployment mounts, is wired to, and syncs from. It carries the same wayfinding line Front door status does, in the same words: what the door is doing is that page, what this deployment mounts is this one, and what is behind the door for somebody arriving is the front door's own public directory. Each surface — the administration API, this console, the enrolment intake and its ACME and EST dialects, the OIDC provider, the AuthZEN decision point, Brokerage, Clearance, Self, and the LDAPS directory — with the address it answers on, which listener answers there, one sentence saying what it is for, and a link to the embedded document that explains it where one exists. There are three listeners named here, not two: the mTLS one, the certificate-less intake, and LDAPS, which is the first surface on this list that is not HTTP at all and so answers on a port of its own with no path to mount under (plan 087; Speaking LDAP is its page). A surface your deployment has switched off stays on the list and names the setting that switches it on; what disappears is its address, not the row, because a surface that vanished when disabled would be indistinguishable from one this build does not have — and the remedies for those two are completely different. Below the surfaces are the companions and the configured upstreams, the latter carrying the same freshness line the Status page computes rather than a second reading of it.

The companions here carry an audience of their own, in a column rather than in the name, and there are more shapes of them than the sidebar shows: AuthboxVault (operators), the claim page (invitees), the front door's status page where ui.proxy_status_url is set (operators), and one row per configured upstream that publishes a console of its own — operators at that upstream, which is the honest reading of a link that leaves this deployment entirely.

The last shape is the showcase applications (plan 153): the small, deliberately readable services a deployment runs behind its own front door, each one proving a single integration surface by being a caller of it rather than by a document asserting it works. They appear only where a deployment composed them (ui.showcases[].address), which is the upstream-console rule and not the vault rule — a showcase is your composition rather than something this product ships, so a deployment that runs none shows no rows at all, and a page listing absent demo applications would report a gap where there is a deliberate choice. The all-in-one lists three. The marked records showcase asks this deployment what you are cleared for and returns the rows you may see, redacted rather than refused — Marked data is its page. The OIDC relying party is "Login with AuthBox" done for real, all the way through discovery, PKCE and an ID token verified against this deployment's own provider — Login with AuthBox. And the SSH target is a stock sshd that trusts this deployment's certificate authority and nothing else — Logging in to a machine. (A fourth, the classification-routed upload, was listed here until plan 167 §3 retired it.)

That last row is the one to look at twice: its address is a command, not a link, and the page renders it as code for you to copy rather than as something to click. The target is reached through the front door's raw-TCP route with authboxclient ssh-proxy, so there is no URL a browser could follow — and a console that dressed that line as a hyperlink would be offering a control that does nothing. Any showcase address that is not an https:// URL is rendered the same way, for the same reason.

This is deliberately not the topology graph, which draws the deployment, and not Status, which says whether it is working. It is the list — and nothing on it was fetched from anywhere: every name is configuration this deployment already made, read back.

Invitations (/ui/invitations)

Outstanding invitations — subject, expiry, whether spent, who created it. A token is shown exactly once, at creation, and never again: AuthBox stores only its digest, so there is nothing here to recover if you lose it and nothing here worth stealing. When your deployment has configured a claim door (public.vault_claim_url), the creation flash also gives you the ready-to-send claim link — the claim page with the invitation and the subject already attached — so you copy one URL instead of assembling three parts. It appears exactly where the token does and never again, because the token is part of it. Where the deployment also runs a courier (ui.vault_url), the flash carries a third thing: the authboxctl invite send line that has the courier mail that invitation. It is a command and not a button, and the token in it is a placeholder rather than the live one above — this console never calls the vault, because a send is decided on the certificate that reaches the courier, and one posted from this page would arrive on the server's certificate rather than yours. Sending is also its own standing, separate from the desk that mints: the flash names the clause you would be missing, and running the command produces the directory's own sentence either way. Mailing an invitation is covered in full in the technical collection. Creating one requires you to administer every group it would grant — an invitation can only do what its creator could already have done directly. The create form also picks the issuance profile the resulting certificate will be judged against, which matters more than it looks: an invitation that names no profile hands its holder whatever the deployment default produces, and for a service that is how an identity comes back shorter than the one you meant to invite.

Mission requests (/ui/requests)

The brokerage queue: who asked for which mission, who decided it, and — separately — whether the grant that decision produced is doing anything right now. Those last two are not the same fact. A request can read accepted while its grant authorizes nothing, which is exactly what an expired window looks like, so the board names the grant's own liveness beside the request's state rather than leaving you to infer one from the other.

Assigning a window is a form on the request's own page (/ui/requests/{id}), where you may decide it. Accepting is not an operator's act at all — only the requester may accept — and it is signature-required, so the page prints the exact authboxctl requests accept <id> command rather than offering a button that could not work.

For the requester themselves the page has a third state. Where this deployment has enabled browser keys (webauthn.rp_id) and the requester has registered one, they get a real button: the server hands the page the exact canonical statement, the page shows it, and their authenticator signs those bytes (plan 068). It is still a signature and still theirs — a browser key is a second signer their certificate key had to admit, never a login, and the request is authenticated by their certificate either way. Where the deployment has enabled them and they have not registered one, the page says so and links Browser keys in the navigation. The verbatim command stays on the page in all three states. See Asking for access for the whole story from the requester's side, and Signing for how to register a key.

The catalog half is on the missions listing: its Ask for column marks the missions you could file a request against — those declared discoverable, plus those you already hold — and the form itself lives on the mission's own page.

Revocation requests (/ui/revocations)

The queue of "please revoke this serial" — who asked, for what, why, and whether it has been fulfilled yet. Read-only, permanently: nothing online in this system signs a revocation list, so there is no button here that could revoke anything. A request is recorded as an intent and closes by itself once a signed list covering that serial reaches the deployment, which happens on the CA host. Nothing marks a row fulfilled by hand, so a row still showing pending after a transfer is a question about whether the list actually loaded.

Audit (/ui/audit)

The decision record: the most recent decisions, and whether the hash chain across the entire log still verifies (not just the window shown on screen). A denial's reasons are recorded here and only here — never returned to the caller who was denied, because a response that explains itself is an oracle for probing somebody else's standing. This page is refused unless you can see every group, since a record names the group that granted and quotes what a mission denied. Seeing every group is not the same as being cleared for every classification, so one more thing is withheld here: a record naming a mission your own classification does not reach keeps its row, its time, its subject and its outcome, and reads a mission you may not see wherever the identifier stood — in the resource, in the granting-mission column, in a reason and in a detail alike. The withholding happens before the search runs, so typing the identifier you are guessing at finds nothing. Nothing is dropped and no number moves: the record still says how much happened, and withholds only what it was about. The search and pager here are a view of that window and nothing more: "nothing matches" means nothing in the last hundred decisions of the active segment, never that it did not happen. authboxctl audit verify <directory> and the export are the instruments for a question about history, and the page says so where somebody about to draw the wrong conclusion will read it.

Enrollment queue (/ui/enrollments)

Every certificate request, its state, and (if you administer this) buttons to approve or reject. The subject column shows what the requester asked for; what actually gets issued is built from the profile's template, so a field the profile fixes or does not allow is replaced or dropped rather than granted. If this deployment requires an actor signature on approval (plan 033), the button is replaced with a note pointing you at authboxclient sign or authbox-agent instead — a browser has no way to reach the private key a real signature needs, so the console says so rather than offering a form that would fail. (Browser keys close that gap for the brokerage's accept, which is the demonstration plan 068 shipped; extending the same treatment to other signature-required operations is mechanical and deliberately not done wholesale.) Each request also has a page of its own (/ui/enrollments/{id}) where the full CSR is displayed next to the approve and reject buttons — signing off on a request you can only see three columns of is not approval — and an expired row can be discarded from there, which is hygiene rather than state management: the queue is in memory and swept on its own TTL regardless.

A group's detail page (/ui/groups/{dn})

Your role on this specific group — reader, editor, admin, or owner — shown up top, since roles cascade downward from an ancestor and never upward, so your standing here may come from a group above rather than this one's own lists. Members, mission bindings, and (if you administer the group) its four access lists are each editable here, one operation at a time. If the group is owned by an upstream authority, every change is refused with a note saying so — the change belongs there, not here. The page prints the group's bang path beside its DN under the same round-trip rule the tree uses, and "what membership here unlocks" lists the operations that name this group — which, for one of the console's own feature groups, is that desk's honest capability list.

The directory (/ui/entities)

Every entity you can see — people, services, and groups alike, since a group IS an entity in this model rather than a separate kind of record. Creating and deleting an entity stays a CLI or API operation, with one deliberate exception: the groups page offers a create form for a GROUP entity, because "a group is an entity" is a modelling fact an operator should not have to reach for the CLI to act on.

"Every entity you can see" now means two questions, and this page asks both. A record carries a classification of its own — DECLARED, on a group exactly as on a person, and never derived from what a record holds or from what a group binds — and it says who may know the record exists. The reader role you have always had says who may see inside a group. They are conjunctive: a record either question excludes is not on this listing, is not in the count beneath it, is not in the tree or the cross-link index, and is not named by any other page here — and addressed directly it gives you the same "no such entity" page a DN nobody ever declared gives you. You cannot tell "there is no such record" from "you hold no role on it" from "you are not cleared for it", which is the point. The count is of the rows drawn and there is no total beside it, because a filtered listing next to an unfiltered count says exactly what the filter removed. One consequence to expect rather than to debug: nothing on this page changes until somebody marks a record, and once one is marked, an operator below it stops seeing it here and everywhere else at once. The remedy is a higher classification level on the viewer, never a lower marking on the record.

The Authority column says who owns each record: the upstream that adopted it here, or local for one this deployment owns itself, which is the common case. The word is a link to this same listing narrowed to that owner (?authority=), so "show me that upstream's people" is one click from any row that names it — and the authority lens's card for a peer offers the same listing as its third link, beside its projects and its missions.

Owning a record and vouching for a value on one are different questions, so they have different parameters: ?asserted-by=<authority> narrows this listing to the subjects one attribute authority currently asserts a value on. For such an authority the two sets do not overlap at all — it owns no record here, and asserts on people this deployment owns outright — which is why one parameter could not have answered both. It composes with ?kind=, ?authority= and the search box like everything else in the grammar, carries its own chip with its own way out, and survives paging. An authority that asserts on nobody you can see answers exactly as a name nobody ever configured does: the same empty listing, in the same bytes, so the address bar is not a way to discover which attribute authorities exist.

A row marked suspended is one the deployment is refusing everywhere right now, and it still holds every membership and attribute beside it — that is the point of the state rather than an oddity of the listing. The reason is not on this page; the entity's own is where it lives. A pause whose end date has passed is not marked, because it is not in force: nothing sweeps the field, and the state is judged when it is read.

An entity's detail page (/ui/entities/{dn})

Every attribute AuthBox currently holds for that entity, where its value came from, and whether you may change it. An attribute the certificate carries always outranks one stored here, and one an upstream owns cannot be changed locally at all — both cases show as not editable, because offering a form that cannot take effect would be worse than not offering one.

The dictionary drives this form. Under each attribute's name the page prints what your deployment declared for it: the kind, the ordered ladder or the list of values where it has one, which direction it compares in when data carries it as a marking, and the DN attribute types that source it. The value field offers those same values as suggestions — plain HTML, so they are there with scripting switched off, and each field offers only its own attribute's vocabulary. A set keeps its multi-line box and gains one field beside it for adding a declared value, because a suggestion list attaches to a single-line input and never to a box. There is no second vocabulary anywhere: everything the form shows is read from the dictionary the server already validates against, so a deployment that adds an attribute or a value to its dataset sees it here with nothing else to change.

None of it is enforcement. The server stays the only validator — a value outside a closed list still travels, and what comes back is the dictionary's own refusal in its own words, exactly as before. That is deliberate: the console suggests, and it never gets to have a second opinion about what is legal. The same declarations are printed beside a mission's restrictions field, where attribute names and values are typed as free JSON text, together with which operators each kind accepts.

The record's own classification is printed on every entity page and not only on a marked one — unmarked is said in a word, because "carries no classification" and "this page did not look" must never render alike. Where you may write it, the control beneath it is one field per marking-direction attribute the dictionary declares rather than a text box, from the same builder the mission form uses: a free-text box would be a way to learn a closed vocabulary by being refused. The fields arrive pre-filled and all of them post every time, because the whole marking is written and a form that sent only the attribute you touched would silently clear the rest — which for a classification is a declassification. An empty submission is a clearing and is sent as a delete, so a truncated request cannot declassify a record by accident. Nothing here judges: that the marking must be one the dictionary can evaluate, and that you must be permitted by the marking you write, are the server's rules, and its refusals are rendered here word for word as every other refused write is. The fields carry the same reach notes the mission form's do, for the same reason and from the same builder; what this one has no preview for is that its refusals are ORDERED — a record you may not see answers "no such entity" before the marking is judged at all — and a form that answered second first would answer a question ahead of the one that has to come first.

The page also lists the certificates this deployment has ISSUED to that subject — serial, profile, validity window, and how it was issued (a claimed invitation, an approval, an ACME order, a renewal). The writer did not always know this; it kept no issuance history at all, on the reasoning that decisions are made from the dataset and never from what was issued. That reasoning holds for decisions and never covered the operator's own question, so the ledger exists now, and each row carries the one action available online: request that the serial be revoked. Requesting is not revoking — it files an intent onto the queue above, and the certificate keeps working until a signed revocation list covering it reaches the deployment.

Missions in force. Beside the memberships, the page answers what the memberships and the attributes exist for: which missions this entity actually holds, and by what route. A mission arrives either through a group the entity is a member of or through a grant made to it directly — the two are a union, and a mission reachable both ways is credited to the group, whose binding has no window of its own to lapse. Each row says whether it is authorizing right now, and where it is not, why: an assigned grant nobody accepted, a mission outside its window, a restriction the entity's attributes do not meet. The mission links to its own page; the carrying group links to its page where you may see it, and where you may not, the mission is still named and the group is not — the same withholding a mission's own "Bound by" list keeps, for the same reason. A grant is named by its id and is not a link, because a grant has no page: it is read on the request that produced it, and one authored into the dataset answers no request at all. Missions whose owning authority is past its freshness budget are listed apart: the entity still holds them and they have stopped authorizing, and the remedy is a sync rather than anything on this page.

Every number in that section is the evaluator's own candidate set, not a walk of the memberships performed by the console — the same union the door computes when it decides a request. A second answer to that question would eventually disagree with the first, and the interesting question would immediately become which of them actually decides.

View as (?as=<dn>) — borrow another subject's eyes, never their authority

When a deployment has turned the lens on and you hold the standing for it, a person's or service's detail page carries a View console as this subject link. It opens the console rendered through that subject's eyes — the Projects tree, the groups, the missions, narrowed to what they can see — so you can answer "what does this user actually see" or "why can't they see X" without downloading their certificate or juggling browser sessions. You can also reach it directly by adding ?as=<dn> to any console URL.

It is view-as, not impersonation. Your own certificate stays the authenticated identity on every request. Nothing is attributed to the subject, and every write is refused while the lens is active — a borrowed view is a place to look, not to act. It is deliberately not the delegated-identity (act-as) mechanism, which carries authority and is restricted to non-person services for exactly the accountability reason this feature does not touch.

The guarantee, stated plainly on the banner. What you see is the intersection of your own visibility and the subject's: an entry appears only where both of you may see it. So you can never see anything you are not already authorized to see — your own authorization is the ceiling, structurally, whatever the settings say. You may see less, organized by their standing rather than yours; viewing as someone broader just shows you your own view back, harmlessly. A subject you cannot see at all resolves to nothing to view, naming no one.

Two gates, and both must hold. A deployment turns the feature on with ui.view_as (off by default — the lens does not exist until a deployment opts in). Then an operator needs the view_as attribute on their own record; admins hold it by default (the shipped -role admin preset confers it and the demo's admin subject carries it), and it is withdrawn the ordinary way, by deleting the value. A ?as= from a caller without the attribute is inert — it renders you as yourself, never an error and never a grant.

While the lens is active a coloured banner sits at the top of every page, naming the subject and stating all of the above, with a one-click Exit view-as back to the page as yourself. Three pages that project a single viewer's own record — the Status/Home/Topology cards, an entity's history and your own history — cannot be narrowed to the intersection and say so inline: they show your view, not the subject's. Every honoured view-as render is audited as your own disclosure act ("viewed the console as X") — even though the intersection discloses nothing new, who looked at whose view is itself a fact an auditor may want.

History (/ui/entities/{dn}/history)

A History section near the foot of the page opens that entity's story: when it was created, given standing, given an attribute, issued a certificate, granted a mission, suspended and reinstated — oldest first, with the evidence for every line.

It is a projection, and the difference matters more than it sounds. There is no history table behind it and no log of its own. Every line is computed when you ask, out of records this deployment already keeps: the hash-chained audit segments still on this host, the issuance ledger, the revocation queue, the brokerage's request and grant rows, and the entity record's own suspension field. A parallel history store would be a second truth that could drift from the chain — and the chain is the one whose integrity is provable.

Three things follow, and the page shows all three rather than leaving them to be known:

The window and the page number are in the URL, like every other listing here: ?since= and ?until= take RFC 3339 instants and are inclusive at both ends, ?q= narrows the rows already on the page, and ?page= addresses the same fifty entries the administration API's own GET /admin/v1/entities/{dn}/history addresses.

What you see is scoped to you. An entry about a group you cannot see is simply not there — not counted, not flagged — which is the same silence the memberships on this page already keep, and for the same reason: a count attached to one named person is itself a disclosure about that person. An actor you cannot see is blanked while the act stays, because the act still happened. And an entity you may not see answers exactly as one that does not exist.

Your history (/ui/you/history)

The You area in the sidebar carries Your history — the same timeline turned on yourself, and the one page here that answers only about you. It is the same projection through a NARROWER filter (docs/plans/090), and the difference is the whole point: a person's own story is mostly other people's information, so this view shows you strictly less than the directory desk would show a reader of you.

The same page is available as JSON at GET /self/v1/history, beside whoami: it resolves you from the connection, takes the same ?since=/?until=/?page= grammar, and carries no way — here or on the console — to ask about anyone else.

Suspension, on the same page

A suspended entity opens with a banner stating the whole record — the reason, who paused it, when, and the date it lifts by itself where there is one. All four in one place because the commonest mistake is reinstating a pause that was going to end anyway. Below the memberships and attributes sits the lifecycle card, which offers exactly one act: suspend where the entity is active, reinstate where it is not.

Both acts are signed. That is not a formality — a suspension's entire value is who said so and why, and a signature is what makes "who said so" survive even a compromise of this server, because the server never held the key that made it. So the card behaves the way the mission-request page's accept step does, in one of three renderings:

The command is on the card in all three, including the one with a button. Whatever the button does, that path does not depend on it.

There is no button in three cases, and each says why rather than offering a control that would be refused: a group (it cannot authenticate, so suspend the members), your own record (nobody suspends themselves), and a record an upstream owns (the lifecycle lives with the owner; identity.deny_list is the local lever for an emergency that cannot wait).

Reinstating shows you what you are about to restore before you can sign it. Suspension preserved everything, so the question on the way back is how much of it comes back live, and the card names the memberships and counts the attributes that go live the moment the state lifts. To restore less, prune first with the ordinary membership and attribute controls — one audit record each — and reinstate what remains. There is no partial-restore state to get wrong.

Documentation (/ui/docs)

This collection. Four of them live here — the user guide (this page among them), the technical documentation, the accreditation package, and the NIST 800-53 control mapping — rendered once at build time and shipped inside the binary you are running; nothing on this page was fetched from anywhere.

/ui/docs is the map: every collection, every document in it, and — beside the accreditation package — this build's own evidence and where a release ships the SBOM. Opening one (/ui/docs/{collection}/{slug}) keeps you inside the console: the document renders in the same frame as every other page, with a rail down the left listing the whole set, grouped by collection and marking the one you are reading, so you can work through a collection without going back to the index between pages. The document itself is embedded exactly as it was rendered; the console supplies the frame around it and nothing else. Turn the collections off (ui.docs) and both addresses stay reachable and say so, rather than answering "not found".