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 narrow the request, never enlarge it. 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
sshForwardLocal, sshForwardRemote one SSH forwarding channel
sshForwardAgent the client's ssh-agent, carried into one session
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.

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, and WebEgress. 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, not context. 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

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 typed ID rather than a display label:

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.

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.