kanidm: the effector ports off lldap — service accounts, one api-token, no root #7

Merged
larandar merged 4 commits from tango/5-kanidm-effector into fastlane 2026-08-12 03:32:35 +00:00
Collaborator

Ports Vedanta's effector off the lldap this fleet no longer runs, onto kanidm — and settles as what Vedanta writes.

The ruling

Vedanta holds its own kanidm identity: service account sa-vedanta, one read-write api-token, delivered through DOTENV_FILE. The alternative was the operator path the off-repo spike uses — ssh as root, kanidmd recover-account idm_admin — refused (owner, 2026-08-11) because recovering idm_admin at will is admin, it forces this unit to run as root and discard module.nix's hardening, and a token's rights can be read out of the directory where root's cannot. Rights are granted at bootstrap, never in the flake; a privileged helper behind Vedanta was refused too — Vedanta is the privileged component.

Probed before written

Against kanidm 1.11 on heimdallr, the standard the lldap effector set. None of this is in the docs:

  • kanidm refuses + in a name (400 schemaviolation) but accepts it in mail — so a+/sa+ stays a JWT sub contract, the entity is a-freki, the plus-addressed mailbox survives, and the two are joined by UUID. This is why the spike writes sa-<persona>: a workaround nobody had recorded as one.
  • A person cannot hold an api-token (MissingClass("service_account")) — the service-account choice is forced, not preferred.
  • A -w token in idm_service_account_admins creates accounts and writes group members. The premise holds.

