docs: define deployment trust boundaries
One canonical security document, linked from the README, platform architecture, and module-development guide, stating what the boundaries actually protect against — and, more importantly, what they do not. Installed modules are trusted compiled backend code. A module runs arbitrary JavaScript inside the API process and shares its PostgreSQL role and pool, so `ModuleDatabase.execute` gives it the API's full database authority over every table, including identity, grants, owner, bootstrap, and audit. Table prefixes, manifests, permission declarations, route validation, frozen contexts, typed facades, and the capability registry are correctness and coordination boundaries that assume a cooperative module; none of them constrains one that is not. There is no process, container, syscall, filesystem, network, database-role, schema, tenant, or resource sandbox between a module and Core, and the document says so in those words, along with what would have to be built before untrusted modules could be accepted. It also records the honest deployment state: Ekklesia is not deployed. There is no production instance, reverse proxy, public listener, or deployment automation anywhere in this repository, and no `.env` has ever been committed. The loopback listen default and the production signup refusal are real, but a config default is not a firewall, so the origin-reachability, proxy-overwrite, header-spoofing, and trusted-peer properties a public launch needs are written as a launch-blocking checklist rather than as guarantees. The per-process authentication limiter is described as exactly that, valid only while signup stays disabled and exactly one instance serves traffic. The CI section records the workflow's verified posture and deliberately stops there: a pull request can edit `runs-on`, so a workflow file cannot be the boundary, and the runner topology is not described as though it had been verified. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f7621a599f
commit
07a3c61083
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user