diff --git a/README.md b/README.md index 2246827..356460d 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,15 @@ exists. Modules remain trusted compiled backend code with no process, role, sche 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. +**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 +declarations, typed facades, and frozen contexts are correctness boundaries, not a sandbox. Ekklesia +is also **not currently deployed**: there is no production instance, reverse proxy, public listener, +or deployment automation, so the origin/proxy/throttling properties a public launch requires are +unproven launch-blockers rather than existing guarantees. Read +[Security model and deployment trust boundaries](docs/development/security.md) before installing a +module or exposing the API. + ## Quick start Prerequisites: Node.js 24 LTS, pnpm 11.12, and Docker with Compose. diff --git a/docs/architecture/platform-architecture.md b/docs/architecture/platform-architecture.md index bc23498..28867d1 100644 --- a/docs/architecture/platform-architecture.md +++ b/docs/architecture/platform-architecture.md @@ -4,6 +4,11 @@ Ekklesia is a long-lived platform from which churches can compose many applicati experiences. It begins as a modular monolith so contracts can stabilize before distribution is introduced. +> **Trust boundary.** Installed modules are trusted compiled backend code, not sandboxed +> plugins: a module runs arbitrary JavaScript in the API process and shares its PostgreSQL +> role. The layering described below is a correctness and ownership boundary, not hostile-code +> isolation. See [Security model and deployment trust boundaries](../development/security.md). + ## Dependency model The arrows below mean "depends on." Presentation and composition depend on stable lower-level diff --git a/docs/development/security.md b/docs/development/security.md new file mode 100644 index 0000000..46e9c3d --- /dev/null +++ b/docs/development/security.md @@ -0,0 +1,154 @@ +# Security model and deployment trust boundaries + +The one canonical statement of what Ekklesia's boundaries actually protect against today, and what +they do not. Read it before installing a module, exposing the API, or reasoning about any other +document's guarantees. + +Related: [ADR 0006](../architecture/decisions/0006-versioned-capabilities-and-compiled-module-runtime.md) +(compiled module runtime), [ADR 0016](../architecture/decisions/0016-module-database-access.md) +(module database access), [ADR 0020](../architecture/decisions/0020-module-runtime-and-migration-execution-hardening.md) +(module routing/lifecycle and migration execution), and +[Module development](../module-development/README.md). + +## Installed modules are trusted compiled backend code + +**Installing a module is equivalent to deploying trusted server code.** It is not equivalent to +installing a browser extension, a plugin from a marketplace, or anything else with a security +boundary around it. There is no such boundary here. + +A module's `dist/server` entrypoint is imported into, and runs arbitrary JavaScript inside, the +`apps/api` process. Within the operating-system account that process runs as, a module can: + +- read and write any file the account can reach, including `.env`, TLS material, and the source tree; +- read `process.env`, `process.argv`, and any other process state; +- open outbound network connections and bind listeners; +- import any installed dependency, including ones Ekklesia never intended it to use; +- monkey-patch Core's own exports, Node built-ins, `globalThis`, or another module's objects; +- keep running work after its lifecycle hooks return. + +It also **shares the API's single PostgreSQL role and connection pool**. `ModuleDatabase.execute` +accepts arbitrary SQL, so a module has exactly the database authority the API has — every table in +the database, including `ekklesia_core_user`, `ekklesia_core_grant`, +`ekklesia_core_platform_owner`, `ekklesia_core_platform_owner_bootstrap`, +`ekklesia_core_authorization_audit`, and every other module's tables. + +## What the module-facing mechanisms actually are + +These are **correctness and coordination boundaries**. Every one of them assumes the module is +cooperative. None of them constrains a module that is not: + +| Mechanism | What it actually provides | +| --- | --- | +| `ekklesia_*` table prefixes and schema discovery | Ownership clarity and collision avoidance between cooperating modules | +| TypeScript types, branded ids, contracts | Compile-time correctness; erased at runtime | +| `ekklesia.module.json` manifest and `manifestVersion` | Declared compatibility and dependency ordering | +| Permission declarations and the composed registry | Which permissions *this* module's own routes may name, checked when Core mounts them | +| Route validation, overlap rejection, body limits | Deterministic, unambiguous HTTP routing and request bounds | +| `Object.freeze` on contexts and services | Prevents accidental mutation; a module can still reach the underlying objects | +| The `ModuleDatabase` facade | A typed, ergonomic surface — not a privilege reduction, since `execute` is arbitrary SQL | +| Capability registry | Explicit inter-module wiring, not access control | +| Bounded stop/rollback deadlines (ADR 0020) | The platform stops *waiting*; module code is not cancelled or terminated | + +**There is no process, container, VM, seccomp/syscall, filesystem, network, PostgreSQL role, +schema-permission, table-ACL, row-level-security, tenant, or resource sandbox between a module and +Core.** A module is not isolated from Core, from other modules, or from the host. + +## What this platform must not claim + +Ekklesia today has **none** of the following, and no document, README, or marketing surface may +imply otherwise: + +- module signing, provenance attestation, or verified publishers; +- a marketplace, registry, or review process for third-party modules; +- sandboxed or hot-installed modules; +- per-module database roles, schemas, or credentials; +- tenant isolation; +- runtime resource limits (CPU, memory, file descriptors, connections) per module. + +## Operator rule + +**Only install modules whose source, provenance, and build output you have reviewed and whose +authors you trust with your production server and database.** Treat a module the way you would +treat a direct commit to `apps/api`: read the code, verify what artifact was built from it, pin it, +and re-review on upgrade. Do not install a module because a manifest looks well-formed — every +validation Ekklesia performs on a manifest is satisfied trivially by hostile code. + +Installing, removing, or editing a module requires an API restart; there is no hot install path, +and none should be added while this model holds. + +## What would have to change before accepting untrusted modules + +Accepting third-party modules that have not been reviewed is an architectural change, not a +configuration change. At minimum it would require: + +- process or container isolation per module, with the API as a supervisor; +- capability-based RPC across that boundary instead of in-process object passing; +- a separate PostgreSQL role and schema per module, with no shared superuser-equivalent pool; +- restricted filesystem and network access per module; +- CPU/memory/connection limits and enforced timeouts that can actually terminate work; +- signed, verifiable module provenance and a defined review/revocation process. + +Until that exists, the trusted-code model above is the security model. + +## Current deployment state + +As of this branch, **Ekklesia is not deployed**. There is no production API instance, no reverse +proxy, no public listener, and no deployment automation of any kind in this repository — no deploy +script, no CI deployment step, no container publish, no orchestration manifest. No `.env` has ever +been committed; only `.env.example`. + +Two production defaults are relevant but are **not** by themselves a deployment guarantee: + +- `apps/api` defaults its listen address to loopback (`127.0.0.1`), so it is not reachable off-host + unless an operator explicitly changes it; +- production configuration refuses to enable public signup at all + (`PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION`). + +A loopback default is not a firewall, and a config default is not a verified boundary. + +### Launch checklist — each item must be proven against the real deployment before public exposure + +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. + +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 + proxy path connects. Security must not depend on an unpublished DNS name or origin IP. +4. The proxy **overwrites** (never appends to, never passes through) the client-IP header Ekklesia + is configured to trust. +5. Rotating a caller-supplied `Forwarded`, `X-Forwarded-For`, `CF-Connecting-IP`, `X-Real-IP`, or + the platform's own internal sanitized header cannot rotate the authentication rate-limit bucket. +6. The actual immediate proxy peer is in Ekklesia's explicit trusted-proxy set, and an untrusted + direct peer's forwarded header is ignored. +7. Proxy logs do not record cookies, authorization values, request bodies, or raw forwarding chains. +8. PostgreSQL is reachable only from the API host, over TLS, with the certificate and hostname + verification Ekklesia already requires. + +### Rate limiting is per-process today + +Better Auth's authentication rate limiter is **in-memory and per API process**. That is acceptable +only while **both** of the following hold, which is the current state: + +- production public signup is disabled (and cannot be enabled — see above); and +- exactly one API instance serves traffic. + +If either changes — public signup is enabled, or a second instance is added — a durable shared or +edge rate limiter becomes mandatory, and it must be proven that its counters are shared across +instances, survive process restart, and cannot be re-bucketed by a spoofed client-IP header. +Ekklesia does not ship one. Do not describe the current limiter as distributed throttling. + +## Continuous integration boundary + +CI runs on self-hosted Gitea Actions runners. The workflow itself is least privilege: top-level +`permissions: contents: read`, both actions pinned by full commit SHA, `pnpm install +--frozen-lockfile`, no `pull_request_target`, no secrets referenced, and checkout configured with +`persist-credentials: false` so the job token is not left in `.git/config` for a later step to read. + +**The runner topology itself is not verified in this repository and is not documented here as +though it were.** A pull request can edit `runs-on` and select any runner label its repository can +reach, so a workflow file cannot be the boundary. Whether untrusted pull-request jobs execute on a +disposable guest, and whether any runner reachable from this repository holds deployment authority +or a host Docker socket, must be established against the actual Gitea instance and runner hosts. +Until that evidence exists, treat pull-request CI as executing with whatever authority the runner +host actually grants. diff --git a/docs/module-development/README.md b/docs/module-development/README.md index 126fdf0..b3314c0 100644 --- a/docs/module-development/README.md +++ b/docs/module-development/README.md @@ -3,6 +3,12 @@ An Ekklesia module is a backend capability package. It owns business behavior and data declarations; it never owns components, styles, layouts, renderers, navigation, or other UI. +> **Before you publish or install a module:** modules are trusted compiled backend code. Your +> module runs arbitrary JavaScript inside the API process and shares its PostgreSQL role, and +> nothing in this guide — manifests, permission declarations, typed facades, route validation, +> frozen contexts — isolates it. An operator installing your module is deploying trusted server +> code. See [Security model and deployment trust boundaries](../development/security.md). + ## Required package shape Each immediate child of `modules/` must be an ESM workspace package with a unique package name, a