| index b2a623761..115d39ed8 100644 |
| --- a/internal/edge/storedoor.go |
| +++ b/internal/edge/storedoor.go |
| @@ -459,7 +459,7 @@ func aclDir(sqid, fleet, dir string) bool { |
| // only two: |
| // |
| // fleets/<fleet>/** its own corner |
| -// apps/<sqid>/<app>/logs/** its own account's app logs (applog.Prefix) |
| +// apps/<sqid>/<app>/<family>/** its own account's app logs, errors, analytics (applog.Prefix) |
| // |
| // It can never write a release artifact, a channel pointer, or an instance |
| // record, which is the tamper surface the whole spec exists to close (F6: |
| @@ -469,15 +469,15 @@ func aclDir(sqid, fleet, dir string) bool { |
| // and a box that could write one could mint or steal any hostname on the |
| // platform, not just its own account's. |
| // |
| -// The logs scope is SEGMENT-EXACT on purpose, and the exactness is the |
| -// security property: the grant is apps/<sqid>/<ONE app segment>/logs/**, never |
| -// apps/<sqid>/** and never a "logs/ appears somewhere in the key" test. A |
| -// blanket own-account write would let a box pin its own channels/*.json (self |
| -// -promoting any build it likes) or mint instances/*.json records |
| -// (self-authorizing routes) — precisely the writes this door exists to |
| -// prevent. Log chunks are the one thing a box legitimately produces about its |
| -// own tenant's apps (applog.ChunkKey), and they are write-once objects under a |
| -// per-box subtree. |
| +// The family scope is SEGMENT-EXACT on purpose, and the exactness is the |
| +// security property: the grant is apps/<sqid>/<ONE app segment>/<ONE family>/**, where |
| +// family is one of [logs, errors, analytics], never apps/<sqid>/** and never |
| +// a "family appears somewhere in the key" test. A blanket own-account write |
| +// would let a box pin its own channels/*.json (self-promoting any build it |
| +// likes) or mint instances/*.json records (self-authorizing routes) — |
| +// precisely the writes this door exists to prevent. Chunks and day files are |
| +// the one thing a box legitimately produces about its own tenant's apps |
| +// (applog.ChunkKey), and they are write-once objects under a per-box subtree. |
| // |
| // HONEST LIMIT: the <box> segment inside that subtree is an unverifiable |
| // pseudonym (status.Label of a machine id the box chose), so one box of an |
| @@ -493,15 +493,21 @@ func aclWrite(sqid, fleet, key string) bool { |
| if strings.HasPrefix(key, "fleets/"+fleet+"/") { |
| return true |
| } |
| - return aclWriteAppLogs(sqid, key) |
| + return aclWriteAppFamily(sqid, key) |
| } |
| |
| -// aclWriteAppLogs implements aclWrite's second scope: exactly |
| -// apps/<sqid>/<app>/logs/<something>, with <app> a single non-empty segment |
| -// and at least one key component below logs/. Anything else — a shorter key, |
| -// a second path shape, "logs" as the app segment — is refused, so the grant |
| -// can never be widened by a key that merely contains the word. |
| -func aclWriteAppLogs(sqid, key string) bool { |
| +// chunkFamilies is the allow-list of app subtrees a box may write: the |
| +// chunk and day-file families it produces about its own tenant's apps |
| +// (applog.Family). Exact segment names — "logs" is not "logsx". |
| +var chunkFamilies = map[string]bool{"logs": true, "errors": true, "analytics": true} |
| + |
| +// aclWriteAppFamily implements aclWrite's second scope: exactly |
| +// apps/<sqid>/<app>/<family>/<something>, with <app> a single non-empty |
| +// segment, <family> one of chunkFamilies, and at least one key component |
| +// below the family. Anything else — a shorter key, a second path shape, |
| +// a family name as the app segment — is refused, so the grant can never be |
| +// widened by a key that merely contains the word. |
| +func aclWriteAppFamily(sqid, key string) bool { |
| if sqid == "" { |
| return false |
| } |
| @@ -513,8 +519,8 @@ func aclWriteAppLogs(sqid, key string) bool { |
| if !ok || app == "" { |
| return false |
| } |
| - below, ok := strings.CutPrefix(tail, "logs/") |
| - return ok && below != "" |
| + fam, below, ok := strings.Cut(tail, "/") |
| + return ok && chunkFamilies[fam] && below != "" |
| } |
| |
| // aclListScope maps a requested listing prefix onto the prefix the door will |