Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/concepts/pdp/new-edge-pdp-architecture.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: New Edge PDP — Architecture
sidebar_label: Architecture
description: What runs inside the New Edge PDP container, which component is responsible for what, and the path a single authorization request takes.
sidebar_position: 2
---

# New Edge PDP Architecture

What runs inside the [New Edge PDP](./new-edge-pdp) container, which component is responsible for what, and the path a single authorization request takes.

Everything runs inside one container. The only externally exposed listeners are the authorization API and the health endpoint.

![Architecture of the New Edge PDP: your app and orchestrator on the left, the single New Edge PDP container holding the authorization API, OPA, the health server, the query and bundle loopbacks, the embedded database, the change-ingest worker and the NATS leaf, with Permit.io and your policy store on the right](/images/pdp/new-edge-pdp-architecture.svg)

<p style={{textAlign: "center", fontSize: "0.9em", opacity: 0.75, marginTop: "-0.5em"}}>
Click the diagram to enlarge it.
</p>

:::note Reading the diagram
The diagram labels the change stream by its internal name, the **WAL** (write-ahead log). `LOCAL_WAL` is this container's own durable copy of it. Elsewhere in these pages it is called the *change stream*.
:::

---

## What runs where

| Component | Kind | Role |
| --- | --- | --- |
| **Authorization API** | In-process | The public REST surface on port `7000`. Opens **only once the PDP is ready**. |
| **OPA policy engine** | Supervised child process | Evaluates Rego. Bound to loopback only — never reachable from outside the container. |
| **Embedded database** | In-process (SurrealDB over RocksDB) | Your environment's fact graph — users, tenants, resource instances, relationships — **on disk**. |
| **NATS leaf node** | Supervised child process | Durable, on-disk event store. Holds the link to Permit's control plane. |
| **Change ingest and repair** | In-process task | Applies incoming changes to the embedded database; detects and repairs gaps. |
| **Query / bundle loopbacks** | In-process | Serve OPA its graph data and its policy bundle over `127.0.0.1`. |
| **Health and readiness** | In-process | Port `7001`. Answers throughout startup, including a long cold start. |

### Ports

| Port | Bound to | Purpose |
| --- | --- | --- |
| `7000` | All interfaces | Authorization API — the only port your services need |
| `7001` | All interfaces | Health and readiness — the port your orchestrator probes |
| `7002` | Loopback | Query loopback: serves OPA its graph data |
| `7003` | Loopback | Bundle loopback: serves OPA its Rego bundle |
| `8181` | Loopback | OPA |
| `4222` / `8222` | Loopback | NATS leaf client and monitoring |

Only `7000` and `7001` are exposed by the container image. Everything else is reachable solely from inside it.

---

## The decision path

```
client → :7000 authorization API
→ 127.0.0.1 OPA
→ 127.0.0.1 query loopback
→ embedded database (local disk)
```

Every hop is loopback or local disk. **No step in answering an authorization query contacts Permit's API**, by construction — the New Edge PDP has no control plane other than its event stream, and cannot reach `api.permit.io` for a decision even if it wanted to.

This is also what separates policy evaluation from graph traversal. OPA evaluates the Rego policy, but whenever that policy needs to know whether a user reaches a resource through a chain of roles and relationships, it calls out to the query loopback, which answers from the embedded database. Relationship queries run against a database built for them rather than against a JSON document held in memory.

---

## Separation of duties

The container is deliberately split so that the thing serving decisions is never blocked by the thing keeping data fresh:

- **The request path** — authorization API, OPA, query loopback, embedded database — only ever reads.
- **The sync path** — NATS leaf, change ingest, gap detector, rebuild orchestrator — only ever writes, on its own tasks.

They meet at the embedded database, which is why a control-plane outage costs freshness but never availability, and why a rebuild can run underneath a PDP that is still answering. See [How It Works](./new-edge-pdp-how-it-works) for what happens along the sync path.

---

## Related documentation

