Skip to content

Security: coderabbitai/celld

Security

docs/security.md

Security

celld is an alpha. It is not safe for hostile multi-tenant use. Security fixes apply to the latest release only, so older alpha builds do not receive fixes.

Separate the public and internal listeners

celld opens two HTTP listeners. The public listener serves the deployed Worker, and the internal listener serves the peer protocol and the operator API.

Use --listen for the public listener. Expose only this listener through a load balancer, a reverse proxy, or a public firewall rule.

Use --internal-listen for the internal listener. Its default address is 127.0.0.1:0, so celld selects an available loopback port at each start. The startup output reports the selected address.

Use --advertise to give peers the address of the internal listener. Bind the internal listener to a private interface, or protect it with a private overlay. Do not expose this listener to the public internet.

An explicit advertised address requires an explicit internal-listener address. Set both command-line options, or use their equivalent environment variables. celld also rejects an explicit non-loopback public listener without an explicit internal listener. This rule identifies an obsolete one-listener configuration.

celld cannot verify that an advertised hostname or a translated port reaches the internal listener. You must route the advertised address to the internal listener, and you must not route it to the public Worker listener.

The public listener reserves only /__celld/health. A healthy node returns a 200 response with {"ok":true}, and an unhealthy node returns a 503 response. The deployed Worker owns /health and every other public path.

The internal listener does not pass an unknown path to the Worker. It returns a 404 response, so an operator request cannot become an application request.

Set the forwarded-header policy

A Worker reads the request URL from request.url. An application can route on the hostname and build an absolute link from this URL. Therefore, celld ignores X-Forwarded-Host and X-Forwarded-Proto by default.

Set --trust-forwarded-headers only when a trusted proxy replaces both headers. celld then reads the last value in each header, so an earlier client value does not override the proxy value. The equivalent environment variable is CELLD_TRUST_FORWARDED_HEADERS=1.

celld always takes the path and query from the request target. It ignores an absolute-form request target's scheme and authority, so a direct client cannot bypass the host policy through the request line.

Protect the internal listener

The operator API does not authenticate its requests. A client that can reach the internal listener can inspect state, start direct work, evict a cell, or stop the process. Therefore, a firewall or a private overlay must restrict access to trusted operators and fleet nodes.

Peer requests on the same listener keep their protocol authentication. Each peer request has an HMAC, a body signature, a clock limit, and replay protection. The private network adds protection, but it does not replace the peer authentication.

celld does not terminate TLS on the internal listener. Use an encrypted overlay such as WireGuard or Tailscale when the private network does not provide the required confidentiality.

Internal operator API

The internal operator API is available in the released binary. The API is an alpha interface, so a release can change its paths or response formats.

  • /state reports the current occupancy, eviction, and restoration values. It remains available while a graceful shutdown drains existing work.
  • /metrics reports the same node lifecycle state as bounded Prometheus text. It includes no cell, request, tenant, or bucket identifiers and remains available during graceful drain.
  • /cell/NAME resolves or activates a cell for an operator check.
  • /evict/NAME evicts a resident cell.
  • /do/NAME sends a direct Durable Object request.
  • POST /shutdown starts a graceful ownership handoff.
  • POST /shutdown?handoff=preserve prepares a clean same-node reload and keeps the ownership records.
  • /__celld/probe serves the signed diagnostic probe.

The peer protocol also uses reserved internal paths. An operator must not call these paths directly, and celld continues to authenticate each peer request.

Protect the fleet bucket

The fleet bucket is the root of authority for the fleet. It stores the deployments, the cell state, the ownership leases, the node leases, and the shared peer-authentication secret.

A person who holds the bucket credentials controls the fleet. Give each credential access to one fleet bucket only, and replace a credential after a suspected disclosure.

Encrypt customer database objects

Set CELLD_DATA_ENCRYPTION_KEYRING to a secret JSON object containing one active key and any retained read keys:

{
  "active_key_id": "2026-08",
  "keys": {
    "2026-07": "BASE64_32_BYTE_KEY",
    "2026-08": "BASE64_32_BYTE_KEY"
  }
}

Key IDs may contain only ASCII letters, digits, periods, underscores, and hyphens, and may be at most 64 characters long.

Celld generates a random 256-bit data key for every LTX body and SQLite checkpoint/fork image, encrypts the database bytes with AES-256-GCM, and wraps that data key with the active versioned AES-256-GCM key-encryption key. Both authenticated-data domains bind the envelope header and exact bucket key, so a copied or modified ciphertext fails restore. Ownership records, epoch seals, deployment pointers, and checkpoint manifests remain plaintext because they are coordination and integrity metadata rather than customer database bytes.

Set CELLD_DATA_ENCRYPTION_REQUIRED=1 on a fleet that carries customer data. Startup then fails without a valid keyring. Rotate without downtime by adding a new key-encryption key, making it active, and retaining every old key until no LTX, checkpoint, fork seed, rollback deployment, or retained object references it. Removing a still-referenced key makes restore fail closed.

CELLD_DATA_ENCRYPTION_ALLOW_PLAINTEXT_READS=1 is only for a reviewed migration of an existing plaintext fleet. New writes are still encrypted, but old objects may be read. New fleets must leave it disabled. The keyring itself must come from a secret manager or KMS-protected deployment channel; never place it in a Worker bundle, bucket object, command line, repository, or log.

Nodes that predate this envelope format cannot read encrypted LTX. Upgrade the complete fleet before enabling encryption, then enable the same retained keyring on every node as one coordinated configuration change. Do not roll back to a pre-encryption binary after the first encrypted object is published.

Require signed deployments

An operator can make bucket deployment pointers fail closed by setting CELLD_DEPLOYMENT_VERIFY_KEYS_FILE on every node. The file is a JSON object whose keys are release-key IDs and whose values are base64-encoded 32-byte Ed25519 public keys. Once configured, a node rejects an unsigned pointer, an unknown key ID, a changed pointer, and a changed deployment manifest before it loads Worker code.

Create a signed pointer with celld deploy --attestation release.json --signing-key release.seed --signing-key-id KEY_ID. The attestation must be a JSON object of string values. Celld adds its compile-time version and commit; the input must not assign the reserved celld_version or celld_commit keys. The signing-key file contains only a base64-encoded 32-byte Ed25519 seed; keep it out of the fleet and expose it only to the deployment job. Nodes need only the public-key map. Rotate by adding the new public key to every node, signing a deployment with the new key, and then removing the old key after every supported rollback deployment has been re-signed or retired.

The signature binds the fleet pointer, rollout percentage, exact manifest bytes, and operator metadata. Module and asset hashes remain part of the Celld manifest validation path. This feature does not make a compromised Worker or a holder of the fleet bucket credentials harmless; it prevents bucket access alone from selecting unapproved Worker code when the signing key is separate.

Keep one writer for each cell

Each cell is a SQLite database with one writer. One node owns a cell at a time, and an ownership epoch fences each cell.

A node that loses its lease cannot modify the current cell state. The ownership and fencing page describes this mechanism.

A fleet has no shared multi-tenant scheduler or shared placement layer. A defective cell can access only its own database, but it can consume resources on its fleet nodes.

Protect the public application

celld does not authenticate the users of the deployed application. It also does not terminate public TLS. Put the required authentication and TLS in front of the public listener.

Keep the internal listener private, and keep the bucket credentials secret. See the limitations page for the complete alpha boundary.

There aren't any published security advisories