docs/security.md
Rama main · réplica de lectura del repositorio original.
# Security checklist
Work through these before you store private knowledge.
## Network
- Publish only TCP 80 and 443, from Caddy.
- Keep the Docker daemon, the database admin port, and SSH behind a firewall.
- Confirm `REMENTUM_PUBLIC_URL` uses the same HTTPS origin as `REMENTUM_DOMAIN`.
- Keep `REMENTUM_DEV_AUTH=false` in production. The API refuses to start with it on there.
- Keep `REMENTUM_TRUSTED_PROXIES` limited to the reverse proxies you actually run. Any address in
that list can spoof its client address and reset its rate-limit bucket. Pin the proxy's address
when clients reach Rementum from a private network, because the default `loopback,uniquelocal`
preset trusts any private address.
The reference Compose file binds PostgreSQL to `127.0.0.1` for administration. It does not expose
PostgreSQL or the embedding service to the public network.
## Secrets
- Restrict `.env` to the deployment administrator.
- Escrow `REMENTUM_MASTER_KEY` off the server and out of database backups.
- Use generated database, cookie, and OAuth signing keys.
- Keep provider keys and Resend keys out of source control and support logs.
- Leave the superuser blanks in `docker-compose.yml` in place: the `api` and `worker` services
override `REMENTUM_DATABASE_ADMIN_URL` and `REMENTUM_POSTGRES_SUPER_PASSWORD` with empty values
so a superuser connection, which bypasses row-level security, never exists inside those
containers. Only `migrate`, `backup`, and `restore` receive it.
- The API, worker, and embedding images contain only production dependencies and built output,
every service runs with `no-new-privileges`, and the Node services drop all Linux capabilities.
## Envelope encryption and data boundary
Rementum implements **application-layer envelope encryption with searchable metadata**:
- **Envelope encryption:** Article bodies, version history, and staged bodies are encrypted with
AES-256-GCM under individual per-brain data keys. The instance master key wraps those data keys and
is held in memory; it is never stored in PostgreSQL or included in database backups.
- **Position-bound authentication (AAD):** Every version body is sealed with Additional Authenticated
Data that names its brain, article, and version number. Readers recompute that value from the row's
position rather than trusting a stored copy, so ciphertext moved between versions or articles fails
to decrypt.
- **Searchable metadata (plaintext):** PostgreSQL stores routing summaries, titles, slugs, links,
audit metadata, MCP usage metadata, and vector embeddings in plaintext so hybrid metadata, full-text,
and vector search operate without decrypting article bodies. MCP usage metadata is limited to the
workspace and optional brain/article ids, the OAuth client id and name, the tool name, and the
timestamp (it does **not** keep tool arguments, prompts, queries, outputs, errors, IP addresses, or
user ids). Use encrypted disks and encrypted backups.
## Article generation mode
Rementum stores submitted titles, one-sentence summaries, and bodies as they are. Nothing is
generated, no article text is sent to an external model, and there is no AI provider to configure.
The summary is searchable metadata and is not covered by article-body encryption.
An agent connected over MCP can propose a brain invitation but never receives a link: the proposal
waits on the brain page until an owner approves it in the browser, which is also where the link is
issued and sent. An instruction planted in an article the agent read therefore cannot grant anyone
access on its own.
## Accounts
Keep public registration off unless you need it. When on, it requires verified email delivery and
applies request rate limits. Prefer team invitations for controlled access, and remove members who no
longer need the team's workspaces.
The web interface uses a 14-day opaque session cookie. PostgreSQL stores only its hash. Logout revokes
the current session; a password reset revokes every web session, MCP OAuth grant, and API key for the
account. OAuth bearer tokens and API keys are accepted only at the exact workspace MCP URL, never by
the REST API, so an MCP client cannot reuse the browser cookie as an API credential.
MCP access tokens last 15 minutes. Refresh tokens and their authorization grants have a 360-day
idle lifetime: issuing a replacement refresh token extends the still-valid grant to cover it.
Clients allowed to refresh tokens are independent of the browser OAuth session, including clients
that omit `offline_access`. Active clients can stay connected beyond the original authorization date. Expired or revoked
grants are never revived, and replaying a refresh token whose replacement was used invalidates its
token family.
Existing valid grants adopt the longer lifetime when refreshed; an already expired grant requires
new authorization. Connections issued by older releases may still carry a browser-session binding;
re-authorize those once to obtain an independent connection.
Clients should persist each replacement refresh token before exiting and coordinate refreshes
across processes sharing credentials, including background processes that share a CLI's credential
store. A client that never read a refresh reply, for example because the machine slept
mid-request, may retry with the old token while the lost replacement is still unused: the API
consumes that replacement and issues a new one. If the lost replacement surfaces later, two holders
exist and the grant is revoked. The trade-off is that a leaked, already rotated token works until
the legitimate client next refreshes, which then revokes the grant for both. The API logs
`oauth_refresh_failed` with a fixed reason category to distinguish missing or expired grants from
refresh-token reuse, and `oauth_refresh_retried` when it recovers a lost reply; it does not log
token values or raw provider error details.
The browser portion of MCP OAuth uses the current web session as its identity. A browser without one
is redirected through the normal sign-in page. Before Rementum grants the scopes requested by the
client, it verifies that the signed-in account belongs to the team that owns the workspace. There is
no second OAuth login. A new client, workspace, or expanded scope set shows a consent screen with the
client identity, callback destination, resource, scopes, and a deny action; an already approved grant
can reconnect silently. Knowing a workspace URL alone grants nothing, and the issued bearer token
remains audience-bound to that one workspace. Successful clients remain individually revocable on
**Connections**.
API keys are the alternative for clients that cannot keep a refresh chain alive. Only the web app can
create, list, or revoke one: the key routes need the `connection:*` scopes, which neither an OAuth
connection nor a key carries, so an agent cannot mint a key for itself. Rementum generates 32 random
bytes, shows the `rmk_` key once, and stores only its SHA-256 hash. A key is bound to the workspace
chosen when it was created and is refused at any other MCP URL. On each request the API checks the
owner's account is still enabled and narrows them to that workspace exactly as it does an OAuth
token, so the key gets the four workspace scopes, never the instance-owner flag, and stops working
when the owner leaves the team. Keys do not expire, so a leaked key works until you revoke it on
**Connections**, reset the password, or delete the workspace. MCP usage analytics list a key's calls
under its name.
A client sends the key as the `apiKey` query parameter of the workspace MCP URL, so connectors that
only take a URL can use it. A URL is copied and logged more readily than a header. The API's request
log and the bundled Caddy's access and error logs replace the value with `[redacted]`, but the client's own
configuration and logs keep it in full, and so does any proxy or CDN in front of Rementum that logs
request URLs. The API reads the key only from that parameter; a key sent as a bearer token is
rejected.
An MCP client identifies itself in one of two ways. A client that publishes an OAuth Client ID
Metadata Document, as Claude Code does at `https://claude.ai/oauth/claude-code-client-metadata`, uses
that HTTPS URL as its client id. Rementum fetches the document, requires it to name the same URL as
its own `client_id`, and takes the allowed callback addresses from it, so no client record is created
or stored. The fetch has a 2.5-second timeout and a 5 KB size limit, refuses hosts that resolve to
loopback, private, or other special-use addresses, and the document is cached for as long as its
cache headers allow. Anything the document says about itself, including its name, is chosen by
whoever controls that URL. Clients without a hosted document register themselves dynamically and
appear under the name they registered. In both cases Rementum validates the redirect URI before it
returns an authorization code.
The instance owner, the account `create-owner` made, has a read-only panel at `/admin` that counts
what the instance holds and lists every registered account (see
[the operations guide](operations.md#instance-panel)). It shows metadata only: addresses, names,
verification state, memberships, connection counts, and timestamps, never an article body, a tool
argument, or a network address. The owner flag is checked by the API and again inside the database
functions that read across teams, so a missed check in one layer exposes nothing. A bearer token
never reaches the panel because the REST API accepts browser sessions only, and an OAuth actor
narrowed to a workspace drops the flag altogether.
Deletion is permanent, immediate, and web-only. There is no trash, and MCP agents cannot delete
anything:
- **Workspaces.** Owners and admins create or rename them; only the team owner deletes one. The last
workspace is protected, and deletion needs the exact workspace name, because it permanently removes
every brain, note, and MCP usage record inside. Revoking a connection or deleting one brain does
not erase that workspace's historical usage metadata; it lasts until the workspace is deleted.
- **Brains.** Only the brain owner deletes a brain, and deletion needs the exact name. It also
destroys the only copy of the brain's wrapped data key, so its encrypted bodies become
unrecoverable even from backups taken afterward.
- **Teams.** Owners and admins create or rename them; only the team owner deletes a team, a user's
last team is protected, and deletion needs the exact name.
## Backups
The backup job refuses to write an unencrypted archive. Test a restore on a separate host, and keep
the age identity apart from the archives. A valid archive without `REMENTUM_MASTER_KEY` cannot decrypt
article bodies.
Report vulnerabilities through the private channel named in the repository `SECURITY.md`. Do not open
a public issue with exploit details or instance secrets.