If you discover a security vulnerability in Aegis, do not open a public issue. Instead, email the maintainer directly at:
Include:
- A description of the vulnerability
- Steps to reproduce
- Impact assessment (what an attacker could do)
- Suggested fix, if you have one
You will receive a response within 48 hours. Confirmed vulnerabilities will be patched and disclosed responsibly.
Aegis is designed for a single user running on a single machine. The threat model assumes:
- One operator who owns the VPS and all connected accounts
- No public-facing ports -- all external access is through a Cloudflare Tunnel
- One machine-to-machine caller (OpenClaw) talking to the data-api
- No multi-user authentication -- there are no user accounts, sessions, or login flows
This is intentional. Aegis is a personal tool, not a SaaS platform.
| Threat | Mitigation |
|---|---|
| Credential theft from database | AES-256-GCM encryption at rest with AAD context |
| Data exfiltration via outbound messages | PII redaction hook scans all outbound messages |
| Tampered audit trail | SHA-256 hash-chained audit log with verification endpoint |
| Runaway LLM costs | Budget guard hook with daily/monthly limits and alerts |
| Unauthorized API access | Bearer token auth with constant-time comparison |
| Container escape | cap_drop: [ALL], no-new-privileges: true, read-only filesystem (prod) |
| Network sniffing between services | Internal Docker networks (no host binding in production) |
| Credential exposure in logs | structlog with secret redaction patterns |
| Threat | Reason |
|---|---|
| Multi-user isolation | Single-user system by design |
| DDoS protection | Cloudflare handles this at the edge |
| Physical server access | Assumed to be under operator control |
| Supply chain attacks on base images | Mitigated by Trivy scanning in CI, but not fully in scope |
Aegis uses a single Bearer token (DATA_API_TOKEN) for machine-to-machine authentication between OpenClaw and the data-api. There is no JWT, no sessions, no TOTP, and no user login.
The token is:
- Auto-generated by
bootstrap.sh(64 hex characters, viaopenssl rand -hex 32) - Stored in
.env(never committed to version control) - Validated using
hmac.compare_digest()for constant-time comparison (prevents timing attacks) - Required on every request to the data-api (except
/health,/docs,/openapi.json)
The OpenClaw gateway uses a separate token (OPENCLAW_GATEWAY_TOKEN) for its Control UI authentication.
All sensitive data stored in PostgreSQL is encrypted with AES-256-GCM before being written to the database:
- Credentials -- API tokens, OAuth refresh tokens, passwords for third-party services (Plaid, Canvas, Garmin, etc.)
- Financial account identifiers -- Plaid access tokens, account IDs
- Health data -- Raw health metrics from Garmin and Apple Health
Each encrypted field uses:
- A unique random nonce (96 bits) per encryption operation
- Authenticated Additional Data (AAD) that includes the user ID and field context, binding the ciphertext to its intended row
- The
ENCRYPTION_MASTER_KEYfrom.env(256-bit key, hex-encoded)
The following are stored in plaintext because they are not sensitive or because encryption would prevent useful queries:
- Transaction amounts and dates (needed for SQL aggregation)
- Transaction categories and merchant names
- Assignment titles and due dates
- Audit log entries (integrity protected by hash chain, not encryption)
- LLM usage records (token counts, cost calculations)
- Content draft text (not considered PII)
The encryption key can be rotated using infrastructure/scripts/rotate-secrets.sh with ROTATE_ENCRYPTION_KEY=true. This requires re-encrypting all stored credentials with the new key before restarting services. The script warns about this explicitly.
Every significant action in the data-api is recorded in a SHA-256 hash-chained audit log:
- Each entry contains an
action,resource_type,resource_id,detail, andmetadata - Each entry's hash is computed from its content plus the hash of the previous entry
- The chain can be verified at any time via
GET /audit/verify - A broken chain indicates tampering
The audit-logger hook in OpenClaw also logs agent-side events (commands, sent/received messages) to the same audit endpoint.
Audit entries are never deleted. The log is append-only.
The pii-guard hook runs on every outbound message (message:sent event) and scans for:
| Pattern | Action |
|---|---|
| Social Security Numbers (XXX-XX-XXXX) | Replaced with [SSN] |
| Credit/debit card numbers (16 digits) | Replaced with [CARD_NUMBER] |
| Bank account numbers (10-17 digits) | Masked to ****XXXX (last 4 visible) |
Redaction happens before the message reaches the delivery channel (WhatsApp). The hook logs redaction counts to stderr for monitoring.
Email addresses and phone numbers are intentionally NOT redacted, as they are considered acceptable to display in context.
The budget-guard hook tracks LLM token usage and cost:
- Daily budget: $5.00 by default (
LLM_DAILY_BUDGET_USD) - Monthly budget: $50.00 by default (
LLM_MONTHLY_BUDGET_USD) - Alerts are sent to WhatsApp at 80%, 95%, and 100% of each threshold
- At 100%, the hook warns the agent to reduce non-essential operations
Budget data is tracked via POST /budget/usage and queryable via GET /budget/usage.
- Zero host port bindings -- no service listens on the host network
- Three isolated Docker networks:
frontend-- OpenClaw gateway + cloudflared (bridge, external-facing)backend-- OpenClaw gateway + data-api (internal, no external access)data-- data-api + PostgreSQL (internal, no external access)
- The
cloudflaredcontainer is the only path to the outside world
- Ports are bound to
127.0.0.1only (localhost, not0.0.0.0):127.0.0.1:18789-- OpenClaw Control UI127.0.0.1:8000-- data-api (for direct testing)127.0.0.1:5432-- PostgreSQL (for direct queries)
All containers run with:
cap_drop: [ALL]-- drop all Linux capabilitiesno-new-privileges: true-- prevent privilege escalation- Non-root users where possible (data-api runs as
aegisuser) - Read-only filesystem in production (
read_only: trueon data-api)
- PostgreSQL:
pgvector/pgvector:pg16(official pgvector image based on PostgreSQL 16) - Data API:
python:3.12-slim(multi-stage build, only runtime dependencies in final image) - OpenClaw: Built from the OpenClaw repository Dockerfile
- Cloudflared:
cloudflare/cloudflared:2025.2.1(pinned version)
Managed by uv (from Astral, the ruff team). The data-api has approximately 15 direct dependencies, all pinned in uv.lock. Key packages:
fastapi,uvicorn-- web frameworksqlalchemy[asyncio],asyncpg-- databasecryptography-- AES-256-GCM encryptionhttpx-- async HTTP client for all integrationsstructlog-- structured loggingpydantic-settings-- configuration management
The GitHub Actions CI pipeline includes:
rufffor Python linting (security rules enabled viaSselector)- Trivy container vulnerability scanning (fails on HIGH/CRITICAL)
- Docker Compose config validation
All secrets are injected via environment variables from .env:
DATA_API_TOKEN-- machine-to-machine authENCRYPTION_MASTER_KEY-- AES-256-GCM keyPOSTGRES_PASSWORD-- database passwordANTHROPIC_API_KEY-- Claude API access- Integration-specific tokens (Plaid, Google, etc.)
For version-controlled secret files, Aegis uses SOPS + age encryption:
- Encrypted files:
secrets/*.enc.yaml - Decrypted at deploy time by
deploy.sh, cleaned up on exit .gitignoreexcludes all unencrypted.yamlfiles insecrets/
infrastructure/scripts/rotate-secrets.sh rotates:
POSTGRES_PASSWORD(updates both.envand the running PostgreSQL instance)DATA_API_TOKENENCRYPTION_MASTER_KEY(only withROTATE_ENCRYPTION_KEY=true, requires credential re-encryption)
External API keys (Plaid, Anthropic, Google, etc.) must be rotated manually via their respective dashboards.
Recommended rotation schedule: every 90 days for internal secrets.