# Policies, Quotas & Audit (/docs/connectors/policies-quotas-audit)



Connectors are the point where Ductor makes outbound calls with tenant
credentials — the moment an inventory action actually touches the world — so they
are governed on four axes: **where** a call may go (egress policy), **which**
providers a tenant may use (tenant policy), **how fast** and **how much** (rate
limits and quotas), and **what happened** (audit). This governance is what lets
an action be dispatched by any worker, human or agent, without handing it the
keys: the same gates apply whether the caller is a workflow step, an API request,
or an agent tool call. This page covers each.

## Egress policy (SSRF protection) [#egress-policy-ssrf-protection]

Egress policy (`domain/connector/egress_policy.go`) governs the outbound
destination of every connector operation — which schemes, hosts, methods, paths,
and redirects a call may reach — per tenant and environment. It is the primary
SSRF defense.

The reusable unit is a **destination manifest** (`ConnectorDestinationManifest`),
a redacted contract that carries no credentials:

* `AllowedSchemes` — defaults to `https`; `http` requires an explicit policy
  waiver and can never be credential-bearing.
* `HostPatterns` — &#x2A;*DNS names only.** Validation rejects raw IP addresses and a
  bare `*`, requires a registrable domain, and allows a wildcard only as
  `*.example.com`. This is what blocks a connector from being pointed at
  `169.254.169.254` or an internal host.
* `HostTemplateFields` — dynamic host segments sourced from `connection_config`
  (a per-customer subdomain, region code, or resource id), each with a declared
  class.
* `PathPrefixes`, `AllowedMethods`, `RedirectPolicy` (`deny` by default,
  `same_host`, or `manifest_allowed`), and `PrivateRouteRequired`.

A tenant binds a manifest through a `ConnectorEgressPolicyRef` with a `Mode`:

| Mode       | Behavior                                                   |
| ---------- | ---------------------------------------------------------- |
| `enforce`  | (Default) Violations are blocked.                          |
| `shadow`   | Violations are logged but allowed — use to trial a policy. |
| `disabled` | No enforcement.                                            |

Egress decisions carry a status (`allowed`, `shadow_allowed`, `denied`,
`needs_policy`, `needs_private_route`, `needs_region`, `unknown_destination`) and
are content-addressed for audit. Operations are typed by `ConnectorOperationKind`
(`action`, `sync`, `trigger`, `proxy`, `webhook`, `function`, `mapper`,
`unified_operation`), so you can scope a policy to just proxy calls, say.

<Callout type="warn" title="Default-deny on destinations, not just allow-listed hosts">
  Because host patterns must be registrable DNS names and IPs are rejected
  outright, a connector cannot be coerced into calling a link-local or private
  address through a crafted `connection_config`. Redirects default to `deny`, so
  a `302` to an internal host doesn't bypass the manifest either.
</Callout>

Egress policies are managed under `/api/v2/connector/policy/egress` (list, get,
upsert, archive, diff versions), with a `POST /api/v2/connector/policy:preview`
to test a hypothetical destination against the active policy.

### Placement: where work runs, not just where it connects [#placement-where-work-runs-not-just-where-it-connects]

Egress answers *where a call may connect*; **placement** answers *where the work
may run* (`docs/connector-system/egress-placement-policies.md`). Together they form
one operator control plane evaluated **before** credentials, connection selection,
and side effects — separate from the low-level `pkg/egress` dialer, which still
performs the final DNS/IP enforcement. Every governed connector call thus produces
two redacted decisions:

* a **connector egress decision** — may this tenant/environment/provider operation
  connect to this destination?
* a **connector placement decision** — may this operation run from this region,
  worker label set, or private route?

