Object storage services (S3, GCS) are effectively infinite, flat key–value stores. They do not understand your application’s concepts of users, tenants, or sharing.
Secure multi-tenant file sharing requires a control plane: an application-owned registry that decouples where bytes live from who is allowed to access them. Storage is the data plane. Authorization lives entirely in your app.
This post focuses on the control plane: modeling files, enforcing tenant isolation, and issuing short-lived delivery grants without ever exposing storage paths as a security boundary.
System description
A database-backed object registry and API layer that acts as the source of truth for file ownership, access policies, share links, lifecycle state, and audit — issuing short-lived delivery grants only after authorization succeeds.
Golden path
Client requests file by object_id → API authenticates user → API resolves object in registry → API evaluates access policy (Tenant / ACL) → API verifies status (Published) → API issues short-lived delivery grant → Client fetches bytes from storageAccess is always requested by object_id (a UUID), never by bucket or key.

Core design
Object registry (the “what”)
Every upload creates a registry record. Minimum fields:
object_id: Opaque, unguessable (UUID)tenant_id: The isolation boundaryowner_principal_id: For granular permissionsstorage_locator:s3://bucket/key/v1(Internal only)status:quarantine|published|deletedpolicy_ref:PRIVATE|TENANT_WIDE|ACL_ID
Soft delete support: The registry is the absolute source of truth for file existence. When a file is marked as deleted in the database, the API must block all access immediately, even if the physical bytes still exist in S3 storage awaiting a background cleanup process.
Authorization decisions are made using object_id + policy. Storage locators are never accepted from clients and never used as auth inputs.
Access is granted only if all of the following hold:
The object belongs to the caller’s
tenant_id(enforced for authenticated paths)The caller satisfies the access policy (Owner / ACL / Share Token)
The object status is
published
Authorization logic must be centralized (e.g., can_access(user, object_id, action)), and every access path must call it.
Share links are a distinct access mode that bypasses normal user auth but requires strict controls.
Use long, random tokens (e.g., 32-byte CSPRNG) to make guessing impossible
Store
SHA256(token)in the DB. If the DB leaks, valid tokens are not exposedNote: Tokens have high entropy, so a fast hash (e.g., SHA-256) is sufficient and avoids bcrypt-style CPU exhaustion on public endpoints
Stateful logic:
link_token→{ object_id, tenant_id, expires_at, revoked_at }The share endpoint hashes the input token, looks up the record, checks
revoked_at, verifies the object ispublished, and then issues a delivery grant
Revocation:
UPDATE links SET revoked_at = NOW()
Hand-off to the data plane
Once authorization succeeds, the control plane issues a delivery grant:
Scoped to a single object
Short-lived (e.g., 5 minutes)
Signed (Pre-signed URL or CDN cookie)
Threat model
Baseline assumptions
Clients and networks are untrusted: They can retry, replay, and lie about metadata
Control plane authority: Your API can derive tenant / user context from the auth token (not the request body)
Least privilege: The API has minimal IAM permissions (e.g.,
s3:GetObject,s3:PutObjecton specific paths), nots3:*Standard infra controls such as TLS, WAF, database AuthN, SQLi prevention, etc. are assumed to be in place. This model focuses on the file sharing logic
A note on risk: you won’t fix everything
This table isn’t a checklist where every row must be fully eliminated. Focus on preventing the worst failures and limiting blast radius. In practice: ship prevention for the High rows first, then add monitoring and response for what you can’t realistically prevent.
Asset | Threat | Baseline Controls | Mitigation Options | Risk |
|---|---|---|---|---|
Tenant isolation | IDOR: Attacker guesses | Opaque IDs | 1. Binding: Enforce 2. No paths: Never accept bucket or key parameters from clients 3. 404s: Cross-tenant requests return 404 (hide existence) | Medium |
AuthZ logic | Confused deputy: One endpoint skips or weakens checks (e.g., "Download" vs "Preview") | Central authentication | 1. Centralize: Single 2. Fail closed: Reject access if policy logic is ambiguous or unhandled | Medium |
Object lifecycle | Leakage: Quarantined or "Soft Deleted" objects remain downloadable | Status field in the object registry | 1. Status check: Reject grants unless 2. Exception: Allow quarantine access only for the uploader | Medium |
Share links | Forever access: Token behaves like a permanent public link | Server-side state | 1. Revoke tokens: DB-backed tokens allow instant revocation 2. Re-auth: Re-verify object status before issuing the grant | High |
Registry integrity | Mass assignment: Attacker modifies | DB permissions | 1. Field locking: Restrict 2. Audit: Log all state changes to an immutable audit trail | Medium |
Offboarding | Zombie access: Deleted tenant’s files remain accessible via old links | Soft delete | 1. Cascade: Revoke all share links when a tenant is disabled 2. Block: Global check for | Low |
Token leakage | Log leaks: Share tokens appear in logs / analytics | Short-lived delivery grants | 1. Redaction: Redact query params in server logs 2. Monitoring: Monitor for unusual access patterns on token endpoints | Low |
Verification checklist
Tenant isolation
Cross-tenant access using a valid
object_idreturns404 Not FoundList endpoints only return objects where
object.tenant_id == caller.tenant_id
Authorization
Every download path (download, preview, thumbnail) calls the centralized auth function
Read-only users cannot generate upload grants or delete objects
Lifecycle
Quarantined / deleted objects never receive delivery grants
Soft-deleting an object immediately blocks new downloads (even if bytes exist)
Share links
Tokens are hashed in the database (SHA-256); plain text is never stored
Revoked links stop working immediately (return 404 or 410)
Share links resolve to a single object only (no directory walking)
Link tokens are random strings (not JWTs)
Implementation & Review
The full threat model matrix, architectural diagrams, and a printable verification checklist for this pattern are available in the Secure Patterns repository. Use these artifacts to guide your design reviews and internal audits.
