docs: close Phase 5D security hardening
CI / verify (pull_request) Successful in 27s
CI / infrastructure-smoke (pull_request) Successful in 12s
CI / migration-smoke (pull_request) Successful in 42s

The documentation still described Phase 5D as in progress, Phase 6 as
blocked, and a disposable-runner isolation proof as a prerequisite for
opening the real pull request. All five passes and the independent
whole-repository re-audit have landed, so those statements no longer
match the accepted operating model.

The README's current status now records Phase 5D as complete under the
current trusted-contributor, not-yet-deployed threat model, with Phase 6
unblocked, and gains a pass 5 closure paragraph covering the re-audit,
the repository-local supply-chain and dependency fixes, the accepted
trusted-code-only CI boundary, and what remains required before
untrusted CI or a public launch. The pass 1-4 paragraphs keep their
technical content unchanged; only their tense changes, so each reads as
what was outstanding at the close of that pass rather than as a current
claim.

`platform-architecture.md` no longer stops at pass 1 or says passes 2-5
are unfinished; it summarizes all five passes and defers the remaining
operational limitations to the security document.

`security.md` states the trusted-code-only operating rule explicitly and
replaces "CI blockers before the real pull request is opened" with
"Requirements before executing untrusted contributions", framing the five
hardening items as prerequisites for relaxing that rule rather than as
Phase 6 blockers, and naming an isolated hosted-runner service as an
accepted alternative. The Gitea 1.24.2 warnings, the scope of
`persist-credentials: false`, and the unverified runner topology and
job-token authority are preserved verbatim; no external Gitea setting is
claimed. The launch checklist is marked as gating a public launch, not
development.

Both Doctor examples are corrected to the real three-migration output,
adding `core:0002_authorization_integrity · 634294731180`. The stale
sentence calling Core authorization the next architectural phase is
refreshed.

ADRs 0017-0020 are untouched: their statements that later passes were
outstanding are historical records made when each was accepted.

