Skip to content

Policy

This page is for policy authors. Authority resolves a catalog resource, evaluates Cedar, and records the decision. Start with the deny-all to typed lab recipe, then use the action and pattern sections. The shared fixtures are authority-1, proxy-1, connector-1, lab-read, lab-denied, and alice@example.test.

How a decision is reached

Authentication establishes the principal and groups. Catalog resolution supplies the typed resource. The protocol derives an action and context. A delegated capability or one-time grant can only narrow the request. Cedar evaluates the effective request. An allow reaches the Connector; a deny fails closed before the downstream operation.

Catalog resolution before policy evaluation

A catalog miss is not a Cedar decision. Declare the resource kind, environment, labels, parent, and serving relationship before writing a policy rule. The admin view can see rows that a user cannot view.

Action map

See the CLI reference and Operations for exhaustive command and event tables.

Action Boundary
view catalog listing
connect session open for SSH, database, TCP, MCP, Kubernetes, or web
dbQuery database statement
kubeRequest one Kubernetes API request
mcpCallTool, mcpReadResource, mcpGetPrompt one MCP operation
viewTools MCP listing
llmCall one model call
httpRequest one web route
desktopChannel one channel of a desktop session, per direction
sshForwardLocal, sshForwardRemote one SSH forwarding channel
sshForwardAgent the client's ssh-agent, carried into one session
sshUseClientKey a jump host authenticating onward with the caller's own key
sshProgram one program a session runs: a shell, an exec, a subsystem
mintOnwardSshCert downstream SSH certificate
killSession terminate a live session
approveRequest approve a JIT request; request creation names the target action
editPolicy, approvePolicyEdit policy proposal
enrollNode, revokeNode, issueJoinToken node lifecycle
manageCatalog catalog mutation
viewNodes enrolled-node registry read
disableUser revoke a person's delegations and disable their access
viewAudit, viewRecordings evidence access
rotateCA certificate authority operation

One connect, seven kinds of resource

connect opens a session for the session-bearing kinds. It does not grant later database statements, Kubernetes requests, MCP calls, model calls, or HTTP routes.

connect and sshProgram are a pair

On SSH the two are one grant in two rules. connect opens the session; sshProgram is asked once per shell, once per exec and once per subsystem, in both models — a session the connector terminates and a hop it makes onward — from epoch 1. A group with connect and no sshProgram opens a session in which nothing runs, and finds out at the first command.

So the shipped template grants both and narrows with a condition: context.program_kind is shell, exec or subsystem, and a repository command also carries context.git_operation and context.repository.

sshUseClientKey is the one the template leaves commented. It is asked only where a hop authenticates onward with the caller's own key — an ssh resource declared onward_auth = "client_key" — so a deployment without one is unaffected whether it is granted or not, and the day such a resource is declared is the day somebody should decide whether this connector may ask a person's agent to sign.

kubeRequest and context.read_only

A kubeRequest grant is one cluster-scoped request. Narrow the one-time marker with the request's verb, resource kind, namespace, name, subresource, and read_only context. It is not a standing cluster grant.

Credential type: context.presented

context.presented describes the verified arrival path: "key" when the caller proved possession of a key, or "cookie" for a browser session. It is present only on connect and httpRequest. A caller cannot set it in request data.

Entity types

Structural

Organization, Project, and Group form the structural hierarchy. Runtime and control-plane entities attach to that hierarchy; they are not structural groups themselves.

Principals

Human principals are User entities. Verified group claims become Group memberships. Workload and delegated-agent principals are Agent entities with short-lived credentials and separate attenuation or owner facts.

Target resources

Top-level target resources include SshHost, Database, KubernetesCluster, TcpService, McpServer, LlmService, WebApp, WebEgress, and RdpHost. Request-aware subresources include Tool, McpResource, McpPrompt, and LlmModel. PostgreSQL, MySQL, and Cockroach backends all use the Database Cedar entity; a Cockroach backend is advertised on the PostgreSQL catalog lane.

Control-plane resources

Control-plane resources include Catalog, EnrolledNode, CertificateAuthority, PolicyDoc, AccessRequest, and AuditLog. A backend rule does not grant an administrative action.

Runtime resources

Session is the runtime resource used by session termination and recording workflows. Model, MCP, Kubernetes, database, HTTP, and forwarding paths derive their own typed resources or context at the implemented boundary.

Context fields per action

Use only fields supplied by that action path. environment and tags are resource attributes; read them from resource. Common context covers login and factor freshness, corporate-network state, optional source address, time, ticket, and recheck facts. Approval and one-time grants are optional only on the actions whose schema declares them. Per-lane fields or resource facts are:

