Privasys
Privasys PlatformDeveloper Platform

Deploy Containers

Deploy a container application on the Privasys Developer Platform inside Enclave OS Virtual (TDX).

Container applications run inside Enclave OS Virtual on Intel TDX hardware, providing hardware-level memory encryption and attestation for standard Docker/OCI images.

Prerequisites

  • A GitHub account (for authentication)
  • A publicly accessible container image (from GitHub Container Registry, Docker Hub, or any OCI registry)

Submitting a container application

  1. Go to Dashboard > New Application
  2. Select Container as the application type
  3. Provide the container image reference (e.g. ghcr.io/org/app:v1.0.0)
  4. Configure the deployment settings (see below)
  5. Submit the application

Container settings

SettingDescription
Image referenceFull image URI including tag or digest. Use a digest (@sha256:...) for reproducibility.
PortThe port your container listens on. Only one HTTP port is exposed through the RA-TLS proxy.
Configure endpointOptional config_api declaration ({ method, path }) describing the in-process endpoint the platform POSTs to once at first request, before any other traffic is allowed through. Declared via the org.privasys.config_api OCI label or in privasys.json. See Configure & Attest.
Encrypted storageToggle to enable an encrypted filesystem volume for persistent data.

Per-deployment configuration is delivered in-process via the declared config_api endpoint, then frozen by the manager. The container persists what it needs (typically on the encrypted volume) and calls setConfigComplete() to lift the freeze. Subsequent restarts replay the sealed config without going back to the platform.

What happens on deployment

When a container application is deployed:

  1. Enclave OS Virtual boots on a TDX-enabled node
  2. The UEFI firmware measures the OS image into TDX RTMR registers
  3. The container image is pulled over an attested channel
  4. The container starts with the configured port and storage; the manager holds the very first request, POSTs the deployment-time configuration to config_api, and only then unblocks normal traffic
  5. An RA-TLS proxy terminates TLS in front of your container, serving certificates that include TDX attestation evidence
  6. The deployment is complete when the container passes a health check

Attestation for containers

Container deployments are attested through TDX RTMR registers:

RegisterMeasures
RTMR[0]UEFI firmware
RTMR[1]OS kernel and initrd
RTMR[2]OS configuration and runtime
RTMR[3]Application layer (container image digest, per-app OID extensions)

Clients can verify the full boot chain from firmware through to your application. The RA-TLS certificate includes quotes signed by the TDX hardware, which can be validated against Intel's provisioning certificates.

Image best practices

Pin to a digest. Tags like :latest or :v1 can change. Using a digest (@sha256:abc123...) ensures that attestation measurements remain stable and verifiable.

Minimise the image. Smaller images reduce the attack surface. Use multi-stage builds and distroless or scratch base images where possible.

Do not embed secrets in the image. Receive secrets at the declared config_api endpoint inside the enclave instead, and (optionally) seal them to encrypted storage for replay across restarts.

Encrypted storage

When encrypted storage is enabled, the container receives a mounted volume that is:

  • Encrypted at rest using keys derived inside the enclave
  • Bound to the enclave identity (sealed data)
  • Transparent to the application (standard filesystem operations)

Data persists across container restarts on the same enclave but is inaccessible outside that specific enclave instance.

Networking

Traffic to and from the container flows through the RA-TLS proxy:

  • Inbound: Clients connect over HTTPS. The proxy terminates TLS using the RA-TLS certificate (which embeds the TDX attestation quote). Requests are forwarded to your container on the configured port. Browsers and plain HTTPS clients, which cannot verify an RA-TLS certificate, reach the container through the platform gateway's terminated leg instead, under the rules of the next section.
  • Outbound: The container can make outbound connections. DNS resolution and egress work normally.

Surfaces: private, static and public

Everything a container serves is private unless the application declares otherwise in its measured manifest. Private routes are reached over RA-TLS by attested clients, or over a sealed session by browsers; on both, nothing between the client and the enclave reads the traffic. A web application also needs two things a sealed session cannot give it: a page that loads before any session exists, and routes that third-party machines (a webhook sender, an OAuth redirect, a plain curl) can reach with ordinary HTTPS. Those are served in the clear on the platform gateway's terminated leg, where the gateway, a Privasys-operated machine outside any enclave, reads the traffic. The standard is therefore:

What an application declares public is served in the clear, visible to verifiers, and anonymous by construction. Everything else is private.

Which surface each audience gets