Documentation only — no production code, test, workflow, dependency,
migration, or schema change.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-07-29 21:59:26 -05:00
co-authored by Claude Opus 5
parent 27088fc054
commit 288c088377
4 changed files with 117 additions and 41 deletions
+46 -16
View File
@@ -8,10 +8,14 @@ surfaces.
Phase 3 is complete, and Phase 4A (Core identity foundation), Phase 4B (identity HTTP transport), Phase 3 is complete, and Phase 4A (Core identity foundation), Phase 4B (identity HTTP transport),
Phase 4C (Core authorization), Phase 5A (module authorization integration), Phase 5B (module HTTP Phase 4C (Core authorization), Phase 5A (module authorization integration), Phase 5B (module HTTP
transport), and Phase 5C (module database access) have landed. Phase 5D (public request perimeter transport), and Phase 5C (module database access) have landed. **Phase 5D (security hardening
hardening against an independent security audit) is in progress; pass 1 of 5 has landed — see against an independent security audit) is complete: all five passes have landed and the final
below. **Phase 5D is not complete, and Phase 6 (Surface work) remains blocked until every audit independent whole-repository re-audit has closed, under the current trusted-contributor,
finding is resolved and a final re-audit passes.** The repository includes not-yet-deployed threat model — and Phase 6 (Surface work) is unblocked.** The operational
requirements that remain are prerequisites for two specific *future* activities — executing
untrusted CI contributions, and a public production launch — and neither gates Phase 6; see the
pass 5 closure below and
[Security model and deployment trust boundaries](docs/development/security.md). The repository includes
public module contracts, independently versioned capabilities, static module manifests, public module contracts, independently versioned capabilities, static module manifests,
deterministic automatic discovery, compiled server loading, a staged capability registry, deterministic automatic discovery, compiled server loading, a staged capability registry,
dependency-ordered lifecycle with rollback, module-owned Drizzle table contracts, deterministic dependency-ordered lifecycle with rollback, module-owned Drizzle table contracts, deterministic
@@ -113,9 +117,10 @@ transport-level failures are observed and logged in sanitized form. See
[API](docs/development/api.md), [Module development](docs/module-development/README.md#http-routes), [API](docs/development/api.md), [Module development](docs/module-development/README.md#http-routes),
and [ADR 0017](docs/architecture/decisions/0017-public-request-perimeter-hardening.md). **This pass and [ADR 0017](docs/architecture/decisions/0017-public-request-perimeter-hardening.md). **This pass
does not add distributed rate limiting, durable public-signup abuse control, or any deployment/TLS does not add distributed rate limiting, durable public-signup abuse control, or any deployment/TLS
guarantee, and it does not complete Phase 5D** — passes 2–5 (identity/database transport hardening, guarantee, and it did not by itself complete Phase 5D** — at the close of this pass, passes 2–5
authorization integrity, module/runtime/migration correctness, and deployment/supply-chain closure (identity/database transport hardening, authorization integrity, module/runtime/migration
plus a final independent re-audit) remain outstanding before Phase 6 (Surface work) may begin. correctness, and deployment/supply-chain closure plus a final independent re-audit) were still
outstanding, and Phase 6 (Surface work) was blocked until they landed.
Phase 5D pass 2 of 5 corrects identity session resolution and enforces a database transport policy. Phase 5D pass 2 of 5 corrects identity session resolution and enforces a database transport policy.
A request carrying no cookies, or only unrelated cookies, now performs **no** identity database A request carrying no cookies, or only unrelated cookies, now performs **no** identity database
@@ -135,9 +140,9 @@ which the pinned driver would otherwise let silently override the policy. See
[ADR 0018](docs/architecture/decisions/0018-identity-session-resolution-and-database-transport.md). [ADR 0018](docs/architecture/decisions/0018-identity-session-resolution-and-database-transport.md).
**This pass adds no secret store, no certificate rotation, no client-certificate authentication, and **This pass adds no secret store, no certificate rotation, no client-certificate authentication, and
no distributed rate limiting; TLS protects the connection, not the exposure, so the network boundary no distributed rate limiting; TLS protects the connection, not the exposure, so the network boundary
remains the deployment's own responsibility. Phase 5D is still not complete** — passes 3–5 remains the deployment's own responsibility. This pass did not by itself complete Phase 5D** — at
(authorization integrity, module/runtime/migration correctness, and deployment/supply-chain closure its close, passes 3–5 (authorization integrity, module/runtime/migration correctness, and
plus a final independent re-audit) remain outstanding. deployment/supply-chain closure plus a final independent re-audit) were still outstanding.
Phase 5D pass 3 of 5 preserves authorization security history and makes explicit-grant management Phase 5D pass 3 of 5 preserves authorization security history and makes explicit-grant management
unambiguous and bounded. Authorization **audit history now survives user deletion**: the audit unambiguous and bounded. Authorization **audit history now survives user deletion**: the audit
@@ -159,9 +164,9 @@ unsigned position cursor. One forward Core migration (`0002_authorization_integr
[ADR 0019](docs/architecture/decisions/0019-durable-authorization-history-and-grant-management.md). [ADR 0019](docs/architecture/decisions/0019-durable-authorization-history-and-grant-management.md).
**This pass adds no roles, teams, tenants, service accounts, scope resolver, user-deletion endpoint, **This pass adds no roles, teams, tenants, service accounts, scope resolver, user-deletion endpoint,
or owner-recovery workflow; audit append-only behavior remains an application convention, not a or owner-recovery workflow; audit append-only behavior remains an application convention, not a
database or cryptographic guarantee. Phase 5D is still not complete** — passes 4 (module/runtime/ database or cryptographic guarantee. This pass did not by itself complete Phase 5D** — at its close,
migration correctness) and 5 (deployment/supply chain plus a final independent re-audit) remain passes 4 (module/runtime/migration correctness) and 5 (deployment/supply chain plus a final
outstanding, and Phase 6 remains blocked. independent re-audit) were still outstanding, and Phase 6 was blocked until they landed.
Phase 5D pass 4 of 5 hardens the module runtime and the migration commands. **Two routes of one Phase 5D pass 4 of 5 hardens the module runtime and the migration commands. **Two routes of one
module that could both match the same request path are now rejected at registration** module that could both match the same request path are now rejected at registration**
@@ -186,8 +191,32 @@ string literal, dollar-quoted body, quoted identifier, or comment. See
**Deadlines bound how long the platform waits; they do not cancel or sandbox trusted in-process **Deadlines bound how long the platform waits; they do not cancel or sandbox trusted in-process
module code, and closing a socket does not prove a handler promise ended. No custom SQL parser module code, and closing a socket does not prove a handler promise ended. No custom SQL parser
exists. Modules remain trusted compiled backend code with no process, role, schema, or tenant exists. Modules remain trusted compiled backend code with no process, role, schema, or tenant
isolation. Phase 5D is still not complete** — pass 5 (deployment/supply chain plus an independent isolation. This pass did not by itself complete Phase 5D** — at its close, pass 5
whole-repository re-audit) remains outstanding, and Phase 6 remains blocked. (deployment/supply chain plus an independent whole-repository re-audit) was still outstanding, and
Phase 6 was blocked until it landed.
Phase 5D pass 5 of 5 closes the phase with an independent whole-repository re-audit and the
repository-local half of deployment and supply-chain hardening. The re-audit swept every layer —
Core, contracts, `apps/api`, scripts, the CI workflow, and documentation — and its repository-local
findings are resolved here: every dependency advisory is remediated (both `pnpm audit` and
`pnpm audit --prod` report no known vulnerabilities), every CI action reference is pinned to a full
immutable commit SHA, installs run `--frozen-lockfile`, no workflow references a repository or
instance secret, every checkout step sets `persist-credentials: false`, a checksum-verified secret
scan found no committed credential in the history, and one canonical
[security document](docs/development/security.md) now states the trusted-module model, the
undeployed state, and the launch checklist in a single place. **CI is accepted as a
trusted-code-only boundary**: the self-hosted Gitea runner executes only code controlled and
reviewed by trusted repository collaborators, and unknown or untrusted pull requests must not
execute on it automatically. What remains is operational and cannot be proven from this repository —
upgrading Gitea past 1.24.2 (which *ignores* the workflow's `permissions`, `concurrency`, and
`timeout-minutes` declarations), clamping job-token authority server-side, enforcing timeout and
teardown in a trusted provisioner/reaper, and proving disposable runner isolation. Those are
prerequisites for **executing untrusted contributions**, alongside the deployment checklist that
gates a **public launch**; moving future public contribution workflows to a service with isolated
hosted runners is an acceptable alternative. **Neither set of requirements blocks Phase 6** — Surface
development proceeds under the current trusted-contributor, not-yet-deployed model. Runner topology
and job-token authority remain unverified and are not described as isolated. See
[Security model and deployment trust boundaries](docs/development/security.md).
**Security model.** Installed modules are trusted compiled backend code that runs arbitrary **Security model.** Installed modules are trusted compiled backend code that runs arbitrary
JavaScript in the API process and shares its PostgreSQL role — table prefixes, manifests, permission JavaScript in the API process and shares its PostgreSQL role — table prefixes, manifests, permission
@@ -249,9 +278,10 @@ Data definitions
✓ Schemas validated ✓ Schemas validated
0 modules · 0 tables 0 modules · 0 tables
✓ Migrations validated ✓ Migrations validated
2 migrations · checksums valid 3 migrations · checksums valid
core:0000_identity_core_tables · f8f2086ef06f core:0000_identity_core_tables · f8f2086ef06f
core:0001_authorization_core_tables · 830e1fbc26d0 core:0001_authorization_core_tables · 830e1fbc26d0
core:0002_authorization_integrity · 634294731180
PostgreSQL PostgreSQL
✓ Database TLS verified ✓ Database TLS verified
+34 -17
View File
@@ -114,28 +114,45 @@ Phase 5A wired module-declared permissions into that same authorization system
manifest, one composed platform permission registry spanning Core's own declarations and every manifest, one composed platform permission registry spanning Core's own declarations and every
installed module's, enforced through the identical middleware Core's own routes use. Phase 5B let a installed module's, enforced through the identical middleware Core's own routes use. Phase 5B let a
module mount its own framework-neutral HTTP routes in production, under module mount its own framework-neutral HTTP routes in production, under
`/api/v1/modules/<module-id>`, enforced and response-sanitized exactly like Core's own routes. This `/api/v1/modules/<module-id>`, enforced and response-sanitized exactly like Core's own routes. Phase
branch completes Phase 5C: typed module database access (`context.services.database`) for a 5C added typed module database access (`context.services.database`) for a module's lifecycle hooks
module's lifecycle hooks and authorized HTTP handlers, over the platform's one existing PostgreSQL and authorized HTTP handlers, over the platform's one existing PostgreSQL pool.
pool. See [ADR 0014](decisions/0014-module-permission-declarations-and-platform-registry.md), See [ADR 0014](decisions/0014-module-permission-declarations-and-platform-registry.md),
[ADR 0015](decisions/0015-static-module-http-routing-and-runtime-lifecycle.md), [ADR 0015](decisions/0015-static-module-http-routing-and-runtime-lifecycle.md),
[ADR 0016](decisions/0016-module-database-access.md), and the repository root [ADR 0016](decisions/0016-module-database-access.md), and the repository root
[README](../../README.md#current-status) for the complete, current status of every phase. [README](../../README.md#current-status) for the complete, current status of every phase.
Phase 5D pass 1 of 5 hardens the public request perimeter against an independent security audit of **Phase 5D is complete.** Five passes hardened the platform against an independent security audit of
the Phase 5C checkpoint: a server-owned client-address resolution boundary (direct mode by default, the Phase 5C checkpoint, and an independent whole-repository re-audit closed the phase:
explicit trusted-proxy mode otherwise) replaces trusting a caller-controlled forwarded header for
the authentication rate limiter; public sign-up is disabled by default behind an explicit typed
opt-in; every public route — Better Auth, Core's grant mutation, and every module route via a new
required `maxBodyBytes` field on the executable route contract — has an explicit, enforced
request-body ceiling with one sanitized `413` contract; and the Node HTTP server has finite
header/request timeouts. See
[ADR 0017](decisions/0017-public-request-perimeter-hardening.md). This pass does not add
distributed rate limiting or durable signup-abuse control, and Phase 5D itself is not complete —
passes 2–5 and a final independent re-audit remain before Phase 6 may begin.
The project continues finishing the module platform's backend foundations before Phase 6 Surface 1. the public request perimeter — a server-owned client-address resolution boundary for the
work begins. Target architecture not yet implemented includes production approval-provider authentication rate limiter, public sign-up disabled by default behind an explicit typed opt-in,
an enforced request-body ceiling on every public route (including a module route's own declared
contract), and finite Node header/request timeouts;
2. identity session resolution, and a mandatory certificate- **and** hostname-verified PostgreSQL
transport with no plaintext or `rejectUnauthorized: false` exception;
3. durable authorization history that survives user deletion, a one-time owner bootstrap that
deleting a user cannot reopen, and bounded, unambiguous explicit-grant management;
4. module route-overlap rejection at registration, finite startup rollback and shutdown deadlines,
and hardened migration command timeouts and SQL execution;
5. deployment and supply-chain closure plus the final re-audit.
See [ADR 0017](decisions/0017-public-request-perimeter-hardening.md),
[ADR 0018](decisions/0018-identity-session-resolution-and-database-transport.md),
[ADR 0019](decisions/0019-durable-authorization-history-and-grant-management.md), and
[ADR 0020](decisions/0020-module-runtime-and-migration-execution-hardening.md).
These guarantees hold under a **trusted-contributor, not-yet-deployed** threat model. The remaining
limitations are operational rather than architectural: CI is a trusted-code-only boundary whose
runner topology and job-token authority are unverified, and Ekklesia is not deployed, so the origin,
proxy, and throttling properties a public launch requires are unproven. Neither set of requirements
blocks Phase 6. Read
[Security model and deployment trust boundaries](../development/security.md) for what each boundary
does and does not protect against, and the repository root
[README](../../README.md#current-status) for the current status of every phase.
The module platform's backend foundations are now in place and Phase 6 Surface work is unblocked.
Target architecture not yet implemented includes production approval-provider
composition, physical storage-bucket provisioning, jobs/workflow automation, richer public resource composition, physical storage-bucket provisioning, jobs/workflow automation, richer public resource
and action contracts, a Dashboard, a Builder, and reference feature modules; none of these are and action contracts, a Dashboard, a Builder, and reference feature modules; none of these are
scheduled by this document. scheduled by this document.
+7 -6
View File
@@ -108,9 +108,10 @@ Data definitions
✓ Schemas validated ✓ Schemas validated
0 modules · 0 tables 0 modules · 0 tables
✓ Migrations validated ✓ Migrations validated
2 migrations · checksums valid 3 migrations · checksums valid
core:0000_identity_core_tables · f8f2086ef06f core:0000_identity_core_tables · f8f2086ef06f
core:0001_authorization_core_tables · 830e1fbc26d0 core:0001_authorization_core_tables · 830e1fbc26d0
core:0002_authorization_integrity · 634294731180
PostgreSQL PostgreSQL
○ Connection check skipped ○ Connection check skipped
@@ -133,11 +134,11 @@ Result: READY WITH WARNINGS
Doctor is a local, human-readable summary. `pnpm verify`, `pnpm ci:verify`, infrastructure smoke, Doctor is a local, human-readable summary. `pnpm verify`, `pnpm ci:verify`, infrastructure smoke,
and migration smoke remain the authoritative automated evidence because they exercise formatting, and migration smoke remain the authoritative automated evidence because they exercise formatting,
boundaries, types, tests, builds, containers, and live mutation/rollback behavior that Doctor does boundaries, types, tests, builds, containers, and live mutation/rollback behavior that Doctor does
not repeat. Core authorization and an explicit platform-owner bootstrap command are the next not repeat. Core authorization and the explicit platform-owner bootstrap command have since shipped,
architectural phase; Doctor does not diagnose authorization, the `apps/api` HTTP transport, runtime and Doctor reports the composed permission registry; it still does not diagnose grant evaluation, the
module health, or production storage yet, and it never constructs an operational Better Auth `apps/api` HTTP transport, runtime module health, or production storage, and it never constructs an
instance, starts the API, or claims sign-in works. See [API](api.md) for `pnpm api:start` and the operational Better Auth instance, starts the API, or claims sign-in works. See [API](api.md) for
identity HTTP transport this repository now has. `pnpm api:start` and the identity HTTP transport this repository now has.
Migration planning and application require `DATABASE_URL`. Keep it in the uncommitted `.env` Migration planning and application require `DATABASE_URL`. Keep it in the uncommitted `.env`
created from `.env.example`: created from `.env.example`:
+30 -2
View File
@@ -111,6 +111,10 @@ A loopback default is not a firewall, and a config default is not a verified bou
Until every one of these is verified on the actual hosts, the deployment is not launch-ready. None Until every one of these is verified on the actual hosts, the deployment is not launch-ready. None
of them can be satisfied by reading this repository. of them can be satisfied by reading this repository.
This checklist gates a **public launch**. It does not gate development: Surfaces, modules, and Core
work all proceed against the local development infrastructure while every item below is still
outstanding.
1. The reverse-proxy path serves the API successfully. 1. The reverse-proxy path serves the API successfully.
2. The origin port is unreachable from the public Internet. 2. The origin port is unreachable from the public Internet.
3. The origin port is also unreachable from any untrusted network/VLAN — only the intended private 3. The origin port is also unreachable from any untrusted network/VLAN — only the intended private
@@ -143,6 +147,17 @@ Ekklesia does not ship one. Do not describe the current limiter as distributed t
CI runs on self-hosted Gitea Actions runners. The inventoried server is **Gitea 1.24.2**, and that CI runs on self-hosted Gitea Actions runners. The inventoried server is **Gitea 1.24.2**, and that
version determines which parts of the workflow are real guarantees and which are not. version determines which parts of the workflow are real guarantees and which are not.
### The current operating rule: trusted code only
**This CI installation is a trusted-code-only boundary.** Only code controlled and reviewed by
trusted repository collaborators may execute on the self-hosted runner. An unknown or untrusted pull
request must **not** execute on it automatically — a maintainer reviews the diff, including any
change to the workflow itself, before it runs.
This is an operating rule, not an enforced technical control, and it is the reason the rest of this
section is safe today. Everything below describes what the repository can and cannot prove about
that runner; none of it is a substitute for the rule.
### What is true of the repository today ### What is true of the repository today
These are properties of files in this repository, enforced by the action or the package manager, These are properties of files in this repository, enforced by the action or the package manager,
@@ -192,7 +207,15 @@ Docker socket, must be established against the actual Gitea instance and runner
evidence exists, treat pull-request CI as executing with whatever authority the runner host actually evidence exists, treat pull-request CI as executing with whatever authority the runner host actually
grants. grants.
### CI blockers before the real pull request is opened ### Requirements before executing untrusted contributions
These five items are the prerequisites for **relaxing the trusted-code-only rule above** — that is,
for letting code this project has not reviewed execute on this Gitea installation. They are backlog
requirements for that specific future capability. **They do not block Phase 6 Surface work**, and
they are not conditions on merging trusted, reviewed work.
None of them can be satisfied by editing this repository; each requires access to the Gitea server
and runner hosts, and each must be proven there.
1. Upgrade Gitea to a reviewed supported release with job-token permission controls — at least the 1. Upgrade Gitea to a reviewed supported release with job-token permission controls — at least the
1.26 feature line. Select the actual release during infrastructure work, after backup and 1.26 feature line. Select the actual release during infrastructure work, after backup and
@@ -204,4 +227,9 @@ grants.
private repository. private repository.
4. Enforce job timeout, cancellation, concurrency, and teardown in the trusted 4. Enforce job timeout, cancellation, concurrency, and teardown in the trusted
provisioner/reaper rather than relying on the ignored workflow keys above. provisioner/reaper rather than relying on the ignored workflow keys above.
5. Complete the disposable-guest runner isolation proof before opening the real pull request. 5. Complete a disposable-guest runner isolation proof: one throwaway guest per job, torn down by a
trusted reaper outside workflow YAML, holding no deployment credential, registration token, or
reusable key.
Moving future public contribution workflows to a service that provides isolated hosted runners — for
example GitHub — is an accepted alternative to satisfying items 1–5 on this installation.