Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 8 min read

Logistics User Files: 5 Isolation Tests for Free-Tier Alternative Storage Providers

A free storage tier is acceptable for a logistics receipt pipeline only after tenant isolation is tested as a system property, not inferred from a provider's bucket model. Keep the original receipt immutable, make every

A free storage tier is acceptable for a logistics receipt pipeline only after tenant isolation is tested as a system property, not inferred from a provider's bucket model. Keep the original receipt immutable, make every object key tenant-scoped, authorize reads through application policy, and treat the storage bill as a capacity-planning input rather than the decision itself.

TL;DR: choose a provider or self-hosted service that exposes the primitives you need, then put an adapter in front of it. The adapter should bind tenant identity to an object namespace, validate uploads before persistence, record an audit event for each state transition, and make deletion or retention changes explicit. A free tier can lower the cost of a pilot; it cannot compensate for a cross-tenant read, an unverified content type, or an original file that cannot be recovered during an audit.

The useful comparison is not a list of services with a zero-cost allowance. It is five isolation tests: can a request for tenant A ever address tenant B's key, can an operator prove which original was retained, can malformed input be rejected before it is trusted, can background jobs preserve the same authorization boundary, and can the design move providers without changing policy? Those answers determine the shape of the system for storing user files.

How should logistics teams choose alternative storage providers for a free tier?

There is no universal best provider. A free tier is useful for a bounded pilot, while a self-hosted alternative may fit a network-bound workload; both become poor choices when the team cannot test restore, authorization, or capacity against its SLO. The right boundary is the one the platform team can operate and verify.

What makes receipt storage an isolation problem?

A receipt is a document with operational and legal weight. In a logistics workflow it may arrive from a carrier, a warehouse, or a customer, and the platform may need to process its fields while retaining the original file for audit. The extracted amount, timestamp, and shipment identifier are useful application data; they are not a substitute for the original bytes.

Tenant isolation fails in less dramatic ways than a public bucket. A key assembled from an untrusted account identifier can put files in the wrong namespace. A signed download URL generated by a worker can outlive the authorization that created it. A retry can overwrite an original with a transformed image. A bulk export can run with platform privileges and forget to filter by tenant. Each path deserves the same review as the upload endpoint.

My decision rule is strict: storage layout may help enforce isolation, but it must never be the only enforcement point. The application should authenticate the caller, resolve the tenant from trusted session state, authorize the receipt, and only then ask a storage adapter for bytes. A caller-supplied tenant ID is a filter value, not an identity claim.

That is the boundary.

Which five tests expose the dangerous boundary?

Run these tests against the complete request path, including workers and administrative tools. A green result means the boundary is enforced in that path, not that the underlying storage service has a convenient feature.

Test Expected property Evidence to keep
Cross-tenant read Tenant A cannot retrieve tenant B's object by guessed, copied, or replayed identifier Denied request and audit record
Cross-tenant write A request cannot create or replace an object outside its authorized namespace Rejected key or policy decision
Original preservation Processing creates derived data without mutating the retained receipt Original digest and version record
Worker parity OCR, thumbnails, exports, and retries apply the same tenant rule Job input, decision, and result metadata
Provider substitution Policy tests pass against another storage adapter Contract-test report

The first two tests are deliberately boring. Boring is the goal. Use opaque receipt IDs, but do not mistake opacity for authorization; a random identifier reduces guessing and does not grant permission. The service must compare the object record's tenant ID with the authenticated tenant before it constructs a storage operation.

For preservation, store metadata beside the object rather than encoding every business fact in a path. At minimum, the record needs an immutable receipt identity, tenant identity, object reference, content length, a digest calculated after validation, and the processing state. If a later job produces a normalized PDF or extracted JSON, those are new artifacts with new identities.

How should the safe implementation separate policy from storage?

An adapter keeps provider-specific calls out of handlers and makes the important contract testable. The interface below is intentionally small. It does not expose arbitrary key access to the rest of the application, and it does not pretend that a storage API can answer a tenant policy question.

package receipts

import (
    "context"
    "fmt"
    "io"
)

type TenantID string
type ReceiptID string

// ObjectStore is deliberately narrower than a general bucket client.
type ObjectStore interface {
    Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) error
    Get(ctx context.Context, key string) (io.ReadCloser, error)
}

