Security Architecture
Encryption, authentication, authorisation, and compliance posture.
Encryption at Rest
Keyring (AES-256-GCM)
All secrets stored in the database (LLM API keys, OAuth tokens) are encrypted with AES-256-GCM before insert.
Ciphertext format: v1:<12-byte IV (base64)>:<auth tag (base64)>:<ciphertext (base64)>
The 32-byte master key lives in:
- Production: Azure Key Vault (
orginabox-keyringsecret) - Local:
~/.orginabox/.keyring(file mode 0600)
On Azure, the API and worker containers use a user-assigned managed identity with Key Vault Secrets User role — no credentials in environment variables.
The current Azure preview still leaves Key Vault on public networking with Azure-services bypass enabled. Secret access is identity-gated, but private endpoints / locked-down network ACLs are still a follow-up before calling the hosted path production-grade.
Database
- PostgreSQL TLS enforced in Azure (Flexible Server with
sslmode=require) - Backups encrypted at rest by Azure
Authentication
JWTs
HMAC-SHA256 JWTs derived from the keyring:
| Token | TTL | Claims |
|---|---|---|
| Access token | 15 minutes | sub, tid, roles, iat, exp |
| Refresh token | 7 days | Stored encrypted in entra_sessions |
Azure AD SSO (PKCE flow)
- Browser →
/v1/auth/login→ redirect tologin.microsoftonline.com - User authenticates with Microsoft
- Callback → exchange code for tokens → extract
tid,oid, and user identity from the id_token - Find-or-create tenant + user rows
- Seeded roles and tenant provisioning are applied in-app
- Issue OIAB JWT, store refresh token encrypted
Automatic Azure AD group-to-role mapping remains a planned follow-up, not a fully wired part of the checked-in callback flow.
Split app registrations (SaaS prod)
Customer and platform traffic route through two distinct Entra app registrations so they can be scoped and audited independently:
- Customer app reg (
AZURE_CLIENT_ID, multi-tenant): handles/v1/auth/*for customer tenant admins and end users. Tenant ID typicallyorganizationsorcommon. Redirect:/v1/auth/callback. - Platform app reg (
AZURE_PLATFORM_CLIENT_ID, single-tenant,appRoleAssignmentRequired=true): handles/v1/platform/auth/*for internal staff only, gated on membership in the platform-admins security group. Tenant ID MUST be the specific platform tenant ID (nevercommon). Redirect:/v1/platform/auth/callback.
Each platform variable (AZURE_PLATFORM_CLIENT_ID, AZURE_PLATFORM_CLIENT_SECRET, AZURE_PLATFORM_TENANT_ID) independently falls back to its AZURE_* counterpart when unset. Self-hosted single-tenant pilots can omit the platform variables entirely and use one app reg for both flows — the split only matters in SaaS prod where customer-facing sign-in and internal staff sign-in must not share an identity.
Per-invite authority pinning
When a first-admin invite is being claimed, /v1/auth/login accepts an optional invite=<token> query param. If the token matches a pending, unexpired tenant_admin_invites row whose tenant has a provisioned entra_tenant_id, the login handler overrides AZURE_TENANT_ID just for that sign-in and uses the invite tenant's directory as the authorize authority (the same authority is re-resolved server-side on the callback for token exchange). This lets AZURE_TENANT_ID=organizations stay the multi-tenant SaaS default while still letting Azure's Home Realm Discovery route B2B guest emails whose domain isn't a verified Azure tenant. Unknown, expired, or revoked invites silently fall back to the env-configured authority — no information is leaked about token validity. The raw invite token is never placed in the Entra authorize or callback URL.
Legacy API Key
When AZURE_CLIENT_ID is unset, Authorization: Bearer <ORGINABOX_API_KEY> is accepted for all API requests. This is single-operator mode.
Authorisation (RBAC)
Every API route runs the auth middleware which:
- Verifies the JWT signature and expiry
- Loads the user's roles from
user_roles + roles - Computes effective permissions (union of all roles)
- Sets
c.set("auth", { user, tenant, roles, rbac })
Route handlers call rbac.agents.create, rbac.admin.view_audit_log, etc. before executing.
Network Security
- All inter-service traffic stays within the Docker / Container Apps network
- Only these ports are publicly exposed:
8787(API),3000(web), gateways - In Azure, Container Apps ingress uses mTLS termination
- In the checked-in Azure preview, Key Vault is still publicly reachable and not yet private-endpoint-only
- noVNC (
:6080) should be firewalled in production — it gives visual access to the sandbox
Sandbox Isolation
Agents execute inside an Ubuntu 24.04 container:
- No access to host filesystem (volume mounts are read-only config only)
- Network egress is unrestricted by default (configure Docker network policies to restrict)
- Each user's workspace is a separate Docker volume
Compliance Checklist
| Control | Status |
|---|---|
| Encryption in transit | TLS everywhere |
| Encryption at rest | AES-256-GCM for secrets; Azure disk encryption for DB |
| Authentication | Azure AD SSO + HMAC-SHA256 JWTs |
| Authorisation | RBAC on every endpoint |
| Audit logging | All actions logged with user + tenant + IP |
| Audit log retention | 2 years for auth/data, 1 year for tool/agent |
| Secret management | Azure Key Vault in production |
| Input validation | Zod on all API request bodies |
| SQL injection | Parameterised queries via Drizzle ORM |
| React rendering | Server components; content rendered via MDX, not raw HTML injection |
