Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Maskura security

This document describes the security model of the Maskura Gateway as it exists in the current streaming architecture (Phases 0–12): who authenticates, how API keys are handled at rest and in use, how objects are transformed and staged, what the trust boundaries are, what a production deployment must configure, and what Maskura explicitly does not guarantee. Anything not described here is not a guarantee.

For the vulnerability reporting process, see SECURITY.md.


1. Overview & trust boundaries

                        ┌─────────────────────────────────────────────────────┐
  Dashboard (browser)   │              Maskura Gateway                       │
   Supabase Auth/JWT ───▶  /dashboard/*  (JWT-validated)                      │
                        │                                                     │
  Maskura SDK / CLI ────▶  S3 data plane  ── Wasm pipeline ──▶ storage        │
  native S3 tools ──────▶  (SigV4 / API key)  (streaming, fail-closed)        │
                        │  (durable journal + staging for transactional paths)│
                        └─────────────────────────────────────────────────────┘
                                                                   (S3/R2/B2/MinIO)

Trust boundaries:

  1. Identity — the data plane is authenticated by Maskura API keys. Requests are accepted only when the SigV4 signature or the SDK header secret verifies against a registered key; the dashboard is authenticated by Supabase Auth (JWT). AUTH_DISABLED=true bypasses auth and is a local-development-only mode.
  2. Data in transit — HTTPS is required in production. SigV4 signatures are computed over-the-wire but the secret-bearing headers must not traverse plaintext HTTP.
  3. Data at rest — objects are stored on the configured backend after the plugin pipeline has run. Staged object bytes are encrypted before they touch the staging artifact store or disk. API key secrets are never stored in plaintext (see §4).
  4. The gateway is a router — it holds backend credentials only to re-sign requests to the configured backend. The zero-trust presigned-URL path forwards without storing any backend credential at all.

Operator trust and the limits of signed policy

The gateway and its operator remain trusted during processing. Maskura does not provide end-to-end execution attestation or per-operation computation proofs. An operator controlling the deployed binary, host, or local trust store can bypass the gateway’s checks; those checks do not independently constrain that operator. Wasm isolation constrains plugins, not the administrator of the runtime.

Signed TOML configuration provides artifact authenticity relative to configured trust roots. It does not prove that a particular read/write used that artifact. The contract for customer-signed policy approvals is approval evidence plus fail-closed enforcement by the trusted gateway, not operator-proof execution.

Policy approval has three deployment states that must not be conflated:

  1. Shipped in the public engine — the request-path PolicyGate seam (crates/gateway/src/policy_gate.rs), inert by default and behaviorally identical to OSS when no gate is configured; typed policy errors surfaced as the S3 XML <Code> element (policy.denied, policy.unprovisioned, policy.expired, policy.tampered; HTTP 403); and verdict consumption: a bound verdict is consumed at storage execution (the request-time destination is re-verified against the frozen binding before commit; drift answers policy.denied), staged multipart uploads persist their create-time verdict and continue under it across envelope rotation/expiry with complete-time limits composed as min(create-time, current), and frozen verdicts are schema-versioned with unknown future versions failing closed.
  2. The hosted enforcement gate — deploying now, per workspace, behind a policy-enforcement state (inactive = inert; enforced = fail-closed policy.unprovisioned without a valid customer approval). For v1, managed storage is the only enforced destination mode; concrete/BYO destinations fail closed at provisioning until their destination wiring lands.
  3. Future enrollment/cutover — no workspace is enforced today; flipping a workspace to enforced is a separate enrollment step that shipping the gate does not authorize.

Approval evidence is versioned canonical-CBOR (a sorted-JSON-keys convention, explicitly not RFC 8949 canonical) ReceiptBody/Checkpoint v2 with SHA-256 digests, carried by WebAuthn ASN.1 DER ES256 assertions bound to the complete receipt digest. Recovery after loss of all authorized keys is a new independently pinned root recorded as a marked discontinuity; a reset never signs an old-chain receipt, and a server-served trust bundle is never an independent pin.

Plaintext is available transiently to the gateway while transformations execute. Client-held decryption keys protect encrypted output; they do not establish confidentiality from the gateway during processing. TLS and SigV4 protect their transport/authentication boundaries, not the identity of the running code. Signed approval artifacts do not prove absence of extra plaintext copies, complete PII detection, or durable storage of an output.

Independent approval verification requires independently trusted signer keys. A server-only hash chain cannot establish complete/latest history or exclude split views; consistency checks need retained checkpoints and must state their freshness limits. Build-provenance attestations and integration-test evidence are separate from execution attestation. See ADR 0022.

Customer verification is available: maskura policy verify checks exported approval evidence (EvidenceBundle v1) offline against customer-retained checkpoints and a customer-pinned trust root (docs/policy-verification.md). The claim is customer-verifiable approval history against customer-retained checkpoints — not execution attestation and not witness-verified. A trust store pin is written only from customer-controlled input (an imported customer artifact or a manual fingerprint); a server-fetched trust bundle can never establish a pin, and a trust reset renders as a recorded discontinuity accepted only explicitly. This changes nothing about the deployment states above: the enforcement rollout state is unchanged.

2. Authentication

Dashboard

  • Supabase Auth (GoTrue); the gateway validates the access token JWT (SUPABASE_JWT_SECRET, with audience validation) for /dashboard/* routes.
  • Local/demo mode (AUTH_DISABLED=true) bypasses auth entirely — do not use that flag outside local development.

S3 data plane — API keys

A Maskura API key is a pair maskura_<32-hex> (access key ID) + maskura_secret_<32-hex> (secret), revealed once at creation. Two authentication paths:

  1. Maskura SDK header path — the client sends the plaintext secret in x-maskura-access-key / x-maskura-secret-key headers or Authorization: Bearer <access_key>:<secret>. The gateway compares sha256(secret) against the stored secret_hash. Requires TLS.
  2. Native S3 tools (SigV4) — the client signs each request with AWS SigV4 using its Maskura key. The gateway recomputes the signature and rejects requests whose signature does not match the stored secret.

The x-maskura-* customer headers are the only supported names. Reserved metering, operation, and usage headers are rejected. Credential prefixes are maskura_, maskura_secret_, and maskura_mcp_. The 0.7 namespace cut intentionally invalidates credentials issued under the pre-Maskura prefixes.

SigV4 verification

Verification is header-first and pre-body: parsing, credential-scope checks, signed-header validation, timestamp checks, and canonical seed signature verification all complete before a request body is polled.

  • Header auth — Authorization: AWS4-HMAC-SHA256 Credential=… with x-amz-date and x-amz-content-sha256 (mandatory for header auth).
  • Query auth (presigned) — X-Amz-* query parameters; presigned requests without an explicit x-amz-content-sha256 header default to UNSIGNED-PAYLOAD, which is only accepted over trusted TLS.
  • Signed-header integrity — host is always signed, and header auth also requires signed x-amz-date and x-amz-content-sha256. In every SigV4 mode, every present x-amz-* header except x-amz-user-agent and x-amz-checksum-mode, plus each present request-semantic header (x-maskura-storage-mode, x-maskura-backend-url, x-maskura-process, x-maskura-stable-fields, x-maskura-encrypt-fields, content-type, content-encoding, and content-md5, plus their exact legacy aliases), must appear in SignedHeaders, occur exactly once, contain valid UTF-8, and already equal AWS SigV4 TrimAll form: no leading or trailing SP/HTAB and every internal SP/HTAB run collapsed to one ASCII space. The gateway rejects rather than rewrites noncanonical values, including comma-equivalent duplicates, so metadata and tagging consumers observe the same sole canonical value that was signed. The two exact exceptions are also exempt from duplicate and canonical value enforcement because the pinned aws-sigv4 presigner excludes them. x-amz-checksum-mode is read-only response-integrity negotiation, not a storage-selection or processing semantic. Optional headers may be absent, so a normal host-only presigned GET remains valid; violations receive the generic signature-mismatch response before the request body is read.
  • Scope validation — region (MASKURA_SIGV4_REGION, default us-east-1), service (s3), and terminator (aws4_request) are enforced; the request timestamp must fall within the clock-skew window (15 minutes) and presigned URLs cannot exceed 7 days.
  • Payload integrity — the request body is verified against the declared payload hash and, for aws-chunked streaming, each chunk signature and the x-amz-trailer-declared checksum trailer are verified incrementally with constant-time comparisons. Supported modes: UNSIGNED-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-UNSIGNED-PAYLOAD-TRAILER, and a raw SHA-256 payload hash. Seed verification hashes the declared x-amz-content-sha256 value exactly as signed: streaming and unsigned tokens are case-sensitive and are not case-folded.
  • Checksums — x-amz-checksum-* (CRC32, CRC32C, CRC64NVME, SHA-1, SHA-256) declared in headers or trailers are verified against the decoded body. Conflicting declarations, missing trailer declarations, duplicate trailers, trailing data after the final frame, and mismatched checksums are rejected. Decoded length must match x-amz-decoded-content-length.
  • Signing-key cache — a bounded, TTL-based cache stores only derived signing keys and secret fingerprints; the plaintext secret exists in memory only transiently during verification and is never logged or persisted.
  • SigV4 verification is skipped only in AUTH_DISABLED local mode.

3. API key handling at rest

Each key stores two artifacts:

  • secret_hash — sha256(secret) for the SDK header path. Not reversible.
  • secret_encrypted — an envelope: v2:{base64(wrapped_dek)}:{base64(nonce)}:{base64(ciphertext+tag)} where the secret is AES-256-GCM encrypted under a fresh, per-key 256-bit data key (DEK), and the DEK is wrapped by a KeyWrapping implementation (crates/gateway/src/key_cipher.rs). v2 envelopes bind the API key identity as AES-GCM additional authenticated data (AAD), preventing an encrypted secret from being moved to another key. v1 envelopes are accepted for legacy keys.

The plaintext secret exists only in the create-key HTTP response and, transiently, in memory during SigV4 verification. It is never logged or persisted.

Credential mutations are bounded before persistence. API-key and MCP labels are trimmed, non-empty, free of control characters, and at most 128 UTF-8 bytes. Non-zero API-key and MCP lifetimes are at most one year (0 means no expiry). Encryption public keys are at most 16 KiB and must be a MASKURA HYBRID PUBLIC KEY PEM carrying an X25519 public key and an ML-KEM-768 encapsulation key, matching the format consumed by the envelope-encryption filter. Credential JSON endpoints also have route-specific body limits; oversized requests receive 413.

Credential creation returns plaintext only after the repository has committed the new credential. File-backed mutations remain hidden from concurrent readers until snapshot persistence succeeds, and failed mutations restore the prior state before readers resume. Repository failures are reported as service unavailable; they are never treated as a missing credential, an empty list, or an authentication denial.

The file-backed repository writes and synchronizes a permission-restricted temporary snapshot, then atomically renames it as the mutation commit boundary. It attempts to synchronize the parent directory after rename; failure is warned but cannot make the already-published snapshot uncommitted. This provides committed atomic visibility, not an absolute power-loss durability guarantee. Missing snapshots start empty; unreadable or invalid snapshots stop startup instead of being replaced. This repository is a single gateway-instance abstraction and does not provide multi-process locking.

Key-wrapping providers

ProviderDurabilityWhen to use
LocalKeyWrapping (MASKURA_SECRET_KEK)Durable (operator-provided 32-byte KEK)Local dev and self-host with a managed KEK
Ephemeral (no KEK configured)Not durable (random in-memory KEK, lost on restart)Local dev only
KMS/Vault-backed wrapping (injected)DurableProduction

build_state accepts an injected Arc<dyn KeyWrapping>. The OSS self-host binary resolves MASKURA_SECRET_KEK / ephemeral via key_cipher::default_wrapping(); a KMS/Vault-backed wrapper can be supplied by an embedding deployment without changing the engine. is_durable() is the load-bearing flag: durable staging fails closed when the active wrapping is not durable (see §7), because a lost DEK would permanently strand staged tenant data.

SecretCipher::decrypt retains its public compatibility behavior and collapses invalid data or wrapping failures to None. Credential repositories use decrypt_result instead, allowing operational KMS/Vault failures to become a generic service-unavailable response rather than an authentication denial.

Non-local deployments must use a durable KMS/Vault-backed wrapping. Running with auth enabled while the active wrapping is the local KEK or an ephemeral key is not secure for shared/non-local use. The gateway logs a prominent warning at startup in this configuration.

Legacy hash-only keys (created before envelopes existed) cannot verify SigV4 signatures — regenerate them if you need native S3 tools.

4. The plugin pipeline (data transformation)

  • Plugins are Wasm components (wasmtime, Component Model + WIT maskura:filter@0.1.0), 64 MiB aggregate guest memory per object, 10K table entries, 4 memories, 512 KiB max stack. No host imports beyond WASI stdout/stderr; the boundary is byte-in/byte-out.
  • A fresh Store and component instance per object (ADR 0007): no runtime state is shared across objects or tenants. Compiled components may be cached by hash; guest state (linear memory, globals, resources) is strictly per-invocation and dropped when the object completes.
  • Fuel and deadlines — per-call and cumulative fuel budgets (MASKURA_WASM_FUEL), a per-call wall-clock deadline (epoch interruption, default 30s) and an object deadline (default 5 minutes), plus cancellation tokens that interrupt active guest calls.
  • Admission control — streaming sessions run on a bounded worker pool with a capped queue and a guest-memory budget (MemoryAdmission); admission failure surfaces as SlowDown rather than unbounded parallelism.
  • The pipeline is deterministic and runs before data reaches storage on the write path: redaction replaces PII, encryption (per-field hybrid X25519 + ML-KEM-768 / AES-256-GCM) keeps the decryption key solely with the client. stable-encrypt (AES-SIV) is opt-in for JOIN keys and derives from the API key secret, never the raw secret.
  • Every authenticated request resolves its user through the WorkspaceStorageRepository to a canonical WorkspaceId. Workspace IDs are opaque, used unchanged, and limited to 1–128 ASCII characters from [A-Za-z0-9._-]; a repository may map multiple users to one workspace. Resolution failure fails closed.
  • The canonical workspace ID, not the user ID, scopes backend configuration, managed placement, multipart identities and quotas, and usage metering. Object sessions remain isolated per invocation.

5. Transformed reads — fail-closed disclosure rules

Read-time processing (x-maskura-process: read) runs the pipeline on the way out: the object in storage is unchanged, and the caller receives only the transformed projection. This is the agent-safe-read path. The guarantees are deliberately strict:

  • Preflight from stored metadata. A transformed read performs a metadata preflight against the stored object before any source body is consumed. The preflight validates Content-Type against a known format (JSON/JSONL/CSV/TSV/text), rejects Range, partNumber, non-identity Content-Encoding, and unknown mandatory formats, and requires stored metadata. Presigned-HTTP backends are rejected for transformed reads because they cannot provide a safe metadata preflight.
  • Version/ETag binding. The source GET must match the preflight: either an immutable version ID equality or a strong ETag equality (weak ETags are not accepted). If the source metadata changed between preflight and GET, the request is rejected — never silently served.
  • No raw fallback. A pipeline error, decoder error, or staging failure fails the request closed. There is no fallback to raw bytes under any condition.
  • Prefix-safe direct streaming only. When every component in the pipeline snapshot is marked prefix-safe for read (operator-declared via MASKURA_PREFIX_SAFE_COMPONENT_HASHES at process start — imported components are unsafe by default and the capability is immutable per component hash), the transformed output is streamed directly with bounded backpressure and no disk spool. Actual source/output bytes, fuel, duration, and component evidence are settled only after successful pipeline completion. EOF is withheld during bounded exact settlement retries. Cancellation before disclosure releases the reservation; cancellation, pipeline failure, or exhausted settlement after disclosure leaves the reservation for bounded control-plane recovery and never records successful customer usage.
  • Encrypted read spool otherwise. Any snapshot containing an unsafe component is rejected unless MASKURA_TRANSFORMED_READ_SPOOL=encrypted is set. The transformed output is then written to a disk spool in independently authenticated AES-256-GCM chunks under a key that lives only in the request task (a stale spool file is unreadable after a restart). The spool quota is reserved before source disclosure, file permissions are 0600, the response body is streamed from the spool only after the full representation is written, and a truncated or corrupted spool terminates the response body. Dropping the response body cancels the replay and releases the reservation.
  • No HEAD on transformed reads — HEAD is rejected until transformed metadata is available.
  • Transformed reads require stored, version-bound metadata and work with S3, managed storage, and in-memory backends (in local mode). Presigned backend URLs remain raw-only.
  • MASKURA_STREAMING_READ_MODE gates the path: off (default) rejects transformed reads entirely; passthrough enables only raw streaming; transformed enables this path.

6. Storage backends

  • Presigned URL proxy (x-maskura-backend-url) — the user generates a presigned URL with their own cloud SDK; Maskura filters and forwards. No backend credential is stored. The gateway applies SSRF controls before any request is made (see §10).
  • Per-workspace backend config — managed, s3_compatible with an endpoint, region, and static access credentials, or aws_role (IAM role ARN
    • region + optional external_id) that is assumed via STS per request. Runtime S3-compatible endpoints are governed by WorkspaceEndpointPolicy (see §10), and dashboard reads return only a redacted configuration.
  • Service storage — Maskura-managed multi-cloud buckets (MASKURA_SERVICE_BUCKETS), tenant-namespaced by workspace and optionally backed by authoritative placement metadata (see §9).
  • Resolution contract — an explicit x-maskura-storage-mode: managed request is resolved first, followed by a presigned URL, a per-workspace configuration, and configured service storage as the default. Only explicit single-tenant mode (AUTH_DISABLED=true or MASKURA_SINGLE_TENANT=true) may continue to the global S3_ENDPOINT and then in-memory storage.
  • Multi-tenant fail-closed boundary — startup rejects S3_ENDPOINT and requires non-empty MASKURA_SERVICE_BUCKETS. An unconfigured workspace therefore uses managed storage; workspace repository failures and unavailable explicit or workspace-managed selections never fall through to a process-global backend.

Multipart uploads are not buffered in gateway memory: parts are staged durably and encrypted before any downstream processing (see §7).

7. Multipart staging — durable, encrypted, fenced

MASKURA_MULTIPART_MODE=staged (default reject) enables client multipart uploads through a durable staging subsystem. In standalone local mode, the repository, artifacts, journal, proofs, and wrapping key are all stored beneath the configured FileStore root. Hosted staged mode retains the Postgres and dedicated staging backend requirements described below.

  • Durable encrypted staging. Each part is written to an encrypted artifact (under local .maskura/multipart/ in local mode, or MASKURA_MULTIPART_STAGING_DIR in hosted mode) framed as MSKMP1 magic + JSON header (containing the wrapped DEK, tenant/upload/part identity, and a digest of the multipart snapshot) followed by AES-256-GCM chunks whose AAD binds the header and chunk number. Hosted artifacts use the dedicated Maskura-controlled object store (MASKURA_MULTIPART_STAGING_BUCKET/ENDPOINT/credentials). Local artifacts remain on the FileStore volume and use its persisted root wrapping key.
  • Durability requirement. Hosted staged multipart requires a durable wrapping (KMS/Vault or a configured MASKURA_SECRET_KEK), DATABASE_URL, and complete staging backend configuration. Local staged multipart generates and persists a mode-0600 wrapping key under .maskura/ and uses file-backed repository and journal implementations; it intentionally requires none of those hosted dependencies.
  • Durable quota reservations. Per-workspace and global staging quotas (MASKURA_MULTIPART_STAGING_TENANT_QUOTA_BYTES / _GLOBAL_QUOTA_BYTES) are reserved by the active multipart repository before any body frame is consumed, temp file opened, or artifact created. Hosted reservations use Postgres row locks; local reservations use the locked file-backed event log. Crash-consistency is handled by a pending-outbox (begin/commit/discard) state machine that reconciliation replays.
  • Snapshot binding. The multipart snapshot (metadata, tags, checksum mode, destination, plugin snapshot, limits) is recorded at initiation; completion replays each artifact through a reader that authenticates the envelope identity, snapshot digest, and AEAD tags before any frame is exposed, and verifies the replayed part against the committed ETag/checksum/size.
  • Completion fencing. CompleteMultipartUpload is serialized by a durable completion lease with a fencing token and a request fingerprint. Idempotent retries replay the stored result; a stale worker that loses its lease is fenced (its renewals and any abort are rejected) — a fenced completion returns ServiceUnavailable instead of corrupting state. Takeover is explicit: a new worker acquires the lease with its own token, and the old worker cannot abort.
  • Expiry and reconciliation. Expired uploads are reaped by a background worker (aborting the destination and cleaning staged parts); orphaned artifact files are removed by startup and periodic cleanup; pending outbox entries are reconciled against the artifact store.

8. Transactional writes — journal, atomic commit, reconciliation

Streaming writes (single-part PUT, and staged multipart when MASKURA_MULTIPART_MODE=staged) run through a durable transaction layer:

  • Operation journal. Hosted deployments use the Postgres-backed OperationJournal; standalone local deployments use the file-backed journal under the FileStore root. (DATABASE_URL) records each operation’s state machine: INTENT → OPEN → COMPLETING → COMMITTED, with COMMIT_UNKNOWN and PROVEN_ABORTED for crash recovery. Streaming writes in a non-development deployment require a durable journal; there is no in-memory fallback.
  • Atomic commit. The transformed output is fully verified (decoded length, SHA-256 of the emitted bytes, per-part ETags/checksums) against the expected object before the destination commit is issued. On any failure the sink is aborted and the destination multipart upload is cleaned up.
  • COMMIT_UNKNOWN reconciliation. When a completion outcome is ambiguous (the request failed after complete was issued), the operation transitions to COMMIT_UNKNOWN. Request-scoped cleanup may reconcile only that exact operation using the backend bound to the request. Per-user operations that cannot be safely reprobed remain pending for an external reconciler with the original credentials; one request never scans or probes another tenant’s operations.
  • Launch sink boundary. Direct S3 output is streamed as a multipart upload. Managed and presigned-HTTP streaming writes are rejected before request-body polling or multipart staging until their commit evidence can share the same authorization identity and reconciliation guarantees.

9. Managed replication — authority and repair fencing

MASKURA_MANAGED_STREAMING_MODE (default off; observe/enforce require MASKURA_MANAGED_STREAMING_TRANSACTIONAL=true and a durable repository) adds authoritative metadata over the consistent-hash placement:

  • Launch admission. Transactional enforce mode accepts only exact lowercase b2 and aws provider kinds. Every backend must use HTTPS, have an explicit stable provider instance/account identity and positive credential epoch, have a unique stable backend ID, and participate in a positive static placement policy. The complete pool is checked before its placement policy is recorded and again before each managed streaming write begins.
  • Placement. Deterministic rendezvous hashing (versioned, MASKURA_MANAGED_PLACEMENT_VERSION) selects a primary and one replica backend per logical object, independent of backend input order.
  • Authority. An ObjectAuthority row records generation, digest, size, metadata, and primary/replica copy status with compare-and-swap semantics (cas_version); concurrent writers cannot silently overwrite authority.
  • Repair fencing. Replication repairs are durable records claimed with a lease owner and token; renewal, completion (compare-and-swap), and failure are all token-checked, so a stale worker cannot apply or duplicate a repair after its lease is taken over. validate_mode refuses to turn managed mode off after authority rows exist, and refuses observe/enforce without DATABASE_URL.

10. Outbound requests — SSRF, DNS, redirect, expiry, address pinning

Presigned-URL handling (PresignedHttpPolicy) applies the following before a single byte is fetched or sent:

  • Host allowlist — the URL host must be in MASKURA_PRESIGNED_HTTP_ALLOWLIST (supports *.suffix wildcards).
  • Scheme — HTTPS is required. MASKURA_PRESIGNED_HTTP_ALLOW_HTTP=true permits HTTP only for an explicit presigned source GET; presigned PUT and DELETE destinations remain HTTPS-only.
  • No userinfo or fragments, and the scheme must be supported.
  • Expiry validation — the URL must carry an explicit expiry (X-Amz-Date+X-Amz-Expires or Expires) with at least MASKURA_PRESIGNED_HTTP_MIN_VALIDITY_SECS (default 30s) remaining.
  • DNS + address pinning — the host is resolved at request time; any non-public address range rejects the request unless the host is in MASKURA_PRESIGNED_HTTP_PRIVATE_ALLOWLIST. The resolved address is pinned on the client (no re-resolution) and proxying is disabled.
  • No redirects — redirects are disabled at the client and redirect responses are rejected outright on reads.

Persisted S3-compatible workspace endpoints use a separate WorkspaceEndpointPolicy; presigned URL policy and expiry rules are not reused:

  • In multi-tenant mode HTTPS is mandatory, URLs cannot contain userinfo, query, or fragment components, and the host must exactly match or be a strict dot-boundary *.suffix entry in MASKURA_WORKSPACE_ENDPOINT_ALLOWLIST.
  • DNS is revalidated before every per-workspace AWS SDK client is constructed. Empty answers, private/reserved addresses, mixed public/private answers, IP-literal allowlist bypasses, and IPv4-mapped private IPv6 are rejected.
  • The per-workspace AWS SDK connector explicitly disables proxies and does not use browser-style redirect following. It performs its own DNS lookup after validation rather than using the validated addresses, so the multi-tenant allowlist is a trusted-provider boundary: operators must allow only provider domains whose DNS tenants cannot control or rebind.
  • Every gateway AWS SDK S3 client is configured for one SDK attempt. Journaled and spooled write transactions retain their own explicit bounded retry budget (currently three attempts), so retries remain visible to transaction evidence, fencing, and reconciliation rather than occurring inside the SDK.
  • HTTP or private addresses are accepted only in explicit single-tenant mode. Private destinations additionally require an exact operator entry in MASKURA_WORKSPACE_ENDPOINT_PRIVATE_ALLOWLIST; wildcard private exceptions are invalid.
  • Public builds provide no common-provider allowlist defaults. Deployments must choose the provider domains they trust.
  • Endpoint-family recognition and streaming authority are separate. The public engine recognizes only explicit AWS S3 global, known regional, legacy regional, external-1, dualstack, and FIPS data-plane forms, plus GCS XML/HMAC, B2 S3, R2, DigitalOcean Spaces, and Wasabi origin grammars. Arbitrary s3-* labels and AWS website, control, Object Lambda, Outposts, accelerate, access-point, and virtual-hosted bucket forms are not accepted. Recognition never grants capabilities.
  • Hosted direct streaming requires an adapter-supplied immutable config version and operator capability/permission attestation. B2 additionally requires version listing/deletion and exact-version recovery. Missing attestations or legacy repository implementations fail closed.
  • Before body polling (including Avro OCF) or provider mutation, the adapter must atomically acquire a database routing lease bound to operation ID, config version, attestation, and routing epoch. A background heartbeat covers body processing, and every provider future is directly raced against renewal after an immediate fence assertion. Fence loss drops that future and forbids later probes, aborts, or mutations. Only committed or proven-aborted operations release the lease. Config transitions and version retirement must conflict while a lease or nonterminal journal row is open.
  • Journal rows store only opaque version/attestation IDs and routing lease/fence values, never credentials. Startup and periodic reconcilers call reconcile_workspace_streaming_operation. Nonterminal recovery claims the journal row first, then performs an expired-only CAS over the persisted lease ID/token/config/attestation/epoch. The private adapter advances both the route fence and journal binding in one transaction before historical credentials are loaded and the same client is reconstructed. There is no fallback to current credentials and an active route lease is never stolen.
  • Journal terminal state precedes route settlement. This ordering preserves the authoritative provider outcome across crashes. The startup/periodic pass also visits workspace-bound COMMITTED and PROVEN_ABORTED rows and idempotently settles their exact lease after atomically verifying the matching journal outcome, closing the crash window between journal commit and request cleanup.
  • Private implementation boundary: the SaaS repository owns encrypted, immutable config-version retention, provider conformance attestation, atomic lease/config-transition transactions, lease recovery, and scheduling the public reconciliation hook. Until it implements every new repository method, hosted PerUserS3 direct streaming remains disabled by rejecting defaults.
  • Process-global S3_ENDPOINT storage still requires explicit MASKURA_STREAMING_S3_PROVIDER. A non-durable journal is accepted only in a debug build with both auth disabled and explicit single-tenant mode; release and hosted/global production paths require a durable journal.

11. Cancellation and cleanup

  • Cancellation. Every streaming operation carries a cancellation token that propagates through the Wasm pipeline, the source body, the sink, and spool replays. Dropping a response body cancels the source and pipeline; a cancelled guest call is interrupted via the epoch engine and surfaced as WASM_CANCELLED. Request aborts never issue destination aborts from a stale worker (fencing checks first).
  • Cleanup/reconciliation workers. Startup and periodic jobs remove stale compatibility spool files, orphaned encrypted multipart spool files, reconcile pending staging outboxes against the artifact store, reap expired multipart uploads, and reconcile managed repairs. Direct COMMIT_UNKNOWN operations require exact-operation reconciliation with the immutable backend config version and routing fence originally bound to the request. The same pass revisits terminal workspace operations until route settlement succeeds; terminal settlement is idempotent and performs no provider request.

12. Bounded parsing and memory

  • The legacy 16 MiB whole-object buffering path was removed (Phase 12): there is no whole-object memory buffer on the data plane. The gateway runs fixed-RSS streaming: source frames are bounded by MASKURA_SOURCE_MAX_FRAME_BYTES and decoded object bytes by MASKURA_MAX_OBJECT_BYTES; per-record decoder limits apply; and MASKURA_LEGACY_MAX_OBJECT_BYTES is no longer load-bearing.
  • CompleteMultipartUpload XML is capped at 1 MiB and parsed by a strict grammar parser (no general XML resolver): DTDs and entities are rejected before tokenization, parts must be sorted and unique, and part ETags/checksums are validated against staged parts.
  • aws-chunked framing is decoded with fixed-size state and explicit limits; oversized frames, duplicate trailers, and trailing data are rejected.
  • Wasm guest memory is capped per object (64 MiB aggregate) with a bounded worker pool and memory budget (see §4).

13. Logging prohibitions

The gateway must never log:

  • Object bytes or transformed payloads (neither plaintext nor ciphertext object content),
  • Staging keys or DEKs (wrapped or unwrapped),
  • Credentials — API key secrets, backend access keys, KEKs, or tokens,
  • Customer identifiers — bucket names, object keys, user IDs, credential identifiers, or tenant-controlled labels,
  • Backend endpoint URLs containing query data (these are rejected before persistence or client construction),
  • Signed URLs (presigned URLs are bearer credentials),
  • Staging artifact ciphertext or key material,
  • Raw database, provider, filesystem, plugin, or transport errors on request and reconciliation paths; these can contain customer identifiers, local paths, schema details, or upstream response bodies.

Operational logs use server-generated operation or receipt IDs, bounded counts, and reviewed low-cardinality categories. Client-facing internal and service-unavailable responses use fixed protocol-appropriate messages rather than raw error text. Local startup diagnostics may name operator-configured filesystem and plugin paths, but request handling never logs customer object identity or raw failure detail.

Local appliance credentials. When no hosted or external storage is configured, the gateway generates a root access key and secret and logs them on the first start. Later starts reuse the saved credential without logging it. This is the only case in which Maskura logs a credential, and it applies only to the single-node local appliance, not hosted or multi-tenant deployments. See ADR 0019.

13.1 Exported telemetry

The logging prohibitions above apply equally to records exported over OTLP. The agent-builder telemetry increment (HTTP request server span, the http.server.request.completed log, and the maskura.http.server.* metrics) is restricted to a fixed allowlist and can never carry customer identifiers.

  • The only span attributes are maskura.request.id, http.request.method, http.route, http.response.status_code, maskura.http.status_class, and maskura.http.outcome. http.route is the router’s matched template (for example /v1/objects/{*key} or unmatched), never a concrete path or query.
  • The completion log carries exactly request_id, method, route, status_class, outcome, and duration_ms, plus trace/span IDs from the server-owned span. It has one fixed event name and no message or error text.
  • Metrics use only normalized method, matched route template, status class, and outcome. Request IDs and tenant, object, operation, receipt, or exception identity are forbidden.
  • The resource carries service.name, service.version, maskura.process.role, and only the allowlisted optional attributes (service.namespace, service.instance.id, deployment.environment.name, cloud.provider, cloud.region, cloud.availability_zone). Other resource keys fail startup.
  • The trace layer rejects every event and every span other than the exact request-span callsite, and it contributes no target, thread, source-location, level, exception, or error-record attributes.
  • A sanitizing HTTP client maps collector transport failures to an opaque error and strips collector response bodies, so no endpoint, header, URL, or collector body can be reflected into a local or exported record. OTEL and HTTP-client dependency targets are suppressed from local output entirely, including WARN and ERROR.
  • Export is disabled unless an OTLP endpoint is configured. Collector DNS, connection, timeout, HTTP, or decoding failures occur in background batch workers and never affect request handling, health, readiness, billing, or reconciliation. Enabling a production collector is a separate operations decision and is not implied by this code.

See ADR 0020 for the trust boundary and signal schemas.

14. Deployment responsibilities

You must handle the following outside the Maskura container:

Non-secret gateway settings may be set by strict TOML keys or their documented environment overrides; environment values take precedence. See the configuration reference for the complete schema. Secrets and other environment-only settings, including MASKURA_SECRET_KEK, MASKURA_SERVICE_BUCKETS, backend credentials, DATABASE_URL, and bootstrap secrets, are not TOML keys. Settings that support both TOML and environment variables, including MASKURA_WORKSPACE_ENDPOINT_*, MASKURA_PRESIGNED_HTTP_*, MASKURA_SIGV4_*, MASKURA_MANAGED_*, and non-secret MASKURA_MULTIPART_STAGING_* values, use those names for their environment overrides.

  • TLS termination — place the gateway behind a TLS-terminating proxy (platform load balancer, ingress, or reverse proxy). SigV4 is signed over-the-wire, but the SDK header path sends secrets in headers, so HTTPS is required for the secret-bearing headers. Set MASKURA_SIGV4_TRUSTED_TLS=true if your proxy terminates TLS and the gateway sees HTTP.
  • Trusted proxy — restrict access to the gateway to the trusted proxy (or bind the listener accordingly); do not expose it directly on the internet without TLS.
  • KMS/Vault readiness — configure a durable KeyWrapping (KMS or Vault) for any non-local deployment. MASKURA_SECRET_KEK is durable but operator-managed plaintext; the ephemeral wrapper loses all wrapped secrets on restart. The gateway will not start durable multipart staging without durable key wrapping.
  • Durable journal / staging dependencies — standalone local mode uses one mounted MASKURA_LOCAL_STORAGE_DIR and automatically places its file-backed repository, journal, proofs, artifacts, and wrapping key beneath that root; it must run as one active process per root. Hosted staged mode requires DATABASE_URL plus the complete MASKURA_MULTIPART_STAGING_* configuration; managed observe/enforce still requires Postgres and transactional capabilities.
  • Backend lifecycle permissions — the backend credentials Maskura uses for direct/managed streaming must be able to create, abort, and discover multipart uploads, complete uploads, and (for reconciliation) perform conditional reads/HEAD. Maskura enables streaming only when the backend can discover and abort incomplete uploads, reconcile completed uploads, and clean up within five minutes.
  • Feature defaults — transformed reads and managed streaming are off/reject by default and must be explicitly enabled: MASKURA_STREAMING_READ_MODE=off, MASKURA_MULTIPART_MODE=reject, MASKURA_MANAGED_STREAMING_MODE=off. Single-part streaming writes are always enabled; staged multipart additionally requires MASKURA_MULTIPART_MODE=staged. Local mode supplies its durable file-backed dependencies from the mounted root; hosted mode requires its Postgres, wrapping, and staging-backend dependencies. If you enable one of these features without its required durable services, Maskura will not start.
  • Outbound credentials — the global S3_ENDPOINT client accepts static S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY and, when those are absent, falls back to the AWS default credential provider chain (EC2 instance profile, ECS task role, EKS IRSA, SSO, OIDC web identity). Per-workspace aws_role backends assume an IAM role via STS: temporary credentials are used once per request, never serialized, and the role ARN plus an optional external_id form the trust boundary (see ADR).
  • Self-host hardening — run the container as non-root; limit egress to the configured backends, KMS/Vault, and Supabase; do not ship MASKURA_KEYS_FILE or keys.json in the image; do not set AUTH_DISABLED in production; put the spool and staging directories on private, capacity-reserved volumes sized for MASKURA_SPOOL_MAX_OBJECT_BYTES/MASKURA_SPOOL_QUOTA_BYTES (including encrypted framing overhead).
  • Dependency/update policy — track releases and apply security fixes promptly; run just deny (cargo-deny) and just audit (cargo-audit) in your pipeline and keep the pinned toolchain (Rust 1.97.0) and Wasmtime version current. Only the latest stable minor is supported (see SECURITY.md).

15. Non-guarantees — the data-plane vs. provider distinction

Maskura is a data-plane processing gateway, not a storage provider. Maskura does not secure the third-party storage it is pointed at:

  • Maskura does not control bucket policies, server-side encryption, access logs, retention, replication, or deletes on the destination. Configure those on the destination itself.
  • Maskura forwards to the destination exactly the transformed representation and relies on the destination’s credentials/permissions for access control. Misconfigured destination credentials, world-readable buckets, or missing destination-side encryption are outside Maskura’s control and are not Maskura vulnerabilities.
  • Maskura does not secure the identity provider (Supabase) or the email/analytics services used by the dashboard.
  • Client-side decryption keys (for per-field encryption) and stable-encrypt keys are held by the client; Maskura never holds them.