AudienceNeedsSurfaceAttestationIdentity the app may trust
Attested client (wallet, CLI, service, another enclave)private dataprivate, over RA-TLSverified by the clientbearer, mutual RA-TLS identity, spend token
Browser user who signed inprivate dataprivate, over a sealed sessionverified by the wallet, inherited by the SDKX-Privasys-Sub asserted by the runtime, or the inner bearer
Browser visitor, not signed inthe page, public readsstatic (the shell) and public (reads), in the cleargateway-vouched onlynone
Third-party machine (webhook sender, OAuth redirect, federation peer, plain curl)public reads, inbound eventspublic, in the cleargateway-vouched onlynone; the payload carries its own proof (a webhook signature, an OAuth state)

"Gateway-vouched" is exact: on its internal leg the gateway verifies the enclave's RA-TLS certificate against the control plane's policy for the route and refuses to forward on a mismatch. A clear caller trusts Privasys operations for that check, the same tier as a non-wallet sign-in, and does not verify the hardware itself.

The three surfaces

SurfaceReached overMethodsWho sees the trafficIdentity the app may trustWhat is on it
privateRA-TLS, or a sealed session on the terminated leganythe client and the enclave onlyX-Privasys-Sub, the inner bearer, X-Privasys-App-*, the payer headerseverything not declared
staticin the clear, terminated legGET, HEADthe gatewaynonethe declared prefixes
publicin the clear, terminated legGET, HEAD, POST, PUT, PATCH, DELETE as declared; OPTIONS answered by the runtimethe gatewaynonethe declared method and path pairs

The declaration

Surfaces are declared in the surfaces block of privasys.json, which rides the org.privasys.manifest image label next to the tools (see the app store page for the label). The label is inside the digest-pinned image configuration that the enclave verifies at load, so the surfaces are measured: a verifier reads off the image exactly what the application serves in the clear, and changing them changes the image digest.

{
  "tools": [ /* your API, as on the MCP tool discovery section below */ ],
  "surfaces": {
    // Path prefixes served in the clear for GET and HEAD: the browser shell
    // (measured code, no user data). An entry of exactly "/" is the root
    // document only. Nothing under /api can be static.
    "static": ["/", "/assets/", "/favicon.svg", "/manifest.webmanifest"],

    // Explicit routes served in the clear. A path ending in "/" is a prefix,
    // any other path is exact. A route is one method: a public GET on a
    // prefix never implies POST. "/" itself cannot be a public route.
    "public": [
      { "method": "GET",  "path": "/api/v1/public/" },
      // "headers" lists the request headers this route needs beyond the
      // default allow-list, typically a webhook sender's signature header.
      // Authorization and Cookie can never be declared.
      { "method": "POST", "path": "/api/v1/webhooks/stripe", "headers": ["Stripe-Signature"] },
      { "method": "GET",  "path": "/.well-known/openid-federation" }
    ],

    // "*": public routes answer any origin, with no credentials, and the
    // runtime answers OPTIONS preflights itself. "same-origin" (the default)
    // sends no cross-origin headers at all; your own page needs none.
    "cors": "*",

    // Per-client rate limit on the clear leg. Defaults: 10 requests per
    // second with a burst of 50.
    "limits": { "rps": 10, "burst": 50 }
  }
}

Applications built before the surfaces block existed declared their shell through the org.privasys.static-unsealed-prefixes image label, a comma-separated list of prefixes. The label still works as an alias for surfaces.static (the runtime takes the union of both). New applications declare the manifest.

What the runtime enforces on a static or public request