- [New Edge PDP](./new-edge-pdp) — overview
- [How It Works](./new-edge-pdp-how-it-works) — sync, consistency, cold start, and resource profile
- [Feature Parity](./new-edge-pdp-feature-parity) — capability comparison against the container PDP
- [Deployment](./new-edge-pdp-deployment) — requirements, observability, and the security model
- [Configuration](./new-edge-pdp-configuration) — environment variable reference (beta)
105 changes: 105 additions & 0 deletions docs/concepts/pdp/new-edge-pdp-configuration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: New Edge PDP — Configuration
sidebar_label: Configuration
description: Environment variable reference for the New Edge PDP (permitio/pdp-v3) — credentials, storage, ports, logging, and storage-engine tuning.
sidebar_position: 6
---

# New Edge PDP Configuration

Environment variable reference for the [New Edge PDP](./new-edge-pdp) (`permitio/pdp-v3`).

:::caution Beta — expect changes
The New Edge PDP is in early access and **this configuration surface is not yet stable**. Variables and defaults on this page may be renamed, replaced, or removed before general availability.

Treat it as a working reference rather than a contract: **pin an image tag**, and re-read this page when you upgrade. If you are depending on a specific variable, [tell us](mailto:support@permit.io) so we can flag it before it moves.
:::

For the [container PDP](/concepts/pdp/overview#run-an-edge-pdp-with-docker) (`permitio/pdp-v2`), see [Configurations](/concepts/pdp/configuration) instead. The two runtimes do not share a configuration surface, and **a variable of the same name may mean something different** on each.

---

## Credentials

| Variable | Default | Description |
| --- | --- | --- |
| `PDP_API_KEY` | **required** | Your environment-level Permit API key. |

`PDP_API_KEY` is the entire credential. This single value:

- binds the container to exactly one Permit environment,
- authenticates it to the control plane,
- carries the control-plane routing, so the PDP finds the right plane on its own,
- and is the bearer token the authorization API accepts from your services.

:::tip No control-plane URL to set
Unlike the container PDP, there is **no `PDP_CONTROL_PLANE` value to configure**. The API key routes the PDP to the correct control plane automatically.
:::

---

## Storage and ports

| Variable | Default | Description |
| --- | --- | --- |
| `EDGE_DB_PATH` | `/var/lib/edge-pdp/db` | Embedded database directory. **Must be on a persistent volume.** |
| `EDGE_DATA_DIR` | `/var/lib/edge-pdp` | Event store and generated credentials. **Must be on a persistent volume.** |
| `EDGE_PUBLIC_PORT` | `7000` | Authorization API — the only port your services need. |
| `EDGE_HEALTH_PORT` | `7001` | Health and readiness. Probe this port, [never `7000`](./new-edge-pdp-deployment#health-and-readiness). |

Both storage paths must be on persistent storage that survives restarts. On ephemeral storage every restart becomes a full cold start. See [Deployment requirements](./new-edge-pdp-deployment#deployment-requirements).

Ports `7002`, `7003`, `8181`, `4222`, and `8222` are used internally and bind to loopback only — see [Architecture](./new-edge-pdp-architecture#ports). Expose **`7000` and `7001`** and nothing else.

---

## Behavior and tuning

| Variable | Default | Description |
| --- | --- | --- |
| `RUST_LOG` | `info` | Log level. Try `info,edge_pdp=debug` when debugging. |
| `PDP_DEBUG` | `false` | Include debug detail in authorization responses. |
| `OPA_TIMEOUT_MS` | `800` | Per-request policy evaluation timeout. |
| `EDGE_PARALLELISM` | `4` | How many change transactions are applied concurrently. |
| `EDGE_DECISION_LOG_OPT_OUT` | `false` | Set to `true` to stop shipping decision logs. |
| `EDGE_DRAIN_TIMEOUT_SECS` | `10` | Shutdown drain budget. |
| `EDGE_CHILD_TERMINATION_TIMEOUT_SECS` | `30` | Child-process shutdown budget. |

Your `terminationGracePeriodSeconds` must be at least the sum of the last two — **40 seconds at defaults**. See [Deployment requirements](./new-edge-pdp-deployment#deployment-requirements).

### Child process logging

| Variable | Default | Description |
| --- | --- | --- |
| `EDGE_NATS_LOG_LEVEL` | follows `RUST_LOG` | Control-plane client log level. |
| `EDGE_OPA_LOG_LEVEL` | follows `RUST_LOG` | Policy engine log level. |

:::warning Debug levels expose your data
`EDGE_NATS_LOG_LEVEL=trace` logs raw message payloads — which means **your authorization data** — and `EDGE_OPA_LOG_LEVEL=debug` logs authorization request and response bodies. Use these only for short, deliberate debugging sessions, and never leave them on in production.
:::

---

## Storage engine

The embedded database inherits storage-engine defaults tuned for cloud-scale workloads, not for a small container. These are the levers that matter for memory footprint:

| Variable | Default | Effect |
| --- | --- | --- |
| `SURREAL_ROCKSDB_BLOCK_CACHE_SIZE` | `536870912` (512 MiB) | Read cache. The single largest lever on resident memory. |
| `SURREAL_ROCKSDB_WRITE_BUFFER_SIZE` | `268435456` (256 MiB) | In-memory write buffer before flushing to disk. |
| `SURREAL_ROCKSDB_MAX_WRITE_BUFFER_NUMBER` | `32` | How many write buffers may exist at once. |
| `SURREAL_ROCKSDB_BACKGROUND_THREADS` | `4` | Background compaction threads. |

At these defaults a container given only a few hundred MiB will be OOM-killed on startup. Start at **4 GiB** and tune down against your own data set. See [Resource footprint](./new-edge-pdp-how-it-works#resource-footprint).

---

## Related documentation

- [New Edge PDP](./new-edge-pdp) — overview
- [Architecture](./new-edge-pdp-architecture) — what runs inside the container and the request path
- [How It Works](./new-edge-pdp-how-it-works) — sync, consistency, cold start, and resource profile
- [Feature Parity](./new-edge-pdp-feature-parity) — capability comparison against the container PDP
- [Deployment](./new-edge-pdp-deployment) — requirements, observability, and the security model
- [Configurations](/concepts/pdp/configuration) — configuration reference for the container PDP
100 changes: 100 additions & 0 deletions docs/concepts/pdp/new-edge-pdp-deployment.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
title: New Edge PDP — Deployment
sidebar_label: Deployment
description: Deployment requirements, health and readiness probes, logging, and the security model for the New Edge PDP.
sidebar_position: 5
---

# Deploying the New Edge PDP

Operational requirements, observability surfaces, and the security model for the [New Edge PDP](./new-edge-pdp).

---

## Deployment requirements

The New Edge PDP has real operational requirements that the container PDP does not. Read these before deploying.

**Persistent storage is mandatory.** Both the embedded database directory and the event store must survive restarts. On ephemeral storage, every restart becomes a full cold start — minutes of startup, and a full snapshot transfer each time.

**Set `terminationGracePeriodSeconds` to at least 40.** That is the combined shutdown budget for draining and for stopping the child processes, at default settings. If the grace period is too short, the container is killed mid-flush and the durable event store is corrupted — forcing a cold start on the next boot.

**Set a generous startup probe.** Cold start can take minutes. Point liveness at `:7001/health` and readiness at `:7001/health/ready`, and give startup enough headroom that a first boot is not killed as a hang.

**Run one container per Permit environment.** The API key binds the container to exactly one environment; there is no multi-environment mode.

**Memory: start at 4 GiB.** See [Resource footprint](./new-edge-pdp-how-it-works#resource-footprint) for tuning.

**Expose ports `7000` and `7001` only.** Everything else binds to loopback inside the container.

**Runs as non-root** (uid/gid `10001`). Ensure your mounted volumes are writable by that user.

:::tip Running locally on macOS
Port `7000` collides with AirPlay Receiver on macOS. Map it to a different host port, for example `-p 7766:7000`, and point your SDK at `http://localhost:7766`.
:::

---

## Health and readiness

Health is served on the **management port (`7001`)**, which is where all probes belong:

| Endpoint | Auth | Behavior |
| --- | --- | --- |
| `GET /health` | None | Always `200`. Reports per-component status. Answers throughout startup. |
| `GET /health/ready` | None | `200` only when every gating component is up, otherwise `503`. |
| `GET /health/detail` | `PDP_API_KEY` bearer | Full diagnostic detail per component. |

Wire **liveness** to `:7001/health` and **readiness** to `:7001/health/ready`. Nine components gate readiness — the control-plane link, each of the four sync planes, the embedded database, OPA, and both loopbacks.

:::warning Probe `7001`, never `7000`
The authorization port (`7000`) also answers `GET /health` — and `GET /healthy`, an alias kept so tooling written for the container PDP keeps working. Neither is a substitute for the management port:

- There is **no `/health/ready` on port `7000`**. A readiness probe pointed at `:7000/health/ready` gets a `404` and the pod never becomes ready.
- Port `7000` does not open at all until the PDP is ready, so any probe against it **fails for the entire cold start** — which can be minutes. A liveness probe there will kill the container before it ever finishes starting.

Port `7001` answers from the moment the process starts, which is exactly what makes it the safe probe target.
:::

`/health/detail` is where you diagnose a lagging or stale PDP: it reports child-process restart counts and exit codes, the PDP's position in the change stream against the control plane's oldest retained change, whether the PDP is currently **serving stale** data and for how long, and whether decision-log shipping is healthy.

:::note Staleness never blocks decisions
Neither staleness nor decision-log failures gate readiness. A PDP that cannot ship its audit logs keeps answering authorization queries — the design deliberately refuses to trade the primary function for the secondary one.
:::

---

## Logs

Structured logs go to stdout at `INFO` by default; `RUST_LOG` overrides (`info,edge_pdp=debug` is a good verbose setting). Both child processes' output is captured and re-emitted on the main log stream with attribution and correct log levels, so container logs are the single place to look.

:::warning Child debug levels expose data
The child processes have their own log levels, and turning them all the way up exposes data: the control-plane client logs raw message payloads at `trace` — which means **your authorization data** — and the policy engine logs authorization request and response bodies at `debug`. Use these only for short, deliberate debugging sessions, and never leave them on in production.
:::

### Decision logs

Decision logs ship from the OPA child directly to Permit over HTTPS, batched, with a bounded in-memory buffer. They require **both** that decision logging is enabled for your environment in Permit **and** that the PDP's local opt-out is not set — the local setting can only turn logging off, never on.

---

## Security model

**Requests are authenticated against the container's own key.** The bearer token on an authorization request is compared, in constant time, against the container's `PDP_API_KEY`. Permit's API is **never** contacted to authenticate or authorize a request — the code that could do so is not compiled into this binary at all.

**One container serves exactly one environment.** The environment is fixed at startup from the API key. A valid key for a *different* environment does not authenticate, and there is no code path by which a caller can steer a request at data outside the container's own environment.

**Credentials never reach the child processes.** Both OPA and the NATS leaf are started with a cleared environment; only the specific variables each one needs are re-added. Credential files are written `0600` with protections against symlink attacks from other containers sharing the volume, and are referenced by path rather than inlined into any generated config.

**The blast radius of a compromised PDP is one environment's data.** It holds no credential that grants access to any other environment, and no ability to reach Permit's management API.

---

## Related documentation

- [New Edge PDP](./new-edge-pdp) — overview
- [Architecture](./new-edge-pdp-architecture) — what runs inside the container and the request path
- [How It Works](./new-edge-pdp-how-it-works) — sync, consistency, cold start, and resource profile
- [Feature Parity](./new-edge-pdp-feature-parity) — capability comparison against the container PDP
- [Configuration](./new-edge-pdp-configuration) — environment variable reference (beta)
- [Monitoring PDPs](/how-to/monitoring-pdps) — fleet visibility in the Permit dashboard
Loading