| 1 | # Census: carlos system box publish-record |
| 2 | |
| 3 | Status: RULED as census-ruling-box-publish-record (platform-1e, R1-R4), on |
| 4 | ea68e8a6. This note records the ruling and the two fix rounds after it: |
| 5 | 3165a3bf applied the ruling, and the second round replaced exact evidence |
| 6 | matching with enrollment's own verifier. platform-1e rules as the host |
| 7 | claim's owner. |
| 8 | |
| 9 | Branch `box-publish-record`, cut from main at b1118925 (region HA 2b). |
| 10 | |
| 11 | ## The ruling |
| 12 | |
| 13 | - **R1.** The verb refuses euid 0 before any store, IMDS or host read. Root |
| 14 | cannot reach `publishBoxRecord` through it. The overlay header records |
| 15 | "root unreachable: verb refuses euid 0 (R1)" rather than a unit row. |
| 16 | - **R2.** The one supported way to run it: |
| 17 | `systemd-run --wait --pipe -p EnvironmentFile=/etc/carlos/host.env -p User=carlos /opt/carlos/carlos system box publish-record`. |
| 18 | - **R3.** No region HA gate. Run after the loop has published, the verb is a |
| 19 | no-op: it keeps the loop's record and writes nothing. |
| 20 | - **R4.** `boxRecordStore.Put` is `nonrecord`, on condition that |
| 21 | `boxRecordStore` is the only store the verb hands `publishBoxRecord`. The |
| 22 | store-guard test asserts zero S3 requests for a delete, a list, an |
| 23 | overwrite and any other key. |
| 24 | |
| 25 | ## What changed |
| 26 | |
| 27 | `carlos system box publish-record` makes one `publishBoxRecord` attempt on a |
| 28 | box, by hand, without `boxRecordLoop`'s `CARLOS_REGION_HA` gate. The dev claim |
| 29 | trial needs records at `control/boxes/<box>.json` for leo-climbing |
| 30 | (carlos-dev-ie-b) and sophie-knitting (carlos-dev-ie). Region HA stays off on |
| 31 | dev. |
| 32 | |
| 33 | - `internal/host/boxpublish.go` (new): `RunSystemBoxPublishRecord`, |
| 34 | `boxPublishRun`, `heldEvidence`, `boxRecordStore` and the verb's lines. |
| 35 | - `system_roles.go`: `runSystemBoxVerb` hands `box publish-record` to the |
| 36 | host verb before `roleArgs`. |
| 37 | - `system.go`: `systemUsage` gains the synopsis and description lines. |
| 38 | - `internal/host/boxrecord.go` is not edited. `publishBoxRecord`, |
| 39 | `boxRecordLoop` and its gate are byte for byte main's. |
| 40 | |
| 41 | The verb's checks, in order: |
| 42 | |
| 43 | 1. The effective uid is not 0 (R1). |
| 44 | 2. The box is not a fleet box (`shouldSkipPublisherElection`). |
| 45 | 3. `CARLOS_DEPLOYMENT_BUCKET` is set. A deployment directory is refused: its |
| 46 | check-then-rename cannot refuse an overwrite, and the verb never opens one. |
| 47 | 4. The box's label is `metricsBoxLabel`, the loop's, and it is a valid box |
| 48 | reference. This is the first host read (`/etc/machine-id`, or IMDS for a |
| 49 | group box). |
| 50 | 5. The store is opened with `openDeployStoreEnv` and must be a |
| 51 | `release.S3Store`. |
| 52 | 6. The record under the key is read through `boxRecordStore`, then this |
| 53 | box's record is read from IMDS once. |
| 54 | 7. If a record is there, enrollment's own verifier decides |
| 55 | (`heldEvidence`). It calls `boxid.Verifier.Enroll` with the account from |
| 56 | this box's current IMDS identity and `boxid.EmbeddedCerts`. The `Expect` |
| 57 | holds the box label, the current AWS region, and the current instance id |
| 58 | as `TofuInstanceID`. Enroll checks: |
| 59 | - a strict version 1 envelope naming this box, with no other fields and |
| 60 | nothing after it; |
| 61 | - AWS's RSA-SHA256 signature under the certificate for the document's |
| 62 | region; |
| 63 | - the account, the region and the instance. |
| 64 | |
| 65 | There is no freshness check. An older record whose `pendingTime` or |
| 66 | `instanceType` has since changed, or whose `written_at` is old, is kept. |
| 67 | Its bytes are never touched, because the record cannot be replaced. |
| 68 | Anything Enroll refuses is refused, with the reason named, and nothing is |
| 69 | written. Keeping a record means only that valid evidence is in place. The |
| 70 | roster and operator checks are still enrollment's. |
| 71 | 8. If nothing is there, `publishBoxRecord` runs through `boxRecordStore` with |
| 72 | the record already read. Its outcome is reported by what reached the |
| 73 | bucket. A failure before any create says nothing was written. A failure |
| 74 | after a create reached the bucket says the write may have happened. A |
| 75 | create answered 412 whose stored bytes are exactly this run's (a retried |
| 76 | create) is reported as published. A record that arrived after step 6 is |
| 77 | held to the same verifier. |
| 78 | |
| 79 | ## Root-read census (TestPlan14RootReadCensus) |
| 80 | |
| 81 | Overlay `internal/provision/testdata/plan14-root-reads-box-publish-record.tsv`, |
| 82 | loaded after `plan14-root-reads-region-ha-2b.tsv`. |
| 83 | |
| 84 | The scan finds no read site in any new or changed unit, so no root-read unit |
| 85 | is added or changed. The only discrepancies were the two source envelopes. |
| 86 | |
| 87 | | Unit | Disposition | Reason | |
| 88 | |---|---|---| |
| 89 | | `@coverage .` | re-pinned to `d123dce7…5402` | `system.go` (usage lines) and `system_roles.go` (dispatch) changed. Neither adds a read. | |
| 90 | | `@coverage internal/host` | re-pinned to `b1fb4b84…7ab3` | `boxpublish.go` is new. Its units have no read site of their own, and root is refused before any read (R1). | |
| 91 | | `internal/boxid/certs.go:EmbeddedCerts` | unchanged, accepted-legacy (J4, root-input) | The verb now calls it for the certificates enrollment verifies with. Its reads are the binary's own embedded files, its digest is unchanged, and root never reaches it through the verb (R1). | |
| 92 | | `internal/boxid` envelope | unchanged (`0c6243af…3794`) | `Verifier.Enroll` has no read site. internal/host already imported internal/boxid (`claimproduction.go` calls `EmbeddedCerts`), so the verb's import graph gains no package. Only `crypto/x509` is added, from the standard library. | |
| 93 | | legacy seal | unchanged (`fc584e11…1b5d`) | The overlay adds no `@legacy` row. | |
| 94 | |
| 95 | ## Record-write census (TestPlan9B8RecordWriteCensus) |
| 96 | |
| 97 | The inventory is re-pinned by `internal/provision/box_publish_record_census_test.go` |
| 98 | (`plan14BoxPublishRecordWriteCensusInventory`, which the test uses). It |
| 99 | appends one entry to `plan14ActivationToolsWriteCensusInventory`. |
| 100 | |
| 101 | | Unit | Kinds | Class | Reason | |
| 102 | |---|---|---|---| |
| 103 | | `internal/host/boxpublish.go:boxRecordStore.Put` | `def:Put,sel:Put` | `nonrecord` (R4) | It writes `control/boxes/<box>.json`, a box identity record, never an instance or sidecar record. It touches no record-key constructor or record path literal. It admits only a create-only Put on its one key. | |
| 104 | |
| 105 | Digest `866978d6c6786a351bbeaf6f9e5382df0b3fd08cbb47aed0c01738f7c5120d7a`. |
| 106 | |
| 107 | Before this branch, the one writer of `control/boxes/` was `boxid.Publish`, |
| 108 | reached only from `publishBoxRecord` in `boxRecordLoop`. The verb is a second |
| 109 | caller of the same chain, not a second writer: `boxid.Publish` (`nonrecord`, |
| 110 | `sel:Put`) is unchanged. `boxRecordStore` wraps the store the chain writes |
| 111 | through, and refuses before the bucket any Put on another key, any Put with an |
| 112 | etag, and every `Delete`, `List` and `ListDirs`. `Get` is admitted on the one |
| 113 | key only. |
| 114 | |
| 115 | ## Tests behind the claims |
| 116 | |
| 117 | `internal/host/boxpublish_test.go` runs the real `S3Store` (through |
| 118 | `openDeployStoreEnv`) against `leasetest`'s S3 server. The server sits behind |
| 119 | a fake of the dev bucket's policy, which denies a PUT under `control/boxes/` |
| 120 | without `If-None-Match: *` and any DELETE there. |
| 121 | |
| 122 | - Fresh box: the record is written at the right key with the right fields, |
| 123 | by one create-only PUT. |
| 124 | - After the loop has published (R3): kept, no PUT, bytes unchanged. |
| 125 | - An older record, correctly signed for the same account, region and |
| 126 | instance but with a different `pendingTime` and `instanceType`: kept as |
| 127 | it is, no PUT, bytes unchanged. |
| 128 | - Anything enrollment's verifier refuses is refused, named, with no PUT and |
| 129 | bytes unchanged. The signing certificates are generated the way boxid's |
| 130 | own tests generate them. The cases: |
| 131 | - a different account; |
| 132 | - a different region (one that has a certificate); |
| 133 | - another instance; |
| 134 | - a bad signature (another key); |
| 135 | - a missing signature; |
| 136 | - a document altered after signing; |
| 137 | - a different box; |
| 138 | - version 2; |
| 139 | - an unknown field; |
| 140 | - trailing data; |
| 141 | - not JSON; |
| 142 | - a document that is not JSON. |
| 143 | - A record arriving between the verb's read and `publishBoxRecord`'s is held |
| 144 | to the same verifier: a valid older one is kept, a forged one refused. |
| 145 | - The embedded production certificates cover eu-west-1. |
| 146 | - A different record created just before the PUT: the bucket's 412 stops it |
| 147 | and nothing is overwritten. |
| 148 | - A create committed but answered 500: the SDK retries, gets 412, and the |
| 149 | verb reports published. |
| 150 | - A create committed but answered 400: the verb says the write may have |
| 151 | happened, and a second run reports it in place. |
| 152 | - Root (R1), a fleet box, and no S3 bucket (a directory set, or a non-S3 |
| 153 | store) are each refused before the machine id, the store or IMDS is |
| 154 | touched, with zero bucket requests. The directory is never written. |
| 155 | - A group box publishes under its group reference, the loop's label. |
| 156 | - `boxRecordStore` (R4): zero S3 requests for a delete, a list, an |
| 157 | overwrite, another box's key and any other key. |
| 158 | - `boxRecordLoop` still returns at once with region HA off, unparseable, or |
| 159 | on a fleet box, and sends nothing. |
| 160 | |
| 161 | `system_roles_test.go` runs `carlos system box publish-record` from |
| 162 | `runSystem` in a child process, and checks the refusal and the exit status. |
| 163 | |