[MIRROR ONLY] A correct and efficient ATProto blob proxy for secure content delivery. codeberg.org/Blooym/porxie
36
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rust 96.7%
Nix 2.2%
Dockerfile 1.2%
Shell 0.1%
85 1 4

Clone this repository

https://tangled.org/blooym.dev/porxie https://tangled.org/did:plc:p5yjdr64h7mk5l3kh6oszryk/porxie
git@tangled.org:blooym.dev/porxie git@tangled.org:did:plc:p5yjdr64h7mk5l3kh6oszryk/porxie

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md
Porxie logo

Porxie#

A correct and efficient ATProtocol blob proxy for secure content delivery.

Features#

  • Blob validation: verifies blob content matches its CID and rejects invalid/tampered content.
  • Secure serving: blobs are always served with secure headers to help improve end-user security.
  • MIME filtering: detects blob content MIME-types and enforces an optional allowlist of permitted types.
  • Policy enforcement: optionally integrate with an external policy service (like an AppView) to control which blobs can be served.
  • In-memory cache: configurable in-memory caching for fast repeat access with support for manual cache purging.

Usage#

NOTE

Porxie does not handle TLS, so it should be placed behind a reverse proxy like Caddy, Traefik, or NGINX. It is also recommended to use a dedicated caching layer in-between Porxie and your clients such as Varnish, Cloudflare, or similar.

Please ensure that any intermediary services between Porxie and the client pass through the following headers or set them the same as Porxie does:

  • Content-Type (if unmodified by the service)
  • Cache-Control
  • Content-Security-Policy
  • Content-Disposition
  • X-Content-Type-Options

Run: Binary#

To run Porxie as a binary, you'll first need to install it locally.

As Porxie is not packaged or pre-built in many places yet, the easiest way to do this is building it via Cargo directly. Ensure you have a relatively up to date version of Rust and Cargo installed before following these steps:

  1. Install the binary, replacing v0.0.0 with the version you want to install:

    cargo install --git https://codeberg.org/Blooym/porxie.git#v0.0.0 porxie
    
  2. Run the server with your chosen configuration options:

    porxie
    

Run: Docker / Containers#

Porxie is available as a pre-built container image on DockerHub and can be used with whatever container setup you use. The published image runs a statically linked binary in a scratch environment as a non-root user by default.

You can use the following compose.yml template as a starting point, adding any configuration options as environment variables:

services:
  porxie:
    image: blooym/porxie:latest
    restart: unless-stopped
    read_only: true
    ports:
      - "6314:6314"
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges

Run: Nix / NixOS Service#

To run Porxie with Nix, you can either use the package directly or the NixOS module, both of which are provided directly in nixpkgs. Please refer to the Nix search page for NixOS service options.

Routes#

  • [GET] /{did}/{cid}: Fetch a blob either from cache or origin.
  • [GET] /xrpc/dev.blooym.porxie.getBlob?did=<did>&cid=<cid>: XRPC Compatibility shim for the fetch blob endpoint.
  • [POST] /xrpc/dev.blooym.porxie.cache.purgeActor?did=<did>: Purge all cached items relating to an actor DID.
  • [POST] /xrpc/dev.blooym.porxie.cache.purgeBlob?cid=<cid>: Purge all cache items relating to a blob CID.

Policy Service#

Porxie can check with an external HTTP "policy" service before serving blobs, which is useful for moderating content or only serving specific content. You build and run this service yourself - Porxie just sends requests to an XRPC endpoint at /xrpc/dev.blooym.porxie.getBlobPolicy and acts on the response accordingly.

Policy decisions will be cached using the request DID+CID by default to reduce load on the policy service. The duration items are cached can be configured, and the cache can be cleared manually for a blob or actor via the relevant endpoint.

By default, Porxie will fail-closed: if the policy service returns an error is otherwise unavailable, the blob request will fail too. This behaviour can be configured to fail-open if availability is more important than applying policies.

See the Configuration section for all available policy options.

Configuration#

All options can be set via flags, environment variables, or a .env file. For the most detailed and up-to-date descriptions, use porxie --help.

Server#

--server-address <SA_ADDRESS>
    Address to bind the server to.

    Use the 'ip:' prefix for an IP address (e.g. 'ip:127.0.0.1:6314'), or on UNIX systems,
    the 'unix:' prefix for a UNIX socket path (e.g. 'unix:/run/porxie.sock').

    [env: PORXIE_SERVER_ADDRESS=]
    [default: ip:127.0.0.1:6314]

