| 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 |
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.
| 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 |
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.
- 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.
| 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 |
- Miri coverage is limited to
expresso-cryptoandexpresso-mail-parser; crates with FFI (OpenSSL, libpq via sqlx) cannot run under Miri. AddressSanitizer (scripts/quality-check.sh --full) covers those paths locally. expresso-searchuses 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.