Lane Fields
database optional db_role on connect; sql_category on dbQuery; optional approval where declared
Kubernetes verb, resource kind, namespace, name, subresource, marker, read_only
MCP typed tool/resource/prompt attributes plus optional approval or one-time grant
model LlmModel resource attributes plus optional approval or one-time grant
web method, host, port, route, canonical path, read-only/inspection/upgrade facts, and presented
SSH forward_target, forward_bind, or login_user on their exact actions
desktop windows_user on connect; channel and direction on desktopChannel

A desktop's channels

desktopChannel is decided while the access certificate is issued, once per channel and direction, and the answers are signed into it. The serving node reads what was decided; a caller cannot widen it by asking.

The backend's configuration (clipboard, audio, drives and the rest) sets the channels the desktop offers. The policy sets which of them this caller may use. A session carries the channels both allow. A permit cannot turn on a channel the backend does not offer.

With no desktopChannel permit, a session carries no channels. To give a desktop a clipboard, permit it explicitly.

A session whose certificate carries no desktopChannel decision is refused at the serving node. The backend's configuration does not apply as a fallback.

A workload or a delegated program holds no access certificate. For it, the Windows account and the channels are decided on its connect call, for the Agent principal, and returned to the node with the permit. A policy that names only people gives an agent a desktop with no channels. When the backend offers several accounts and the policy leaves more than one open to the agent, the session is refused.

Every channel is decided per direction, clipboard_files included: permitting clipboard_files:in permits files into the desktop and nothing out of it.

permit (
    principal in Group::"staff",
    action == Action::"desktopChannel",
    resource is RdpHost
) when {
    context.channel == "clipboard" || context.channel == "audio"
};

// Allow copy from the desktop; forbid paste into it.
forbid (
    principal,
    action == Action::"desktopChannel",
    resource is RdpHost
) when {
    context.direction == "in"
};

