machine-grant design r5 (#436): duckworth-0060 B1 enrollment recheck and key, B2 consume then claim, N1-N11, bus.tester dev mark (mos-1139)
This commit is contained in:
@@ -1,15 +1,43 @@
|
||||
# Machine grant path, design draft r4 (Vikunja #436, row 210 follow-up)
|
||||
# Machine grant path, design draft r5 (Vikunja #436, row 210 follow-up)
|
||||
|
||||
Owner: Gizmoduck. Reviewer: Duckworth. Sizing and row assignment: Sage. Design only. Code starts when
|
||||
Duckworth approves the design and row 210 lands (sage-1527).
|
||||
|
||||
Sources: Q188 (DECISIONS.md:1642, jason-master-restart 8ecf252), mos-1115, mos-1120, mos-1125, mos-1127, mos-1131, mos-1133,
|
||||
sage-1527, Duckworth's r1 review (REVIEW-r1.md, duckworth/reviews d025ef492, corrected at 749080115),
|
||||
Sources: Q188 (DECISIONS.md:1642, jason-master-restart 8ecf252), mos-1115, mos-1120, mos-1125, mos-1127, mos-1131, mos-1133, mos-1139,
|
||||
sage-1527, Duckworth's r1 review (REVIEW-r1.md, duckworth/reviews d025ef492, corrected at 749080115), Duckworth's
|
||||
r3 and r4 review (REVIEW-r3.md, duckworth/reviews 9dceb2148, duckworth-0060),
|
||||
Honker's PRD 1.8 draft (honker/row194-b2 3c30f1036, REQ-TRUST-2 and REQ-GRANT-1 to -4), row 210 DESIGN r3
|
||||
(e021dfa26) and its approved candidate 455964f23. Code cites are `path:line` at origin/refactor f41729178
|
||||
unless they say "210:", which means the row 210 candidate 455964f23 (the same code as the branch head
|
||||
a2fd30390). Host facts come from ops-01's read-only check of .53 and .54 at 22:21Z (mos-1127).
|
||||
|
||||
## r5 changes (from r4 1c5705d6d)
|
||||
|
||||
Duckworth's review of r3 and r4 (duckworth-0060) asked for changes, B1 and B2, with notes N1 to N11. mos-1139
|
||||
says to fix both and rules on N8.
|
||||
|
||||
- B1: E2 step 1 is now one transaction: `#consumeGrant`, a check that the host isn't enrolled yet, then the
|
||||
insert. A second approved enrollment for the same `hostId` ends `grant.failed host-enrolled`. `hosts` gets
|
||||
the primary key (business, host id) in `3i-3j.sql`. G17 and two mutants cover it.
|
||||
- B2: X2 consumes first and claims second, in one turn of the event loop. A busy claim ends `grant.failed
|
||||
host-busy` with a message. At start, a grant decision resolved yes with no `action.allowed` ends
|
||||
`grant.failed interrupted` and never runs late (X3). `hosts.revoke` records `grant.revoked` even with no
|
||||
reference and no claim, so X5's first bullet now matches the code. G11 and G23 cover it.
|
||||
- N1: the `ID_PATTERN` cite is marked `210:`.
|
||||
- N2: X5 states the build rule that `issue` and its `commit` stay synchronous, with a mutant.
|
||||
- N3, N4: `hosts.revoke` is its own verb in `bus/src/host-grants.mjs`, not 210's `credential-verbs.mjs`. It
|
||||
writes a `pending` marker first, and the start reconcile finishes a revoke a restart cut short (section 6,
|
||||
G27).
|
||||
- N5: the writer also refuses `disk`, `libvirt` and `incus-admin` (E3).
|
||||
- N6: a first `add` creates the file, and a leftover regular tmp is unlinked under the `flock` (E3, G25).
|
||||
- N7: the installer's line is written out exactly (E3).
|
||||
- N8: a dev instance is a business whose `bus.tester` var is exactly true (mos-1139, section 6).
|
||||
- N9: the row a run sheet strips row 210's `hosts` keys before the `--migrate` re-pin (S1).
|
||||
- N10: the writer list in the r3 changes now includes `hosts.check`. ops-01 reads only the three key types the
|
||||
keyscan asks for (E1).
|
||||
- N11: Duckworth withdrew a PRD note. Section 6 step 1's record delete stays.
|
||||
- Row b grows by the revoke verb and its marker. My estimate goes from 1.5 days to 1.75 (section 12).
|
||||
|
||||
## r4 changes (from r3 d8bcf8066)
|
||||
|
||||
mos-1133 corrects S1's migration lines. r3 said the 3i-3j re-pin needs `--migrate` with Jason present (P19
|
||||
@@ -22,8 +50,8 @@ else changed.
|
||||
mos-1131 (from duckworth-0057 and mos-1129): under PRD 1.8 only an Approve creates a grant, and a hand edit
|
||||
of the business file makes nothing. So W1 is gone.
|
||||
|
||||
- Enrolled hosts and grants are records in the bus store, written only by the executor, revoke and
|
||||
`hosts.remove`, each in the same transaction as its event (section 5, S1). Nothing writes them into the
|
||||
- Enrolled hosts and grants are records in the bus store, written only by the executor, revoke,
|
||||
`hosts.check` and `hosts.remove`, each in the same transaction as its event (section 5, S1). Nothing writes them into the
|
||||
business file.
|
||||
- Row 210's hand-edited `hosts` keys stay a test path until row a (#436a) moves `host.run` to the store
|
||||
records. After that the loader refuses those keys (S1).
|
||||
@@ -172,8 +200,10 @@ same address, each enrolled on its own.
|
||||
|
||||
### E1. The request
|
||||
|
||||
ops-01 reads the host's key fingerprints on the host, read-only (`/etc/ssh/ssh_host_*_key.pub`). Host keys
|
||||
never travel by message (mos-1132). The request carries ordinary fields: address, port, the target user,
|
||||
ops-01 reads the host's ed25519, ecdsa and rsa key fingerprints on the host, read-only
|
||||
(`/etc/ssh/ssh_host_{ed25519,ecdsa,rsa}_key.pub`). Those are the three types the keyscan asks for, so a
|
||||
fingerprint of any other type would make the raise refuse `host-key-mismatch` (N10). Host keys never travel
|
||||
by message (mos-1132). The request carries ordinary fields: address, port, the target user,
|
||||
whether that user has sudo, and the expected fingerprints. On the #433 VM, Mos or Sage passes them to the
|
||||
chief as the tester (Q173). On .53 and .54, Jason sends the request in the Console (Q118), with text Mos
|
||||
builds from ops-01's values. The chief raises a `credential.mint` decision with the binding (section 4, R4):
|
||||
@@ -210,8 +240,15 @@ Not every host can be enrolled yet. jarvis on .53 and melanie on .54 wait for #4
|
||||
|
||||
On Approve the bus child runs `hosts.enroll`, with the same start rules as a grant (section 5, X1):
|
||||
|
||||
1. writes `host.enroll.step {decision, host, step: 'registry'}`, then the `hosts` store record (S1) with
|
||||
the bound `address, port, user, sudo, hostKeys`, in one transaction
|
||||
1. **Recheck and consume** in one store transaction (B1): `#consumeGrant(business, decision, 'host-enroll')`
|
||||
(X1), a check that no `hosts` record has this (business, `hostId`), then `host.enroll.step {decision,
|
||||
host, step: 'registry'}` and the `hosts` record (S1) with the bound `address, port, user, sudo,
|
||||
hostKeys`. If the host is already enrolled, the transaction writes only the spend and `grant.failed
|
||||
{code: 'host-enrolled'}`, and the human and the chief get a message. The primary key (S1) refuses a
|
||||
second insert as well. So of two approved enrollments for one `hostId`, the second never replaces the
|
||||
pin, the user or the sudo mark under live grants, and enrolling again still means `hosts.remove` first
|
||||
(E4). Enrollment needs no `#inflight` claim, because a second enrollment stops here, before anything
|
||||
else is written.
|
||||
2. generates an installer key pair with `ssh-keygen -t ed25519 -N '' -C mosaic-installer-<tag>`, where `<tag>`
|
||||
is the hash of section 5's X2 step 2 over (business, `@installer`, hostId), in a 0700 dir under
|
||||
`<configDir>/secrets/run`, the dir row 210 already sweeps at start
|
||||
@@ -254,6 +291,15 @@ Host config, all root-owned and written by ops-01 from the approved enrollment m
|
||||
- `/etc/ssh/mosaic-keys/`: root, 0755. `mosaic-installer`'s own line lives here, in a root-owned
|
||||
`mosaic-installer` file that ops-01 writes by hand.
|
||||
|
||||
The installer's line is exactly this, with `<from>` the same value as `/etc/mosaic-keys/from` (N7):
|
||||
|
||||
```
|
||||
restrict,command="/usr/local/sbin/mosaic-keys",from="<from>" ssh-ed25519 <base64> mosaic-installer-<tag>
|
||||
```
|
||||
|
||||
E2 step 4's message carries the line with the public key filled in, and the I3 run sheet's host step quotes
|
||||
it. So a hand-written line can't drop `restrict` or `command=`.
|
||||
|
||||
`mosaic-installer` is a dedicated user, never `localadmin` and never an agent user (mos-1125 point 2). It
|
||||
needs no entry in any sshd list, because .53 and .54 have no `AllowUsers`, `AllowGroups`, `DenyUsers`,
|
||||
`DenyGroups`, `ForceCommand` or `Match` block and `pam_access` is off (mos-1127 point 1).
|
||||
@@ -266,6 +312,9 @@ Requests:
|
||||
| `add <user> <type> <base64> <tag>` | appends `from="<from>",restrict <type> <base64> <tag>` unless the tag exists |
|
||||
| `remove <user> <tag>` | drops the line with that exact tag |
|
||||
|
||||
`remove` of a tag that isn't there changes nothing and exits 0, so a revoke and an executor undo can both run
|
||||
it.
|
||||
|
||||
What `mosaic-keys-write` checks, at every request (mos-1125 points 3 and 4):
|
||||
|
||||
- the request matches the patterns again. Each tag matches `^mosaic-[0-9a-f]{32}$`, and each type and
|
||||
@@ -274,8 +323,10 @@ What `mosaic-keys-write` checks, at every request (mos-1125 points 3 and 4):
|
||||
last component, so the pattern is what stops a `../x` relative to the dir fd (ops-01 point a).
|
||||
- `<user>` exists, is in `users`, and isn't uid 0, uid under 1000, `mosaic-installer` or `localadmin`. These
|
||||
names and the uid rule are in the code, not in a file. `users` is always required.
|
||||
- `<user>` is in none of the groups `sudo`, `admin`, `wheel`, `docker`, `lxd` and has sudo rights, unless it
|
||||
is in `privileged`. `privileged` only lifts this check. Being in it alone allows nothing (ops-01 Q-O2
|
||||
- `<user>` is in none of the groups `sudo`, `admin`, `wheel`, `docker`, `lxd`, `disk`, `libvirt` and
|
||||
`incus-admin`, and has no sudo rights, unless it is in `privileged`. A group the host doesn't have counts
|
||||
as no membership. `disk` reads and writes raw block devices, and `libvirt` and `incus-admin` start guests
|
||||
with host mounts, so each is root by another route (N5). `privileged` only lifts this check. Being in it alone allows nothing (ops-01 Q-O2
|
||||
point 1).
|
||||
- the sudo test is `LC_ALL=C sudo -l -U <user>`. Any output other than the exact line
|
||||
`User <user> is not allowed to run sudo on <host>.` counts as having sudo (ops-01 Q-O2 point 4).
|
||||
@@ -288,8 +339,12 @@ How it writes, so no path trick moves the write:
|
||||
- takes `flock` on `.lock` in that dir, a name the user pattern can't match, so sshd never reads it and
|
||||
`list` never shows it (ops-01 point b)
|
||||
- opens the user's file relative to the dir fd with `O_NOFOLLOW`, and refuses one that isn't a regular
|
||||
root-owned file
|
||||
- writes a tmp with `O_CREAT | O_EXCL | O_NOFOLLOW`, sets mode 0644, `fsync`, `renameat` over the file,
|
||||
root-owned file. A first `add` for a user finds no file. `ENOENT` there means an empty list, and the
|
||||
rename creates the file. `list` and `remove` on a missing file print nothing and change nothing (N6).
|
||||
- the tmp is `.<user>.tmp`, a name the user pattern can't match. Under the `flock`, a leftover tmp can only
|
||||
come from a writer that crashed, so a regular root-owned one is unlinked first. A tmp that is a symlink
|
||||
or not a regular file refuses, and nothing is written (N6).
|
||||
- writes the tmp with `O_CREAT | O_EXCL | O_NOFOLLOW`, sets mode 0644, `fsync`, `renameat` over the file,
|
||||
then `fsync` on the dir
|
||||
- logs to syslog the result, user, tag and key fingerprint. The fingerprint is sha256 over the decoded
|
||||
blob, so no `ssh-keygen` runs
|
||||
@@ -431,16 +486,21 @@ spent and refuses `decision-consumed`. It is a private method, and only the exec
|
||||
|
||||
### X2. Steps
|
||||
|
||||
Before step 1 the executor claims the (agent, `host.<hostId>`) entry in `#inflight` (X5). A claim that
|
||||
finds an entry refuses `host-busy`, the same as 210's `use` (210: `credentials.mjs:355`).
|
||||
|
||||
1. **Recheck and consume** inside one transaction: `#consumeGrant` (X1), the agent and its definition still
|
||||
declare `host`, the definition hashes to `definitionSha256`, the host record hashes to
|
||||
1. **Recheck, consume, claim.** One store transaction: `#consumeGrant` (X1), the agent and its definition
|
||||
still declare `host`, the definition hashes to `definitionSha256`, the host record hashes to
|
||||
`registrySha256`, the host is `ready`, the `commands` in the raise event hash to `commandsSha256`, and no
|
||||
revoke of this (agent, host) is newer than the raise. Any failure is `grant.failed` with its code, and
|
||||
nothing remote has happened.
|
||||
nothing remote has happened. Right after the commit, with no `await` between, the executor claims the
|
||||
(agent, `host.<hostId>`) entry in `#inflight` (X5). The store is `DatabaseSync` (`store.mjs:1`), so the
|
||||
transaction and the claim run in one turn of the event loop, and no revoke or `host.run` can run between
|
||||
them. A claim that finds an entry, usually a running `host.run` from the same agent (210:
|
||||
`credentials.mjs:355`), ends `grant.failed {code: 'host-busy'}` with a message to the human and the
|
||||
chief, and the chief raises again (B2). The decision is spent before the claim, so every Approve ends in
|
||||
`grant.applied` or `grant.failed`, never in silence.
|
||||
I chose failing over waiting for the running call. A wait could last as long as 210's timeout while
|
||||
Jason's inbox says "started", and a failure with a message is easier to see than a slow grant.
|
||||
2. **Key.** The tag is `mosaic-` and the first 32 hex digits of sha256 over business, agent and hostId
|
||||
joined with a NUL byte. Names match `ID_PATTERN` (`vocabulary.mjs:143`), which has no NUL, so the join is
|
||||
joined with a NUL byte. Names match `ID_PATTERN` (210: `vocabulary.mjs:143`, `:127` at f41729178), which has no NUL, so the join is
|
||||
one to one, and the tag has a fixed length the host pattern accepts (B4). For a new grant,
|
||||
`ssh-keygen -t ed25519 -N '' -C <tag>` in a fresh 0700 dir under `<configDir>/secrets/run`. For a
|
||||
commands change with a live key, skip to step 5.
|
||||
@@ -502,6 +562,12 @@ Then it records `grant.failed {code: 'interrupted', tag}` with the tag listed ev
|
||||
chose a new decision over an automatic retry because Jason approved one attempt at one moment, and a retry
|
||||
after a restart might be hours later. The remote undo only takes access away, so it needs no decision.
|
||||
|
||||
A bus child can also die after the human's resolve commits and before step 1 runs. That leaves a grant
|
||||
decision resolved yes by the human via cli with no `action.allowed`. At start the reconcile spends each one
|
||||
through `#consumeGrant`, in the same transaction as `grant.failed {code: 'interrupted'}`, and Jason gets a
|
||||
message (B2). It never runs late, for the reason above, and a later start finds it spent. Enrollments get the
|
||||
same treatment.
|
||||
|
||||
### X4. Taking effect in the running bus (B1)
|
||||
|
||||
The running bus never reloads the business file (F11). The store records (S1) sidestep that:
|
||||
@@ -527,15 +593,21 @@ The executor's `#inflight` entry is an `AbortController` (210: `credentials.mjs:
|
||||
`revoke`'s `abort()` reaches (210: `credentials.mjs:289`). A new `Credentials.claim(instance, service)` sets
|
||||
it and returns the signal, and `release` deletes it.
|
||||
|
||||
- A revoke before the executor starts writes `grant.revoked`. Step 1's "no revoke newer than the raise"
|
||||
refuses.
|
||||
- A revoke during steps 2 to 5 aborts the claim. With no reference yet, `revoke` today refuses
|
||||
`credential-unknown` (210: `credentials.mjs:235-238`). r2 changes that: a `host.<id>` service with no
|
||||
reference but a live claim aborts the claim and answers `{file: false, pending: true}`, and the broker
|
||||
records `grant.revoked {pending: true}`. With neither, it still refuses `credential-unknown`.
|
||||
- A revoke before step 1 goes through `hosts.revoke` (section 6), which records `grant.revoked` for the
|
||||
(agent, host) even when `Credentials` holds no reference and no claim. Step 1's "no revoke newer than the
|
||||
raise" check finds it and ends `grant.failed {code: 'revoked'}` (B2). No revoke can fall between step 1's
|
||||
transaction and its claim (X2).
|
||||
- A revoke during steps 2 to 5 aborts the claim. `hosts.revoke` doesn't call 210's `revoke` when
|
||||
`Credentials` has no reference, because that refuses `credential-unknown` (210: `credentials.mjs:235-238`).
|
||||
It calls a new `Credentials.abortClaim(instance, service)`, which aborts a live claim and answers
|
||||
`{pending: true}`, and the broker records `grant.revoked {pending: true}`.
|
||||
- The executor checks `signal.aborted` after each `await`. At step 5 it checks right before `issue`, and
|
||||
nothing between the check and `issue` awaits, so no revoke can run in between. That matters, because
|
||||
`issue` clears the revoked mark (210: `credentials.mjs:279`).
|
||||
- Build rule (N2): `issue` and the `commit` it calls stay synchronous. `commit` is a store write and the
|
||||
store is `DatabaseSync` (`store.mjs:1`), so nothing in `issue` yields today. An async `commit` would let a
|
||||
revoke run between the key file write and the memory fill, and `issue` would then clear the revoked mark.
|
||||
A mutant covers it.
|
||||
- On abort the executor undoes as X3's failure table does at that step, then ends
|
||||
`grant.failed {code: 'revoked'}`.
|
||||
- A revoke after step 5 is an ordinary revoke (section 6).
|
||||
@@ -549,9 +621,10 @@ Two tables in the bus store, added by a new migration `3i-3j.sql` next to `3h-3i
|
||||
(`bus/src/migrations/`):
|
||||
|
||||
- `hosts`: business, host id, address, port, user, sudo, host keys, installer fingerprint, state
|
||||
(`pending-install` or `ready`), the enrolling decision id
|
||||
(`pending-install` or `ready`), the enrolling decision id. Primary key (business, host id), so the store
|
||||
refuses a second enrollment of one host (B1).
|
||||
- `host_grants`: business, agent, host id, commands, rotate-by date, tag, the decision id that made or last
|
||||
changed it
|
||||
changed it. Primary key (business, agent, host id).
|
||||
|
||||
The executor, revoke, `hosts.check` and `hosts.remove` are the only writers, each in the same transaction as
|
||||
its event. No broker verb takes these records from a request body. Every write cites a decision id or the
|
||||
@@ -577,7 +650,10 @@ A new store schema is a migration (mos-1133):
|
||||
P19 371 predates Q173 and doesn't apply.
|
||||
- On .53 and .54 the plan is for row a to land before the installs (row a in review by 10-24, installs
|
||||
10-26). Both hosts then start at 3j and need no migration.
|
||||
- If row a slips past an install, r5 or the row a packet says so. That host then goes 3i to 3j through I3's
|
||||
- The row a run sheet removes row 210's `hosts` keys and role grants from the business file before the
|
||||
`--migrate` re-pin, because the loader refuses them from then on (G14). Any 210 grant in use stops there,
|
||||
and the agent needs a chief raise and an Approve to get it back (N9).
|
||||
- If row a slips past an install, a later round or the row a packet says so. That host then goes 3i to 3j through I3's
|
||||
upgrade driver (#432), and Mos rules on it at that point.
|
||||
|
||||
## 6. Revoke
|
||||
@@ -593,18 +669,38 @@ Who may revoke follows mos-1124 d and PRD 1.8 r3's REQ-GRANT-4 (d16ccdb91):
|
||||
- the tester, with `--tester` (210 D9 already gives the tester `revoke`), on a dev instance only. On any
|
||||
other instance a tester revoke refuses `tester-dev-only`.
|
||||
|
||||
A dev instance is a business whose var `bus.tester` is exactly `true` (mos-1139, N8). `vars.mjs:125` defines
|
||||
the var for the business layer only, with a default of `false`, and `broker.mjs:544` already refuses to bind a
|
||||
tester unless it is exactly `true` (`tester-off`). `hosts.revoke` checks the same var again for the grant's
|
||||
business. A missing value or any other value means not dev, so `tester-dev-only` fails closed. I2's install
|
||||
never sets the var, so .53 and .54 are never dev (mos-1139).
|
||||
|
||||
Q188's "Mos" is read as the assistant role, Jason's main interaction agent (mos-1132). That is Mos's reading,
|
||||
labelled as such in the PRD 1.8 approval notes, and Jason settles it at prd.approve. This replaces mos-1120's
|
||||
"Mos revokes with `--tester`".
|
||||
|
||||
`hosts.revoke` is its own broker verb in a new module, `bus/src/host-grants.mjs`, next to the executor. It
|
||||
doesn't go through 210's `credential-verbs.mjs:21-24`, which refuses `credential-unknown` when the business
|
||||
file has no reference. After row a no `host.<id>` reference is in the business file at all (N3).
|
||||
|
||||
Order, local first so the part that matters can't wait on the network:
|
||||
|
||||
1. 210 `revoke`: memory, abort the `host.run` or the executor in flight (X5), unlink the key file. The call
|
||||
answers `credential-unavailable`. This is the "instant" part, and it doesn't touch the network.
|
||||
2. One store transaction deletes the `host_grants` record and records `grant.revoked`. The next `host.run`
|
||||
refuses `host-not-granted` (X4). A new grant needs a new decision.
|
||||
3. `remove <user> <tag>` on the host through the installer. If the host can't be reached, the event is
|
||||
`grant.revoked {hostLineLeft: <tag>}`, and `sweep` clears it later.
|
||||
1. One store transaction deletes the `host_grants` record, if there is one, and records `grant.revoked
|
||||
{tag, hostLine: 'pending'}`. The tag is computed (X2 step 2), so it exists even with no record (N4). The
|
||||
next `host.run` refuses `host-not-granted` (X4). A new grant needs a new decision.
|
||||
2. Right after the commit, with no `await` between: 210 `revoke` if `Credentials` holds a reference
|
||||
(memory, abort the `host.run` or the executor in flight, unlink the key file), or `abortClaim` if it
|
||||
holds only a claim (X5). Steps 1 and 2 are the "instant" part, and neither touches the network. If the
|
||||
unlink fails, the verb reports it. The key is already out of memory, and the start reconcile unlinks the
|
||||
file.
|
||||
3. `remove <user> <tag>` on the host through the installer. Success records `grant.revoke.closed {tag,
|
||||
hostLine: 'removed'}`. If the host can't be reached, it records `grant.revoke.closed {tag, hostLineLeft:
|
||||
<tag>}`, and `sweep` clears it later.
|
||||
|
||||
A restart between the steps (N4) leaves a `grant.revoked` with `hostLine: 'pending'` and no
|
||||
`grant.revoke.closed`. At start, before `Credentials` loads, the reconcile unlinks that key file if it is
|
||||
there. No reference loads for it, because the record is gone (X4). After the bus is up, the reconcile runs
|
||||
step 3 and closes the marker. `status` shows an open marker as a revoke in progress.
|
||||
|
||||
Step 3 is defense in depth. After step 1 the stack holds no copy of the key. The line only matters if
|
||||
something copied the key file before the revoke, which section 9's same-user limit allows.
|
||||
@@ -696,22 +792,23 @@ such argument. No real host is contacted.
|
||||
| G8 | each failure row of X3 leaves the stated state, and a failed `remove` leaves `hostLineLeft` |
|
||||
| G9 | a registry, role definition or commands change between raise and Approve refuses `decision-mismatch` before any remote step |
|
||||
| G10 | a second start for the same decision refuses `decision-consumed`, and `#consumeGrant` refuses an unresolved or agent-resolved decision |
|
||||
| G11 | a bus restart after `install`, after the key file is written and before `commit`, and after `commit` each reconcile to the state before the decision: no key file, no grant, no line, `grant.failed interrupted` with the tag. A restart after an enrollment's `registry` step drops the entry and the installer key |
|
||||
| G12 | revoke: the call in flight dies, the key file is gone, the grant entry is gone, the host line is gone, in that order |
|
||||
| G11 | a bus restart after `install`, after the key file is written and before `commit`, and after `commit` each reconcile to the state before the decision: no key file, no grant, no line, `grant.failed interrupted` with the tag. A restart after an enrollment's `registry` step drops the entry and the installer key. A restart after the resolve commits and before step 1 spends the decision, ends `grant.failed interrupted` with a message, and nothing runs later, for a grant and for an enrollment (B2) |
|
||||
| G12 | revoke: the grant record is gone and the `pending` marker written, then in the same turn the call in flight dies and the key file is gone, then the host line is gone and the marker closed, in that order |
|
||||
| G13 | revoke with the host down still does steps 1 and 2 and reports `hostLineLeft` |
|
||||
| G14 | `organization.set`, `limits.set` and `instances.set` still refuse `hosts` and `manual`. After row a, a business file with `hosts` or a role grant refuses at load |
|
||||
| G15 | no event, message, log line or receipt holds private key material (210 D8 check on every body) |
|
||||
| G16 | `mosaic-keys` refuses bad requests without calling the writer. The writer refuses a user not matching the pattern (`../x` included), a `privileged` user not in `users`, users outside `users`, root, uid under 1000, `mosaic-installer`, `localadmin`, a member of each refused group, a user with a `sudo -l` line who isn't in `privileged`, bad tags and bad key text, and takes `from=` from the host file |
|
||||
| G17 | a `host-enroll` raise refuses loopback and this machine's names, bad fingerprints, an enrolled id and the reserved users. A keyscan set that differs from the request's fingerprints by one key, extra or missing, refuses `host-key-mismatch` and stores nothing. On Approve the executor writes the entry and the installer key 0600, records `host.enrolled`, and its message holds no private material |
|
||||
| G16 | `mosaic-keys` refuses bad requests without calling the writer. The writer refuses a user not matching the pattern (`../x` included), a `privileged` user not in `users`, users outside `users`, root, uid under 1000, `mosaic-installer`, `localadmin`, a member of each refused group (`disk`, `libvirt` and `incus-admin` included), a user with a `sudo -l` line who isn't in `privileged`, bad tags and bad key text, and takes `from=` from the host file |
|
||||
| G17 | a `host-enroll` raise refuses loopback and this machine's names, bad fingerprints, an enrolled id and the reserved users. A keyscan set that differs from the request's fingerprints by one key, extra or missing, refuses `host-key-mismatch` and stores nothing. On Approve the executor writes the entry and the installer key 0600, records `host.enrolled`, and its message holds no private material. Of two approved enrollments for one `hostId`, the second ends `grant.failed host-enrolled` with its decision spent, and the first record, pin and installer key are unchanged. A direct second insert fails on the primary key (B1) |
|
||||
| G18 | `hosts.check` with a wrong pin exits 255, refuses `host-key-mismatch`, runs nothing and leaves the host `pending-install` |
|
||||
| G19 | a grant naming a host that isn't `ready` refuses `host-not-ready` |
|
||||
| G20 | `hosts.check` from the chief, the tester and the human marks the host `ready`, and any other role refuses |
|
||||
| G26 | revoke by the human works. The assistant's works in its own organization and refuses another's. The tester's works on a dev instance and refuses `tester-dev-only` elsewhere. Any other role refuses |
|
||||
| G26 | revoke by the human works. The assistant's works in its own organization and refuses another's. The tester's works where `bus.tester` is exactly `true`, and refuses `tester-dev-only` where the grant's business has it missing or set to anything else (N8). Any other role refuses |
|
||||
| G21 | `hosts.remove` refuses `host-in-use` while a grant names the host, `host-line-left` while a line is left, and any agent other than the tester |
|
||||
| G22 | one bus, no restart: enroll, check, grant, a `host.run` that works, a commands change that the next `host.run` uses, and a revoke that the next `host.run` refuses `credential-unavailable` (B1) |
|
||||
| G23 | a revoke during install (a scratch `mosaic-keys` that waits on a fifo) ends `grant.failed revoked` with no key file, no grant and no line. A revoke before the executor starts refuses at step 1. A revoke with no reference and no claim still refuses `credential-unknown` (B2) |
|
||||
| G23 | a revoke during install (a scratch `mosaic-keys` that waits on a fifo) ends `grant.failed revoked` with no key file, no grant and no line. A revoke before the executor starts, with no reference and no claim, records `grant.revoked`, and step 1 then ends `grant.failed revoked`. A grant whose claim finds a running `host.run` ends `grant.failed host-busy` with its decision spent and a message (B2) |
|
||||
| G24 | agent and host names at 64 characters with `.` and `_` give distinct tags of the fixed length. `sweep` leaves another business's line and an unrecorded `mosaic-` line on the same user (B4) |
|
||||
| G25 | the writer refuses a symlinked `/etc/ssh/mosaic-keys`, a symlinked user file, a group-writable dir and an existing tmp, and a written file is root-owned 0644 (B5) |
|
||||
| G25 | the writer refuses a symlinked `/etc/ssh/mosaic-keys`, a symlinked user file, a group-writable dir, and a tmp that is a symlink or not a regular file. A leftover regular tmp is unlinked and the write goes ahead. A first `add` creates the file. A written file is root-owned 0644 (B5, N6) |
|
||||
| G27 | `hosts.revoke` works for an agent whose business file has no credentials reference (N3). A restart after revoke step 1, and one after step 2, each end at start with no key file, no record, the line removed and the marker closed (N4) |
|
||||
|
||||
Mutants, a first list. The row's BUILD.md numbers them and adds more:
|
||||
|
||||
@@ -747,6 +844,14 @@ Mutants, a first list. The row's BUILD.md numbers them and adds more:
|
||||
- start the executor on an arbiter resolve
|
||||
- start it on `no`
|
||||
- write the private key into the event body
|
||||
- drop the "not yet enrolled" check in E2 step 1
|
||||
- drop the `hosts` primary key
|
||||
- claim before consume, or put an `await` between them
|
||||
- skip the start check for a resolved, unspent grant decision
|
||||
- make `commit` async
|
||||
- route `hosts.revoke` through `credential-verbs.mjs`
|
||||
- skip the revoke marker reconcile
|
||||
- treat a missing `bus.tester` as dev
|
||||
|
||||
## 11. Not in this draft
|
||||
|
||||
@@ -771,7 +876,8 @@ Answered since r1:
|
||||
- Row a (#436a): enrollment as the approved request, the four `tools/host` files, the store records and
|
||||
their migration, `host.run` reading them, and `hosts status`. More than r2's 1 day, because of the
|
||||
migration and moving 210's tests. My estimate is 1.5 days.
|
||||
- Row b: the raise binding, the executor, reconcile and revoke, after row a, about 1.5 days.
|
||||
- Row b: the raise binding, the executor, reconcile and revoke, after row a. r5 adds the `hosts.revoke`
|
||||
verb and its marker, so my estimate goes from 1.5 days to 1.75.
|
||||
- Both come after row 210 lands and need the 1.8 line before landing. Target: in review by 10-24, so
|
||||
row a lands before the 10-26 installs (S1, mos-1133).
|
||||
- Q-M1 (mos-1120): Mos revokes as the tester. Replaced by mos-1132, see below and section 6.
|
||||
|
||||
Reference in New Issue
Block a user