machine-grant design r2 (#436): credential.mint binding, enrollment as a request, Duckworth B1-B5 and N1-N3, ops-01 host side (mos-1125, mos-1127)
This commit is contained in:
1 parent
f6b6a04b60
commit
eb4c756a14
1 file changed
+439
-177
@@ -1,11 +1,38 @@
|
|||||||
# Machine grant path, design draft r1 (Vikunja #436, row 210 follow-up)
|
# Machine grant path, design draft r2 (Vikunja #436, row 210 follow-up)
|
||||||
|
|
||||||
Owner: Gizmoduck. Reviewer: Duckworth. Sizing and row assignment: Sage. Design only, no code until Sage
|
Owner: Gizmoduck. Reviewer: Duckworth. Sizing and row assignment: Sage. Design only. Code starts when
|
||||||
confirms the row and owner (mos-1115).
|
Duckworth approves the design and row 210 lands (sage-1527).
|
||||||
|
|
||||||
Sources: Q188 (DECISIONS.md:1642, jason-master-restart 8ecf252), mos-1115, row 210 DESIGN r3 (e021dfa26) and
|
Sources: Q188 (DECISIONS.md:1642, jason-master-restart 8ecf252), mos-1115, mos-1120, mos-1125, mos-1127,
|
||||||
its approved candidate 455964f23. Code cites are `path:line` at origin/refactor f41729178 unless they say
|
sage-1527, Duckworth's r1 review (REVIEW-r1.md, duckworth/reviews d025ef492, corrected at 749080115),
|
||||||
"210:", which means the row 210 candidate 455964f23 (the same code as the branch head a2fd30390).
|
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).
|
||||||
|
|
||||||
|
## r2 changes (from r1 f6b6a04b6)
|
||||||
|
|
||||||
|
- Enrollment is a request the chief raises and Jason approves, not a CLI Jason types (mos-1120 point 3).
|
||||||
|
Section 3 is rewritten. `hosts.enroll` stays, as the verb the executor runs.
|
||||||
|
- The action is `credential.mint` with a `grant` binding, not a new `host.grant` (sage-1527 Q-S3, Honker's
|
||||||
|
REQ-GRANT-1). The hard-waits list doesn't change. R2 says why the binding still carries G1 to G10.
|
||||||
|
- The binding adds a hash of the agent's role definition, because REQ-GRANT-2 voids an approval when the
|
||||||
|
definition changes. It also carries the full commands list (N2).
|
||||||
|
- The host side follows ops-01's answer to Q-O1 (mos-1125): lines in a root-owned
|
||||||
|
`/etc/ssh/mosaic-keys/%u`, a dedicated install user, a forced command that only parses, and a root
|
||||||
|
writer in this repo that checks everything again (E3). This answers B5.
|
||||||
|
- A target user with sudo is enrolled by name, and Jason's Approve of the enrollment covers it (E1, E3,
|
||||||
|
mos-1125's recommendation). Without this no `host.sudo` grant could be installed.
|
||||||
|
- B1: a grant, a commands change, an enrollment and a revoke take effect in the running bus (X4).
|
||||||
|
- B2: the executor holds an `#inflight` entry that revoke aborts, and a revoke works before any grant
|
||||||
|
exists (X5).
|
||||||
|
- B3: progress events before each step, and a start-time reconcile that undoes to the state before the
|
||||||
|
decision (X3).
|
||||||
|
- B4: the tag is a hash, recorded in events, and `list` and `sweep` only touch this business's recorded
|
||||||
|
tags (X2, step 2).
|
||||||
|
- N1 to N3 answered in X1, R4 and E4.
|
||||||
|
- Section 9 adds ops-01's point that a host line has no `command=` (mos-1125).
|
||||||
|
- Answers recorded: Q-S1 to Q-S4, Q-M1, Q-O1. New questions Q-M2, Q-S5 and Q-O2.
|
||||||
|
|
||||||
## 0. What Q188 asks for, and what this draft builds
|
## 0. What Q188 asks for, and what this draft builds
|
||||||
|
|
||||||
@@ -49,8 +76,10 @@ first is useless for Q188's goal without the second.
|
|||||||
- F4. **Resolving a decision runs nothing.** `decision.resolve` records a `decision_events` row and messages
|
- F4. **Resolving a decision runs nothing.** `decision.resolve` records a `decision_events` row and messages
|
||||||
the raiser (`broker.mjs:1752-1828`). An effect happens later, when a verb consumes the decision through
|
the raiser (`broker.mjs:1752-1828`). An effect happens later, when a verb consumes the decision through
|
||||||
`#consumeAuthority`, which refuses `decision-consumed` if an `action.allowed` event other than the raise
|
`#consumeAuthority`, which refuses `decision-consumed` if an `action.allowed` event other than the raise
|
||||||
already cites it (`broker.mjs:808-831`). The landing path is the only place where stack code reacts to a
|
already cites it (`broker.mjs:808-831`). `#consumeAuthority` needs the raiser's own agent session: it
|
||||||
resolution, and even there the agent's own `land_work` call does the work (`cli/src/launcher.mjs:555-600`).
|
checks `raised_by_role` and the raiser's thread against the caller (`broker.mjs:764`, `:786`). The
|
||||||
|
landing path is the only place where stack code reacts to a resolution, and even there the agent's own
|
||||||
|
`land_work` call does the work (`cli/src/launcher.mjs:555-600`).
|
||||||
- F5. **The human proof.** `mosaic decide` resolves through `human-cli.mjs` (`cli/src/cli.mjs:142-175`,
|
- F5. **The human proof.** `mosaic decide` resolves through `human-cli.mjs` (`cli/src/cli.mjs:142-175`,
|
||||||
`cli/src/transport.mjs:55-75`). `verifyHuman` is a same-UID cooperative check (`bus/src/human.mjs:50-52`),
|
`cli/src/transport.mjs:55-75`). `verifyHuman` is a same-UID cooperative check (`bus/src/human.mjs:50-52`),
|
||||||
and `#human` needs `via === 'cli'` and `outsideAgent` (`broker.mjs:375-378`).
|
and `#human` needs `via === 'cli'` and `outsideAgent` (`broker.mjs:375-378`).
|
||||||
@@ -62,9 +91,13 @@ first is useless for Q188's goal without the second.
|
|||||||
- F7. **Row 210's pieces this path reuses:**
|
- F7. **Row 210's pieces this path reuses:**
|
||||||
- the agent key store at `<configDir>/secrets/agents/<business>/<agent>/host.<id>` (210:
|
- the agent key store at `<configDir>/secrets/agents/<business>/<agent>/host.<id>` (210:
|
||||||
`bus/src/credentials.mjs:153-157`)
|
`bus/src/credentials.mjs:153-157`)
|
||||||
- `issue`, which writes tmp, fsync, rename and then calls `commit` (210: `credentials.mjs:243-280`)
|
- `issue`, which writes tmp, fsync, rename, then calls `commit`, then sets memory and clears the revoked
|
||||||
|
mark (210: `credentials.mjs:243-280`, the clear at `:279`)
|
||||||
- `revoke`, which drops memory, aborts the call in flight and unlinks the file (210:
|
- `revoke`, which drops memory, aborts the call in flight and unlinks the file (210:
|
||||||
`credentials.mjs:284-298`)
|
`credentials.mjs:284-298`, the abort at `:289`). It refuses `credential-unknown` for a service with no
|
||||||
|
loaded reference (210: `credentials.mjs:235-238`).
|
||||||
|
- the busy map `#inflight`, instance and service to an `AbortController` (210: `credentials.mjs:118`,
|
||||||
|
`busy` at `:332-334`, used by `host-run.mjs:249`)
|
||||||
- pinned `known_hosts` built from the registry's `hostKeys` with `StrictHostKeyChecking=yes` (210:
|
- pinned `known_hosts` built from the registry's `hostKeys` with `StrictHostKeyChecking=yes` (210:
|
||||||
`bus/src/host-run.mjs:36`, `:170`)
|
`bus/src/host-run.mjs:36`, `:170`)
|
||||||
- F8. **Nothing on refactor generates keys or contacts a host.** No `ssh-keygen` or ssh spawn exists in
|
- F8. **Nothing on refactor generates keys or contacts a host.** No `ssh-keygen` or ssh spawn exists in
|
||||||
@@ -74,102 +107,191 @@ first is useless for Q188's goal without the second.
|
|||||||
(`webui/src/public/bus.js:3-5`). `decision.resolve` is not in `BUS_VERBS` (`webui/src/serve.mjs:62`).
|
(`webui/src/public/bus.js:3-5`). `decision.resolve` is not in `BUS_VERBS` (`webui/src/serve.mjs:62`).
|
||||||
Vikunja #435 adds Approve after Q175 sign-in (#412).
|
Vikunja #435 adds Approve after Q175 sign-in (#412).
|
||||||
- F10. **`credential.mint` is already a hard wait with no executor** (`vocabulary.mjs:38`).
|
- F10. **`credential.mint` is already a hard wait with no executor** (`vocabulary.mjs:38`).
|
||||||
|
- F11. **The running bus never reloads the business file, except the limits.** The broker keeps a clone
|
||||||
|
made once (210: `broker.mjs:234`), and `host.run` reads `b.hosts` and `b.roles[role].hosts` from it (210:
|
||||||
|
`broker.mjs:1731-1735`). `Credentials` loads its references once, in the constructor (210:
|
||||||
|
`bus/src/runtime.mjs:85-98`, `credentials.mjs:125-147`). Only the limits read the file at each ask, and
|
||||||
|
`instances.set` answers `restart: true` (`business/src/instance-settings.mjs:73`, `:155`).
|
||||||
|
- F12. **The business file has no room for state on a host or a grant.** A registry entry takes only
|
||||||
|
`address, port, user, hostKeys` (210: `business/src/hosts.mjs:75`), and a role's grant only `commands`
|
||||||
|
(210: `hosts.mjs:123`). This design keeps it that way. Host state, tags and progress live in events.
|
||||||
|
|
||||||
## 2. Shape
|
## 2. Shape
|
||||||
|
|
||||||
```
|
```
|
||||||
agent --message--> chief --decision.raise host.grant--> Jason's inbox
|
agent --message--> chief --decision.raise credential.mint + grant--> Jason's inbox
|
||||||
|
|
|
|
||||||
mosaic decide (later: Console Approve, #435)
|
mosaic decide (later: Console Approve, #435)
|
||||||
|
|
|
|
||||||
bus child grant executor (on resolve, approve only)
|
bus child grant executor (on resolve, approve only)
|
||||||
keygen -> install via host installer -> verify -> business file -> key store
|
keygen -> install via host installer -> verify -> key file + business file + live bus
|
||||||
```
|
```
|
||||||
|
|
||||||
Revoke runs the other way: memory, then the in-flight call, then the key file, then the host line, then the
|
Revoke runs the other way: memory, then the in-flight call, then the key file, then the business file, then
|
||||||
business file.
|
the host line.
|
||||||
|
|
||||||
## 3. Host enrollment (once per host, human only)
|
## 3. Host enrollment (once per host, a request like a grant)
|
||||||
|
|
||||||
Installing a key on a host needs something on the host that already trusts the stack. Nothing on refactor
|
Installing a key on a host needs something on the host that already trusts the stack. Nothing on refactor
|
||||||
holds that trust, so each host gets one enrollment. This is the step that remains for ops-01, once per
|
holds that trust, so each host gets one enrollment. This is the step that remains for ops-01, once per
|
||||||
host. Per-agent installs no longer need ops-01.
|
host, and it goes on the ops-01 run sheet in the installer's I3 row (#432, mos-1120). Per-agent installs no
|
||||||
|
longer need ops-01.
|
||||||
|
|
||||||
### E1. What Jason enters
|
A host here is a registry entry, so it has one user (F12). Two users on one machine are two host ids with the
|
||||||
|
same address, each enrolled on its own.
|
||||||
|
|
||||||
`mosaic hosts enroll <hostId> --address A --port P --user U --install-user I`, with the host's public keys on
|
### E1. The request
|
||||||
stdin, one `ssh-ed25519`, `ecdsa-sha2-nistp256` or `ssh-rsa` line each. ops-01 reads them on the host
|
|
||||||
(`/etc/ssh/ssh_host_*_key.pub`) and hands them to Jason out of band. The pin comes from the host side, never
|
|
||||||
from a first connection (210 section 4). The checks are 210 D3 layer 2's: the address rules, refusing
|
|
||||||
loopback and this machine's names, the port range, the user pattern, and the key types.
|
|
||||||
|
|
||||||
The Console form for this (Q188: "Jason enters ... in the WebUI") waits on #412 like Approve does. Until
|
ops-01 reads the host's public keys on the host (`/etc/ssh/ssh_host_*_key.pub`) and submits them with the
|
||||||
then the CLI is the only way in.
|
address, port, the target user and whether that user has sudo. How that reaches the chief is Q-M2. The chief
|
||||||
|
raises a `credential.mint` decision with the binding (section 4, R4):
|
||||||
|
|
||||||
### E2. What the stack does
|
```
|
||||||
|
grant: { "method": "host-enroll", "host": "<hostId>", "address": "A", "port": 22, "user": "U",
|
||||||
|
"sudo": true, "hostKeys": ["ssh-ed25519 AAAA...", ...] }
|
||||||
|
```
|
||||||
|
|
||||||
The bus child, on a human-only `hosts.enroll` verb:
|
The raise checks are 210 D3 layer 2's: the address rules, refusing loopback and this machine's names, the
|
||||||
|
port range, the user pattern and the key types. It also refuses a `hostId` that is already enrolled, a user
|
||||||
|
named `root`, `localadmin` or `mosaic-installer`, and a `sudo` that isn't a boolean.
|
||||||
|
|
||||||
1. writes the `hosts.<hostId>` registry entry through the grant writer (section 5, W1), with `hostKeys` set
|
The rendered block shows each host key's SHA256 fingerprint as well as the line, so Jason can compare it with
|
||||||
to the stdin lines
|
what ops-01 reads on the host. With `"sudo": true` it adds one line in plain words: "U has sudo on this host.
|
||||||
2. generates an installer key pair with `ssh-keygen -t ed25519 -N '' -C mosaic-<business>-installer-<hostId>`
|
Approving lets later grants for U be installed, and section 9's first limit applies to U."
|
||||||
in a 0700 dir under `<configDir>/secrets/run`, the dir row 210 already sweeps at start
|
|
||||||
|
Jason approves with `mosaic decide` until #412 brings the Console inbox. Jason types no host data (mos-1120:
|
||||||
|
pasting keys he got from ops-01 checks nothing his Approve doesn't).
|
||||||
|
|
||||||
|
The pin still comes from the host side, never from a first connection (210 section 4). E4's check fails
|
||||||
|
closed on a wrong pin, so a key altered on the way to the chief makes the enrollment fail. It doesn't get
|
||||||
|
an agent onto the host.
|
||||||
|
|
||||||
|
Not every host can be enrolled yet. jarvis on .53 and melanie on .54 wait for #423 steps 4 and 5
|
||||||
|
(mos-1125 point 5).
|
||||||
|
|
||||||
|
### E2. What the executor does
|
||||||
|
|
||||||
|
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.<hostId>` registry entry
|
||||||
|
through the grant writer (W1) with the bound `address, port, user, hostKeys`, and applies it to the
|
||||||
|
running broker (X4)
|
||||||
|
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
|
||||||
3. moves the private half to `<configDir>/secrets/hosts/<business>/<hostId>/installer`, mode 0600 in 0700
|
3. moves the private half to `<configDir>/secrets/hosts/<business>/<hostId>/installer`, mode 0600 in 0700
|
||||||
dirs, by the same tmp, fsync, rename steps as 210 `issue`
|
dirs, by the same tmp, fsync, rename steps as 210 `issue`. The path follows row 199 D1's
|
||||||
4. prints the installer's `authorized_keys` line for ops-01:
|
`secrets/agents/<business>/...` (Q-S2, mos-1120, sage-1527)
|
||||||
|
4. records `host.enrolled {decision, host, registrySha256, installerFingerprint, user, sudo}` and messages
|
||||||
|
the human and the chief with what ops-01 installs (E3)
|
||||||
|
|
||||||
```
|
No private material goes out. A failure at step 2 or 3 drops the registry entry again through the writer and
|
||||||
from="<stack host address>",restrict,command="/usr/local/sbin/mosaic-keys" ssh-ed25519 AAAA... mosaic-<business>-installer-<hostId>
|
from the broker, and ends `grant.failed`, as X3 does. A restart after step 1 does the same (X3).
|
||||||
```
|
|
||||||
|
|
||||||
No private material goes out. The secrets dir layout is new. Sage or Mos may prefer
|
The host's state lives in events, not in the business file (F12):
|
||||||
`secrets/mosaic-stack/...`, which is the Console's (`webui/src/sso.mjs:35`). That is question Q-S2.
|
|
||||||
|
|
||||||
### E3. The host side: `mosaic-keys`
|
- `pending-install`: a `host.enrolled` exists and no later `host.checked` matches it
|
||||||
|
- `ready`: the latest `host.checked` names the same `registrySha256` and `installerFingerprint` as the
|
||||||
|
latest `host.enrolled`, and the current registry entry still hashes to that value
|
||||||
|
|
||||||
One small script in this repo, under `tools/host/mosaic-keys`, reviewed like any code. ops-01 installs it
|
A hand edit of the registry entry changes its hash, so the host stops being `ready` until the next check.
|
||||||
once per host as root, mode 0755, owned by root. It is the installer key's forced command, and it reads one
|
|
||||||
request from `SSH_ORIGINAL_COMMAND`:
|
### E3. The host side
|
||||||
|
|
||||||
|
ops-01 answered Q-O1 with six changes (mos-1125). The stack side doesn't change. The host side is four
|
||||||
|
files in this repo under `tools/host/`, each reviewed like code, and Duckworth reviews the writer, the
|
||||||
|
sudoers line and the sshd drop-in before .53 (mos-1125 point 6):
|
||||||
|
|
||||||
|
| File | Installed as | Does |
|
||||||
|
|---|---|---|
|
||||||
|
| `mosaic-keys` | `/usr/local/sbin/mosaic-keys`, root, 0755 | the installer key's forced command. Parses `SSH_ORIGINAL_COMMAND` against fixed patterns and runs `sudo -n /usr/local/sbin/mosaic-keys-write <request>`. Nothing else. |
|
||||||
|
| `mosaic-keys-write` | `/usr/local/sbin/mosaic-keys-write`, root, 0755 | checks everything again and writes the line |
|
||||||
|
| `mosaic-keys.sudoers` | `/etc/sudoers.d/mosaic-keys`, root, 0440 | `mosaic-installer ALL=(root) NOPASSWD: /usr/local/sbin/mosaic-keys-write` |
|
||||||
|
| `mosaic-keys.sshd.conf` | `/etc/ssh/sshd_config.d/<nn>-mosaic-keys.conf` | `AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 /etc/ssh/mosaic-keys/%u` |
|
||||||
|
|
||||||
|
The drop-in keeps both paths the hosts use today. On .53 and .54, `sshd_config` includes
|
||||||
|
`sshd_config.d/*.conf` at line 12 and no file sets `AuthorizedKeysFile` (mos-1127 point 2). Root never
|
||||||
|
writes into a user's home (mos-1125 point 1).
|
||||||
|
|
||||||
|
Host config, all root-owned and written by ops-01 from the approved enrollment message:
|
||||||
|
|
||||||
|
- `/etc/mosaic-keys/users`: the users the writer may touch
|
||||||
|
- `/etc/mosaic-keys/privileged`: the users with sudo that Jason approved by name (E1 `"sudo": true`)
|
||||||
|
- `/etc/mosaic-keys/from`: the `from=` value. The stack can't choose it.
|
||||||
|
- `/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.
|
||||||
|
|
||||||
|
`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).
|
||||||
|
|
||||||
|
Requests:
|
||||||
|
|
||||||
| Request | Effect |
|
| Request | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `list <user>` | prints each `authorized_keys` line of `<user>` whose comment starts `mosaic-` |
|
| `list <user>` | prints each line of `/etc/ssh/mosaic-keys/<user>` whose tag matches the tag pattern |
|
||||||
| `add <user> <type> <base64> <tag>` | appends `from="<stack>",restrict <type> <base64> <tag>` unless the tag exists, through tmp and rename, keeping owner and mode |
|
| `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, through tmp and rename |
|
| `remove <user> <tag>` | drops the line with that exact tag |
|
||||||
|
|
||||||
Rules the script enforces on the host, so a stolen installer key can do no more than they allow:
|
What `mosaic-keys-write` checks, at every request (mos-1125 points 3 and 4):
|
||||||
|
|
||||||
- `<user>` must be in a host-side allowlist file, `/etc/mosaic-keys/users`, written by ops-01. The install
|
- the request matches the patterns again. Each tag matches `^mosaic-[0-9a-f]{32}$`, and each type and
|
||||||
user itself is never on it.
|
base64 match E1's patterns.
|
||||||
- The script writes the `from=` value from `/etc/mosaic-keys/from`, also ops-01's. The stack can't choose it.
|
- `<user>` exists, is in `users`, and isn't uid 0, uid under 1000, `mosaic-installer` or `localadmin`. These
|
||||||
- Each tag matches `^mosaic-[a-z0-9-]{1,96}$`, and each type and base64 matches the same patterns as E1.
|
names and the uid rule are in the code, not in a file.
|
||||||
- The script never touches a line without the `mosaic-` prefix.
|
- `<user>` is in none of the groups `sudo`, `admin`, `wheel`, `docker`, `lxd` and has no `sudo -l` line,
|
||||||
- The script logs each request to syslog.
|
unless it is in `privileged`. This is the answer to mos-1125's conflict (Q-O2 asks ops-01 to confirm it).
|
||||||
|
- it never touches a line whose tag doesn't match the pattern.
|
||||||
|
|
||||||
The install user `I` needs write access to `<user>`'s `authorized_keys` only. A `sudoers` line for exactly
|
How it writes, so no path trick moves the write:
|
||||||
`/usr/local/sbin/mosaic-keys-write` (a root helper the script calls) is ops-01's choice. The design needs the
|
|
||||||
effect and not a particular mechanism, and Duckworth's #423 condition (duckworth-0035) applies to whatever
|
|
||||||
ops-01 runs.
|
|
||||||
|
|
||||||
This still doesn't make the installer key safe to lose. A key that can add a line for a NOPASSWD sudo user
|
- opens `/etc/ssh/mosaic-keys` with `O_DIRECTORY | O_NOFOLLOW` and refuses unless it is root-owned and not
|
||||||
is root on that host. Section 9 says so.
|
group or world writable
|
||||||
|
- takes `flock` on a lock file in that dir
|
||||||
|
- 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,
|
||||||
|
then `fsync` on the dir
|
||||||
|
- logs to syslog the request, user, tag, key fingerprint and `SSH_CONNECTION`
|
||||||
|
|
||||||
|
The writer is python3, standard library only, because `os.open` takes `dir_fd` and `O_NOFOLLOW` and a shell
|
||||||
|
script can't do that safely. Ubuntu with OpenSSH 9.6p1 ships python3. If ops-01 wants another language, the
|
||||||
|
checks above are the contract.
|
||||||
|
|
||||||
|
sshd's `StrictModes yes` (on both hosts, mos-1127 point 3) also refuses a key file whose path is writable by
|
||||||
|
anyone but root or the user.
|
||||||
|
|
||||||
|
The install proof runs on the #433 VM first (mos-1125 point 6), as ops-01 does it (mos-1127): `sshd -t`,
|
||||||
|
`systemctl reload ssh`, then a new connection. An existing session survives the reload. Rollback removes
|
||||||
|
the drop-in and reloads, which restores the old value. The run sheet's host step (#432, I3) cites these.
|
||||||
|
|
||||||
|
This still doesn't make the installer key safe to lose. A key that can add a line for a privileged user is
|
||||||
|
root on that host. Section 9 says so.
|
||||||
|
|
||||||
### E4. Check
|
### E4. Check
|
||||||
|
|
||||||
`mosaic hosts check <hostId>` connects with the installer key, the pinned `known_hosts` and 210's ssh
|
After ops-01 installs the files and the installer line, `hosts.check <hostId>` connects as `mosaic-installer`
|
||||||
options, and runs `list <user>`. A wrong pin exits 255 and refuses `host-key-mismatch`, as 210 does. Enrollment
|
with the installer key, the pinned `known_hosts` and 210's ssh options, and runs `list <user>`. A wrong pin
|
||||||
is done when the check passes. `status` shows each host's pin fingerprints, the installer fingerprint and the
|
exits 255 and refuses `host-key-mismatch`, as 210 does, and the host stays `pending-install`. A pass records
|
||||||
last check time.
|
`host.checked {host, registrySha256, installerFingerprint}`, and the host is `ready`. Only a `ready` host can
|
||||||
|
be named in a grant (R4).
|
||||||
|
|
||||||
`mosaic hosts remove <hostId>` is human only. It refuses while any grant names the host, then deletes the
|
The chief, the tester or the human may run `hosts.check`. It reads the host's lines and widens nothing past
|
||||||
installer key and the registry entry. ops-01 removes the installer line on the host.
|
what Jason approved, so it needs no decision. `status` shows each host's state, pin fingerprints, installer
|
||||||
|
fingerprint and last check time.
|
||||||
|
|
||||||
|
`hosts.remove <hostId>` is for the human or the tester, as a revoke is (REQ-GRANT-4), because it only takes
|
||||||
|
access away. It refuses `host-in-use` while any grant names the host and `host-line-left` while any event's
|
||||||
|
`hostLineLeft` names the host and no later `sweep` cleared it (N3). Then it deletes the installer key, drops
|
||||||
|
the registry entry through the writer and from the broker, and records `host.removed`. ops-01 removes the
|
||||||
|
installer line on the host. Enrolling the host again takes a new request.
|
||||||
|
|
||||||
## 4. The request and the decision
|
## 4. The request and the decision
|
||||||
|
|
||||||
### R1. Up the chain
|
### R1. Up the chain
|
||||||
|
|
||||||
An agent that needs host access sends the chief a message, as any request to the chief goes today
|
An agent that needs host access sends the chief a message, as any request to the chief goes today
|
||||||
(`#checkMessaging`, `broker.mjs:617-627`). There is no structured request verb in r1. F1 gives the reason:
|
(`#checkMessaging`, `broker.mjs:617-627`). There is no structured request verb (Q-S1). F1 gives the reason:
|
||||||
nothing records who reports to whom, so the bus can't check a chain beyond "the chief raises it".
|
nothing records who reports to whom, so the bus can't check a chain beyond "the chief raises it".
|
||||||
|
|
||||||
"Not every agent may request" (Q188) is enforced at the raise. The target agent's definition must declare
|
"Not every agent may request" (Q188) is enforced at the raise. The target agent's definition must declare
|
||||||
@@ -177,50 +299,59 @@ nothing records who reports to whom, so the bus can't check a chain beyond "the
|
|||||||
it can ask, but the chief's raise for it refuses `host-not-declared`. Which definitions declare `host` is
|
it can ask, but the chief's raise for it refuses `host-not-declared`. Which definitions declare `host` is
|
||||||
the reviewed list of agents that may hold a host grant.
|
the reviewed list of agents that may hold a host grant.
|
||||||
|
|
||||||
A structured `grant.ask` verb (the agent files the request, the chief raises it citing the request id, and
|
|
||||||
the broker checks the pair) is the stronger form. It is question Q-S1.
|
|
||||||
|
|
||||||
### R2. The action
|
### R2. The action
|
||||||
|
|
||||||
`host.grant` is a new action in `ACTIONS` and in neither `ROUTED` list, so it is a hard wait for every role
|
The request is a `credential.mint` decision, as Honker's REQ-GRANT-1 says. `credential.mint` is already a
|
||||||
(F3, `vocabulary.mjs:88`). PRD 1.7 has no requirement for it. Like `host.sudo` in 210 section 5, it needs a
|
hard wait (`vocabulary.mjs:38`, F10), so it classifies `gated` for every role and routes to the human (F3).
|
||||||
PRD line (section 8). It is never routed, because Q188 keeps grants with Jason.
|
No role file may list it (REQ-WAIT-2), so it can never be routed, and the hard-waits list doesn't change.
|
||||||
|
|
||||||
I chose a new action over `credential.mint` (F10). `credential.mint` names no host or argv, and a test pins
|
r1 used a new `host.grant` action because `credential.mint` names no host or argv. That reason doesn't
|
||||||
the hard-wait list, so either choice changes vocabulary. The narrower name keeps a future Gitea mint
|
hold. The binding lives in the raise's `grant` argument and the raise event body, not in the action name,
|
||||||
separate.
|
so G1 to G10 test the same things either way. The executor starts only for a decision whose raise carries
|
||||||
|
a `grant` binding.
|
||||||
|
|
||||||
|
A `credential.mint` raise without `grant` keeps today's behaviour. Any agent may raise one, Jason may
|
||||||
|
answer it, and nothing runs. It is a question for a hand-made credential, as it is now.
|
||||||
|
|
||||||
### R3. Who may raise it
|
### R3. Who may raise it
|
||||||
|
|
||||||
The broker refuses a `host.grant` raise unless the raiser's definition is `chief-of-staff`
|
The broker refuses a raise that carries `grant` unless the raiser's definition is `chief-of-staff`
|
||||||
(`b.roles[s.role].definition === STAFF_TO`, the check at `broker.mjs:593`). Refusal: `grant-chief-only`. A tester
|
(`b.roles[s.role].definition === STAFF_TO`, the check at `broker.mjs:593`). Refusal: `grant-chief-only`. A
|
||||||
thread can't raise it, because it routes to the human (`broker.mjs:1683`).
|
tester thread can't raise it, because it routes to the human (`broker.mjs:1683`). Any other action with a
|
||||||
|
`grant` argument refuses `invalid-request`.
|
||||||
|
|
||||||
### R4. What the raise binds
|
### R4. What the raise binds
|
||||||
|
|
||||||
New raise argument `grant`, required when `action === 'host.grant'` and refused otherwise:
|
New raise argument `grant`, allowed only with `action === 'credential.mint'`. For a host grant:
|
||||||
|
|
||||||
```
|
```
|
||||||
grant: { "agent": "<instance>", "host": "<hostId>", "commands": [["systemctl", "status"], ...],
|
grant: { "method": "host", "agent": "<instance>", "host": "<hostId>",
|
||||||
"rotateBy": "YYYY-MM-DD" }
|
"commands": [["systemctl", "status"], ...], "rotateBy": "YYYY-MM-DD" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`host-enroll` is E1's. A later draft adds `gitea` and `github`. Any other `method` refuses
|
||||||
|
`invalid-request`.
|
||||||
|
|
||||||
Checks at raise, all inside the raise transaction:
|
Checks at raise, all inside the raise transaction:
|
||||||
|
|
||||||
- `agent` is a role instance of this business, and its definition declares `host` (R1)
|
- `agent` is a role instance of this business, and its definition declares `host` (R1)
|
||||||
- `host` is enrolled (section 3)
|
- `host` is `ready` (E2, E4), or the raise refuses `host-not-ready`
|
||||||
- `commands` passes 210 D6's argv rules: no wildcard, no shell, no empty prefix, and the existing
|
- `commands` passes 210 D6's argv rules: no wildcard, no shell, no empty prefix, and the existing
|
||||||
refusals for `bash` and the like
|
refusals for `bash` and the like
|
||||||
- `rotateBy` is a date inside 210's rotation window
|
- `rotateBy` is a date inside 210's rotation window
|
||||||
|
|
||||||
The broker appends a rendered block to the question, as 210 D4 does for `host.sudo`: agent, host id,
|
The broker appends a rendered block to the question, as 210 D4 does for `host.sudo`: agent, host id,
|
||||||
address, user, each argv prefix JSON-quoted, the rotate date, and whether this is a new key or a change to
|
address, user, whether the user has sudo (from `host.enrolled`), each argv prefix JSON-quoted, the rotate
|
||||||
an existing grant's commands. The chief can't shape that block. The whole question keeps the 4096-character
|
date, and whether this is a new key or a change to an existing grant's commands. The chief can't shape that
|
||||||
cap (`broker.mjs:126`).
|
block. If the question with the block exceeds the 4096-character cap (`broker.mjs:126`), the raise refuses
|
||||||
|
`invalid-request`, as 210 D4 does (N2). The chief then splits the commands over two requests.
|
||||||
|
|
||||||
The raise event body gets `hostGrant = {agent, host, commandsSha256, rotateBy, registrySha256}`.
|
The raise event body gets `grant = {method, agent, host, commands, commandsSha256, rotateBy, registrySha256,
|
||||||
`registrySha256` hashes the host's registry entry at raise time. If ops-01 re-pins the host between raise and
|
definitionSha256}`. The full `commands` list is stored there, and the executor reads it from that event at
|
||||||
Approve, the executor refuses `decision-mismatch`, and the chief raises again.
|
X2 step 1 and checks it against `commandsSha256` (N2). `registrySha256` hashes the host's registry entry at
|
||||||
|
raise time, and `definitionSha256` hashes the agent's role definition file. REQ-GRANT-2 voids an approval
|
||||||
|
when the agent, its definition, the target or the rights change after the raise. If either hash differs at
|
||||||
|
X2 step 1, the executor refuses `decision-mismatch` and the chief raises again.
|
||||||
|
|
||||||
### R5. Jason decides
|
### R5. Jason decides
|
||||||
|
|
||||||
@@ -235,41 +366,58 @@ the client, so #435 needs no executor change.
|
|||||||
At resolve. Today a resolution runs nothing (F4), and the raiser comes back later to consume the decision.
|
At resolve. Today a resolution runs nothing (F4), and the raiser comes back later to consume the decision.
|
||||||
Here the raiser is the chief, and Q188 says no agent creates the grant, so the chief must not be the one
|
Here the raiser is the chief, and Q188 says no agent creates the grant, so the chief must not be the one
|
||||||
whose call does it. The bus child starts the executor itself after the human's `decision.resolve` commits,
|
whose call does it. The bus child starts the executor itself after the human's `decision.resolve` commits,
|
||||||
when all of these hold: the decision's action is `host.grant`, the route is `human`, the choice equals
|
when all of these hold: the decision's action is `credential.mint` and its raise carries `grant`, the route
|
||||||
`approvalChoice`, and `via === 'cli'`.
|
is `human`, the choice equals `approvalChoice`, and `via === 'cli'`. A resolve by an arbiter, the tester or
|
||||||
|
an agent starts nothing (REQ-GRANT-2).
|
||||||
|
|
||||||
The resolve call returns at once with `{grant: "started"}`. It does not wait on ssh. Jason sees the outcome
|
The resolve call returns at once with `{grant: "started"}`. It does not wait on ssh. Jason sees the outcome
|
||||||
in `mosaic hosts status` and as a message in his inbox from `system` when the executor ends.
|
in `mosaic hosts status` and as a message in his inbox from `system` when the executor ends.
|
||||||
|
|
||||||
The executor consumes the decision through the same `action.allowed` event `#consumeAuthority` writes
|
The executor can't use `#consumeAuthority`, because that needs the raiser's agent session (F4, N1). It gets
|
||||||
(`broker.mjs:808-831`), with `operation: 'host.grant'`, in a transaction before any remote step. A second
|
its own path, `#consumeGrant(business, decisionId, method)`. In one transaction it checks the decision is
|
||||||
start finds it spent and refuses `decision-consumed`. One executor runs at a time per (agent, host),
|
resolved yes by the human via cli, checks `#spent` the same way (`broker.mjs:808-831`), and writes the
|
||||||
through 210's busy map (210: `credentials.mjs:118`).
|
`action.allowed` event with `operation: 'grant.<method>'` and actor `system/grant`. A second start finds it
|
||||||
|
spent and refuses `decision-consumed`. It is a private method, and only the executor calls it.
|
||||||
|
|
||||||
### X2. Steps
|
### X2. Steps
|
||||||
|
|
||||||
1. **Recheck** inside one transaction: the decision is resolved yes by the human via cli and unspent, the
|
Before step 1 the executor claims the (agent, `host.<hostId>`) entry in `#inflight` (X5). A claim that
|
||||||
agent and its definition still declare `host`, the registry entry hashes to `registrySha256`, and no
|
finds an entry refuses `host-busy`, the same as 210's `use` (210: `credentials.mjs:355`).
|
||||||
revoke of this (agent, host) is newer than the raise. Then write the consume event. Any failure is
|
|
||||||
`grant.failed` with its code, and nothing remote has happened.
|
1. **Recheck and consume** inside one transaction: `#consumeGrant` (X1), the agent and its definition still
|
||||||
2. **Key.** For a new grant, `ssh-keygen -t ed25519 -N '' -C <tag>` in a fresh 0700 dir under
|
declare `host`, the definition hashes to `definitionSha256`, the registry entry hashes to
|
||||||
`<configDir>/secrets/run`, where tag is `mosaic-<business>-<agent>-<hostId>`. For a commands change
|
`registrySha256`, the host is `ready`, the `commands` in the raise event hash to `commandsSha256`, and no
|
||||||
with a live key, skip to step 5.
|
revoke of this (agent, host) is newer than the raise. Any failure is `grant.failed` with its code, and
|
||||||
3. **Install.** Connect as `I` with the installer key, pinned (210's options), and send
|
nothing remote has happened.
|
||||||
`add <user> <type> <base64> <tag>`. If the tag exists on the host, `remove` it first, because an old line
|
2. **Key.** The tag is `mosaic-` and the first 32 hex digits of sha256 over business, agent and hostId
|
||||||
under the same tag belongs to a key the stack no longer holds.
|
joined with a NUL byte. Names match `ID_PATTERN` (`vocabulary.mjs:143`), 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.
|
||||||
|
3. **Install.** Write `grant.step {decision, agent, host, user, tag, step: 'install'}`. Connect as
|
||||||
|
`mosaic-installer` with the installer key, pinned (210's options), and run `list <user>`. If the tag is
|
||||||
|
there, `remove` it first: the tag is this agent's on this host, and step 2 only runs when the stack holds
|
||||||
|
no live key for it. Then `add <user> <type> <base64> <tag>`.
|
||||||
4. **Verify.** Connect as `<user>` with the new key through a short-lived ssh-agent (210 D7) and run `true`.
|
4. **Verify.** Connect as `<user>` with the new key through a short-lived ssh-agent (210 D7) and run `true`.
|
||||||
The connection proves the line, the `from=` and the pin together. A failure here runs `remove <user> <tag>`
|
The connection proves the line, the `from=` and the pin together. A failure here runs `remove <user> <tag>`
|
||||||
and ends `grant.failed`.
|
and ends `grant.failed`.
|
||||||
5. **Business file.** One write through the grant writer (W1): `roles.<agent>.hosts.<hostId>.commands` set to
|
5. **Go live.** Write `grant.step {..., step: 'commit', previous}`, where `previous` is the grant's
|
||||||
the decided list, and for a new key `roles.<agent>.credentials["host.<hostId>"] = {"store": true,
|
`commands` before this decision, or `null` for a new key. Then check the abort signal (X5), and with no
|
||||||
"rotateBy": D}`.
|
`await` between, run 210 `issue(agent, "host.<hostId>", privateKey, rotateBy, commit)`. For a new key the
|
||||||
6. **Store.** 210 `issue(agent, "host.<hostId>", privateKey, rotateBy)` writes the key file and fills
|
reference is registered first (X4). `issue` writes the key file, then calls `commit`, which does one W1
|
||||||
memory. The tmp dir from step 2 is removed in a `finally`.
|
write (`roles.<agent>.hosts.<hostId>.commands` set to the decided list, and for a new key
|
||||||
7. **Event and message.** `grant.applied {decision, agent, host, fingerprint, commandsSha256}` and a message
|
`roles.<agent>.credentials["host.<hostId>"] = {"store": true, "rotateBy": D}`) and applies it to the
|
||||||
to the human and the chief. The private key appears in no event, message or log (210 D8's
|
broker (X4). Then `issue` fills memory. For a commands change, step 5 is the abort check, the W1 write
|
||||||
|
and the broker apply, with no `issue`. The tmp dir from step 2 is removed in a `finally`.
|
||||||
|
6. **Event and message.** `grant.applied {decision, agent, host, user, tag, fingerprint, commandsSha256}`
|
||||||
|
and a message to the human and the chief. The private key appears in no event, message or log (210 D8's
|
||||||
`assertCleanKey` runs on every event body).
|
`assertCleanKey` runs on every event body).
|
||||||
|
|
||||||
|
After step 6, or after any failure, the executor releases its `#inflight` entry in a `finally`.
|
||||||
|
|
||||||
|
The tag is recorded in `grant.step` and `grant.applied`, so `list` output and `sweep` can be matched to this
|
||||||
|
business's grants without trusting a comment on the host (B4).
|
||||||
|
|
||||||
### X3. Failure and restart
|
### X3. Failure and restart
|
||||||
|
|
||||||
The steps are ordered so a failure leaves nothing an agent can use:
|
The steps are ordered so a failure leaves nothing an agent can use:
|
||||||
@@ -280,47 +428,105 @@ The steps are ordered so a failure leaves nothing an agent can use:
|
|||||||
| 2 | tmp dir | `finally`, and the start sweep |
|
| 2 | tmp dir | `finally`, and the start sweep |
|
||||||
| 3 | maybe a line with a key nobody holds | `remove <user> <tag>`, best effort |
|
| 3 | maybe a line with a key nobody holds | `remove <user> <tag>`, best effort |
|
||||||
| 4 | a line, no grant | `remove`, best effort |
|
| 4 | a line, no grant | `remove`, best effort |
|
||||||
| 5 | a line, no grant | `remove`, best effort |
|
| 5 before `commit` | a line, a key file, no reference in the file | unlink the key file, unregister (X4), `remove` |
|
||||||
| 6 | a line, a grant, no key file: calls refuse `credential-unavailable` (199 D3a) | `remove`, then a writer pass that drops layers 3 and 4 |
|
| 5 after `commit` | a line, a grant, a key file | W1 back to `previous`, broker apply, 210 `revoke`, `remove` |
|
||||||
|
|
||||||
A best-effort `remove` that fails adds `hostLineLeft: <tag>` to `grant.failed`, and `status` shows it until
|
A best-effort `remove` that fails adds `hostLineLeft: <tag>` to `grant.failed`, and `status` shows it until
|
||||||
`mosaic hosts sweep <hostId>` (human or tester) succeeds. `sweep` runs `list <user>` and removes every
|
`mosaic hosts sweep <hostId>` (human or tester) succeeds. `sweep` runs `list <user>` and removes each line
|
||||||
`mosaic-` line whose tag has no live grant.
|
whose tag is in this business's recorded `grant.step` tags and has no live grant. A line with any other tag
|
||||||
|
is left alone, so a sweep can't touch another business's line on the same user (B4).
|
||||||
|
|
||||||
A bus child that dies mid-executor leaves a spent decision with no `grant.applied` or `grant.failed`. At start,
|
**Restart.** A bus child that dies mid-executor leaves a spent decision with no `grant.applied` or
|
||||||
the bus records `grant.failed {code: 'interrupted'}` for each such decision and runs no remote step. Jason
|
`grant.failed`. At start, before `Credentials` and the broker load, the bus reconciles each one from its last
|
||||||
gets a message. The chief raises again, and Jason approves again. I chose a new decision over an automatic
|
`grant.step` (or `host.enroll.step`), back to the state before the decision (B3):
|
||||||
retry because Jason approved one attempt at one moment, and a retry after a restart might be hours later.
|
|
||||||
|
| Last step | Local undo, before load | Remote undo, after the bus is up |
|
||||||
|
|---|---|---|
|
||||||
|
| none | none (the tmp dir goes in 210's run-dir sweep) | none |
|
||||||
|
| `install` | none | `remove <user> <tag>` |
|
||||||
|
| `commit`, new key | unlink the key file if present, W1 drops the grant and the reference if present | `remove <user> <tag>` |
|
||||||
|
| `commit`, commands change | W1 sets `commands` back to `previous` | none, the key and line didn't change |
|
||||||
|
| enrollment `registry` | delete the installer key if present, W1 drops the registry entry | none |
|
||||||
|
|
||||||
|
Then it records `grant.failed {code: 'interrupted', tag}` with the tag listed every time, and `hostLineLeft:
|
||||||
|
<tag>` if the remote undo failed. Jason gets a message. The chief raises again, and Jason approves again. I
|
||||||
|
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.
|
||||||
|
|
||||||
|
### X4. Taking effect in the running bus (B1)
|
||||||
|
|
||||||
|
The running bus never reloads the business file (F11), so each write has to reach memory in the same call:
|
||||||
|
|
||||||
|
- The broker gets one method, `applyGrant(business, delta)`, that sets or deletes exactly W1's keys in its
|
||||||
|
clone: `hosts.<hostId>` and `roles.<agent>.hosts.<hostId>`. It isn't a verb, and no request reaches it.
|
||||||
|
The executor, revoke, `hosts.enroll` and `hosts.remove` call it right after each W1 commit. The next
|
||||||
|
`host.run` reads the new entry (210: `broker.mjs:1731-1735`).
|
||||||
|
- `Credentials` gets `register(instance, service, ref)`, which runs the constructor's reference checks for
|
||||||
|
one entry (210: `credentials.mjs:128-143`, moved into a shared method) and sets `#refs`, and
|
||||||
|
`unregister`, which drops an entry with no token. The executor registers before `issue`, because `issue`
|
||||||
|
refuses an unknown service (210: `credentials.mjs:235-238`).
|
||||||
|
- Revoke keeps the reference, which reads as revoked with no file (199), so a later grant needs no new
|
||||||
|
`register`.
|
||||||
|
|
||||||
|
The broker's whole-business clone stays as it is. Only W1's keys move at runtime, so a hand edit of any
|
||||||
|
other key still needs a restart, as today.
|
||||||
|
|
||||||
|
### X5. Revoke while the executor runs (B2)
|
||||||
|
|
||||||
|
The executor's `#inflight` entry is an `AbortController` (210: `credentials.mjs:118`), the same entry that
|
||||||
|
`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`.
|
||||||
|
- 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`).
|
||||||
|
- 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).
|
||||||
|
|
||||||
|
While the executor holds the claim, a `host.run` for the same agent and host refuses `host-busy`. That only
|
||||||
|
matters for a commands change on a live key, and it lasts until the executor ends.
|
||||||
|
|
||||||
### W1. The grant writer
|
### W1. The grant writer
|
||||||
|
|
||||||
A second writer on the same lock, revision, tmp, rename and receipt steps as `organizationWriter.write`
|
A second writer on the same lock, revision, tmp, rename and receipt steps as `organizationWriter.write`
|
||||||
(F6), with its own key set:
|
(F6), with its own key set:
|
||||||
|
|
||||||
- `hosts.<hostId>`, written by `hosts.enroll` and `hosts.remove` only
|
- `hosts.<hostId>`, written by `hosts.enroll` (the executor) and `hosts.remove` only
|
||||||
- `roles.<agent>.hosts.<hostId>` and `roles.<agent>.credentials["host.<hostId>"]`, written by the executor
|
- `roles.<agent>.hosts.<hostId>` and `roles.<agent>.credentials["host.<hostId>"]`, written by the executor
|
||||||
and by revoke only
|
and by revoke only
|
||||||
|
|
||||||
The receipt's actor is `grant:<decisionId>`, `revoke:<by>` or `enroll:human`. No broker verb takes these
|
The receipt's actor is `grant:<decisionId>`, `enroll:<decisionId>`, `revoke:<by>`, `remove:<by>` or
|
||||||
keys from a request body. `organization.set`, `limits.set` and `instances.set` keep refusing `hosts`, which
|
`reconcile:<decisionId>`. No broker verb takes these keys from a request body. `organization.set`,
|
||||||
210 T3 already tests.
|
`limits.set` and `instances.set` keep refusing `hosts`, which 210 T3 already tests.
|
||||||
|
|
||||||
This replaces 210 section 4's hand edits for layers 2 to 4. A hand edit of the business file still works,
|
This replaces 210 section 4's hand edits for layers 2 to 4. A hand edit of the business file still works,
|
||||||
because the file is Jason's, and the loader checks it as 210 D3 does.
|
because the file is Jason's, and the loader checks it as 210 D3 does. It takes effect at the next restart.
|
||||||
|
|
||||||
## 6. Revoke
|
## 6. Revoke
|
||||||
|
|
||||||
Q188: instant, by Jason, Mos or the tester. `mosaic hosts revoke <agent> <hostId> --reason R`, with
|
Q188: instant, by Jason, Mos or the tester. `mosaic hosts revoke <agent> <hostId> --reason R`, with
|
||||||
`--tester` for the tester (210 D9 already gives the tester `revoke`). Mos revokes as the tester or through
|
`--tester` for the tester (210 D9 already gives the tester `revoke`). Mos revokes as the tester with
|
||||||
Jason's terminal. Q-M1 asks Mos which.
|
`--tester`, never from Jason's terminal (mos-1120, Q173). The authority is Jason's yes to Q10 point 6.
|
||||||
|
Every revoke is logged with who made it (REQ-GRANT-4).
|
||||||
|
|
||||||
|
Honker's REQ-GRANT-4 names "the user, the assistant or the tester". The assistant is a stack role
|
||||||
|
(`vocabulary.mjs:131`), and Q188 names Mos, who acts as the tester. This design gives revoke to the human
|
||||||
|
and the tester only. Q-S5 asks whether the assistant role should have it too.
|
||||||
|
|
||||||
Order, local first so the part that matters can't wait on the network:
|
Order, local first so the part that matters can't wait on the network:
|
||||||
|
|
||||||
1. 210 `revoke`: memory, abort the `host.run` in flight (SIGKILL to the ssh process group), unlink the key
|
1. 210 `revoke`: memory, abort the `host.run` or the executor in flight (X5), unlink the key file. The call
|
||||||
file. The call answers `credential-unavailable`. This is the "instant" part, and it doesn't touch the
|
answers `credential-unavailable`. This is the "instant" part, and it doesn't touch the network.
|
||||||
network.
|
2. The grant writer drops `roles.<agent>.hosts.<hostId>`, and `applyGrant` drops it from the broker (X4).
|
||||||
2. The grant writer drops `roles.<agent>.hosts.<hostId>`. The credential reference stays as a store
|
The credential reference stays as a store reference with no file, which reads as revoked (199). A new
|
||||||
reference with no file, which reads as revoked (199). A new grant needs a new decision.
|
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
|
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.
|
`grant.revoked {hostLineLeft: <tag>}`, and `sweep` clears it later.
|
||||||
|
|
||||||
@@ -330,8 +536,8 @@ something copied the key file before the revoke, which section 9's same-user lim
|
|||||||
Remote processes that the killed ssh started may keep running (210 section 6). Revoke cuts the stack's
|
Remote processes that the killed ssh started may keep running (210 section 6). Revoke cuts the stack's
|
||||||
connection, not what already runs on the host.
|
connection, not what already runs on the host.
|
||||||
|
|
||||||
`hosts remove` (E4) refuses while grants exist. Revoking the installer itself is `hosts remove` and ops-01
|
`hosts remove` (E4) refuses while grants exist or a line is left. Revoking the installer itself is
|
||||||
deleting the line.
|
`hosts remove` and ops-01 deleting the line.
|
||||||
|
|
||||||
## 7. Row 210's per-call decision becomes manual mode
|
## 7. Row 210's per-call decision becomes manual mode
|
||||||
|
|
||||||
@@ -343,28 +549,41 @@ What changes is when the class is `gated`:
|
|||||||
- Under PRD 1.7, `host.sudo` is a hard wait, so every call is gated. That is today.
|
- Under PRD 1.7, `host.sudo` is a hard wait, so every call is gated. That is today.
|
||||||
- Under PRD 1.8 with REQ-CRED-4, `host.sudo` is routed, and a definition that lists it within-role runs
|
- Under PRD 1.8 with REQ-CRED-4, `host.sudo` is routed, and a definition that lists it within-role runs
|
||||||
granted commands with no decision (210 section 5).
|
granted commands with no decision (210 section 5).
|
||||||
- Manual mode (Q188) puts the per-call decision back. I propose a business-file key, human-only and written
|
- Manual mode (Q188) puts the per-call decision back. Honker's REQ-TRUST-2 takes the list form: the
|
||||||
through the grant writer, `"manual": ["host.sudo"]`, a list of routed actions that classify `gated` for
|
organization file holds a list of actions in manual mode, empty by default, set only by the user. While
|
||||||
every role while listed. `#classification` checks it first (`broker.mjs:597-609`). A list rather than a
|
an action is listed, it classifies `gated` for every role. `#classification` checks the list first
|
||||||
flag, so Jason can make host commands manual without making every routed action manual.
|
(`broker.mjs:597-609`).
|
||||||
|
|
||||||
The manual mode key is PRD 1.8 territory. Honker drafts REQ-CRED-4 through Sage. I bring this proposal to that
|
Manual mode is built from REQ-TRUST-2, not in the grant rows (sage-1527 Q-S3).
|
||||||
draft and don't build it in the grant rows. Q-S3 asks Sage where it belongs.
|
|
||||||
|
|
||||||
## 8. PRD lines (proposal for 1.8, beside REQ-CRED-4, through Sage)
|
## 8. PRD lines
|
||||||
|
|
||||||
> **REQ-CRED-5.** Only the chief raises a host grant. Jason approves it, and stack code then creates the key,
|
Honker's PRD 1.8 draft (honker/row194-b2 3c30f1036) carries this design as REQ-GRANT-1 to -4, generalized to
|
||||||
> installs it on the host, and records the grant. No agent writes a grant or holds a host key's private half
|
all three methods. There is no REQ-CRED-5. Duckworth's PRD review (duckworth-0057) notes that this design
|
||||||
> outside the store. A grant names an agent, a host and exact argv prefixes. Revoke takes effect at once on
|
uses `credential.mint` with the method in the `grant` body, not a new `host.grant` action. Points r2 sends
|
||||||
> the stack's side, and the host line is removed when the host can be reached.
|
Honker to match:
|
||||||
|
|
||||||
I don't edit the PRD. Sage takes this to Mos with Honker's REQ-CRED-4.
|
- REQ-GRANT-2 says a host's identity comes from the host side "when the user enrolls it". Under mos-1120
|
||||||
|
the user approves the enrollment and the stack runs it, so "when the user approves its enrollment" fits
|
||||||
|
better.
|
||||||
|
- REQ-GRANT-4 names the assistant as a revoker. Q188 names Mos, who acts as the tester (Q-S5).
|
||||||
|
- An enrollment names a target user with sudo, and the Approve covers that user by name (E1, mos-1125).
|
||||||
|
|
||||||
|
I don't edit the PRD.
|
||||||
|
|
||||||
## 9. Limits, said plainly
|
## 9. Limits, said plainly
|
||||||
|
|
||||||
- **The installer key is root on its host.** It can add a line for a NOPASSWD sudo user. It sits in
|
- **The installer key is root on a host with a privileged user.** It can add a line for a user in
|
||||||
`<configDir>/secrets`, readable by the stack's OS user like every other key (199 D6). `mosaic-keys` limits
|
`privileged`, and that user has sudo. It sits in `<configDir>/secrets`, readable by the stack's OS user
|
||||||
which users it can touch, not what those users can do.
|
like every other key (199 D6). The writer limits which users it can touch, and a user with sudo is
|
||||||
|
touchable only after Jason approved that user by name in the enrollment (E1, E3). It doesn't limit what
|
||||||
|
those users can do.
|
||||||
|
- **A host line has no `command=`** (ops-01, mos-1125). `add` writes `from=` and `restrict` only, so the host
|
||||||
|
doesn't enforce the granted argv prefixes. The bus enforces them (210 D6). A copied agent key is a full
|
||||||
|
shell as that user from the stack's address. A forced command per key that checks
|
||||||
|
`SSH_ORIGINAL_COMMAND` against the granted list would close that, but it means the writer also carries
|
||||||
|
every commands change to the host, and 210's argv encoding has to round-trip through it. I'd make it a
|
||||||
|
follow-up row and not part of rows a and b.
|
||||||
- **The seat network isn't confined** (`seat/src/sandbox.mjs:29-31`, 210 section 6). An agent granted a
|
- **The seat network isn't confined** (`seat/src/sandbox.mjs:29-31`, 210 section 6). An agent granted a
|
||||||
command that writes a file as root can plant its own key and ssh around the bus. `from=` doesn't help,
|
command that writes a file as root can plant its own key and ssh around the bus. `from=` doesn't help,
|
||||||
because the sandbox shares the stack's address. A network-confined sandbox closes it (Sage's follow-up).
|
because the sandbox shares the stack's address. A network-confined sandbox closes it (Sage's follow-up).
|
||||||
@@ -373,45 +592,73 @@ I don't edit the PRD. Sage takes this to Mos with Honker's REQ-CRED-4.
|
|||||||
- **Approve is not "runs without a click"** until PRD 1.8 routes `host.sudo` (section 0).
|
- **Approve is not "runs without a click"** until PRD 1.8 routes `host.sudo` (section 0).
|
||||||
- **Enrollment still needs ops-01 once per host.** I don't see a way around it without a trust anchor on the
|
- **Enrollment still needs ops-01 once per host.** I don't see a way around it without a trust anchor on the
|
||||||
host, and I'd rather say so than claim the flow is hands-free.
|
host, and I'd rather say so than claim the flow is hands-free.
|
||||||
- **The pin trusts what Jason pastes.** Whoever writes the stdin of `hosts enroll` chooses the trusted key.
|
- **The pin trusts the path from ops-01 to the chief.** A key changed on the way makes E4 fail closed,
|
||||||
|
unless whoever changed it can also intercept the stack's connection to the host. Jason's Approve shows
|
||||||
|
the fingerprints, which he can compare with ops-01's if he wants to.
|
||||||
|
- **Tags assume one stack per business id.** Two stacks that both run a business with the same id and grant
|
||||||
|
the same agent name on the same host user would compute the same tag. Nothing runs two stacks today.
|
||||||
|
|
||||||
## 10. Tests and mutants (for the rows, not built here)
|
## 10. Tests and mutants (for the rows, not built here)
|
||||||
|
|
||||||
Tests use a scratch sshd on 127.0.0.1 with throwaway keys, as row 210 does, with `mosaic-keys` running as the
|
Tests use a scratch sshd on 127.0.0.1 with throwaway keys, as row 210 does, with `mosaic-keys` running as the
|
||||||
scratch sshd's forced command against a scratch `authorized_keys`. No real host is contacted.
|
scratch sshd's forced command. The writer's checks are tested by calling its functions with a temp dir as
|
||||||
|
the root and a fake group and sudo lookup. The installed entry point fixes the `/etc` paths and passes no
|
||||||
|
such argument. No real host is contacted.
|
||||||
|
|
||||||
| Test | Claim |
|
| Test | Claim |
|
||||||
|---|---|
|
|---|---|
|
||||||
| G1 | a `host.grant` raise from a role that isn't the chief refuses `grant-chief-only`, and nothing is stored |
|
| G1 | a `credential.mint` raise with `grant` from a role that isn't the chief refuses `grant-chief-only`, and nothing is stored. A `grant` on any other action refuses `invalid-request` |
|
||||||
| G2 | a raise for an agent whose definition lacks `host` refuses `host-not-declared` |
|
| G2 | a raise for an agent whose definition lacks `host` refuses `host-not-declared` |
|
||||||
| G3 | a raise with a wildcard, shell or empty argv prefix refuses, the same cases as 210 D6 |
|
| G3 | a raise with a wildcard, shell or empty argv prefix refuses, the same cases as 210 D6 |
|
||||||
| G4 | the rendered block is appended by the broker, the chief's question text can't forge it, and the 4096 cap holds |
|
| G4 | the rendered block is appended by the broker, the chief's question text can't forge it, and a question over 4096 characters with the block refuses `invalid-request` |
|
||||||
| G5 | `host.grant` classifies gated for every role and routes to `human`, and a tester thread can't raise it |
|
| G5 | a `credential.mint` raise with `grant` routes to `human`, a tester thread can't raise it, and an approved `credential.mint` without `grant` runs nothing |
|
||||||
| G6 | a yes from the human via cli starts the executor, while a no, a withdraw, or an arbiter resolve start nothing |
|
| G6 | a yes from the human via cli starts the executor, while a no, a withdraw, or an arbiter resolve start nothing |
|
||||||
| G7 | executor end to end on scratch sshd: line installed with the `from=` and tag, verify login works, layers 3 and 4 written with a receipt, key file 0600, `grant.applied` |
|
| G7 | executor end to end on scratch sshd: line installed with the `from=` and tag, verify login works, layers 3 and 4 written with a receipt, key file 0600, `grant.applied` with the tag |
|
||||||
| G8 | each failure row of X3 leaves the stated state, and a failed `remove` leaves `hostLineLeft` |
|
| G8 | each failure row of X3 leaves the stated state, and a failed `remove` leaves `hostLineLeft` |
|
||||||
| G9 | a registry change between raise and Approve refuses `decision-mismatch` before any remote step |
|
| 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` |
|
| G10 | a second start for the same decision refuses `decision-consumed`, and `#consumeGrant` refuses an unresolved or agent-resolved decision |
|
||||||
| G11 | a bus restart mid-executor gives `grant.failed interrupted` and runs no remote step |
|
| 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 |
|
| 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 |
|
||||||
| G13 | revoke with the host down still does steps 1 and 2 and reports `hostLineLeft` |
|
| 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` |
|
| G14 | `organization.set`, `limits.set` and `instances.set` still refuse `hosts` and `manual` |
|
||||||
| G15 | no event, message, log line or receipt holds private key material (210 D8 check on every body) |
|
| G15 | no event, message, log line or receipt holds private key material (210 D8 check on every body) |
|
||||||
| G16 | `mosaic-keys`: users outside its list, lines without the `mosaic-` prefix, bad tags and bad key text all refuse, and `from=` comes from the host file |
|
| G16 | `mosaic-keys` refuses bad requests without calling the writer. The writer refuses 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 | `hosts enroll` refuses loopback and this machine's names and bad key lines, and prints no private material |
|
| G17 | a `host-enroll` raise refuses loopback and this machine's names, bad key lines, an enrolled id and the reserved users. On Approve the executor writes the entry and the installer key 0600, records `host.enrolled`, and its message holds no private material |
|
||||||
| G18 | `hosts check` with a wrong pin exits 255, refuses `host-key-mismatch` and runs nothing |
|
| 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`, including after a hand edit changes the registry hash |
|
||||||
|
| G20 | `hosts.check` from the chief, the tester and the human marks the host `ready`, and 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) |
|
||||||
|
| 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) |
|
||||||
|
|
||||||
Mutants, a first list. The row's BUILD.md numbers them and adds more:
|
Mutants, a first list. The row's BUILD.md numbers them and adds more:
|
||||||
|
|
||||||
- drop the chief check
|
- drop the chief check
|
||||||
|
- start the executor on a `credential.mint` with no `grant`
|
||||||
|
- skip the `ready` check
|
||||||
|
- skip the definition hash compare
|
||||||
- drop the definition check
|
- drop the definition check
|
||||||
- skip the commands hash compare
|
- skip the commands hash compare
|
||||||
- skip the registry hash compare
|
- skip the registry hash compare
|
||||||
- swap the order of install and the business write
|
- swap the order of install and the business write
|
||||||
- skip `remove` on a verify failure
|
- skip `remove` on a verify failure
|
||||||
- skip the start sweep
|
- skip the start sweep
|
||||||
- drop `from=` in `mosaic-keys`
|
- skip the reconcile's remote `remove`
|
||||||
- let `mosaic-keys` touch an unprefixed line
|
- skip the `grant.step` before install
|
||||||
|
- skip `applyGrant` after a grant, and after a revoke
|
||||||
|
- skip `register` before `issue`
|
||||||
|
- drop the abort check before `issue`
|
||||||
|
- add an `await` between the abort check and `issue`
|
||||||
|
- revoke ignores a claim with no reference
|
||||||
|
- build the tag without the business
|
||||||
|
- `sweep` removes any `mosaic-` line
|
||||||
|
- drop `from=` in the writer
|
||||||
|
- let the writer touch a line outside the tag pattern
|
||||||
|
- drop `O_NOFOLLOW` on the user file
|
||||||
|
- drop one group from the writer's refused list
|
||||||
|
- let the writer accept a sudo user not in `privileged`
|
||||||
- run revoke's host step before the local steps
|
- run revoke's host step before the local steps
|
||||||
- start the executor on an arbiter resolve
|
- start the executor on an arbiter resolve
|
||||||
- start it on `no`
|
- start it on `no`
|
||||||
@@ -419,25 +666,40 @@ Mutants, a first list. The row's BUILD.md numbers them and adds more:
|
|||||||
|
|
||||||
## 11. Not in this draft
|
## 11. Not in this draft
|
||||||
|
|
||||||
- Gitea and GitHub grants. The same flow: chief raise, Jason approves, the executor mints a token through
|
- Gitea and GitHub grants (`method` `gitea` and `github`). The same flow: chief raise, Jason approves, the
|
||||||
an admin credential Jason enters, writes the reference, and revoke deletes the token at the service.
|
executor mints a token through an admin credential Jason enters, writes the reference, and revoke deletes
|
||||||
The admin scope is refused today (`vocabulary.mjs:105-106`), so that is a design of its own.
|
the token at the service. The admin scope is refused today (`vocabulary.mjs:105-106`), so that is a
|
||||||
|
design of its own.
|
||||||
- The Console Approve (#435) and the Console token entry. Both wait on Q175 sign-in (#412).
|
- The Console Approve (#435) and the Console token entry. Both wait on Q175 sign-in (#412).
|
||||||
- The network-confined sandbox (Sage's follow-up from 210 section 6).
|
- The network-confined sandbox (Sage's follow-up from 210 section 6).
|
||||||
|
- A per-key forced command on the host (section 9).
|
||||||
|
- Changing an enrolled host's user or sudo mark. Today that is `hosts remove` and a new enrollment.
|
||||||
|
|
||||||
## 12. Questions
|
## 12. Questions
|
||||||
|
|
||||||
- **Q-S1 (Sage).** Is "message the chief, and the definition gates the raise" enough for "up the chain"
|
Answered since r1:
|
||||||
in the first row? Or do you want the `grant.ask` verb (R1) now? It adds one verb and one table. I'd
|
|
||||||
start without it.
|
- Q-S1 (sage-1527): no `grant.ask` in row a. Messaging the chief, with the definition gating the raise, is
|
||||||
- **Q-S2 (Sage, Mos).** Where should installer keys live: `secrets/hosts/<business>/<hostId>/installer`
|
enough. If review finds a forgery path, the verb comes then. Duckworth agrees (N4).
|
||||||
(my proposal) or under `secrets/mosaic-stack/`?
|
- Q-S2 (mos-1120, sage-1527): `<configDir>/secrets/hosts/<business>/<hostId>/installer`.
|
||||||
- **Q-S3 (Sage).** Does manual mode (section 7) go into Honker's PRD 1.8 draft beside REQ-CRED-4, and is
|
- Q-S3 (sage-1527): manual mode is Honker's REQ-TRUST-2, and REQ-CRED-5 is folded into REQ-GRANT-1 to -4.
|
||||||
REQ-CRED-5 (section 8) one line Sage carries with it?
|
- Q-S4 (sage-1527): the split stands, both rows are mine, and Duckworth reviews both.
|
||||||
- **Q-S4 (Sage).** My suggested split, yours to size. Row a: enrollment, `mosaic-keys`, the grant writer,
|
- Row a: enrollment as the approved request, the four `tools/host` files, the grant writer, `applyGrant`
|
||||||
and `hosts status`. Row b: the `host.grant` action, the raise binding, the executor and revoke. Row b
|
and `hosts status`, about 1 day.
|
||||||
waits on row a and on 210's landing. Both need the PRD line before landing, as 210 did.
|
- Row b: the raise binding, the executor, reconcile and revoke, after row a, about 1.5 days.
|
||||||
- **Q-M1 (Mos).** How does Mos revoke: as the tester (`--tester`) or from Jason's terminal?
|
- Both come after row 210 lands and need the 1.8 line before landing. Target: in review by 10-24.
|
||||||
- **Q-O1 (ops-01, through Mos).** Is a root-owned forced-command script with a host-side user list and
|
- Q-M1 (mos-1120): Mos revokes as the tester, with `--tester`.
|
||||||
`from=` file acceptable on the hosts? If ops-01 would rather run something else on the host, the
|
- Q-O1 (ops-01 through mos-1125): yes, with the six changes now in E3.
|
||||||
stack-side design stays the same as long as the three requests in E3 behave as stated.
|
|
||||||
|
Open:
|
||||||
|
|
||||||
|
- **Q-O2 (ops-01).** mos-1125's conflict: the writer refuses every sudo user, and a `host.sudo` grant needs
|
||||||
|
one. r2 takes Mos's recommendation. The enrollment names the user and marks sudo, Jason's Approve covers
|
||||||
|
it, ops-01 writes it to `/etc/mosaic-keys/privileged`, and the writer refuses a sudo user not in that
|
||||||
|
file. I've asked ops-01 to confirm.
|
||||||
|
- **Q-M2 (Mos).** How do ops-01's host keys and host details reach the chief? I don't know whether ops-01 is
|
||||||
|
a role instance in the business that can message the chief, or whether Mos relays them as the tester.
|
||||||
|
The design only needs them in the chief's raise. E4 fails closed on a changed key either way.
|
||||||
|
- **Q-S5 (Sage, Honker).** REQ-GRANT-4 lists the assistant as a revoker, while Q188 names Jason, Mos and the
|
||||||
|
tester. Should the assistant role get `hosts.revoke`? Taking access away needs no new trust, so I'd allow
|
||||||
|
it if Jason wants it, but it is a new authority for a stack role and should be his call.
|
||||||
Reference in new issue
Block a user