Policy and OrgPolicy

Constrain what a workstation manifest may name: which git repositories it may clone, where it may connect out to, and which LLM models an Integration may name.

Anyone who can write a manifest can point a workstation at any repository and let it reach anything the network routes. A Policy (namespaced) or an OrgPolicy (organization-wide) puts a bound on that, answering one question: what may a workstation manifest name?

This page documents three governed items, the kinds of value a policy can limit:

  • Git source locations (gitSources): the repositories a workstation clones into itself, via spec.sources[].git.url and spec.dotfilesRepo.url.
  • Egress destinations (egressDestinations): the places a workstation may connect out to, via spec.egress.destinations[], and the hosts an Integration connects its workstations to.
  • LLM models (llmModels): the model an llm Integration names in spec.llm.endpoint.model.
apiVersion: core.ringleader.dev/v1
kind: OrgPolicy      # cluster-scoped, written by an org admin
kind: Policy         # namespaced,      written by a namespace admin

An apply that names something the resolved policy denies is refused at apply, with a message naming the field and the value that was rejected. Nothing is silently dropped.

remote: forbidden: spec.egress.destinations[1].cidr names "8.8.8.8/32",
which your organization's or namespace's policy does not permit (HTTP 403)

The message stops there on purpose, and never names the policy object that decided: an OrgPolicy is cluster-scoped, an ordinary namespace member cannot read one, and naming it would disclose an object they have no access to. Which policy decided, and at what priority, goes to the control plane’s audit log rather than onto the wire. The field and the value are enough to fix most manifests. For a refusal you did not expect, list your namespace’s own Policy objects (see CLI below); if none of them explains it, an OrgPolicy you cannot read decided, and an org admin can tell you which. Reading the audit record itself needs whoever operates your control plane.

No policy objects means no constraint

Every governed item ships with a permissive default, so a deployment with no policy objects behaves exactly as it did before policies existed. You opt in by writing one.

Example

An organization that allows its own GitHub org and nothing else:

apiVersion: core.ringleader.dev/v1
kind: OrgPolicy
metadata:
  name: acme-baseline
spec:
  org: acme
  priority: 200
  rules:
    gitSources:
      default: deny
      allow:
        - github.com/acme
        - "*.internal.acme.example"

A single team loosening it for one vendor repository, which is possible only because the organization left room for it (see Priority below):

apiVersion: core.ringleader.dev/v1
kind: Policy
metadata:
  name: vendor-exception
  namespace: payments
spec:
  priority: 50
  rules:
    gitSources:
      allow:
        - github.com/vendor/sdk

Fields

FieldTypeDescription
orgstringOrgPolicy only, required. The organization this policy belongs to.
priorityintWhich policy wins when several apply. Higher outranks lower. On a namespaced Policy it is capped at 100.
rulesobjectOne block per governed item kind: gitSources, egressDestinations and llmModels. An unknown key is refused at apply, so a typo cannot produce a policy that constrains nothing.

A rule block

FieldTypeDescription
defaultstringallow or deny, the verdict for anything neither list matches. Omit it to defer to a lower-priority policy’s default. If no policy declares one, the default is allow.
allow[]stringEntries that permit a value.
deny[]stringEntries that refuse a value.

Declaring default and declaring nothing are genuinely different: an omitted default defers, a declared one outranks.

Git source entries

A gitSources entry is a host with an optional path prefix, never a full URL:

allow:
  - github.com                      # every repository on the host
  - github.com/acme                 # every repository under the acme owner
  - github.com/acme/platform        # exactly this repository, and anything below it
  - "*.internal.example"            # any sub-domain (never the bare apex)
  - git.example:2222                # a non-default port
  • Drop the scheme. https://github.com/acme is refused; write github.com/acme.
  • No user part. Write github.com/acme, not git@github.com/acme.
  • No whitespace.
  • Paths match on segment boundaries. github.com/acme covers github.com/acme/platform but never github.com/acme-labs/x.
  • *. matches one or more leading labels and never the apex. *.example.com covers git.example.com, not example.com.
  • Matching is case-insensitive on both host and path.

The same entry means the same thing wherever it is written, so a rule you test in one policy behaves identically in another.