These rules are enforced by the enclave runtime, so a mistake in application code cannot turn a public route into a channel for account data.

  1. Anonymous by construction. Before the application sees the request, the runtime keeps only an allow-list of request headers (Accept, Accept-Encoding, Accept-Language, Cache-Control, Content-Type, Content-Length, If-None-Match, If-Modified-Since, Range, User-Agent, Origin, X-Forwarded-Proto, X-Forwarded-Host, and the route's declared headers) and deletes every other one. Authorization, Cookie and every X-Privasys-* header are gone, the runtime never asserts X-Privasys-Sub or a payer, and nothing on the request identifies an account. A credential that still arrives (a bearer your page attached to a public fetch) is dropped and counted: the application's attestation tab shows that credentials reached the gateway on a public route, so you learn on dev, before production.
  2. Nothing for the browser to send. On the response the runtime keeps only Content-Type, Content-Length, Content-Encoding, Content-Disposition, Content-Range, Accept-Ranges, Cache-Control, Expires, ETag, Last-Modified, Location, Retry-After, Vary and Date, plus its own CORS headers. Set-Cookie never passes, so no cookie is ever set on the application origin and a browser has none to send past the gateway. An application header such as X-User-Id is dropped. Static responses carry Referrer-Policy: same-origin.
  3. The application can tell. Every request carries X-Privasys-Transport, set by the runtime and never by a client: public on the clear leg, sealed from a sealed session, attested when TLS ended at the enclave. A private handler refuses public.
  4. No payer, no price. A route that carries an x-privasys.price cannot be static or public. The platform refuses the version, and the runtime refuses to load the image, with a clear message. The configure endpoint can never be public either.
  5. Bounded. A per-client token bucket (keyed by the client address the gateway forwards) applies to every clear request, with defaults of 10 requests per second and a burst of 50, overridable in surfaces.limits. A full bucket answers 429 with Retry-After. Private routes are bounded by the session and spend machinery instead.
  6. Preflight. When cors is "*", the runtime answers OPTIONS on a public path itself: 204, Access-Control-Allow-Origin: *, the declared methods and headers, no credentials. Otherwise a preflight is refused.
  7. No upgrade. A WebSocket, or any Upgrade, on a public route is 403. The sealed WebSocket is the only browser socket.
  8. Everything else is private. An undeclared path on the terminated leg answers 403 sealed-transport-required.

The confirmation at deploy

Public routes run inside the enclave, so anyone can verify from the code that they leak nothing. The remaining exposure is a developer putting personal data on a route the gateway reads. The platform makes that a deliberate, recorded act: when a version's manifest adds a static prefix or a public route beyond the set an owner last confirmed, deploying or staging that version is refused until an owner confirms that those routes carry no personal data. The confirmation names the exact routes and stays with the version, who and when, next to the attestation that records the routes themselves.

The CLI asks during privasys apps deploy and privasys apps versions stage, and --confirm-surfaces answers it for scripts. The confirmation also exists as its own verb, and privasys apps describe prints what a version serves in the clear:

privasys apps versions confirm-surfaces my-app [version]
privasys apps describe my-app

The confirmation is asked once per version and only when something new goes into the clear. A version that declares nothing, or adds nothing beyond the last confirmed set, ships without a question. Because the surfaces are measured, a change to them also changes the image digest, so the owner approves the upgrade as for any measurement change (see Application lifecycle).

The gateway logs TLS metadata and route outcomes only, never a header, a query string or a body, and keeps nothing that transits it. The data you place on a declared surface is your responsibility; Privasys forwards it and retains none of it. The application's store listing and attestation tab show the routes served in the clear, so a user choosing an application sees your declaration before trusting it.

What you must never do

  • Serve account data on a public route. The runtime strips every identity, so a public handler has nothing to look up a user with; do not add your own.
  • Trust a header on a public route. Only the allow-list and your declared headers arrive, and an anonymous caller set them. A webhook is authenticated by its signature over the body, never by a header value alone.
  • Price a public route. A public caller cannot pay. The version is refused.
  • Put a secret in a path or a query string. URLs are visible to the gateway on every terminated-leg request, sealed ones included. Keep page state in the hash.
  • Attach a credential to a public fetch. Call public URLs with credentials: 'omit' and no Authorization; anything else has crossed the gateway before the runtime drops it.
  • Point a vanity domain at anything but a redirect to the enclave host. A proxy in front of the enclave would read every route.

The container-app-web template is a small service with all three surfaces declared and a public handler kept in its own router, with the four curl checks as its smoke test. The tutorial walks it end to end.

MCP tool discovery

Container applications can be exposed as MCP tool servers by adding a privasys.json manifest to their GitHub repository. When the platform detects this file, it reads the declared tools and surfaces them in the Developer Platform's AI Tools tab.

Adding a privasys.json manifest

Create a privasys.json file in the root of your repository:

{
  "container_mcp": {
    "tools": [
      {
        "name": "browse",
        "description": "Browse a URL and return the page content.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "url": {
              "type": "string",
              "description": "The URL to browse."
            }
          },
          "required": ["url"]
        }
      }
    ]
  }
}

Each tool in the tools array follows the standard MCP tool format: a name, a human-readable description, and a JSON Schema inputSchema for the parameters.

How it works

  1. When you create or update a container application with a GitHub commit URL, the platform checks the repository for a privasys.json file.
  2. If container_mcp is present, the tool manifest is stored alongside the application.
  3. The AI Tools tab in the Developer Platform displays your container's tools, parameter schemas, and connection instructions.
  4. AI agents can discover and call your container's API over attested connections — the same TDX attestation that protects all container traffic.

No code changes to your container are required. The privasys.json file is read by the platform at submission time, not at runtime.

Comparison with WASM MCP

WASMContainer
Tool sourceAuto-derived from WIT types and doc commentsDeclared in privasys.json manifest
Schema formatGenerated JSON Schema from WITHand-written JSON Schema
Tool invocationConnect RPC to WASM functionHTTP to container endpoint
AttestationSGX (per-function code hash)TDX (VM boot chain)

See MCP Tools for the WASM-specific MCP reference.

Next steps

Edit on GitHub