This document describes the security concept for the “Ontheia” system, consisting of WebUI, host (backend) and database (Postgres). It serves as a reference for implementation and as a template for security audits.
- Confidentiality: Protection of user data, AI prompts and API keys.
- Integrity: Protection against unauthorized manipulation of agent configurations and memory contents.
- Availability: Protection against denial of service through resource limits (MCP servers, LLM quotas).
- Isolation: Strict separation between different users (multi-tenancy) and between the host system and the MCP servers (sandboxing).
- Password storage: bcrypt (
bcryptjs) with a cost factor of 12; the salt is part of the hash.
- Sessions:
- Opaque session tokens (UUID) stored in
app.sessions — no JWT, no cookies.
- The token is sent by the WebUI as
Authorization: Bearer <token> and held in localStorage.
- Session lifetime: 7 days, sliding renewal on use (a session only ends after 7 days of inactivity); sessions can be revoked server-side (
revoked flag).
- CSRF: Structurally not applicable — no credential is sent automatically by the browser, so a foreign origin cannot ride along on an existing session.
- Trade-off: A token in
localStorage is readable by JavaScript, so it is exposed by a successful XSS. This is why the strict CSP in section 6 is a load-bearing control, not a nicety.
- Multi-factor authentication (MFA): (planned for phase 2).
- Role-based access model (RBAC):
admin: Full access to system configuration, MCP server management and all user resources.
user: Access to own chats, agents and assigned tools.
- Database level (RLS): PostgreSQL Row Level Security ensures that users can only access their own records in the
app and vector schemas.
- Tenant separation: Isolation at namespace level in the memory adapter (
vector.user.<user_id>.*).
- Runtime environment: Docker Rootless by default for all MCP servers.
- Hardening flags (enforced by the orchestrator):
--read-only: The container file system is read-only.
--tmpfs /tmp:rw,nosuid,nodev,size=64m: Limited writable storage for temporary data.
--cap-drop=ALL: Drops all Linux capabilities.
--security-opt no-new-privileges: Prevents privilege escalation.
- Resource limits:
- CPU: max. 1 core (configurable).
- Memory: max. 512 MB (configurable).
- PIDs: max. 256 processes.
- Allowlists:
- Docker images: Only explicitly approved images (
config/allowlist.images).
- Packages: Validation of npm/PyPI packages when using
uvx or npx.
| Protection | Description |
|---|
| Path boundary (skill_dir) | Every skill file access checks resolved.startsWith(skill_dir). Path traversal attacks (../../etc/passwd) are blocked. |
| Scope permissions | User-scope skills: only owner can write. Global-scope skills: admin only. All authenticated users can read. |
| Script execution (cli-tools) | Scripts run exclusively via the cli-tools MCP server, which runs in Docker Rootless. The ALLOWED_COMMANDS allowlist limits which commands can be executed. |
| Code adaptation by LLM | The LLM adapts code templates from SKILL.md and passes them as arguments to uv run or python3 -c. No persistent script files are written by the LLM outside of explicit write_skill_resource calls. |
| No self-installation | The cli-server cannot install packages permanently. uv run --with <package> creates isolated environments that are discarded after the process ends. |
| RLS on app.skills | The app.skills table is subject to Row Level Security. Users see only global and their own skills. |
| Content security | Skills must not contain malware, exploit code, or misleading content (Principle of Lack of Surprise, agentskills.io standard). |
- Network isolation: MCP servers run in a dedicated Docker network (
ontheia-net) without direct access to the host or other containers (unless explicitly configured).
- Egress control: Global allowlist for outbound connections (
config/allowlist.urls).
- WebUI protection:
- Strict Content Security Policy (CSP) to prevent XSS — see CSP template.
frame-ancestors 'none' and X-Content-Type-Options against clickjacking and MIME sniffing.
- In transit: All connections (WebUI → host, host → LLM provider) must be encrypted via TLS (HTTPS/WSS).
- At rest: Encryption of database volumes and file systems (infrastructure level).
- Secret management:
- API keys do not belong in configuration files inside the repository.
- Secret references (
secret:NAME) resolved from environment variables at runtime are the recommended form; only then does the value never leave the process ENV.
- A provider key typed directly into the Admin UI is stored in the database in plain text. It is masked in logs and previews and is not returned over the API — but it is not encrypted. Its protection therefore rests on the encryption of the database volume (see “At rest”).
- Masking of secrets in logs and UI previews.
- Schema validation: All API requests are checked against JSON schemas (
contracts/schemas/).
- Sanitizing: Cleaning of AI-generated content (Markdown, HTML) before display in the WebUI.
- Rate limiting: Protection of API endpoints against brute force and DoS attacks.
- Audit logs: Logging of all security-relevant actions (logins, MCP server starts, access to memory namespaces).
- Metrics: Monitoring of error rates and resource consumption via Prometheus.
- Alerting: Notification on suspicious activity (e.g. repeated failed logins, sandbox escape attempts).
| Area | Check | Status | Note |
|---|
| AuthN | Are passwords securely hashed? | [x] | bcrypt (cost 12) |
| AuthN | Are session tokens opaque, server-side revocable and expiring? | [x] | app.sessions, 7 days, revoked flag |
| AuthZ | Does RLS take effect correctly in the database? | [x] | Verified via rls_audit.sql |
| Sandbox | Do MCP servers really run as rootless Docker? | [x] | Enforced by orchestrator |
| Sandbox | Are resource limits (cpu, mem) enforced? | [x] | Configurable via config |
| Network | Is the CSP in the WebUI active and strict? | [x] | Via Fastify Helmet |
| Network | Does the egress allowlist work for MCP servers? | [x] | Enforced by orchestrator |
| Secrets | Are API keys never handed out to clients? | [x] | Redacted at the API boundary (since 0.5.0) |
| Secrets | Are provider keys encrypted at rest in the DB? | [ ] | No — only secret: references keep the value out of the database |
| Input | Are all API inputs validated against schemas? | [x] | Ajv integration active |
| Audit | Are MCP server starts recorded in the audit log? | [x] | Logging active in the host |
| Skills | Is path traversal blocked for skill file access? | [x] | safeSkillPath() in SkillService |
| Skills | Are skill scope permissions enforced server-side? | [x] | Via RLS + handler check |
| Skills | Does skill script execution run through cli-tools (Docker Rootless)? | [x] | cli-tools in own container |