Skip to content

Security: JiwaniZakir/sentinel

Security

SECURITY.md

Security Policy

Reporting Vulnerabilities

If you discover a security vulnerability in Aegis, do not open a public issue. Instead, email the maintainer directly at:

zakir@aegis-platform.dev

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.

Security Model Overview

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 Model

In scope

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

Out of scope

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

Authentication

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, via openssl 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.

Encryption

What is encrypted

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_KEY from .env (256-bit key, hex-encoded)

What is NOT encrypted

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)

Key rotation

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.

Audit Log

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, and metadata
  • 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.

PII Redaction

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.

Budget Guardrails

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.

Network Security

Production (docker-compose.yml)

  • 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 cloudflared container is the only path to the outside world

Development (docker-compose.override.yml)

  • Ports are bound to 127.0.0.1 only (localhost, not 0.0.0.0):
    • 127.0.0.1:18789 -- OpenClaw Control UI
    • 127.0.0.1:8000 -- data-api (for direct testing)
    • 127.0.0.1:5432 -- PostgreSQL (for direct queries)

Container hardening

All containers run with:

  • cap_drop: [ALL] -- drop all Linux capabilities
  • no-new-privileges: true -- prevent privilege escalation
  • Non-root users where possible (data-api runs as aegis user)
  • Read-only filesystem in production (read_only: true on data-api)

Dependencies and Supply Chain

Docker base images

  • 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)

Python dependencies

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 framework
  • sqlalchemy[asyncio], asyncpg -- database
  • cryptography -- AES-256-GCM encryption
  • httpx -- async HTTP client for all integrations
  • structlog -- structured logging
  • pydantic-settings -- configuration management

CI security scanning

The GitHub Actions CI pipeline includes:

  • ruff for Python linting (security rules enabled via S selector)
  • Trivy container vulnerability scanning (fails on HIGH/CRITICAL)
  • Docker Compose config validation

Secret Management

Runtime secrets

All secrets are injected via environment variables from .env:

  • DATA_API_TOKEN -- machine-to-machine auth
  • ENCRYPTION_MASTER_KEY -- AES-256-GCM key
  • POSTGRES_PASSWORD -- database password
  • ANTHROPIC_API_KEY -- Claude API access
  • Integration-specific tokens (Plaid, Google, etc.)

At-rest secrets

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
  • .gitignore excludes all unencrypted .yaml files in secrets/

Rotation

infrastructure/scripts/rotate-secrets.sh rotates:

  • POSTGRES_PASSWORD (updates both .env and the running PostgreSQL instance)
  • DATA_API_TOKEN
  • ENCRYPTION_MASTER_KEY (only with ROTATE_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.

There aren’t any published security advisories