type Receipt struct {
    ID         ReceiptID
    Tenant     TenantID
    Object     string
    Digest     string
    Bytes      int64
    MediaType  string
}

func objectKey(tenant TenantID, receipt ReceiptID) string {
    return fmt.Sprintf("tenants/%s/receipts/%s/original", tenant, receipt)
}

func (s *Service) StoreOriginal(ctx context.Context, caller TenantID, receipt Receipt, body io.Reader) error {
    if caller == "" || receipt.Tenant == "" || caller != receipt.Tenant {
        return fmt.Errorf("tenant authorization failed")
    }
    if receipt.ID == "" || receipt.Bytes <= 0 || receipt.MediaType == "" {
        return fmt.Errorf("invalid receipt metadata")
    }

    key := objectKey(receipt.Tenant, receipt.ID)
    return s.objects.Put(ctx, key, body, receipt.Bytes, receipt.MediaType)
}

This sample is a boundary sketch, not a complete upload validator. The real path should enforce a size limit before buffering a body, allow only the document types the workflow requires, and inspect the file signature rather than trusting a filename or a client-provided media type. OWASP's File Upload Cheat Sheet also calls out generated filenames, storage outside the web root, and malware analysis as controls to consider according to risk.

The original write should be create-once by policy. If the storage primitive cannot express that guarantee, the metadata service must reject a second write for the same receipt ID and the adapter must make retries idempotent. A retry after a network timeout is normal; a retry that silently replaces evidence is a data-integrity defect.

What does the free tier change in capacity planning?

It changes the pilot's spending profile. It does not change the capacity model.

Estimate at least these separate quantities: retained original bytes, derived artifact bytes, request volume, retrieval volume, and the number of audit or export operations. Receipt processing often creates more than one object per upload, so a plan based only on original file size will understate retention. Keep the variables explicit in a review document: average receipt size, receipts per tenant, tenant count, retention duration, retry rate, and the growth margin required by the SLO. The important limitation is that a free allowance describes a commercial boundary, not the workload's future shape; a receipt that is retained, transformed, indexed, downloaded for an audit, and copied during a migration consumes more operational capacity than its upload suggests.

A free allowance can also have boundaries that matter more than storage capacity, such as request limits, egress treatment, regional availability, lifecycle behavior, or operational support. Those terms change, so record the provider's current public terms when making the decision and recheck them before production. Do not turn a temporary allowance into an SLO.

The buy-versus-build decision has a more useful shape than β€œfree versus paid”:

Choice What it buys What the platform team still owns
Managed object storage Durable object primitives and a maintained service boundary Tenant policy, validation, metadata, recovery tests, and exit path
Self-hosted object storage Control over placement, software version, and network boundary Hardware or cluster capacity, upgrades, replication, on-call, and restore operations
Filesystem or database blobs A simple local integration for a narrow workload Scaling, backup correctness, access control, and migration tooling

The third option can be reasonable for a small internal workflow, but it is a poor default for retained originals when backup and restore behavior are not tested. The first option still needs an ownership model. A managed service removes some infrastructure work; it does not remove incident response for an authorization bug. That is the central trade-off: a managed provider reduces infrastructure ownership, while a self-hosted alternative increases control and also increases the team's restore, upgrade, and on-call obligations. Neither choice is suitable when the team has not assigned those obligations to an owner.

How do verification and rollback work after deployment?

Verification must cover the paths that engineers tend to forget. Exercise an upload, a download, a failed authorization, a duplicate retry, a processing job, a derived artifact request, a tenant export, and a retention operation. Run the same contract tests against each adapter. The test fixture should include two tenants with similarly named receipts; otherwise a missing tenant predicate can pass unnoticed.

Observe decisions, not just errors. A useful audit event includes a request or job ID, tenant ID, receipt ID, operation, authorization result, object reference, and timestamp. Avoid putting receipt contents or secrets into logs. Metrics should distinguish denied access, validation rejection, missing object, processing failure, and provider failure, because they lead to different on-call actions.

Rollback has two layers. For an application release, disable the new processing path while preserving the original-write contract, then drain or quarantine jobs whose authorization context is incomplete. For a storage migration, copy originals without deleting the source, compare recorded digests, verify tenant-scoped reads, and switch reads only after the comparison succeeds. Keep the source until the retention policy and recovery test permit removal.

Do not use a rollback that restores availability by weakening tenant checks. That trades a visible incident for an invisible one.

References

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.