Skip to content

Security: podheitor/expresso-v4

Security

docs/SECURITY.md

Security

What runs where

Component Runtime Network exposure
expresso-auth Container (port 8100) Public — OIDC RP; relays to Keycloak
expresso-mail Container (ports 25/143/587/8001) SMTP/IMAP/Submission public; HTTP API internal
expresso-calendar Container (port 8002) Internal + CalDAV (behind reverse proxy)
expresso-contacts Container (port 8003) Internal + CardDAV (behind reverse proxy)
expresso-drive Container (port 8004) Internal; MinIO S3 on 9000 (LAN only)
expresso-notifications Container (port 8006) Internal SSE; Redis cross-pod relay
expresso-search Container (port 8007) Internal; bearer-token gated
expresso-chat Container Internal; WebSocket over TLS
expresso-meet Container Internal; WebRTC signalling
expresso-admin Container Internal; superadmin role required
expresso-compliance Container Internal; audit-log reader
Keycloak Container (port 8080) Public IdP; all JWT issuance
PostgreSQL Container (port 5432) LAN only; RLS enforced per tenant
Redis Container (port 6379) LAN only; notification relay
MinIO Container (port 9000/9001) LAN only; drive object storage
NATS JetStream Container (port 4222) LAN only; event bus

Reporting vulnerabilities

Please report security issues via GitHub Security Advisories (private disclosure): https://github.com/<org>/expresso/security/advisories/new

Do not open public issues for security vulnerabilities — they expose users before a fix is available.

Expected response time: acknowledgement within 2 business days, patch timeline within 14 days for critical issues.

Threat model

Untrusted inputs and mitigations

Input surface Where it enters Gate covering it
JWT access tokens Every service via Authenticated extractor expresso-auth-client JWKS validation + expiry check
OIDC state / code expresso-auth callback PKCE S256; state stored in-memory with TTL; is_safe_local_redirect for post-login URL
SMTP envelope (MAIL FROM / RCPT TO) expresso-mail SMTP session milter pipeline; SPF/DKIM/DMARC via mail-auth
IMAP commands expresso-mail IMAP session imap-codec parser; per-user RLS on DB queries
CalDAV/CardDAV XML bodies expresso-calendar, expresso-contacts quick-xml parse with bounded depth; PROPFIND depth 0/1 enforced
Drive file uploads expresso-drive tus.io endpoint 50 GB hard cap; 16 MiB per chunk; filename ≤ 255 bytes; MIME ≤ 255 bytes
Search queries expresso-search Tantivy query parser; bearer token required
Sieve scripts expresso-mail sieve endpoint sieve-rs parser; script size cap enforced
Webhook / iMIP email bodies expresso-imip-dispatch Envelope parsed by expresso-imip; only REPLY/CANCEL/REQUEST accepted
Admin API calls expresso-admin superadmin role check on every handler

Multi-tenancy boundary

Row-Level Security (RLS) is bootstrapped in migration 20260417143000_rls_bootstrap_and_mailbox_stats.sql. Every query in service handlers binds tenant_id from the validated JWT — never from user-supplied body parameters. Cross-tenant reads require an explicit superadmin role and are logged to the audit table.

Secret storage

  • JWT signing keys managed by Keycloak (RSA/EC); services hold no signing keys, only JWKS URLs.
  • Database credentials, MinIO keys, NATS credentials — environment variables only; never in source.
  • DKIM private keys — generated by scripts/dkim-keygen.sh; stored in volume, not in repo.

CI security gates

Gate Workflow Frequency
cargo-deny advisories + licenses + bans ci.yml Every push/PR
cargo-audit (RustSec) security.yml Every push to main + daily cron
Gitleaks (secret scan, full history) security.yml Every push/PR
CodeQL (security-extended queries) codeql.yml Every push/PR + weekly
Miri (UB — FFI-free crates) security.yml Every push to main
Dependabot (Cargo + Actions) dependabot.yml Weekly

Known limitations / accepted risks

  • Miri coverage is limited to expresso-crypto and expresso-mail-parser; crates with FFI (OpenSSL, libpq via sqlx) cannot run under Miri. AddressSanitizer (scripts/quality-check.sh --full) covers those paths locally.
  • expresso-search uses a static bearer token (SEARCH_SERVICE_TOKEN); rotate it on each deployment.
  • CalDAV/CardDAV depth-infinity PROPFIND is rejected at the protocol layer, but a malicious client can still issue many concurrent depth-1 requests — rate limiting at the reverse proxy is expected.

There aren't any published security advisories