Egress destination entries

An egressDestinations entry is written exactly as a destination is written in a workstation’s own egress block: a network, a bare address, or a name with an optional *. wildcard.

The same rules also judge each Integration when it is applied. They judge these hosts:

  • The host an llm endpoint connects to, whether you wrote its url or the vendor supplies it. For vendor: openrouter that host is openrouter.ai, and a refusal names spec.llm.endpoint.vendor.
  • Every host a credential injection rule names.
  • The host of an mcp server’s url.

An Integration that names a host the policy does not permit is refused. Hosts listed under httpProxy.exclude are not judged.

rules:
  egressDestinations:
    default: deny
    allow:
      - 10.0.0.0/8
      - 203.0.113.7
      - github.com
      - "*.githubusercontent.com"

Entries are matched by containment, not equality, and the asymmetry is the point: a rule permitting *.example.com covers a workstation asking for api.example.com, while a rule permitting api.example.com does not cover one asking for *.example.com, because that workstation is asking for more than the rule grants.

A name and an address never cover one another, in either direction. Deciding that would mean resolving the name, and a policy verdict must not depend on what DNS answered at the moment it was evaluated. The consequence is worth knowing rather than discovering: under default: deny, an allow list holding only names denies every cidr: destination, and the reverse. An organization that means to permit both writes both.

Why this needs a policy at all

Configuration layers merge by unioning egress destinations, which only widens. A member can always author a higher-priority layer over their own workstation and add whatever they like to that union, so a restriction expressed as a layer is not a restriction. A floor has to live somewhere a member cannot write, which is what a policy is.

LLM model entries

An llmModels entry limits the model an llm Integration names in spec.llm.endpoint.model. An entry is an exact model name, or a prefix followed by one trailing *:

rules:
  llmModels:
    default: deny
    allow:
      - anthropic/claude-sonnet-4.5     # exactly this model
      - "openai/gpt-5*"                 # every model whose name starts with openai/gpt-5
  • Matching is case-insensitive.
  • A * anywhere but at the end is refused, and so is a bare *. Use default to allow or deny every model.
  • Only the model written in the Integration is judged. A program on the workstation can still ask the gateway for a different model when it calls it.

Priority, and who wins

Several policies can apply to one namespace: its own Policy objects, and every OrgPolicy of the organization that owns it. They resolve in this order.

  1. The default is the one declared by the highest-priority block that declares any. If two blocks tie at that priority with different defaults, the answer is deny: a tie is an unresolved disagreement, and the restrictive answer is the safe one.
  2. The verdict comes from the highest priority at which any block matches the value, considering only blocks at or above the priority the winning default was declared at. No match there leaves the default standing.
  3. A conflict at that priority, where the value matched both an allow and a deny, is resolved by tier first: an OrgPolicy beats a Policy. Within one tier, the verdict that departs from the resolved default wins, on the reasoning that an explicit entry exists to be an exception. With default: allow that means the deny wins; with default: deny, the allow does.

The cap is the whole mechanism

An OrgPolicy’s priority is unbounded; a namespaced Policy’s is capped at 100. That single asymmetry is how an organization chooses its posture:

  • Set an OrgPolicy above 100 and it supersedes every namespace policy. Teams can neither widen nor narrow it.
  • Set one below 100 and it is a default that teams may refine with their own Policy objects.

A namespace admin cannot outrank their own organization by typing a bigger number.

Who may write one

KindWritten byRead by
OrgPolicyan org admin, for their own organizationorg admins only. It is cluster-scoped, so a namespace member cannot read one even though it governs their applies.
Policya namespace adminmembers of that namespace

A policy is a governance fact rather than a personal resource: there is no “my own policy” that an ordinary member can author for themselves.

An operator ceiling sits above both

A deployment may be configured with its own allowlist of permitted git hosts. That is a ceiling: a policy can only narrow within it, never widen past it. If a repository is refused and no policy of yours mentions it, the deployment itself does not permit that host, so ask whoever operates your control plane.

CLI

rl apply -f orgpolicy.yaml
rl orgpolicy get                          # alias: opol
rl policy get -n payments                 # alias: pol
rl policy describe vendor-exception -n payments

See also