What is here

  • src/kanidm.rs replaces src/lldap.rs (deleted) — name + displayname + mail in one POST, read back the UUID, join the roster group. GET /v1/self for reconcile, which now names the SPN it was green as.
  • The gate is untouched: mint() still routes through Steward::evaluate. Partial success is reported, not rolled back — a failed group join leaves the account, logs the UUID, and says so. Nothing grows a revoke path.
  • ldap3 out; reqwest gains TLS (feature rustls), aimed at the public origin rather than kanidm's self-signed loopback.
  • module.nix: lldap.urlkanidm.url + kanidm.group, hardening untouched. justfile: the gigachad recipe stops reading a sops path that was deleted at flake-ops#51 phase 5.
  • AGENTS.md and CONTEXT.md enter the tree — they were untracked through the whole port, invisible to nix build and to reviewers — carrying two governance rulings (ticket #6) and the hazard section rewritten as a landed port.

just check green (3 tests, up from 1); nix build .#default green.

Reviewer's attention, please

  1. directory_name() diverges from the spike deliberately. It is +- and nothing else, so a+frekia-freki. mint.nu names every persona sa-<persona>, which collapses the hand/service distinction the prefix law exists to draw. If that flattening was intentional upstream, this is the line to argue with.
  2. Nothing has minted end to end. sa-vedanta does not exist yet — blocked on lar.ad/flake-ops#98, and the roster group is separately broken (lar.ad/flake-ops#99: undeployed, and overwriteMembers will wipe it once it lands).
  3. Three commits, one concern each; the third is a one-line .gitignore fix for operator files AGENTS.md always claimed were ignored but never were.

Closes #5
Closes #6

Ports Vedanta's effector off the lldap this fleet no longer runs, onto kanidm — and settles *as what* Vedanta writes. ## The ruling Vedanta holds its **own kanidm identity**: service account `sa-vedanta`, one read-write api-token, delivered through `DOTENV_FILE`. The alternative was the operator path the off-repo spike uses — ssh as root, `kanidmd recover-account idm_admin` — refused (owner, 2026-08-11) because recovering idm_admin at will *is* admin, it forces this unit to run as root and discard `module.nix`'s hardening, and a token's rights can be read out of the directory where root's cannot. Rights are granted at bootstrap, **never in the flake**; a privileged helper behind Vedanta was refused too — Vedanta *is* the privileged component. ## Probed before written Against kanidm 1.11 on heimdallr, the standard the lldap effector set. None of this is in the docs: - kanidm **refuses `+` in a `name`** (`400 schemaviolation`) but **accepts it in `mail`** — so `a+`/`sa+` stays a JWT `sub` contract, the entity is `a-freki`, the plus-addressed mailbox survives, and the two are joined by **UUID**. This is why the spike writes `sa-<persona>`: a workaround nobody had recorded as one. - **A person cannot hold an api-token** (`MissingClass("service_account")`) — the service-account choice is forced, not preferred. - A `-w` token in `idm_service_account_admins` creates accounts and writes group members. The premise holds. ## What is here - `src/kanidm.rs` replaces `src/lldap.rs` (deleted) — name + displayname + mail in one `POST`, read back the UUID, join the roster group. `GET /v1/self` for `reconcile`, which now names the SPN it was green as. - The gate is untouched: `mint()` still routes through `Steward::evaluate`. **Partial success is reported, not rolled back** — a failed group join leaves the account, logs the UUID, and says so. Nothing grows a revoke path. - `ldap3` out; `reqwest` gains TLS (feature `rustls`), aimed at the public origin rather than kanidm's self-signed loopback. - `module.nix`: `lldap.url` → `kanidm.url` + `kanidm.group`, hardening untouched. `justfile`: the gigachad recipe stops reading a sops path that was deleted at `flake-ops#51` phase 5. - `AGENTS.md` and `CONTEXT.md` enter the tree — they were untracked through the whole port, invisible to `nix build` and to reviewers — carrying two governance rulings (ticket #6) and the hazard section rewritten as a landed port. `just check` green (3 tests, up from 1); `nix build .#default` green. ## Reviewer's attention, please 1. **`directory_name()` diverges from the spike deliberately.** It is `+` → `-` and nothing else, so `a+freki` → `a-freki`. `mint.nu` names *every* persona `sa-<persona>`, which collapses the hand/service distinction the prefix law exists to draw. If that flattening was intentional upstream, this is the line to argue with. 2. **Nothing has minted end to end.** `sa-vedanta` does not exist yet — blocked on `lar.ad/flake-ops#98`, and the roster group is separately broken (`lar.ad/flake-ops#99`: undeployed, *and* `overwriteMembers` will wipe it once it lands). 3. Three commits, one concern each; the third is a one-line `.gitignore` fix for operator files AGENTS.md always claimed were ignored but never were. Closes #5 Closes #6
lldap left the fleet at flake-ops#51 on 2026-08-10; its sops keys went at
phase 5 the day after. Every directory line here addressed it, so the
gigachad recipe was not stale but broken — it read a deleted secret path.

Vedanta now holds its OWN kanidm identity: service account `sa-vedanta`,
one read-write api-token, delivered through DOTENV_FILE. The alternative
was the operator path the off-repo spike uses — ssh as root, `kanidmd
recover-account idm_admin` — and it was refused (owner, 2026-08-11) for
reasons worth not re-arguing: recovering idm_admin at will IS admin, so it
is not the narrow grant it looks like; it forces this unit to run as root
and discard module.nix's NoNewPrivileges and ProtectSystem=strict; and a
token's rights can be read out of the directory, where root's cannot. The
rights themselves are granted at bootstrap, never in the flake (owner: "no
admin in the flake"), by lar.ad/flake-ops#98. A privileged helper standing
behind Vedanta was refused too: Vedanta IS the privileged component.

Every call was made against the live instance before it was written, the
standard the lldap effector set. What that bought, none of it in the docs:

- kanidm refuses `+` in a NAME (400 schemaviolation) but accepts it in
  MAIL. So a+/sa+ stays a JWT `sub` contract, the entity is a-freki, the
  plus-addressed mailbox survives where it is load-bearing, and the two
  are joined by UUID. This is why the spike writes `sa-<persona>` — a
  workaround nobody had recorded as one.
- A person cannot hold an api-token (MissingClass("service_account")), so
  the service-account choice flake-ops#72 ruled is forced, not preferred.
- A `-w` token in idm_service_account_admins creates accounts and writes
  group members. That premise is true, which is the whole reason this
  shape works.

The gate is untouched: mint() still routes through Steward::evaluate and
only then effects. Partial success is reported, not rolled back — if the
roster group join fails the account exists and the error says so, with the
uuid already logged. Nothing here grows a revoke path.

reqwest gains TLS (feature `rustls`, not the pre-0.13 `rustls-tls`
spelling), aimed at the public origin rather than kanidm's self-signed
loopback, so no client here is taught to accept an unverified peer.

Refs Ting/Vedanta#5, lar.ad/flake-ops#51, #72, #97, #98
Both files existed untracked through the whole kanidm port, which meant
`nix build` could not see them and neither could a reviewer. They are
rules and state, not project bookkeeping — the board stays the place work
is tracked (AGENTS.md says so itself).

Two rulings from the session that ported the effector (Ting/Vedanta#6):

- UNSUPERVISED WORK NEEDS A TICKET. An agent working without the
  freeholder present has no one to answer a scope question, so the ticket
  IS the scope. Supervised work does not: whether a session runs HITL is
  the freeholder's call, and a freeholder in the loop is a better scope
  authority than any issue text. This commit and its ticket are that
  clause being used — both written after the work.
- ONE PR PER SESSION, NOT PER TICKET. The session is the delivery unit;
  the old "scoped to one ticket" pulled toward splitting a single story
  across PRs. Branches still name the primary ticket; the rest are named
  in the PR body.

CONTEXT.md's largest hazard section — "the directory moved under us" —
becomes a landed port, carrying the five findings the live instance forced
and the two that are still flake-ops' to fix (lar.ad/flake-ops#99: the
`agents` group is undeployed, and overwriteMembers will wipe it once it
lands). The old "bind account is an acknowledged over-grant" open question
becomes the honest one: sa-vedanta's rights are sufficient, not minimal.

Refs Ting/Vedanta#5, #6, lar.ad/flake-ops#99
AGENTS.md has always said `.doctor.just` and `.fastlane.just` are
gitignored, self-distributing copies owned by Ting/Jostoph — but this
repo's .gitignore never listed them, so `just -f .doctor.just wire` left
them staged and a routine `git add -A` would have committed both. One is
overwritten from its canonical URL by `doctor::sync`; the other's mere
presence is the development-seam toggle. Neither is this repo's to carry.

Refs Ting/Vedanta#5
`/run/secrets/vedanta/kanidm_token` is not created by deploying — sops-nix
materializes DECLARED secrets and nothing else, and flake-ops declares no
`vedanta/*` yet (lar.ad/flake-ops#91). This recipe exists precisely to run
code in production BEFORE that lands, so it now falls back to the sops file
the token was minted into (lar.ad/flake-ops#98) and uses the host path when
something finally does declare one.

The credential goes over STDIN, never the command line: `ssh host
"TOKEN=abc vedanta"` puts it in heimdallr's process table for the life of
the call, readable by anyone on the box.

And a missing credential now fails at 78 (EX_CONFIG) instead of limping.
Observed for real: `VEDANTA_KANIDM_TOKEN="$(cat missing)"` does not stop a
shell — `cat` writes to stderr, the substitution yields an EMPTY string,
and kanidm answers `401 notauthenticated`, which reads as "your token is
wrong" when the truth is "you have no token".

Verified end to end against the live directory:

    INFO vedanta: reconcile: kanidm reachable, no reconciliation rules
    defined yet — no-op [email protected]

Refs Ting/Vedanta#5, lar.ad/flake-ops#91, #98
Author
Collaborator

Verified in production, 2026-08-11

sa-vedanta was minted by the freeholder (lar.ad/flake-ops#98) and this branch now runs green against the real directory:

INFO vedanta: reconcile: kanidm reachable, no reconciliation rules defined yet — no-op
              [email protected]

That is the packaged binary, copied to heimdallr, authenticating as the minted service account — and reconcile naming who it was green as, which the lldap bind it replaces could never report.

Added one commit (5003add) off the back of the first live run. Two findings:

  1. /run/secrets/vedanta/kanidm_token is not created by deploying. sops-nix materializes declared secrets and nothing else, and flake-ops declares no vedanta/* — that is #91, still untouched. So the recipe falls back to the sops file the token was minted into, and uses the host path once something declares one. The credential travels over stdin, never the command line, so it never appears in heimdallr's process table.
  2. An absent credential was becoming an empty one. VEDANTA_KANIDM_TOKEN="$(cat missing)" does not stop a shell: cat writes to stderr, the substitution yields "", and kanidm answers 401 notauthenticated — which reads as "your token is wrong" when the truth is "you have no token". Both paths now refuse at 78 (EX_CONFIG) and name the file they wanted.

Still true: no persona has been minted end to end, because #99 (agents undeployed, and overwriteMembers will wipe it) is unfixed.

## Verified in production, 2026-08-11 `sa-vedanta` was minted by the freeholder (lar.ad/flake-ops#98) and this branch now runs green against the real directory: ``` INFO vedanta: reconcile: kanidm reachable, no reconciliation rules defined yet — no-op [email protected] ``` That is the packaged binary, copied to heimdallr, authenticating as the minted service account — and `reconcile` naming *who* it was green as, which the lldap bind it replaces could never report. Added one commit (`5003add`) off the back of the first live run. Two findings: 1. **`/run/secrets/vedanta/kanidm_token` is not created by deploying.** sops-nix materializes *declared* secrets and nothing else, and flake-ops declares no `vedanta/*` — that is #91, still untouched. So the recipe falls back to the sops file the token was minted into, and uses the host path once something declares one. The credential travels over **stdin, never the command line**, so it never appears in heimdallr's process table. 2. **An absent credential was becoming an empty one.** `VEDANTA_KANIDM_TOKEN="$(cat missing)"` does not stop a shell: `cat` writes to stderr, the substitution yields `""`, and kanidm answers `401 notauthenticated` — which reads as *"your token is wrong"* when the truth is *"you have no token"*. Both paths now refuse at `78` (`EX_CONFIG`) and name the file they wanted. Still true: no persona has been minted end to end, because #99 (`agents` undeployed, and `overwriteMembers` will wipe it) is unfixed.
larandar approved these changes 2026-08-12 03:32:25 +00:00
larandar merged commit 5003addb59 into fastlane 2026-08-12 03:32:35 +00:00
larandar deleted branch tango/5-kanidm-effector 2026-08-12 03:32:35 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Ting/Vedanta!7
No description provided.