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 storage

Access 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 boundary

  • owner_principal_id: For granular permissions

  • storage_locator: s3://bucket/key/v1 (Internal only)

  • status: quarantine | published | deleted

  • policy_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.

Authorization (the “who”)

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 exposed

    • Note: 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 is published, 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:PutObject on specific paths), not s3:*

  • 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 object_id to access other tenants' files

Opaque IDs

1. Binding: Enforce object.tenant_id == user.tenant_id on every lookup

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 can_access() authorization function used by all endpoints

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 status == published (default deny)

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 tenant_id or policy fields to steal ownership

DB permissions

1. Field locking: Restrict UPDATE access on ownership fields (immutable after creation)

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 tenant.isActive in the auth flow

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_id returns 404 Not Found

    • List 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.

Keep reading