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:
parent
77296aff35
commit
78021ce982
38 changed files with 320 additions and 320 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue