Skip to content

Security Model

  1. Defense in depth — multiple layers of protection
  2. Least privilege — RBAC with granular permissions
  3. Zero trust between tenants — PostgreSQL Row-Level Security
  4. Encryption everywhere — at rest and in transit
  5. Tamper-evident audit — hash-chained audit log

Every tenant-scoped table has a PostgreSQL RLS policy:

ALTER TABLE credentials ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON credentials
USING (tenant_id = current_setting('app.tenant_id')::UUID);

The API middleware sets the tenant context on every request:

SET LOCAL app.tenant_id = '<tenant-uuid>';

This ensures that even if application code has a bug, one tenant’s data cannot leak to another — the database enforces isolation.

LayerMechanism
DatabasePostgreSQL RLS — queries filtered by tenant_id
APIAuth middleware extracts tenant from API key/session
RBACPer-route permission checks against user’s role
Object storageBucket prefix: tenants/{tenant_id}/
Rate limitingPer-tenant counters
Audittenant_id + user_id in all log entries

bsk_live_<48 hex characters>
  • Storage: SHA-256 hash only — plaintext never stored
  • Validation: Hash incoming key, lookup in api_keys table
  • Scoping: Optional permission subset (e.g., only credentials:issue)
  • Shown once: Key displayed at creation, never retrievable again
  • Generation: 32 bytes of cryptographic randomness (hex-encoded)
  • Storage: SHA-256 hash in sessions table
  • TTL: Configurable (default 24 hours)
  • Invalidation: Deleted on logout
  • Algorithm: Argon2id (memory-hard, GPU-resistant)
  • Salt: Unique per password (SaltString from OsRng)
  • Verification: Constant-time comparison

DataProtection
DID private keysStored as encrypted bytes in PostgreSQL
API keysSHA-256 hashed (one-way)
PasswordsArgon2id hashed (one-way)
CredentialsJSONB in PostgreSQL (column-level encryption optional)
Audit logsHash-chained for tamper detection
ChannelProtection
API requestsTLS 1.3 (enforced via HSTS)
Database connectionsTLS (configurable)
Inter-service (containers)Container network (isolated)

Every mutation is logged with a hash chain for tamper detection:

Entry N-1 Entry N
┌──────────────────┐ ┌──────────────────┐
│ id: uuid-1 │ │ id: uuid-2 │
│ action: issued │ │ action: revoked │
│ prev_hash: null │───────→│ prev_hash: uuid-1│
│ timestamp: ... │ │ timestamp: ... │
└──────────────────┘ └──────────────────┘

If any entry is modified or deleted, the chain breaks — detectable by verifying prev_hash references.

ActionTrigger
user.registeredNew user registration
credential.issuedCredential issuance
credential.verifiedCredential verification
credential.revokedCredential revocation
did.createdDID creation
did.deactivatedDID deactivation
consent.grantedConsent recorded
consent.revokedConsent withdrawn
member.invitedTeam invitation
member.removedTeam member removed
member.role_changedRole changed
api_key.createdAPI key created
api_key.revokedAPI key revoked

5 built-in roles with 25 granular permissions:

RoleKey capabilities
OwnerEverything — billing, tenant deletion, all operations
AdminTeam management, all operations (no tenant deletion)
MemberCredential operations, DIDs, compliance (no team management)
ViewerRead-only access
API-onlyProgrammatic operations (no team management)

Custom roles can combine any subset of the 25 permissions.


All API responses include:

HeaderValue
Strict-Transport-Securitymax-age=31536000; includeSubDomains
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
X-XSS-Protection1; mode=block
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()