phase-5/security-hardening #14
@@ -8,10 +8,14 @@ surfaces.
|
||||
|
||||
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
|
||||
transport), and Phase 5C (module database access) have landed. Phase 5D (public request perimeter
|
||||
hardening against an independent security audit) is in progress; pass 1 of 5 has landed — see
|
||||
below. **Phase 5D is not complete, and Phase 6 (Surface work) remains blocked until every audit
|
||||
finding is resolved and a final re-audit passes.** The repository includes
|
||||
transport), and Phase 5C (module database access) have landed. **Phase 5D (security hardening
|
||||
against an independent security audit) is complete: all five passes have landed and the final
|
||||
independent whole-repository re-audit has closed, under the current trusted-contributor,
|
||||
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,
|
||||
deterministic automatic discovery, compiled server loading, a staged capability registry,
|
||||
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),
|
||||
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
|
||||
guarantee, and it does not complete Phase 5D** — passes 2–5 (identity/database transport hardening,
|
||||
authorization integrity, module/runtime/migration correctness, and deployment/supply-chain closure
|
||||
plus a final independent re-audit) remain outstanding before Phase 6 (Surface work) may begin.
|
||||
guarantee, and it did not by itself complete Phase 5D** — at the close of this pass, passes 2–5
|
||||
(identity/database transport hardening, authorization integrity, module/runtime/migration
|
||||
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.
|
||||
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).
|
||||
**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
|
||||
remains the deployment's own responsibility. Phase 5D is still not complete** — passes 3–5
|
||||
(authorization integrity, module/runtime/migration correctness, and deployment/supply-chain closure
|
||||
plus a final independent re-audit) remain outstanding.
|
||||
remains the deployment's own responsibility. This pass did not by itself complete Phase 5D** — at
|
||||
its close, passes 3–5 (authorization integrity, module/runtime/migration correctness, and
|
||||
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
|
||||
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).
|
||||
**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
|
||||
database or cryptographic guarantee. Phase 5D is still not complete** — passes 4 (module/runtime/
|
||||
migration correctness) and 5 (deployment/supply chain plus a final independent re-audit) remain
|
||||
outstanding, and Phase 6 remains blocked.
|
||||
database or cryptographic guarantee. This pass did not by itself complete Phase 5D** — at its close,
|
||||
passes 4 (module/runtime/migration correctness) and 5 (deployment/supply chain plus a final
|
||||
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
|
||||
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
|
||||
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
|
||||
isolation. Phase 5D is still not complete** — pass 5 (deployment/supply chain plus an independent
|
||||
whole-repository re-audit) remains outstanding, and Phase 6 remains blocked.
|
||||
isolation. This pass did not by itself complete Phase 5D** — at its close, pass 5
|
||||
(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
|
||||
JavaScript in the API process and shares its PostgreSQL role — table prefixes, manifests, permission
|
||||
@@ -249,9 +278,10 @@ Data definitions
|
||||
✓ Schemas validated
|
||||
0 modules · 0 tables
|
||||
✓ Migrations validated
|
||||
2 migrations · checksums valid
|
||||
3 migrations · checksums valid
|
||||
core:0000_identity_core_tables · f8f2086ef06f
|
||||
core:0001_authorization_core_tables · 830e1fbc26d0
|
||||
core:0002_authorization_integrity · 634294731180
|
||||
|
||||
PostgreSQL
|
||||
✓ Database TLS verified
|
||||
|
||||
@@ -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
|
||||
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
|
||||
`/api/v1/modules/<module-id>`, enforced and response-sanitized exactly like Core's own routes. This
|
||||
branch completes Phase 5C: typed module database access (`context.services.database`) for a
|
||||
module's lifecycle hooks and authorized HTTP handlers, over the platform's one existing PostgreSQL
|
||||
pool. See [ADR 0014](decisions/0014-module-permission-declarations-and-platform-registry.md),
|
||||
`/api/v1/modules/<module-id>`, enforced and response-sanitized exactly like Core's own routes. Phase
|
||||
5C added typed module database access (`context.services.database`) for a module's lifecycle hooks
|
||||
and authorized HTTP handlers, over the platform's one existing PostgreSQL pool.
|
||||
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 0016](decisions/0016-module-database-access.md), and the repository root
|
||||
[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
|
||||
the Phase 5C checkpoint: a server-owned client-address resolution boundary (direct mode by default,
|
||||
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.
|
||||
**Phase 5D is complete.** Five passes hardened the platform against an independent security audit of
|
||||
the Phase 5C checkpoint, and an independent whole-repository re-audit closed the phase:
|
||||
|
||||
The project continues finishing the module platform's backend foundations before Phase 6 Surface
|
||||
work begins. Target architecture not yet implemented includes production approval-provider
|
||||
1. the public request perimeter — a server-owned client-address resolution boundary for the
|
||||
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
|
||||
and action contracts, a Dashboard, a Builder, and reference feature modules; none of these are
|
||||
scheduled by this document.
|
||||
|
||||
@@ -108,9 +108,10 @@ Data definitions
|
||||
✓ Schemas validated
|
||||
0 modules · 0 tables
|
||||
✓ Migrations validated
|
||||
2 migrations · checksums valid
|
||||
3 migrations · checksums valid
|
||||
core:0000_identity_core_tables · f8f2086ef06f
|
||||
core:0001_authorization_core_tables · 830e1fbc26d0
|
||||
core:0002_authorization_integrity · 634294731180
|
||||
|
||||
PostgreSQL
|
||||
○ 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,
|
||||
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
|
||||
not repeat. Core authorization and an explicit platform-owner bootstrap command are the next
|
||||
architectural phase; Doctor does not diagnose authorization, the `apps/api` HTTP transport, runtime
|
||||
module health, or production storage yet, and it never constructs an operational Better Auth
|
||||
instance, starts the API, or claims sign-in works. See [API](api.md) for `pnpm api:start` and the
|
||||
identity HTTP transport this repository now has.
|
||||
not repeat. Core authorization and the explicit platform-owner bootstrap command have since shipped,
|
||||
and Doctor reports the composed permission registry; it still does not diagnose grant evaluation, the
|
||||
`apps/api` HTTP transport, runtime module health, or production storage, and it never constructs an
|
||||
operational Better Auth instance, starts the API, or claims sign-in works. See [API](api.md) for
|
||||
`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`
|
||||
created from `.env.example`:
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
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.26 feature line. Select the actual release during infrastructure work, after backup and
|
||||
@@ -204,4 +227,9 @@ grants.
|
||||
private repository.
|
||||
4. Enforce job timeout, cancellation, concurrency, and teardown in the trusted
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user