diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47c7153..28eb3a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,11 +60,10 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - # Best-effort: a PAT without repo:write_metadata returns 403 here. - # The image push above is already committed, so don't fail the job. + # Requires DOCKERHUB_TOKEN with the `repo:write_metadata` scope. + # See docs/RELEASE.md §1 for the operator setup steps. - name: Sync Docker Hub description uses: peter-evans/dockerhub-description@v5 - continue-on-error: true with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b70e53..4b1f655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,11 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ### Security - **Portal CORS preflight gains a deny-cache.** `PortalCorsMiddleware.HandlePreflightAsync` now caches both allow and deny outcomes for the same TTL as the per-app signing-key lookup (default 60 s). Browsers don't cache rejected preflights, so before this change every `OPTIONS` against `/api/v1/portal/*` from a disallowed origin re-scanned the portal-enabled application set and the in-process JSON deserialize loop — a low-effort DB hammer vector. New regression test `Preflight_Deny_Decision_Is_Cached_Within_Ttl` pins the behaviour. +### Build +- **`release.yml` removes the `continue-on-error` workaround on the `Sync Docker Hub description` step.** The step previously soft-failed on every release because `DOCKERHUB_TOKEN` lacked the `repo:write_metadata` scope; the workaround masked the underlying secret-misconfiguration. The token now needs the documented scope set (see `docs/RELEASE.md` §1) so a release that loses the Docker Hub overview sync surfaces as a hard CI failure instead of vanishing silently. + ### Documentation +- **`docs/RELEASE.md` §1 — `DOCKERHUB_TOKEN` scope requirement made explicit.** The secret table now lists the three required scopes (`repo:read`, `repo:write`, `repo:write_metadata`) and where to set them on Docker Hub. Avoids the recurring "release ran but Docker Hub overview is stale" debugging session. - **ADR-004 — Portal Signing Key Storage: Plaintext + Instant Invalidation, No Rotation Grace.** Locks in the v0.2.0 decisions around per-app `PortalSigningKey` at-rest storage (`varchar(64)` plaintext, mirroring the existing `SigningSecret` pattern), the no-grace rotation lifecycle (instant local invalidation, TTL-bounded multi-replica), and the one-shot reveal contract on `enable` / `rotate`. Documents the threat model, alternatives considered (`pgcrypto`, rotation grace, external KMS), and the explicit revisit triggers (compliance regimes, recurring host-integration friction, key-reuse beyond JWT verification). - **ADR-005 — Portal CORS Preflight Deny-Cache TTL.** Locks in PR #104's choice to cache both allow and deny outcomes for `PortalAuth:LookupCacheTtlSeconds` (default 60s), reusing the existing per-app cache window for symmetry. Documents why no synchronous invalidation hook from `PUT /portal/origins` (the cache key is origin-scoped, the operator action is app-scoped — the bookkeeping cost outweighs the ≤60s staleness), and what would justify revisiting (operator UX complaints, memory-pressure attacks via origin enumeration, richer CORS semantics). - **`docs/API.md` §3.8 — Portal API (Customer-Facing JWT).** Replaces the doc-drift gap surfaced by the v0.2.0 audit (where the portal API surface existed in code and CHANGELOG but was absent from `docs/API.md`). Covers HS256 JWT contract, capability scopes, per-app CORS, rate-limit partition reuse, cross-tenant `404` semantics, every route under `/api/v1/portal/*`, the dashboard portal-admin routes, the portal-specific error code table, and an end-to-end Node.js (`jose`) + cURL probe. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 6e72f0f..0fd5eec 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -24,7 +24,7 @@ Add these secrets: | Secret | Description | |--------|-------------| | `DOCKERHUB_USERNAME` | Docker Hub username | -| `DOCKERHUB_TOKEN` | Docker Hub access token | +| `DOCKERHUB_TOKEN` | Docker Hub Personal Access Token. **Required scopes: `repo:read`, `repo:write`, `repo:write_metadata`.** The third scope is what lets the `Sync Docker Hub description` step in `release.yml` push the README to the Hub overview; without it that step fails with `403`. Create the token at Docker Hub → Account Settings → Personal Access Tokens. | | `NUGET_API_KEY` | NuGet.org API key | | `NPM_TOKEN` | npm automation token (for portal package — see Section 6) |