docs: fix 329 Microsoft.Contractions hits with proper safety checks

Apply contraction fixes across ~40 doc files (setup, integrations, lifecycle,
networking, scheduler, swarm, tools, trust-boundary, UI, etc.). Skipped 14 hits:
- 10 where words appear in ALL CAPS for deliberate emphasis (is NOT, do NOT, etc.)
- 4 where text could not be safely located due to markdown formatting or column position

Applied via systematic scan with checks for fenced code blocks, inline code spans,
and intentional caps. Preserves sentence-initial capitalization throughout.
This commit is contained in:
iris 2026-09-07 14:28:23 +02:00 committed by mara
commit 78021ce982
38 changed files with 320 additions and 320 deletions

View file

@ -47,14 +47,14 @@ and deploys — an eval failure asking the operator to write the address
down is the cheaper outcome. **Upgrading past this release means setting
both once.**
`domain` is required too, but you no longer _write_ it: it is read from
`domain` is required too, but you no longer _write_ it: it's read from
this hive's own entry in the directory, whose `domain` defaults to
`<name>.<swarm.domain>`. So a conventional swarm states no addresses at
all, and a hive addressed by something else states it in the one place
the other hives read — `swarm.hives.edge.domain = "edge.elsewhere.example";`.
Setting `services.hyperhive.domain` directly still works and still wins,
with a **deprecation warning**. The reason it's deprecated is not tidiness:
with a **deprecation warning**. The reason it's deprecated isn't tidiness:
that option is local to one host, the directory is copied to every host,
so a value written only there leaves every peer pointing somewhere else
with nothing detecting the disagreement.
@ -70,8 +70,8 @@ form qualified labels (`iris@pr1ma.example.com`).
`swarm.name` is purely display — it surfaces in the dashboard chrome
header and per-agent system prompts, and federated hives at different
domains can share one. `hiveName` surfaces in the same places but is
_not_ only display: it is the leftmost label of the hive's domain. That
`swarm.name` sits under `swarm` and `hiveName` does not is the whole
_not_ only display: it's the leftmost label of the hive's domain. That
`swarm.name` sits under `swarm` and `hiveName` doesn't is the whole
distinction — one names this hive, the other names the group it belongs
to.
@ -120,7 +120,7 @@ services.hyperhive.swarm.hives = {
```
One attrset describing **every** hive in the swarm, **including this
one**, keyed by that hive's `hiveName`. It is meant to be _identical on
one**, keyed by that hive's `hiveName`. it's meant to be _identical on
every host_ — write it once, share it, and each host reads it correctly
because `services.hyperhive.hiveName` says which entry is itself.
@ -130,7 +130,7 @@ hive. That assertion is load-bearing rather than pedantic — "my peers"
is derived as _everything that isn't me_, so a directory that doesn't
contain you derives every hive as a peer and you peer with yourself.
`domain` is required per entry and deliberately undefaulted: it is
`domain` is required per entry and deliberately undefaulted: it's
conventionally `<name>.<swarm.domain>`, but a wrong domain that
evaluates cleanly points at a real machine that isn't the one you meant.
@ -138,7 +138,7 @@ evaluates cleanly points at a real machine that isn't the one you meant.
> inside a swarm comes from the swarm root ([`ca.md`](ca.md)): every
> hive chains to it, so one anchor replaces per-hive pinning entirely.
> What that genuinely drops is trusting a hive whose root this swarm
> does _not_ own — another swarm's, or one keeping its own CA. That is
> does _not_ own — another swarm's, or one keeping its own CA. that's
> a cross-swarm problem and wants a mechanism designed for it. (An
> earlier `certFingerprint` field existed for exactly that gap, pinning
> a peer's TLS leaf for hive-c0re's own peer HTTPS checks — removed
@ -160,7 +160,7 @@ evaluates cleanly points at a real machine that isn't the one you meant.
**container's** trust bundle, independent of this directory.
⚠️ **That container currently trusts no swarm-internal CA**, so a
self-signed gateway certificate does not federate. The swarm root
self-signed gateway certificate doesn't federate. The swarm root
can't be listed there: `security.pki.certificateFiles` is
read when the system is _built_, and the root is a runtime file (its
key must never enter the store), so there is no build-time name for
@ -183,7 +183,7 @@ hiveName = "pr1ma"; hiveName = "edge";
swarm.hives = { … }; swarm.hives = { … }; # byte-identical
```
That is the point of the shape, and it removes a class of bug rather
that's the point of the shape, and it removes a class of bug rather
than saving typing: a per-host peer list let two hosts hold _different_
facts about the same third hive — a stale endpoint, a rotated
fingerprint — with nothing to detect the disagreement. One entry per
@ -276,7 +276,7 @@ service it points at: `services.hyperhive.swarm.snapshotStore.{address,
port}` tells this hive where the swarm's `btrfs receive` endpoint is, so
`hivectl agent <name> subvol snapshot push` has somewhere to stream to.
It is genuinely swarm-scoped rather than per-peer — a swarm has exactly
it's genuinely swarm-scoped rather than per-peer — a swarm has exactly
one store, because the receiver keys destinations by _agent_ so a
migrating agent keeps one unbroken incremental chain. See
[snapshot-store.md](../networking/snapshot-store.md).
@ -288,9 +288,9 @@ daemon on this host. **Off by default and deliberately not derived from
`services.hyperhive.enable`**: a swarm has one controller, so enabling it
is a statement about swarm topology, not about whether hyperhive is
installed. Every hive runs `hive-c0re` (the agents on that host); one
hive additionally runs this (what is true across hives).
hive additionally runs this (what's true across hives).
What it serves, why it is a unix socket rather than a port, and the
What it serves, why it's a unix socket rather than a port, and the
socket-directory constraint that governs where `socketPath` may point:
[`swarm-controller/README.md`](../../swarm-controller/README.md).
@ -298,19 +298,19 @@ socket-directory constraint that governs where `socketPath` may point:
One row per hive in `swarm.hives`, saying when it last reported and what
it said. Hives publish upward through the swarm queue; the controller
never reaches down to collect, so a hive that cannot reach the swarm
still knows its own state — you just cannot see it from here.
never reaches down to collect, so a hive that can't reach the swarm
still knows its own state — you just can't see it from here.
A hive publishes only once it has been given the three
`swarm.statusPublish` coordinates below. A hive that has not reads
`never_reported` — it is not broken, it just has nothing to say upward.
`never_reported` — it's not broken, it just hasn'thing to say upward.
| freshness | what to do about it |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `fresh` | nothing — reported within `staleAfterSeconds` |
| `stale` | the hive stopped reporting. Its last payload is still shown, so check `age_seconds` and the payload for what it managed to say |
| `never_reported` | this hive has never reported at all — normally a deployment that hasn't happened, not an outage |
| `unknown` | something is publishing under a name that is not in `swarm.hives` — a typo in the roster, or a hive that was removed and is still running |
| freshness | what to do about it |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `fresh` | nothing — reported within `staleAfterSeconds` |
| `stale` | the hive stopped reporting. Its last payload is still shown, so check `age_seconds` and the payload for what it managed to say |
| `never_reported` | this hive has never reported at all — normally a deployment that hasn't happened, not an outage |
| `unknown` | something is publishing under a name that's not in `swarm.hives` — a typo in the roster, or a hive that was removed and is still running |
Every row also carries `last_seen_unix` and `age_seconds` if you want to
apply your own threshold. The timestamp is the one the queue recorded on
@ -341,20 +341,20 @@ not distribute it. Copy `hive-<hiveName>.secret` out of the swarm host's
`deploy.authelia.hostClientSecretDir` with whatever secret management the
deployment already uses.
The identity is not a choice — a hive authenticates as `hive-<hiveName>`
The identity isn't a choice — a hive authenticates as `hive-<hiveName>`
and publishes under `hiveName`, the same name that keys `swarm.hives`.
If a hive stops reporting, its own dashboard is the place to look: a
failure to publish raises a warning banner there after three consecutive
misses. It stays `warn` rather than `crit` on purpose — a hive that
cannot reach the queue is not itself unhealthy, so it does not start
calling itself degraded for being unable to say it is fine.
can't reach the queue isn't itself unhealthy, so it doesn't start
calling itself degraded for being unable to say it's fine.
The endpoint answers **503** when this host has no swarm queue
configured, or has one and cannot read it — deliberately not an empty
configured, or has one and can't read it — deliberately not an empty
list, which would look like a silent swarm rather than a controller that
cannot see. The body says which. Status survives a controller restart:
it is stored in the queue, not in the daemon.
can't see. The body says which. Status survives a controller restart:
it's stored in the queue, not in the daemon.
### Swarm-wide forge webhooks
@ -375,19 +375,19 @@ leftover at startup. A webhook has exactly one target URL, so per-hive
registration never added a recipient — it took delivery away from
whichever hive registered before it.
**The `agent-configs` org is not yet.** Each hive still registers its own
**The `agent-configs` org isn't yet.** Each hive still registers its own
`pull_request` hook there, so that repo has two — the hive's and the
controller's — and **both are expected; do not delete either.** Removing
controller's — and **both are expected; don't delete either.** Removing
a hive's stops it acting on config PRs; removing the controller's just
gets recreated on its next start.
Nothing to configure. The hooks are registered only when this host also
serves the swarm UI vhost — that is what publishes the endpoint, and a
hook the forge cannot reach would collect failed deliveries while
serves the swarm UI vhost — that's what publishes the endpoint, and a
hook the forge can't reach would collect failed deliveries while
looking healthy. The HMAC secret is generated on first start and kept
(see [`docs/agent-lifecycle/persistence.md`](../agent-lifecycle/persistence.md)).
To check it is working, push to `internal/knowledge` and look for
To check it's working, push to `internal/knowledge` and look for
`webhook: verified delivery` in `journalctl -u swarm-controller`. A
refused delivery logs `webhook: refused delivery` with the reason.

View file

@ -5,7 +5,7 @@ hive's own CA, and that hive CA signs the gateway leaf. A peer that
trusts the root once validates every hive in the swarm, present and
future, instead of being pinned to each one by hand.
That is the whole point of the hierarchy — it turns per-peer trust from
that's the whole point of the hierarchy — it turns per-peer trust from
O(n²) hand-pinning into one anchor per swarm.
## Two provisioning modes, one structure
@ -19,7 +19,7 @@ What differs is who puts the artifacts on disk:
`services.hyperhive.swarm.ca.autoConfigure` selects between them, and is
**off by default**: a swarm's services and its hives can live on
different hosts, and a host cannot tell whether it is the one holding
different hosts, and a host can't tell whether it's the one holding
the root, so setting the swarm CA up is an operator action rather than
something a host assumes. Turn it on for an all-on-one-host deployment
and the hierarchy costs no configuration.
@ -43,13 +43,13 @@ The swarm's service names (`swarm.forge.domain`,
`swarm.matrix.gatewayHost`, `swarm.authelia.domain`) default to
siblings of the hive domain — `forge.<swarm.domain>`, not
`forge.<hive domain>`. The hive CA's leaf is a **single-label** wildcard
over the hive's own domain, so it cannot cover them; only the
over the hive's own domain, so it can't cover them; only the
swarm-services leaf can, and this host signs that one only under
`autoConfigure`. A hive with neither serves the hive leaf on those names
and every client sees a name mismatch.
So `hive-tls` emits an eval-time **warning** naming the uncovered names.
It is deliberately not an assertion: this module can see what _it_ is
it's deliberately not an assertion: this module can see what _it_ is
able to issue, but not an operator-installed sub-CA in
`swarm.ca.stateDir`, an external ACME setup, or a certificate delivered
by any other means. If you have arranged one, the warning is expected
@ -89,16 +89,16 @@ host:
scp /var/lib/swarm-ca/root.pem <peer-host>:/var/lib/swarm-ca/root.pem
```
That is the point of the hierarchy: the root is installed **once per
that's the point of the hierarchy: the root is installed **once per
swarm host**, not once per peer, so a hive joining later needs no edit on
the hives already running. A hive whose cert already chains to a public
CA has nothing to install — `peer-config` says so instead.
Handing a peer this hive's own `ca.pem` would not work even as a
one-off: once a hive CA is an intermediate under the swarm root, it is
no longer something a verifier can build a chain _to_ — OpenSSL will not
one-off: once a hive CA is an intermediate under the swarm root, it's
no longer something a verifier can build a chain _to_ — OpenSSL won't
terminate a chain at a trusted non-self-signed certificate without
`-partial_chain`. That is why the tool distributes the root, not a
`-partial_chain`. that's why the tool distributes the root, not a
per-hive file.
## Adopting the hierarchy on an existing hive
@ -107,23 +107,23 @@ A hive that predates the swarm root carries a self-signed `ca.pem`, and
adopting the hierarchy means replacing it. That invalidates an anchor
consumers already trust, and they refresh on their own schedule — agents
only pick up new trust when their container restarts, peers only on
their own rebuild. **Who is allowed to decide that is what splits the
their own rebuild. **Who is allowed to decide that's what splits the
two cases.**
### Where this host owns the root (`autoConfigure`)
Adoption happens by itself, once. `hive-tls-ca.service` notices that
`ca.pem` does not chain to the root, keeps the old certificate as
`ca.pem` doesn't chain to the root, keeps the old certificate as
`ca-previous.pem`, and re-issues under the root; the next leaf is signed
by the new CA.
It is safe to automate here precisely because this is the
it's safe to automate here precisely because this is the
all-on-one-host shape: every consumer is on this box, so "when will they
have refreshed" is knowable rather than guessed.
The old CA stays in `trust-bundle.pem` afterwards, so adoption is
**additive to the anchor set before it is subtractive** — a container
that has not restarted yet still validates. Removing `ca-previous.pem`
**additive to the anchor set before it's subtractive** — a container
that hasn't restarted yet still validates. Removing `ca-previous.pem`
is a deliberate later step: how long is long enough is a property of the
deployment, not something the unit can know.
@ -141,15 +141,15 @@ rm <deploy.hive-controller.tls.stateDir>/ca.pem <deploy.hive-controller.tls.stat
systemctl restart hive-tls-ca.service
```
Failing rather than warning is deliberate: a hive whose CA does not
Failing rather than warning is deliberate: a hive whose CA doesn't
chain to the root it has been given is misconfigured, and a warning in a
build log is not something anyone reads twice.
build log isn't something anyone reads twice.
To keep the current CA on purpose — a hive that deliberately stays
outside the hierarchy, or one mid-migration — `touch` the marker file
named in the message. That is a decision, and it is recorded as one.
named in the message. that's a decision, and it's recorded as one.
A hive with **no** root configured at all is not affected by any of
A hive with **no** root configured at all isn't affected by any of
this: it self-signs exactly as it always has.
## Distributing the root
@ -159,11 +159,11 @@ The root **key** is a runtime file for the reason in
The root **certificate** is a runtime file as a _consequence_ — it lives
beside the key under `swarm.ca.stateDir` — and that has a cost worth
naming, because it is subtle and it bites at a distance:
naming, because it's subtle and it bites at a distance:
> **Nothing whose trust store is assembled at build time can reference
> the swarm root.** `security.pki.certificateFiles` is read inside the
> derivation; the root does not exist there.
> derivation; the root doesn't exist there.
Two consumers, and only one of them is fine:

View file

@ -19,9 +19,9 @@ so the store is a perfectly good place for it.
**The generator and the reader are usually in different containers.** They share
the host's network namespace, which makes them feel co-located, but their
filesystem roots are separate. That is why delivery is a **host-side copy rather
filesystem roots are separate. that's why delivery is a **host-side copy rather
than a bind mount**: `nixos-container` refuses to start when a bind source is
missing, and a secret minted on another container's first boot does not exist
missing, and a secret minted on another container's first boot doesn't exist
yet. Binding it would make one container wait on a file that waits on a
container that starts after it.
@ -30,20 +30,20 @@ container that starts after it.
Every row below is read against one of these. This is a different cut
from the [deployment shapes](services.md#deployment-shapes) --- those
say _where services run_, these say _who is responsible for a secret
file being there_ --- so the two lists do not line up one-to-one, and
file being there_ --- so the two lists don't line up one-to-one, and
neither is a renaming of the other.
| topology | what it means | who places secrets |
| ------------------ | ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **all-local** | one host runs the swarm's shared services and its own hive | nobody — each secret is generated where it is read, or copied by a host unit |
| **swarm-managed** | the swarm's services run on a host with `swarmctl` | `swarmctl` writes what it owns; the rest is still generated in place |
| **hive elsewhere** | a hive that federates with a swarm it does not host | the operator provides the file and names it in config |
| topology | what it means | who places secrets |
| ------------------ | ---------------------------------------------------------- | --------------------------------------------------------------------------- |
| **all-local** | one host runs the swarm's shared services and its own hive | nobody — each secret is generated where it's read, or copied by a host unit |
| **swarm-managed** | the swarm's services run on a host with `swarmctl` | `swarmctl` writes what it owns; the rest is still generated in place |
| **hive elsewhere** | a hive that federates with a swarm it doesn't host | the operator provides the file and names it in config |
## Swarm-level — one of each per swarm
| secret | generated by | lives at | hive elsewhere |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| swarm root CA cert | `swarm-ca.nix` first-boot unit, when `autoConfigure` is set | `/var/lib/swarm-ca/root.pem` | operator copies the **cert** in; it is public |
| swarm root CA cert | `swarm-ca.nix` first-boot unit, when `autoConfigure` is set | `/var/lib/swarm-ca/root.pem` | operator copies the **cert** in; it's public |
| swarm root CA key | same unit | `/var/lib/swarm-ca/root-key.pem`, `0600` | stays on whichever host holds it — see the constraint below |
| swarm-services sub-CA (cert + key) | `swarm-ca.nix`, signed by the root | `/var/lib/swarm-ca/services-ca{,-key}.pem` | issued where the root lives |
| authelia session, JWT and storage-encryption keys | authelia's first-boot unit, in-container | `/var/lib/authelia-swarm/{session,jwt,storage-encryption}.key` | generated in place; nothing outside that container reads them |
@ -56,31 +56,31 @@ neither is a renaming of the other.
| wireguard private key | **the operator**`wg genkey` | whatever `deploy.wireguard.privateKeyFile` names | always operator-provided; nothing generates this for you |
| queue auth-callout nkeys (user seed + account seed) | `swarm-nats-callout-keys` first-boot unit, when `deploy.nats.autoGenerateCallout` is set | `/var/lib/swarm-nats-callout/{callout-user,issuer}.seed`, `0600` | operator mints both with `nk` and names them in `deploy.nats.calloutUserSeedFile` / `deploy.nats.calloutIssuerSeedFile` |
| the secret store's own contents | openbao, on first `bao operator init`**an operator action, not a unit** | inside the `swarm-bao` container, at its own `/var/lib/openbao`, kept across rebuilds by `ephemeral = false`. ⚠️ **Not a host path**: `nixos-container destroy swarm-bao` takes the raft data with it, so back up the container's tree, not `/var/lib/`. Only the store's TLS material (`/var/lib/swarm-bao-tls`) and its PKCS11 token (`/var/lib/swarm-bao-token`) are host-level | n/a — there is one store; a hive elsewhere is a _client_ of it and holds none of this |
| the secret store's unseal material | the HSM/TPM under `deploy.bao.seal = "pkcs11"`; openbao itself under `"shamir"` | in the token; or held by whoever ran `bao operator init`, which is what `"shamir"` means and why it is stated rather than inferred | n/a — only the host running the store seals anything |
| the secret store's unseal material | the HSM/TPM under `deploy.bao.seal = "pkcs11"`; openbao itself under `"shamir"` | in the token; or held by whoever ran `bao operator init`, which is what `"shamir"` means and why it's stated rather than inferred | n/a — only the host running the store seals anything |
The three keys authelia mints for itself are generated in-container precisely
because nothing outside that container ever reads them. **That is the test worth
because nothing outside that container ever reads them. **that's the test worth
applying to any secret added here** — and the client secret's plaintext half is
the one row that fails it, which is the entire reason a delivery step exists.
There are two telemetry collectors and they land on opposite sides of that test.
The **hive's** collector needs no delivery step. It authenticates to the swarm's
collector as its own hive, and it is a host unit rather than a container, so on
collector as its own hive, and it's a host unit rather than a container, so on
an all-local swarm it reads authelia's file where it lies and no second copy is
made. On any other topology it is an ordinary "operator provides the file"
made. On any other topology it's an ordinary "operator provides the file"
case — see `services.hyperhive.otel.clientSecretFile`.
The **swarm's** collector does need one. It runs in a container, so its copy is
placed by `swarm-otel-oidc-secret.service` and lands at
`/var/lib/swarm-otel-oidc/<client-id>.secret` — the same shape as the forge and
homeserver rows below, and for the same reason: the container that mints the
secret is not the container that reads it.
secret isn't the container that reads it.
The copy is only made when authelia is enabled **on this host** and something
published is being scraped; otherwise no secret is needed and none is placed.
⚠️ **Do not read that delivery unit as the only way this collector is fed.**
⚠️ **don't read that delivery unit as the only way this collector is fed.**
Whether it authenticates follows the _credential_, never another service's
placement: a swarm collector may run on a host that holds neither store and no
authelia, and then the secret is an ordinary operator-provided file named in
@ -91,7 +91,7 @@ not the definition of the case.
### Minting the queue's callout nkeys
`deploy.nats.autoGenerateCallout` mints both keypairs on the host before the queue
starts. It is on by default only under `singleHostSwarm` — the one
starts. It's on by default only under `singleHostSwarm` — the one
topology where the queue, its responder and the operator are the same person. On
every other topology, mint them yourself:
@ -108,12 +108,12 @@ admit anyone to the queue, so it belongs wherever the responder runs and nowhere
else.
A hive that sets neither the public keys nor `autoGenerateCallout` fails at
eval, naming the option it wants. That is deliberate: a queue that started
eval, naming the option it wants. that's deliberate: a queue that started
without them would accept `CONNECT {"user":"auth"}` from anyone sharing the
host's network namespace, and nothing would look wrong until somebody connected.
**All four or none** — the seed paths are required too, not just the public
keys. They are two halves of the same pair: the server verifies with the public
keys. they're two halves of the same pair: the server verifies with the public
half, the responder signs with the private one. Supplying only the public keys
used to pass eval and leave the queue with an auth-callout nobody answers, which
refuses every client rather than degrading — and a refusal reaches the client as
@ -139,8 +139,8 @@ bounded wait, 120s — and then **fail loudly** rather than skipping. A silent s
produces a service whose login button always fails, which is a symptom several
layers from its cause.
The store's **first reader** is the matrix registration token, and it is worth
saying why that one: it is an opaque 32-byte value with no second file and no
The store's **first reader** is the matrix registration token, and it's worth
saying why that one: it's an opaque 32-byte value with no second file and no
format. Authelia's OIDC secret needs a `.secret` _and_ a matching `.digest`, so
starting there would have meant debugging "can a reader authenticate and get
bytes back" and "did we write authelia's file format right" at once, with an
@ -153,8 +153,8 @@ locally minted token in place, so a hive with no store behaves exactly as it
did before.
⚠️ **Service↔store mTLS is its own trust domain.** A credential you must
already hold to authenticate cannot be fetched from the thing it authenticates
you to, so the store's identity cannot come from an authority the store
already hold to authenticate can't be fetched from the thing it authenticates
you to, so the store's identity can't come from an authority the store
distributes — which excludes the hive CA and the swarm CA both, and has nothing
to do with the gateway's HTTPS certificates either way. `glue-bao-tls.nix`
mints a CA that signs exactly two things, the store's server certificate and a
@ -163,14 +163,14 @@ internal CA deletes that file and names its own paths in
`deploy.bao.serverCertFile` / `clientCaFile`; the store itself has no opinion.
A hive that reads from a store on **another** machine names the reader's half —
`clientCertFile`, `clientKeyFile`, `serverCaFile` — and places that leaf by hand.
It is the one credential that cannot come out of the store, being what opens it;
it's the one credential that can't come out of the store, being what opens it;
everything else a hive needs does.
## The constraint that decides where the root lives
A hive CA carries `nameConstraints=permitted;DNS:<hive domain>`, and **a swarm
service name is a sibling of the hive domain rather than a child** — `forge.<swarm>`
next to `<hive>.<swarm>`. So a hive CA cannot issue a certificate for a swarm
next to `<hive>.<swarm>`. So a hive CA can't issue a certificate for a swarm
service. Not by policy: by construction, and openssl enforces it.
Whatever holds the swarm root is therefore what makes swarm-service certificates
@ -188,4 +188,4 @@ possible at all. Two things follow:
State three things, in the row you add above: **who mints it**, **which
container reads it**, and **what happens when they differ**. If they differ, it
needs a delivery unit, and the unit copies — it does not bind.
needs a delivery unit, and the unit copies — it doesn't bind.

View file

@ -10,9 +10,9 @@ services.hyperhive.deploy.allSwarmServices = true;
```
**`deploy.allSwarmServices` is what "the swarm's shared services run
here" means: every once-per-swarm service that is _optional_ takes its
`enable` from it.** That is the whole rule, stated once — the per-service
sections below do not repeat it, so a service that stops deriving is a
here" means: every once-per-swarm service that's _optional_ takes its
`enable` from it.** that's the whole rule, stated once — the per-service
sections below don't repeat it, so a service that stops deriving is a
visible difference rather than one more paragraph saying the same thing.
`singleHostSwarm` is the all-on-one-box switch above it: it defaults
@ -20,13 +20,13 @@ both `deploy.allSwarmServices` and `swarm.ca.autoConfigure` (the swarm CA
is generated here). Each derived toggle can still be set on its own,
which wins, so "all local except X" needs no further option.
**Both default to off**, and that is deliberate: a host cannot tell
whether it is meant to be the swarm's service host, so this is an
**Both default to off**, and that's deliberate: a host can't tell
whether it's meant to be the swarm's service host, so this is an
operator saying so rather than something inferred. With them off, a hive
is a _client_ of those services — it configures how to reach them and
runs none of them.
The forge is the exception, and not because it is per-hive: it is
The forge is the exception, and not because it's per-hive: it's
swarm-wide but **not optional**, being the canonical store for the meta
flake and every agent's config repo, so it deploys with hyperhive itself
and has no `enable` to derive from anything.
@ -82,7 +82,7 @@ starts with nobody in it rather than failing to start — a provider with
no subjects yet is the correct state before anything has provisioned
them. Session and storage keys are generated in the container on first
boot and never rotated automatically; replacing one invalidates data
already written (sessions, the encrypted store), so that is an operator
already written (sessions, the encrypted store), so that's an operator
action.
Storage is local sqlite and the notifier writes to a file. Both are
@ -112,11 +112,11 @@ services.hyperhive.deploy.grafana.enable = false;
⚠️ **This starts a database that grows for as long as the swarm runs.**
See `retentionPeriod` below before leaving it at its default.
| Option | When you'd touch it |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deploy.victoriametrics.retentionPeriod` | Default `5y`. Lower it once you have measured how fast this swarm actually fills a disk — the default is deliberately generous because too-short silently discards history you cannot get back. |
| `swarm.grafana.oidc.role` | Default `Admin` for everyone who logs in. Lower to `Viewer`/`Editor` if the swarm grows operators who should not be able to reconfigure Grafana. |
| `deploy.grafana.datasourceUrl` | Only if you front VictoriaMetrics with something else. It defaults to the store on this host, which is the only thing it can reach. |
| Option | When you'd touch it |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deploy.victoriametrics.retentionPeriod` | Default `5y`. Lower it once you have measured how fast this swarm actually fills a disk — the default is deliberately generous because too-short silently discards history you can't get back. |
| `swarm.grafana.oidc.role` | Default `Admin` for everyone who logs in. Lower to `Viewer`/`Editor` if the swarm grows operators who shouldn't be able to reconfigure Grafana. |
| `deploy.grafana.datasourceUrl` | Only if you front VictoriaMetrics with something else. It defaults to the store on this host, which is the only thing it can reach. |
**Logging in.** Grafana is behind swarm SSO, so the accounts are the
authelia ones — there is no separate Grafana password, and the local
@ -138,7 +138,7 @@ the hive's daemons and infra containers, and the harness units inside every
agent container. The collector below is what writes to it.
**Reading them.** Open Grafana, pick **Explore**, and choose the
`VictoriaLogs` datasource — it is provisioned for you. Grafana's _Logs
`VictoriaLogs` datasource — it's provisioned for you. Grafana's _Logs
Drilldown_ app is deliberately not installed: it only supports Loki, and
no setting here changes that, so Explore is the log browser for this
swarm.
@ -170,7 +170,7 @@ Every hive's own collector reaches this one by its gateway name,
`swarm.otel.domain` (default `otel.<swarm domain>`) — the same
by-domain-through-the-gateway shape every other swarm service uses, not a
loopback URL an operator has to redirect. There is nothing to set on a hive
that does not run the swarm's services; the name resolves through the
that doesn't run the swarm's services; the name resolves through the
gateway either way.
| Option | When you'd touch it |

View file

@ -1,6 +1,6 @@
# Swarm SSO
The swarm runs one authelia, and it is two things at once: the **session
The swarm runs one authelia, and it's two things at once: the **session
provider** every protected vhost checks (`auth_request`), and — once any
client is declared — an **OIDC provider** issuing tokens to relying
parties: the forge and the matrix homeserver.
@ -18,7 +18,7 @@ name all follow `deploy.authelia`, so there is nothing to turn on
separately. (Details, including why a client hive must not declare that
vhost: [`../networking/gateway.md`](../networking/gateway.md).)
**Authelia does not start until at least one user exists.** The user
**Authelia doesn't start until at least one user exists.** The user
store is generated empty — deliberately, since seeding a default account
would put a credential in a config file — but authelia validates it at
startup and treats "no users" as fatal:
@ -31,7 +31,7 @@ error reading the authentication database: could not validate the schema:
It then exits 1 and systemd restarts it, so a swarm that has been
enabled but not bootstrapped shows a **crash-looping unit** and `502 Bad
Gateway` from the vhost — not a login page with nobody able to use it.
The gateway is working in that state; the upstream is not up.
The gateway is working in that state; the upstream isn't up.
⚠️ So the step below is **required to finish the install**, not an
optional first-login convenience. Run it before concluding anything is
@ -48,14 +48,14 @@ this password is stored nowhere — record it now
```
The password is generated, hashed, and printed once; only the hash is
kept. `swarmctl` reads and writes authelia's `users.yml` directly — it is
kept. `swarmctl` reads and writes authelia's `users.yml` directly — it's
the one user store, shared with `swarm-authelia-bridge`, which creates
agent identities in the same file. No restart: authelia watches it. Full
reference: [`../tools/swarmctl-cli.md`](../tools/swarmctl-cli.md).
You can edit `users.yml` by hand, and `swarmctl` will read what you
wrote. ⚠️ It rewrites the whole file on every change, so **comments and
formatting do not survive**; values and unrecognised keys do.
formatting don't survive**; values and unrecognised keys do.
This step stays manual on purpose. Bootstrapping an identity provider
non-interactively means a secret arriving from somewhere — a file, an
@ -78,12 +78,12 @@ mara is now in groups: admins
Two behaviours worth knowing before you rely on them:
- **`--remove-group` fails if the user is not in that group.** Every
other flag is idempotent — setting what is already set is fine, so a
"make these four things true" call does not break when one of them
- **`--remove-group` fails if the user isn't in that group.** Every
other flag is idempotent — setting what's already set is fine, so a
"make these four things true" call doesn't break when one of them
already was. Revocation is the exception on purpose: a typo'd group
name that reported success would leave an account holding access you
believe you took away, and that is the one outcome nobody re-checks.
believe you took away, and that's the one outcome nobody re-checks.
- **The resulting group list is printed** because group names have no
registry anywhere. A misspelled `--add-group` creates a real group that
no access-control rule mentions, so the user gains nothing and no error
@ -105,7 +105,7 @@ the delivery step below and the rest of authelia's keys don't.
## Getting the plaintext to the relying party
Three cases, and they are genuinely different mechanisms rather than one
Three cases, and they're genuinely different mechanisms rather than one
mechanism with flags.
### 1. All-local — one host runs both
@ -116,12 +116,12 @@ service's container, and the service's own module contributes its client
entry — callback URL included — to authelia's client list.
The callback is built once and read twice, so the redirect URI authelia is
told to allow and the one the service actually sends cannot drift apart. A
told to allow and the one the service actually sends can't drift apart. A
mismatch there is a rejected login with no error text worth reading.
⚠️ The delivery is a copy, not a `bindMounts` entry, and deliberately so:
nixos-container refuses to start a container whose bind source is
missing, and this secret does not exist until authelia's first boot has
missing, and this secret doesn't exist until authelia's first boot has
run. Binding it would make the service wait on a file that waits on a
container that starts after it — on a fresh hive, a permanent stall
presenting as "the forge is broken", several layers from its cause.
@ -155,7 +155,7 @@ evaluation error.
## Where each relying party differs
The registration half is identical; what each service does with the
result is not.
result isn't.
| | forge | matrix |
| ------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
@ -175,7 +175,7 @@ Two consequences worth stating plainly:
spoken by non-browser clients holding matrix access tokens — every
agent's own daemon — as well as by federation, so `/_matrix/` is served
directly and authenticates itself. The forward-auth vhosts protect
browser surfaces; this is not one of them.
browser surfaces; this isn't one of them.
## Machine clients
@ -198,7 +198,7 @@ valid token refused at the target, which reads like a broken credential
rather than a broken name.
**Tokens are signed (`RS256`), not opaque**, because a resource server
that cannot call the provider back is a real case here: the telemetry
that can't call the provider back is a real case here: the telemetry
collector verifies offline against `/jwks.json`, and an opaque token gives
it nothing to verify. The queue's responder introspects instead — a
different question asked of the same token, and the reason both
@ -240,13 +240,13 @@ All three matter. A change that silently deleted the browser page would
pass a deny-only check, and one that quietly stopped denying would pass a
page-only check. This was verified that way when the split was introduced.
## What this does not do
## What this doesn't do
- **It does not disable local login.** Each service keeps its password
- **It doesn't disable local login.** Each service keeps its password
database and gains a second door. An identity provider that can take a
service offline when it hiccups is worse than one with two ways in.
Making authelia the only path is a separate, reversible switch per
service (tuwunel's `login_with_password`, forgejo's own setting).
- **It does not provision users.** Agents are created and destroyed
- **It doesn't provision users.** Agents are created and destroyed
continuously, so the subject set belongs to a program rather than to a
config file; today that program is `swarmctl`.

View file

@ -23,7 +23,7 @@ reader and to a certificate — so there is no separate option to pin.
> The apex must differ from `services.hyperhive.domain`. The gateway's
> default server already answers for the hive domain, and two vhosts
> claiming one `server_name` do not error — nginx picks one — so this is
> claiming one `server_name` don't error — nginx picks one — so this is
> an assertion rather than a runtime surprise.
## 🔑 You must be in the `admins` group
@ -57,8 +57,8 @@ between "an operator's page" and "anyone with a session".
## What it costs to be reachable
The apex is published to the hive's resolver like every other swarm
service, so agent containers can resolve it. That is deliberate and it is
not a hole: **reachability is not the access control here.** An agent
service, so agent containers can resolve it. that's deliberate and it's
not a hole: **reachability isn't the access control here.** An agent
that resolves the name and connects still has no operator session, and
the subrequest denies it.