Destination manifests are materialized from provider, action, sync, webhook, proxy,
and function metadata, each with a status: `declared` (bounded contract),
`dynamic_needs_policy` (dynamic host such as a customer instance URL),
`unknown` (catalog can't safely reduce it — block promotion), or `denied`. The
recommended rollout is **shadow → enforce**: stand policies up in `shadow`, run
`PreviewConnectorPolicy` for direct/route/sync/webhook/proxy/function paths, repair
`unknown_destination`, `private_route_required`, and `placement_denied` results,
then flip production policy to `enforce` only once shadow denials are empty or
explicitly waived. In route mode, a denied candidate connection is skipped; if every
candidate is denied, routing returns a policy-denied outcome instead of resolving
credentials and failing later.

<Callout type="info" title="Egress and permissions are different questions">
  A permission manifest answers whether a *token* is authorized for a provider
  operation; egress/placement policy answers whether a *tenant/environment* may run
  that operation from a placement to a destination. Both are required, and their
  repair paths stay separate: reauthorization fixes permissions, policy authoring
  fixes destination and placement decisions.
</Callout>

## Tenant provider policy [#tenant-provider-policy]

Tenant provider policy (`domain/connector/tenant_policy.go`) decides **which
providers** a tenant may connect at all. It is enforced at connection creation and
at OAuth start, callback, and refresh. The semantics are tighten-only:

* Deny precedes allow.
* An empty `AllowProviders` means "all providers except those denied."
* An absent policy row means unrestricted.
* A `RequiredReleaseStage` can require providers to be at least `beta` or `ga`.
* `RequiredCompliance` can require providers flagged for PII, GDPR, or SOC 2 scope.

The policy also holds per-provider **BYO OAuth overrides**: a tenant can supply
their own OAuth client id and secret for a provider. The secret is stored as AEAD
ciphertext (AAD bound to `tenant_id|provider_key`) and is never returned — reads
mask it to `{client_id, has_secret, redirect_url, scopes}`. Managed under
`/api/v2/connector/tenant-policy/{tenant_id}` (set, get, delete), with a
zero-trust check that the caller's tenant matches the path.

## Quotas [#quotas]

Quota (`domain/connector/quota.go`) tracks how much of a provider's rate budget a
tenant has left, so routing can steer away from a connection that's about to be
throttled. Quota facts make provider-throttle state **durable and routing-safe**,
complementing — not replacing — the live Redis rate limiter below. Crucially,
quota is **derived from the provider's own rate-limit response headers**, not an
internal hard counter. The state is split into three records:

* **`ConnectorQuotaPolicy`** — the tenant/provider bucket, optional
  connection/action/sync/proxy scope, response header names, spend cost, freshness
  TTL, and route-safe reserve.

* **`ConnectorQuotaObservation`** — append-only evidence from local gate decisions
  or provider responses, storing only numeric/time metadata plus safe correlation
  ids.

* **`ConnectorQuotaFact`** — the current snapshot routing reads.

* **Buckets.** A `ConnectorQuotaScope` combines tenant, provider, connection,
  action, sync, proxy operation, endpoint class, and surface. Leaving optional
  fields empty makes a broader bucket (e.g. tenant + provider only). The bucket
  key drives all quota state.

* **Header policy.** Ductor reads standard headers by default —
  `X-RateLimit-Limit` / `RateLimit-Limit`, the matching `-Remaining` and
  `-Reset`, and `Retry-After` — configurable per policy. Only header *names* are
  persisted, never payloads.

* **Readiness.** A rolled-up `ConnectorQuotaFact` grades a bucket `ready`,
  `near_exhausted`, `exhausted`, `backing_off`, `unknown`, or `stale`. Routing
  gates on it: `ready`/`near_exhausted` proceed; `unknown` proceeds only if the
  policy allows unknown quota; `backing_off` waits until its next-eligible time;
  `stale` is treated as not-ready.

* **Route-safe reserve.** A policy can keep a headroom of units unspent so
  routing stops using a connection before it hits a hard 429.

Quota is managed under `/api/v2/connector/quota/...` — list facts, get a fact by
bucket key, preview the impact of a hypothetical spend, upsert or delete a policy,
list observations, and clear a backoff.

## Rate limiting [#rate-limiting]

Where quota is advisory and header-derived, rate limiting is an active gate
(`cmd/ductor/fx_connector_ratelimit.go`). It is **default-off**: enabled by
`connector.action_rate_limit.enabled`, and when on it requires the flow Redis (it
fails startup otherwise). The limiter is GCRA (a leaky-bucket variant) implemented
in Lua over Redis, wrapped in a capacity gate.

Rate-limit scopes (`domain/connector/ratelimit_scope.go`) mirror quota buckets —
tenant, provider, connection, action, sync, proxy, surface, plus run/step
correlation — and are threaded on the context so the gate can key on exactly the
granularity you configure. A `DenialHintStore` caches upstream `429`/`5xx`
`Retry-After` hints (keyed `tenant|provider|action`) and is consulted *before* the
gate for fast-fail. The rate-limit interceptor sits in the `connector_interceptors`
chain, ordered so the audit interceptor observes throttling decisions.

<Callout type="info" title="Quota vs. rate limit">
  **Quota** answers "is this provider about to throttle us?" from the provider's
  own headers, and steers routing. **Rate limit** answers "are we exceeding the
  budget *we* set?" and actively rejects with `ErrThrottled`. They're
  complementary: quota avoids provider-side 429s, the rate limiter enforces your
  own fairness policy across tenants.
</Callout>

## Execution admission and fairness [#execution-admission-and-fairness]

Quota steers routing and the rate limiter enforces your own budget, but neither is
a *fairness* gate across tenants competing for the same runtime and provider
capacity. That is **execution admission** (Plan 100,
`docs/connectors/execution-admission.md`) — the shared gate connector work enters
*before* it consumes capacity: actions, async action jobs, proxy calls, sync
starts, connector functions, provider webhook lifecycle operations, mapper
executions, and external MCP tool actions all pass through it.

Admission is one gate with many ways to say no. The insight is that it does not
own any of those reasons — quota, budgets, entitlements, and placement each keep
their own authority, and admission composes their verdicts into a single receipt:

```mermaid
flowchart TD
  w([connector work]) --> gate{execution admission}
  gate -->|capacity free| adm([admitted])
  gate -->|group full| q([queued / deferred])
  gate -->|quota plane| bq([blocked_by_quota])
  gate -->|budget plane| bb([blocked_by_budget])
  gate -->|entitlement plane| be([blocked_by_entitlement])
  gate -->|placement plane| bp([blocked_by_placement])
```

Policies are tenant/environment-scoped and **fail-closed** for required enterprise
facts, grouping by tenant, environment, provider, execution kind, and resource
class. They control max concurrent and max queued executions per fairness group,
provider rate/burst hints, resource-class weights, an interactive reserve, and
bounded bulk catch-up. Every decision is one of `admitted`, `queued`, `deferred`,
`blocked_by_quota`, `blocked_by_budget`, `blocked_by_entitlement`,
`blocked_by_readiness`, `blocked_by_placement`, `blocked_by_capacity`, or
`rejected`, recorded as a redacted receipt. Admission *composes* the other planes
— quota still owns throttle state, budgets own spend, entitlements own capability
access, placement owns destination/worker placement — it does not replace them.

## Connection selection strategies [#connection-selection-strategies]

In route mode, something has to pick *which* eligible connection to use.
**Connection selection** (`application/connector/routing`,
`docs/connectors/connection-selection-strategies.md`) is the strategy plane that
chooses one connection from an eligible fallback chain and returns a rich decision
— policy lineage, candidate scores, hard eliminations, missing/stale facts, and
repair hints — rather than a bare pick. If no policy is active for the request
subject, the selector falls back to `priority_chain` (the existing route-row
behavior). Other built-in strategies include `least_throttled`, `healthiest`,
`scope_capability_match`, `region_affinity`, `cost_aware`, `weighted_canary`,
`weighted_failover`, and `sticky_by_entity`. Route-mode output records a
`connection_selection_decision_id` so the ledger and operator workflows can
correlate an execution back to the selector decision. Preview, compare, and
validation never resolve credentials, call providers, or mutate state.

## Audit [#audit]

Every governed connector event is recorded in an append-only, tamper-evident
audit log (`infrastructure/connector/auditstore/`, table `connector_audit_log`).
Rows are immutable — `UPDATE` and `DELETE` are blocked by a database trigger — and
each row chains to the previous one by hash.

An audit row records the event type and result, the actor (subject and type — an
operator or an `end_user`), the connection, provider, and action keys, the
residency region, and a JSONB metadata blob. Per-tenant sequence numbers
(`audit_seq`) are unique, so a concurrent write surfaces as a retryable race
rather than a gap. The chain is verifiable end-to-end: `VerifyScan` streams rows
into a hash-chain verifier that detects any tampering or missing link.

Because the chain is computed over redacted metadata — the audit model, like the
lifecycle-event model, forbids credentials, tokens, raw errors, and payload
bodies — the log is safe to retain and export without leaking secrets. This
tamper-evident record of what an action did is one of the evidence inputs a
<Term name="Receipt" /> draws on when it joins the story of a cleared unit of
work.

## Where to go next [#where-to-go-next]

* [Connections](/docs/connectors/connections) — where tenant policy and egress policy are enforced.
* [Authentication](/docs/connectors/authentication) — how BYO OAuth secrets are encrypted.
* [Architecture](/docs/connectors/architecture) — the interceptor chain that applies rate limiting and audit.
