|
|
|
@@ -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.
|