mirror of
https://github.com/Sea-Haven-Industries/seahaven-door-unlock-api.git
synced 2026-09-30 04:53:10 +00:00
* feat(phones): add T57W door-unlock-with-sp template * feat: add firmware dir and add latest 3cx supported yealink firmware
113 lines
5.9 KiB
Markdown
113 lines
5.9 KiB
Markdown
# Door-Unlock Token Rotation & History Scrub — INFRA-105
|
||
|
||
> **Status: EXECUTED 2026-07-06.** Token rotated (SSM `/seahaven/door-unlock/auth-token` v3,
|
||
> 16:23 ET), all phones re-provisioned, git history scrubbed with `git filter-repo` and
|
||
> force-pushed (main + this branch + all tags), and the three token-validating Lambdas
|
||
> force-recycled to flush the never-expiring in-memory token cache (see step 1 note). The
|
||
> previously committed token is invalid and no longer present on any origin ref.
|
||
>
|
||
> **Residual (accepted):** GitHub-controlled read-only `refs/pull/*` refs still reference
|
||
> pre-scrub commits (only GitHub Support can purge); harmless since the token is rotated.
|
||
|
||
## Background
|
||
|
||
The Yealink Push-XML provisioning templates hard-coded the live door-system auth token in
|
||
plaintext, in both the `/unlock` and `/lockdown` query strings, across:
|
||
|
||
- `yealinkT58W-door-unlock.ph.xml`
|
||
- `yealinkT54W-door-unlock.ph.xml`
|
||
- `yealinkT54W-door-unlock-with-sp.ph.xml`
|
||
- `yealinkT57W-door-unlock-with-sp.ph.xml`
|
||
|
||
The token is the exact secret the API Gateway authorizer (`lambda/authorizer/authorizer-handler.ts`)
|
||
validates against SSM SecureString `/seahaven/door-unlock/auth-token` via `timingSafeEqual`.
|
||
It is present in git history since the first template commit `6d5f665`. Anyone with repo read
|
||
access (or history) could unlock the door or trigger building lockdown over the internet.
|
||
|
||
This branch replaces the token with the placeholder `__DOOR_UNLOCK_TOKEN__`. The real value is
|
||
injected at provisioning time and must never be committed again.
|
||
|
||
## Cutover procedure (run in a maintenance window)
|
||
|
||
### 1. Rotate the token
|
||
|
||
```sh
|
||
NEW_TOKEN=$(openssl rand -hex 32)
|
||
aws ssm put-parameter \
|
||
--name /seahaven/door-unlock/auth-token \
|
||
--type SecureString --overwrite \
|
||
--value "$NEW_TOKEN" --region us-east-1
|
||
|
||
# CRITICAL: the SSM overwrite ALONE does NOT invalidate the old token.
|
||
# The authorizer + unlock + lockdown handlers cache the token in module scope with
|
||
# NO TTL (authorizer-handler.ts getAuthToken, unlock/lockdown loadSecrets), so any warm
|
||
# Lambda container keeps honoring the OLD token until AWS recycles it — unbounded, up to
|
||
# hours on a low-traffic API. You MUST force a cold start to guarantee invalidation:
|
||
for fn in door-unlock-api-authorizer door-unlock-api-unlock door-unlock-api-lockdown; do
|
||
aws lambda update-function-configuration --region us-east-1 \
|
||
--function-name "$fn" --description "token-rotation $(date -u +%Y-%m-%dT%H%M%SZ)"
|
||
done
|
||
# (The API Gateway authorizer result cache is a separate 5-min TTL keyed on the presented
|
||
# token string; stale ALLOW entries for the old token expire within 5 min on their own.)
|
||
```
|
||
|
||
> **Design debt (INFRA-105 follow-up):** give the module-scope token cache a short TTL (or
|
||
> read SSM per-invocation) so future rotations are self-healing and this manual cold-start
|
||
> step is unnecessary. Tracked as a confirmed HIGH from the 2026-07-06 `/sh-security-review`.
|
||
|
||
### 2. Re-provision all Yealink phones
|
||
|
||
Render each template with the new token (do NOT commit the rendered output):
|
||
|
||
```sh
|
||
for f in yealink*.ph.xml; do
|
||
sed "s/__DOOR_UNLOCK_TOKEN__/$NEW_TOKEN/g" "$f" > "/tmp/rendered-$f"
|
||
done
|
||
```
|
||
|
||
Push `/tmp/rendered-*` to the phones via the 3CX/Yealink provisioning path. Verify one phone's
|
||
`/unlock` key works against the new token before doing the rest. Delete the rendered files after.
|
||
|
||
> The token must physically live on the phones — type-17 URL keys can't send headers — so the
|
||
> rendered XML always contains the secret. Keep it out of source control and off shared storage.
|
||
|
||
### 3. Scrub git history
|
||
|
||
```sh
|
||
# git-filter-repo (preferred)
|
||
# Put the old 64-hex token value in a gitignored/temp file, never inline in this doc:
|
||
git filter-repo --replace-text <(printf '%s==>__DOOR_UNLOCK_TOKEN__\n' "$OLD_TOKEN")
|
||
# then force-push every ref; coordinate with anyone holding clones (they must re-clone)
|
||
git push --force --all && git push --force --tags
|
||
```
|
||
|
||
> **Do it in a mirror clone** (`git clone --mirror`), scrub there, and force-push, rather than
|
||
> filter-repo'ing your working clone. Then re-sync your local clone: delete stale local tags and
|
||
> `git fetch --tags --force`, and delete any local branches whose tips predate the scrub (they
|
||
> retain the old blob even after the scrub). Verify: `for r in $(git for-each-ref
|
||
> --format='%(refname)'); do git grep -q "$OLD_TOKEN" "$r" && echo "HIT $r"; done`.
|
||
|
||
After force-push, the old token is gone from history but is **already compromised** — rotation
|
||
(step 1, including the forced Lambda cold start) is what actually invalidates it, not the scrub.
|
||
Note `refs/pull/*` on GitHub are read-only and cannot be force-updated — they retain the old
|
||
blob until GitHub Support purges them; acceptable once the token is rotated.
|
||
|
||
## Gates before merge/deploy
|
||
|
||
- **/sh-security-review** — auth surface, required. Run 2026-07-06: gate = BLOCK on 5 confirmed
|
||
pre-existing auth-design HIGHs (never-expiring token cache ×2, no replay/IP restriction,
|
||
lockdown toggle de-escalation, unlock-during-lockdown). None are introduced by this branch,
|
||
which adds only this runbook. The residual-secret concern (SC-01) was REFUTED post-scrub.
|
||
Tracked for follow-up; see the project memory. Merge decision is Adam's given the block is on
|
||
legacy design, not this diff.
|
||
- **GPT-4.1 cross-review** — only if a handler/IAM change accompanies this (placeholdering alone does not).
|
||
- Deploy-then-merge per handbook; the pre-push scanner hook passes with machine-level suppressions
|
||
for the placeholder/CDK-bundling/LogRetention false-positives (see
|
||
`~/.config/sea-haven/security-review/seahaven-door-unlock-api/suppressions.json`).
|
||
|
||
## Follow-up design fix
|
||
|
||
Treat the provisioning XML as a generated/secret artifact: keep only the placeholdered template
|
||
in git, render with the SSM value at provisioning time. Consider a small provisioning script in
|
||
this repo that pulls the token from SSM and renders, so the secret is never written to disk longer
|
||
than the push requires.
|