--server-admin-password <SA_SERVER_ADMIN_PASSWORD>
    Admin password for authenticating priviledged requests.

    When unset, all authenticated endpoints will reject requests with HTTP 401.

    Authenticated requests always expect the username `admin` as per specification.

    [env: PORXIE_SERVER_ADMIN_PASSWORD=]

Blob#

--blob-allowed-mimetypes <BA_BLOB_ALLOWED_MIMETYPES>
    Blob mimetypes that can be served.

    Validation is done loosely via content inference. Further validation can be done by a
    layer above this proxy, such as an image transformation service. When inference fails,
    the blob's type falls back to `application/octet-stream`. When that type is allowed,
    blobs failing inference can still be served.

    When using the CLI, the flag can be used multiple times. When setting via environment
    variable, values are comma-separated
    (e.g. `PORXIE_BLOB_ALLOWED_MIMETYPES="video/*,image/*"`).

    [env: PORXIE_BLOB_ALLOWED_MIMETYPES=]
    [default: image/*]

--blob-max-size <BA_BLOB_MAX_SIZE>
    Maximum blob size that can be fetched and served.

    Blobs that exceed this limit will return HTTP 413.

    The minimum value is 512kb and the maximum is the system's total memory.

    [env: PORXIE_BLOB_MAX_SIZE=]
    [default: 50mb]

--blob-cache-header <BA_BLOB_CACHE_HEADER>
    The Cache-Control header value to send alongside blob responses.

    This does not affect internal cache lifetimes, only how downstream clients such as CDNs
    and browsers are instructed to cache responses. Intermediary caches may need to be
    cleared manually for changes to take effect quickly.

    [env: PORXIE_BLOB_CACHE_HEADER=]
    [default: "public, max-age=604800, immutable"]

--blob-processing-timeout <BA_BLOB_PROCESSING_TIMEOUT>
    Maximum duration a blob can be processed by this server before aborting

    [env: PORXIE_BLOB_PROCESSING_TIMEOUT=]
    [default: 1m]

--blob-http-timeout <BA_BLOB_FETCH_TIMEOUT>
    Maximum duration before blob fetch requests are timed out

    [env: PORXIE_BLOB_HTTP_TIMEOUT=]
    [default: 30s]

--blob-http-connect-timeout <BA_BLOB_FETCH_CONNECT_TIMEOUT>
    Maximum duration before an attempted connection to a blob upstream is aborted.

    This value should be lower than --blob-http-timeout.

    [env: PORXIE_BLOB_HTTP_CONNECT_TIMEOUT=]
    [default: 10s]

Identity#

--identity-plc-url <IA_PLC_URL>
    URL of the PLC instance used for `did:plc` lookups.

    Can typically be left as default unless using a custom or local development setup.

    [env: PORXIE_IDENTITY_PLC_URL=]
    [default: https://plc.directory]

--identity-http-timeout <IA_IDENTITY_HTTP_TIMEOUT>
    Maximum duration before identity resolution requests are timed out

    [env: PORXIE_IDENTITY_HTTP_TIMEOUT=]
    [default: 10s]

--identity-http-connect-timeout <IA_IDENTITY_HTTP_CONNECT_TIMEOUT>
    Maximum duration before a connection attempt to an identity upstream is aborted.

    This value should be lower than --identity-http-timeout.

    [env: PORXIE_IDENTITY_HTTP_CONNECT_TIMEOUT=]
    [default: 8s]

Cache#

--cache-allocation <CA_CACHE_ALLOCATION>
    Total memory allocation for the internal cache.

    Blobs are cached using an LFU policy. The most frequently requested blobs are kept longest when the cache approaches its limit.

    For production deployments, a CDN or caching layer in front of this server is recommended for lower latency and better global availability.

    The minimum value is 8mb and the maximum is the system's total memory.

    [env: PORXIE_CACHE_ALLOCATION=]
    [default: 512mb]

--cache-blob-tti <CA_CACHE_BLOB_TTI>
    How long blobs can be idle in the cache before expiring

    [env: PORXIE_CACHE_BLOB_TTI=]
    [default: 7days]

--cache-ownership-ttl <CA_CACHE_OWNERSHIP_TTL>
    How long blob ownership can be cached before expiring

    [env: PORXIE_CACHE_OWNERSHIP_TTL=]
    [default: 1day]

--cache-policy-ttl <CA_CACHE_POLICY_TTL>
    How long policy decisions can be cached before expiring

    [env: PORXIE_CACHE_POLICY_TTL=]
    [default: 1h]

--cache-identity-ttl <CA_CACHE_IDENTITY_TTL>
    How long identity lookups (DID resolution, etc) can be cached before expiring

    [env: PORXIE_CACHE_IDENTITY_TTL=]
    [default: 1h]

Policy Service#

--policy-url <PA_POLICY_URL>
    Policy service URL that DID+CID pairs will be checked against.

    Requests are sent via XRPC tp <url>/xrpc/dev.blooym.porxie.getBlobPolicy?did=<did>&cid=<cid>.

    [env: PORXIE_POLICY_URL=]

--policy-request-headers <PA_POLICY_REQ_HEADERS>
    Headers sent alongside all requests to the policy service.

    Each header must be in the format "Name: value". When using the CLI, the flag can be
    used multiple times. When setting via environment variable, headers are
    pipe-separated (|).

    As pipes are used as a delimiter, they cannot be contained in headers.

    Example (cli): '--policy-request-headers "X-Hello: world" --policy-request-headers "X-Foo: bar"'

    Example (env): 'PORXIE_POLICY_REQUEST_HEADERS="X-Hello: world|X-Foo: bar"'

    [env: PORXIE_POLICY_REQUEST_HEADERS=]

--policy-fail-open
    Allow requests to proceed if the policy service is unavailable.

    Warning: enabling this means restricted blobs may be served when the policy service
    is unreachable.

    [env: PORXIE_POLICY_FAIL_OPEN=]

--policy-http-timeout <PA_POLICY_HTTP_TIMEOUT>
    Maximum duration before policy service requests are timed out

    [env: PORXIE_POLICY_HTTP_TIMEOUT=]
    [default: 30s]

--policy-http-connect-timeout <PA_POLICY_HTTP_CONNECT_TIMEOUT>
    Maximum duration before an attempted connection to the policy service is aborted.

    This value should be lower than --policy-http-timeout.

    [env: PORXIE_POLICY_HTTP_CONNECT_TIMEOUT=]
    [default: 10s]

Examples#

NOTE

The examples below are starting points to demonstrate what is possible with Porxie. They will likely need further modification to suit your needs and are not intended to be used as-is.

Porxie & Imgproxy#

Imgproxy can be placed in front of Porxie to handle image transformations such as resizing, cropping, and format conversions.

Using Docker Compose, an example compose.yml would look like this:

services:
  porxie:
    image: blooym/porxie:latest
    restart: unless-stopped
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges
    environment:
      PORXIE_BLOB_ALLOWED_MIMETYPES: "image/*"
      PORXIE_BLOB_MAX_SIZE: 25mb
  imgproxy:
    image: darthsim/imgproxy:latest
    restart: unless-stopped
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges
    depends_on:
      - porxie
    environment:
      # See https://docs.imgproxy.net/configuration/options for all options.
      IMGPROXY_BIND: ":8080"
      IMGPROXY_BASE_URL: "http://porxie:6314/"
      IMGPROXY_ALLOWED_SOURCES: "http://porxie:6314/"
      IMGPROXY_MAX_SRC_FILE_SIZE: 25000000
      IMGPROXY_CACHE_CONTROL_PASSTHROUGH: true
      IMGPROXY_RETURN_ATTACHMENT: true
      IMGPROXY_STRIP_METADATA: true

Replicating cdn.bsky.app#

Bluesky's CDN typically serves images using URLs like https://cdn.bsky.app/img/{preset}/plain/{did}/{cid}. By configuring imgproxy with presets and enabling preset-only mode, you can create a compatiable service. The presets below are based on what Bluesky used at the time of writing and may not be up-to-date.

IMGPROXY_PRESETS: >-
  avatar=rs:fill:1000:1000:1:1/g:ce/ext:webp,
  avatar_thumbnail=rs:fill:128:128:1:1/g:ce/q:70/ext:webp,
  feed_thumbnail=rs:fit:1000:0/q:70/ext:webp,
  feed_fullsize=ext:webp,
  banner=rs:fill:3000:1000:1:1/g:ce/ext:webp
IMGPROXY_ONLY_PRESETS: true

Refer to the imgproxy documentation for details on creating and modifying presets.

Operational Notes#

  • You will need to manually configure a cache rule when using Cloudflare Proxying as otherwise they do not seem to cache the content (which is indicated by the cf-cache-status returning DYNAMIC instead of HIT/MISS/REVALIDATED). To do this, go to the 'Cache Rules' configuration and add a rule for the hostname you run Porxie on that sets "Cache Eligibility" to "Eligible for Cache".
    Screenshot of a Cache Rule on the Cloudflare Dashboard The Cloudflare dashboard showing the creation of a 'Cache Rule' that matches when the Hostname equals "porxie.example.com" and sets the cache eligibility to "Eligible for Cache."