secrets: validate CENTRALIS_TEAM at render time, like CENTRALIS_TOKENS #144

Open
admin_emi wants to merge 1 commit from feat/centralis-team-validation into dev
Owner

CENTRALIS_TOKENS has a parse-and-prove check in both renderers because it is JSON, full of double quotes, living in a file bash expands, and because Centralis fails closed on it. CENTRALIS_TEAM is the same kind of value with none of the same guard.

It is the allowlist for Google sign-in. Centralis' buildOAuth() requires a Google client and at least one team member, so an empty or malformed team does not fail loudly — it turns OAuth off and leaves the server bearer-token-only, which looks exactly like never having configured it.

The sharper failure is a partial one. parseTeam() (centralis src/config.ts:381-423) skips an unparseable entry with a console.error and carries on, so an address with a typo'd domain drops exactly one person, behind a log line on a container start nobody watches. Entries parseTeam would skip are therefore refused here, where a human is reading the output.

Accepts all three shapes parseTeam accepts — email->role, email->object, and an array of objects — and matches its case-insensitive duplicate detection. Rejects invalid JSON, a non-object/array, an entry without an @, a duplicate address, and an empty team.

Added to both renderers with identical logic (verified by diffing the blocks with comments stripped). The parity argument this migration rests on is that the two backends produce the same file; a validation present on only one side means one backend can write a file the other would have refused.

Verification

Unit-tested the validator against 10 inputs — all three valid shapes, role-omitted, unset, invalid JSON, non-email, duplicate address, empty object, scalar — all behave as intended.

End to end against the live vault with verify-centralis-render.sh prod:

9 keys quoted
CENTRALIS_TOKENS parses: 2 subject(s) — emi, jin
9 keys survive `set -a; . <file>` byte-for-byte
VERDICT: PASS — the render is value-identical to the live file
compose model : IDENTICAL (docker compose config digests match)

That is also the first time the Centralis renderer has been run against a live vault since it was merged.

`CENTRALIS_TOKENS` has a parse-and-prove check in both renderers because it is JSON, full of double quotes, living in a file bash expands, and because Centralis fails closed on it. `CENTRALIS_TEAM` is the same kind of value with none of the same guard. It is the allowlist for Google sign-in. Centralis' `buildOAuth()` requires a Google client **and** at least one team member, so an empty or malformed team does not fail loudly — it turns OAuth off and leaves the server bearer-token-only, which looks exactly like never having configured it. The sharper failure is a partial one. `parseTeam()` (centralis `src/config.ts:381-423`) **skips** an unparseable entry with a `console.error` and carries on, so an address with a typo'd domain drops exactly one person, behind a log line on a container start nobody watches. Entries `parseTeam` would skip are therefore refused here, where a human is reading the output. Accepts all three shapes `parseTeam` accepts — `email->role`, `email->object`, and an array of objects — and matches its case-insensitive duplicate detection. Rejects invalid JSON, a non-object/array, an entry without an `@`, a duplicate address, and an empty team. Added to **both** renderers with identical logic (verified by diffing the blocks with comments stripped). The parity argument this migration rests on is that the two backends produce the same file; a validation present on only one side means one backend can write a file the other would have refused. ### Verification Unit-tested the validator against 10 inputs — all three valid shapes, role-omitted, unset, invalid JSON, non-email, duplicate address, empty object, scalar — all behave as intended. End to end against the live vault with `verify-centralis-render.sh prod`: ``` 9 keys quoted CENTRALIS_TOKENS parses: 2 subject(s) — emi, jin 9 keys survive `set -a; . <file>` byte-for-byte VERDICT: PASS — the render is value-identical to the live file compose model : IDENTICAL (docker compose config digests match) ``` That is also the first time the Centralis renderer has been run against a live vault since it was merged.
admin_emi added 1 commit 2026-08-01 15:51:48 +00:00
secrets: validate CENTRALIS_TEAM at render time, like CENTRALIS_TOKENS
All checks were successful
PR build (required check) / changes (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 2s
2194aaa1c4
CENTRALIS_TOKENS has a parse-and-prove check in both renderers because it is
JSON, full of double quotes, living in a file bash expands, and because
Centralis fails closed on it. CENTRALIS_TEAM is the same kind of value with
none of the same guard.

It is the allowlist for Google sign-in. Centralis' buildOAuth() requires a
Google client AND at least one team member, so an empty or malformed team does
not fail loudly -- it turns OAuth off and leaves the server bearer-token-only,
which looks exactly like never having configured it.

The sharper failure is a partial one. parseTeam() (centralis src/config.ts)
SKIPS an unparseable entry with a console.error and carries on, so an address
with a typo'd domain drops exactly one person, behind a log line on a container
start nobody watches. Entries parseTeam would skip are therefore refused here,
where a human is reading the output.

Accepts all three shapes parseTeam accepts -- email->role, email->object, and
an array of objects -- and matches its case-insensitive duplicate detection.
Rejects invalid JSON, a non-object/array, an entry without an @, a duplicate
address, and an empty team. Prints member addresses and roles on success, the
same argument the adjacent CENTRALIS_TOKENS line makes for printing subjects:
seeing the list is how an operator notices somebody is missing.

Added to BOTH renderers with identical logic. The parity argument this
migration rests on is that the two backends produce the same file; a validation
present on only one side means one backend can write a file the other would
have refused.

Verified end to end against the live vault with verify-centralis-render.sh:
9 keys, 2 subjects, VERDICT PASS, compose model digests identical.
admin_emi force-pushed feat/centralis-team-validation from 2194aaa1c4 to 7b3ea69ef9 2026-08-01 15:58:01 +00:00 Compare
All checks were successful
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 2s
Required
Details
This pull request is blocked because it's outdated.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/centralis-team-validation:feat/centralis-team-validation
git checkout feat/centralis-team-validation
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Jinemi/thermograph#144
No description provided.