channel is one of clipboard, clipboard_files, audio, drives, printers, ports, smart_cards: the words the backend's configuration uses. direction is from the desktop's point of view: in towards it (a paste, one of the caller's disks), out away from it (a copy, its sound).

Policy patterns

Granting a group access

Start with separate typed view and connect permits:

permit (principal in Group::"lab-readers",
        action == Action::"view",
        resource == Database::"lab-read");
permit (principal in Group::"lab-readers",
        action == Action::"connect",
        resource == Database::"lab-read");

The positive test is lab-read in gdsgate ls and a successful session. The nearest negative is lab-denied. Keep the two actions separate.

Restrict by environment

permit (principal in Group::"lab-readers",
        action == Action::"connect",
        resource is Database)
when { resource.environment == "dev" };

A production row is the nearest negative. Verify the catalog environment.

Restrict by label and tag

permit (principal in Group::"operators",
        action == Action::"connect",
        resource is SshHost)
when { resource.hasTag("owner") && resource.getTag("owner") == "platform" };

An unlabelled host or different owner tag must be denied.

Catalog visibility for one resource

A visible lab-denied row with a denied connect is a useful negative. An absent row can mean view denial, so inspect both the catalog and decision event.

Database session roles

permit (principal in Group::"analysts",
        action == Action::"connect",
        resource is Database)
when { context has db_role && context.db_role == "readonly" };

This matches a logical profile. It does not create a database account.

Per-statement database authorization

permit (principal in Group::"analysts",
        action == Action::"dbQuery",
        resource is Database)
when { context.sql_category == "read" };

The nearest negative is a write, DDL, or unsafe copy. The Connector must refuse before the statement reaches the backend.

Per-database access

Name the database by its typed ID. Display labels do not match:

permit (principal in Group::"analysts",
        action == Action::"connect",
        resource == Database::"lab-read");

Authorizing a workload

permit (principal in Group::"workload-readers",
        action == Action::"connect",
        resource is Database)
when { principal is Agent };

An expired or revoked workload is the nearest negative.

Authorizing a delegated agent

An Agent is constrained by both the human delegation and standing policy. An agent cannot turn a sandbox claim into a policy allow. See AI agents.

Kubernetes request authorization

permit (principal in Group::"operators",
        action == Action::"kubeRequest",
        resource == KubernetesCluster::"cluster-dev")
when {
  context.read_only
};
permit (principal in Group::"operators",
        action == Action::"kubeRequest",
        resource == KubernetesCluster::"cluster-dev")
when {
  context has "step_up_grant" &&
  context.step_up_grant.for_action == "kubeRequest" &&
  context.verb == "delete" &&
  context.resource_kind == "pods" &&
  context.namespace == "lab" &&
  context.resource_name == "secret-1" &&
  context.subresource == "" &&
  !context.read_only
};

A different verb, namespace, name, subresource, read-only class, or missing one-time marker is denied. The schema's type is KubernetesCluster in current source; keep that exact resource type in a deployment policy.

MCP per-tool policy

Static allowed_tools is the first boundary. Set enforce_tool_policy when Cedar must decide individual tools, resources, and prompts.

Destructive tools

Classify destructive tools in the Connector configuration and require a typed rule or step-up. A tool name alone is not a security proof.

[[connector.backends]]
resource = "tools-dev"
kind = "mcp"
addr = "BACKEND_ADDR_FROM_OWNER"
enforce_tool_policy = true

Listing visibility (viewTools)

Grant viewTools separately from tool invocation. A tool may be visible but denied for call, or absent from the listing.

MFA step-up and per-tool JIT

Bind a one-time grant to the server, tool, and mcpCallTool action. It is spent by the first matching call. A JIT approval is similarly bounded by action and TTL.

The two MFA channels

Identity-provider login freshness and gateway step-up are separate facts. An old login does not satisfy a tool-specific step-up rule.

One-time step-up grants

Use the user command. The nearest negative is a different resource or action, which must not consume the grant.

Choosing a factor level

A passkey or TOTP-backed grant is still one-time and still subject to standing policy. Record the factor requirement and owner.

Examples

Test allowed search, denied mutation, and a second call after the grant is spent. Link to AI agent model resources for launch.

Model calls

llmCall is evaluated per call and matches an LlmModel beneath an LlmService:

permit (principal in Group::"model-users",
        action == Action::"llmCall",
        resource == LlmModel::"models-dev.test-small");

An unlisted model or revoked delegation is the nearest negative. This is not a reusable connect session.

HTTP(S) application routes

permit (principal in Group::"web-users",
        action == Action::"httpRequest",
        resource == WebApp::"app-dev")
when { context.method == "GET" || context.method == "HEAD" };

A gateway 403 is a policy refusal. An upstream 403 is a response after allow. WebApp and WebEgress are different targets.

SSH -L and -R

Grant local and remote forwarding separately and constrain the destination. A connect permit alone does not grant a forwarding channel.

A delegation grant is coarser than these two actions: one token, ssh:forward@<host>, covers both, so the mint requires the delegator to hold both actions before it issues the token. A rule that constrains the destination does not satisfy that check. The token names a host and never an address, so the mint asks whether the delegator may forward on the host at all, and refuses naming the token where the answer depends on which address was asked for. Per attempt the two actions are still asked separately, with the real destination in context.forward_target / context.forward_bind. See CLI → gdsgate delegate.

SSH -A

sshForwardAgent is a separate action because it is a separate capability. -L and -R carry a route; -A carries the client's ssh-agent into the session, and while the forwarded socket exists, anything that can reach it authenticates as the client wherever the client's keys are trusted. That includes programs the session starts and root on the Connector host. There is no destination to constrain, so constrain the principal and the resource, and require a recent factor if the deployment offers the capability at all.

A permit for sshForwardLocal does not permit -A, and neither does connect. The backend must also set allow_agent_forward = true; without it the Connector refuses before Authority is asked.

Authenticating onward with the caller's key

sshUseClientKey is what a jump host asks before it uses the caller's own key to authenticate to the host beyond it. It is a separate action from sshForwardAgent, and the two differ in what survives the request:

sshForwardAgent sshUseClientKey
What is created a socket in the Connector's filesystem nothing
Who can sign anything under that account, root included the Connector process
For how long the whole session one authentication
Layer 1 allow_agent_forward = true on the backend onward_auth = "client_key" on the catalog resource

A permit for one does not permit the other. Write both if a session needs both.

The resource must carry onward_auth = "client_key" in the catalog: without it the Connector refuses before Authority is asked, whatever a rule says. Set it with authority set-resource --onward-auth.

No delegation grant carries this action. A delegate holds no key of the person's, so a delegated session is refused before the policy is consulted.

Downstream certificate minting (SSH model B)

mintOnwardSshCert is distinct from connect. Use it only when the Connector and target owner have completed the downstream certificate handoff.

Session termination

killSession should be scoped by owner, resource, or incident role. Ending a session does not erase its audit row.

Approving access requests

Grant approveRequest to an approver group separate from the requester group and deny self-approval. Scope it by environment, resource, and action.

Requiring JIT approval for all access

Require approved_request on the same resource, action, requester, and TTL. A stale or differently scoped approval is the nearest negative.

Administrative actions

Keep policy, catalog, enrollment, revocation, audit, and CA operations on dedicated control identities. A backend rule never grants manageCatalog, issueJoinToken, or rotateCA.

The starting policy

With no matching permit, Cedar denies by default. Add typed lab view/connect and the reviewed control-plane and break-glass rules; do not add an unconditional forbid, because a forbid always overrides every permit:

permit (principal in Group::"lab-readers",
        action == Action::"view",
        resource == Database::"lab-read");
permit (principal in Group::"lab-readers",
        action == Action::"connect",
        resource == Database::"lab-read");

The positive test is lab-read. The nearest negative is lab-denied.

Prove the starting boundary before adding another permit:

Probe Expected result
alice@example.test in lab-readers lists and connects to Database::"lab-read" allow, with the matching typed rules in the decision record
a principal outside lab-readers requests the same resource deny because no permit matches
lab-readers requests SshHost::"lab-read" or Database::"lab-typo" deny because the type or ID does not match
the lab-read catalog row is absent catalog miss before Cedar; no policy edit can repair it

Keep lab-denied visible with a separate narrow view permit when the test needs a visible refusal. A missing row, a hidden row, and a denied connection are different evidence and must not share one generic failure label.

Administrative actions granted to people

Separate viewAudit, editPolicy, manageCatalog, enrollNode, revokeNode, issueJoinToken, and rotateCA. Give each action a named owner.

The gdsgate- group namespace

Reserve gdsgate- names for control roles. Sanitize external group claims so an ordinary group cannot become a control identity.

Seeding the first policy

Seed only before the store has an active version. Later edits use pull, strict validate, and push so the store records version and audit.

The break-glass rule

Every accepted policy keeps a narrowly scoped break-glass permit. A candidate that removes it must be rejected by the meta-invariant. The emergency route still requires local filesystem ownership and incident recording.

Rehearse lockout recovery only in the isolated lab. Save and strict-validate a known-good policy first. Then prove that a candidate denying the normal policy administrator does not block an Authority-host operator who owns the configured emergency socket:

gdsgate --config "$EMERGENCY_CONFIG" authority policy validate "$KNOWN_GOOD_POLICY"
gdsgate --config "$EMERGENCY_CONFIG" authority policy push \
  "$KNOWN_GOOD_POLICY" --route emergency

Expected result: the known-good version hot-reloads, the normal administrator route works again, and the audit row names the emergency route and operator context. The nearest negative is a missing socket, wrong filesystem owner, invalid known-good file, or a candidate rejected by the break-glass invariant. Do not weaken socket permissions or edit the store directly. Close the incident only after a normal pull, diff, allow-and-deny probe, and audit verification.

A worked group-scoped policy

The following shape separates catalog visibility, session connect, and read-only database statements:

permit (principal in Group::"analysts",
        action == Action::"view",
        resource is Database)
when { resource.environment == "dev" };
permit (principal in Group::"analysts",
        action == Action::"connect",
        resource is Database)
when { resource.environment == "dev" };
permit (principal in Group::"analysts",
        action == Action::"dbQuery",
        resource is Database)
when {
  resource.environment == "dev" &&
  context.sql_category == "read"
};

The positive test is a read against lab-read. The nearest negative is a write or lab-denied.

Validating a policy

Run strict validation before any push:

gdsgate --config "$ADMIN_CONFIG" authority policy validate "$POLICY_FILE"
gdsgate --config "$ADMIN_CONFIG" authority policy pull --output "$CURRENT_POLICY"
gdsgate --config "$ADMIN_CONFIG" authority policy push "$POLICY_FILE"

A stale base, invalid document, or failed meta-invariant keeps the previous active policy. Do not treat parser success as proof of intended scope.

Editing the policy remotely

Authenticate through an allowed admin route, preserve the pulled base version, review the diff with a second owner, validate, and push. Recover a stale base by pulling again.

Script-friendly: pull / validate / push

umask 077
gdsgate --config "$ADMIN_CONFIG" authority policy pull --output "$POLICY_FILE"
gdsgate --config "$ADMIN_CONFIG" authority policy validate "$POLICY_FILE"
gdsgate --config "$ADMIN_CONFIG" authority policy push "$POLICY_FILE"
rm -f "$POLICY_FILE"

The nearest negative is strict validation or stale-version failure. The old active policy remains.

Interactive: authority policy edit

The interactive editor still runs strict validation and the same meta-invariant checks. Use the script-friendly flow for reviewable artifacts.

Scope of editPolicy

editPolicy governs policy source and its version. It does not grant backend connect, catalog mutation, audit viewing, or emergency socket access.