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.