Skip to content
API Reference
Configuration

Governance Policies

Configure fine grained access control policies

Governance policies control what users and applications can access within your Keycard zones.

The policy system has two levels:

  • Policies are individual authorization rules (for example, “permit user Alice access to Google Calendar”)
  • Policy sets bundle policies together and deploy them across the entire zone

Keycard uses default-deny: every authorization request needs an explicit permit for both the user and the application acting on their behalf. Without a matching permit, Keycard denies access. A forbid policy always overrides a permit, so you can layer restrictive rules on top of permissive baselines.

Keycard uses Cedar by AWS as its policy language. Cedar is an open-source authorization language built for fine grained access control. It is formally verified and statically analyzable.

You author policies in Cedar and submit them through the API in one of two formats:

  • cedar_raw is human readable Cedar syntax, concise and easy to write
  • cedar_json is Cedar’s JSON AST representation, verbose but machine-friendly

Keycard validates both formats against the schema before storing them. On retrieval, use the ?format=cedar query parameter to get human readable output, or ?format=json (default) for the JSON representation.

Cedar supports two effects (permit and forbid), uses deny-wins conflict resolution, and provides when/unless conditions for attribute based rules.

Safety guarantees. Keycard validates policies against a typed schema before storing them. The system rejects invalid policies at authoring time, not at runtime. The Cedar engine can prove properties about policy sets without executing them.

Constrained language. Cedar is intentionally not a general purpose language. Its small, well defined grammar keeps policy authoring straightforward. Cedar has no loops and no side effects, only declarative rules.

Model friendly. The constrained semantics and well defined schema make Cedar excellent for AI assisted policy authoring. LLMs can generate correct policies reliably when given the schema as context.

The Cedar schema maps entity types to the Keycard data model:

Entity TypeMaps To
Keycard::UserZone users are the people who authenticate into a zone
Keycard::ApplicationRegistered applications and OAuth clients that act on behalf of users, or on its own
Keycard::ResourceThird party API resources configured in the zone (for example, Google Calendar, GitHub)
Keycard::RegistrationMethodEnum entity for application registration methods
Keycard::CredentialTypeEnum entity for application credential types

Each entity type has attributes you can reference in Cedar when and unless conditions.

PropertyTypeDescription
nameStringThe application’s display name
registration_methodRegistrationMethodHow the application was registered. See Keycard::RegistrationMethod for values
credential_typeCredentialType?The credential type used for authentication. Optional, absent when not yet determined. See Keycard::CredentialType for values
traitsSet<String>Behavioral traits that activate trait-specific experiences and workflows. Possible values: “gateway” (application acts as an API gateway), “mcp-provider” (application provides MCP tools)
dependenciesSet<Resource>Resources the application is configured to access directly (service-to-service)
PropertyTypeDescription
emailStringThe user’s email address from their identity provider
PropertyTypeDescription
identifierStringThe resource’s unique identifier
nameStringHuman-readable resource name
scopesSet<String>OAuth scopes associated with the resource

Enum entity for application registration methods. Reference in Cedar as Keycard::RegistrationMethod::“value”.

ValueDescription
Keycard::RegistrationMethod::“managed”Created via the management API
Keycard::RegistrationMethod::“dcr”Registered dynamically via OAuth 2.0 Dynamic Client Registration

Enum entity for application credential types. Reference in Cedar as Keycard::CredentialType::“value”.

ValueDescription
Keycard::CredentialType::“token”Workload identity (short-lived tokens)
Keycard::CredentialType::“password”Client ID and secret
Keycard::CredentialType::“public-key”Key-based assertion
Keycard::CredentialType::“url”URL-based identity
Keycard::CredentialType::“public”Public client (no secret)

The schema defines a Claims type used for JWT claim-based policy conditions.

PropertyTypeDescription
emailString?Email claim from the JWT, if present
groupsSet<String>?Group memberships from the upstream IdP (e.g., Okta groups, Entra ID groups)

Policies have access to a context object carrying runtime authorization state:

AttributeTypeDescription
on_behalfBoolWhether this is a delegated request (application acting for a user)
subjectUser?The end user on whose behalf an application acts. Optional, absent for direct application access
scopesSet<String>?OAuth scopes in the current request. Optional, absent when no scopes are requested
actor_claimsClaims?JWT claims for the actor making the request. Optional, absent when claims are not available
subject_claimsClaims?JWT claims for the subject that an application acts on behalf of. Optional, absent when there is no subject

Keycard manages schema versions using a date based format (for example, 2026-03-16). Policies you write against a schema version are guaranteed to continue working. Keycard only makes additive changes to a published version, such as new entity types or attributes, and never removes or alters existing definitions.

Platform RBAC roles control who can manage policies through the management API:

  • Organization Administrators and Zone Managers can create, modify, and activate policies (full CRUD on policies, policy versions, policy sets, and policy set versions)
  • Zone Members can view policies and policy sets but can’t create, modify, or activate them

The Keycard platform creates and manages these policies. You can identify them by owner_type: "platform" in the API. You can’t modify or delete managed policies. Keycard groups them into a managed policy set called default-zone-policies.

Expand each entry to view the full Cedar content.

default-user-grants : Permits all authenticated users access to all resources

The permissive baseline. This policy grants every authenticated user access to every resource in the zone, regardless of the specific resource or action.

@id("default-user-grants")
permit (
principal is Keycard::User,
action,
resource
);
default-app-delegation : Permits applications to act on behalf of users

Allows applications to perform actions on behalf of a user when the request is a delegated request (context.on_behalf == true). This is the foundation for OAuth based delegation flows where an application acts for a user.

@id("default-app-delegation")
permit (
principal is Keycard::Application,
action,
resource
) when {
context.on_behalf == true
};
default-app-direct-access : Permits applications to access resources directly

Allows applications as consumers to access resources directly (without acting on behalf of a user), scoped to the application’s configured resource dependencies. The application can only access resources that appear in its dependencies set.

@id("default-app-direct-access")
permit (
principal is Keycard::Application,
action,
resource
) when {
principal.dependencies.contains(resource)
};

Customer policies (owner_type: "customer") let you define your own authorization rules and assemble them into policy sets that supersede the default managed policy set. When you activate a customer policy set, it replaces default-zone-policies as the active set for your zone.

You can include managed policies alongside your custom policies in the same policy set. This is safe because all policy versions, including managed ones, are immutable. When Keycard updates a managed policy, it issues a new version rather than modifying an existing one. Because your policy set pins exact version IDs, there’s no risk of breaking changes from upstream updates. You control when to adopt a newer managed policy version by creating a new policy set version with an updated manifest.

The governance system has four resource types: policies, policy versions, policy sets, and policy set versions.

A policy is a named container for authorization rules. It has metadata (name, description) but no Cedar content. The actual rules live in policy versions.

Invariants:

  • Policy names are unique within a zone
  • Metadata (name, description) can be updated at any time without affecting active authorization
  • Policies are soft deleted (archived), never hard deleted
  • Platform owned policies can’t be updated or archived by customers

A policy version is an immutable snapshot of Cedar content, validated against a specific schema version. Once created, the Cedar content and schema reference never change.

Invariants:

  • Policy versions are immutable. You can’t modify the Cedar content or schema version after creation
  • Each version is validated against the Cedar schema before Keycard stores it
  • Keycard computes and stores a SHA-256 hash of the canonicalized policy content for integrity verification
  • You can archive a policy version only if it isn’t referenced by an active policy set
  • Version numbers auto-increment within each policy

A policy set is a named deployment unit that bundles policies together. Like policies, it has metadata but no policy content. The actual bundle is defined in policy set versions.

Invariants:

  • Policy set names are unique within a zone
  • Platform owned policy sets can’t be updated or archived by customers
  • A policy set can’t be archived while it has an active binding

A policy set version is an immutable manifest that pins exact policy version IDs. When you activate a policy set version, Keycard evaluates exactly the policies listed in that manifest.

Invariants:

  • Policy set versions are immutable. The manifest can’t change after creation
  • The manifest must contain at least one entry
  • Each manifest entry references a specific policy ID and policy version ID, both of which must exist and not be archived
  • Keycard computes a SHA-256 hash of the canonicalized manifest for integrity verification and audit
  • You can archive a policy set version only if it isn’t the currently active version
  • Activating a version atomically replaces the previously active version
  • Version numbers auto increment within each policy set

The lifecycle for creating and deploying a custom policy is:

  1. Create a policy : a named container for your rule
  2. Author a policy version : the immutable Cedar content, validated against a schema
  3. Assemble into a policy set : bundle policies into a deployment unit, optionally including managed policy versions
  4. Activate the policy set : bind and activate it for your zone, replacing the default set

Because both policy versions and policy set versions are immutable, you can always trace exactly which Cedar content was active at any point in time. Rolling back means activating a previous policy set version.

Policy versions are immutable. Once you create a version, nobody can alter its Cedar content. To change a rule, create a new version. The content hash stored at creation time remains valid forever, and any tampering is detectable.

A policy set version pins exact policy version IDs in an immutable manifest. When Keycard evaluates an authorization request, it uses exactly the policies in the active manifest. There is no window where a partially updated set of policies could be evaluated.

Because every policy set version is immutable and preserved, you can revert to any previous configuration by activating an earlier policy set version. The previous manifest still references the same immutable policy versions it always did.

Every version of every policy and every manifest is preserved with SHA-256 content hashes. You can reconstruct exactly which Cedar content was active at any point in time, and verify that the content hasn’t been modified since creation.

Creating a policy version doesn’t affect live authorization. You can author, validate, and review policies without risk. Changes only take effect when you explicitly activate a policy set version.

This walkthrough shows you how to create and activate a custom policy. You’ll authenticate with the API, write a Cedar policy, bundle it into a policy set, and deploy it to your zone.

  1. Authenticate

    All management API requests require a Bearer token. Obtain one by exchanging your service account’s client ID and client secret:

    import requests
    token_response = requests.post(
    "https://api.keycard.ai/service-account-token",
    data={
    "grant_type": "client_credentials",
    "client_id": KEYCARD_CLIENT_ID,
    "client_secret": KEYCARD_CLIENT_SECRET,
    },
    ).json()
    base = "https://api.keycard.ai"
    headers = {"Authorization": f"Bearer {token_response['access_token']}"}
    Example response
    {
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 3600
    }
  2. Review the schema

    List the available Cedar schemas for your zone to find the latest schema version.

    schemas = requests.get(
    f"{base}/zones/{zone_id}/policy-schemas",
    headers=headers,
    ).json()
    print(schemas)
    Example response
    {
    "items": [
    {
    "id": "01JKXYZ123ABC456DEF789GH",
    "version": "2026-03-16",
    "cedar_schema": "namespace Keycard {\n entity User = {\n \"email\": String,\n };\n entity Application = {\n \"credential_type\": String,\n \"dependencies\": Set<Resource>,\n };\n entity Resource = {\n \"name\": String,\n };\n};\ntype Claims = {\n \"email\": String?,\n \"groups\": Set<String>?,\n};\ncontext = {\n \"on_behalf\": Bool,\n \"subject\": User,\n \"scopes\": Set<String>,\n \"actor_claims\": Claims,\n \"subject_claims\": Claims,\n};\n",
    "created_at": "2026-03-16T10:00:00Z"
    }
    ]
    }
    Example response
    {
    "items": [
    {
    "id": "01JKXYZ123ABC456DEF789GH",
    "version": "2026-03-16",
    "cedar_schema": "namespace Keycard {\n entity RegistrationMethod enum [\"managed\", \"dcr\"];\n entity CredentialType enum [\"token\", \"password\", \"public-key\", \"url\", \"public\"];\n\n entity User {\n email: String,\n };\n\n entity Application {\n name: String,\n registration_method: RegistrationMethod,\n credential_type?: CredentialType,\n traits: Set<String>,\n dependencies: Set<Resource>,\n };\n\n entity Resource {\n identifier: String,\n name: String,\n scopes: Set<String>,\n };\n\n type Claims = {\n email?: String,\n groups?: Set<String>,\n };\n\n action any appliesTo {\n principal: [User, Application],\n resource: Resource,\n context: {\n on_behalf: Bool,\n subject?: User,\n scopes?: Set<String>,\n actor_claims?: Claims,\n subject_claims?: Claims,\n },\n };\n}\n",
    "created_at": "2026-03-16T10:00:00Z"
    }
    ]
    }
  3. Create a policy

    Create a named policy container. You’ll add the Cedar content as a version in the next step.

    policy = requests.post(
    f"{base}/zones/{zone_id}/policies",
    headers=headers,
    json={
    "name": "require-token-credentials",
    "description": "Require token credential type for all application access",
    },
    ).json()
    policy_id = policy["id"]
    Example response
    {
    "id": "01JKXYZ456DEF789ABC123GH",
    "zone_id": "01JKXYZ789ABC456DEF123GH",
    "name": "require-token-credentials",
    "description": "Require token credential type for all application access",
    "owner_type": "customer",
    "created_at": "2026-03-03T10:00:00Z",
    "updated_at": "2026-03-03T10:00:00Z",
    "archived_at": null
    }

    Save the id from the response. You’ll need it for the next step.

  4. Author the policy version

    Create an immutable policy version with Cedar content and a schema version. Submit the policy as cedar_raw (human readable Cedar syntax). The API also accepts cedar_json (JSON AST). You must provide exactly one.

    cedar_policy = """\
    @id("require-token-credentials")
    forbid (
    principal is Keycard::Application,
    action,
    resource
    ) unless {
    principal has credential_type && principal.credential_type == Keycard::CredentialType::"token"
    };"""
    version = requests.post(
    f"{base}/zones/{zone_id}/policies/{policy_id}/versions",
    headers=headers,
    json={
    "cedar_raw": cedar_policy,
    "schema_version": "2026-03-16",
    },
    ).json()
    version_id = version["id"]
    Example response
    {
    "id": "01JKXYZ789ABC456DEF123GH",
    "policy_id": "01JKXYZ456DEF789ABC123GH",
    "version": 1,
    "schema_version": "2026-03-16",
    "cedar_raw": "@id(\"require-token-credentials\")\nforbid (\n principal is Keycard::Application,\n action,\n resource\n) unless {\n principal has credential_type && principal.credential_type == \"token\"\n};",
    "content_sha256": "a3f5d8c9e2b1a4c7d6e5f8a9b2c1d4e7",
    "created_at": "2026-03-03T10:01:00Z",
    "archived_at": null
    }

    Save the id from the response. This is the policy_version_id you’ll reference in the policy set manifest.

    Example response
    {
    "id": "01JKXYZ789ABC456DEF123GH",
    "policy_id": "01JKXYZ456DEF789ABC123GH",
    "version": 1,
    "schema_version": "2026-03-16",
    "cedar_raw": "@id(\"require-token-credentials\")\nforbid (\n principal is Keycard::Application,\n action,\n resource\n) unless {\n principal has credential_type && principal.credential_type == Keycard::CredentialType::\"token\"\n};",
    "content_sha256": "a3f5d8c9e2b1a4c7d6e5f8a9b2c1d4e7",
    "created_at": "2026-03-03T10:01:00Z",
    "archived_at": null
    }

    Save the id from the response. This is the policy_version_id you’ll reference in the policy set manifest.

  5. Create a policy set

    Create a policy set that will serve as the deployment unit for your policies.

    policy_set = requests.post(
    f"{base}/zones/{zone_id}/policy-sets",
    headers=headers,
    json={
    "name": "custom-zone-policies",
    "scope_type": "zone",
    },
    ).json()
    policy_set_id = policy_set["id"]
    Example response
    {
    "id": "01JKXYZ123ABC789DEF456GH",
    "zone_id": "01JKXYZ789ABC456DEF123GH",
    "name": "custom-zone-policies",
    "scope_type": "zone",
    "owner_type": "customer",
    "created_at": "2026-03-03T10:02:00Z",
    "updated_at": "2026-03-03T10:02:00Z",
    "archived_at": null
    }

    Save the id from the response.

  6. Create a policy set version

    Create an immutable policy set version with a manifest that references your custom policy version.

    ps_version = requests.post(
    f"{base}/zones/{zone_id}/policy-sets/{policy_set_id}/versions",
    headers=headers,
    json={
    "manifest": {
    "entries": [
    {
    "policy_id": policy_id,
    "policy_version_id": version_id,
    }
    ]
    },
    "schema_version": "2026-03-16",
    },
    ).json()
    ps_version_id = ps_version["id"]
    Example response
    {
    "id": "01JKXYZ456DEF123ABC789GH",
    "policy_set_id": "01JKXYZ123ABC789DEF456GH",
    "version": 1,
    "schema_version": "2026-03-16",
    "manifest": {
    "entries": [
    {
    "policy_id": "01JKXYZ456DEF789ABC123GH",
    "policy_version_id": "01JKXYZ789ABC456DEF123GH"
    }
    ]
    },
    "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
    "created_at": "2026-03-03T10:03:00Z",
    "archived_at": null
    }

    Save the id from the response.

    Example response
    {
    "id": "01JKXYZ456DEF123ABC789GH",
    "policy_set_id": "01JKXYZ123ABC789DEF456GH",
    "version": 1,
    "schema_version": "2026-03-16",
    "manifest": {
    "entries": [
    {
    "policy_id": "01JKXYZ456DEF789ABC123GH",
    "policy_version_id": "01JKXYZ789ABC456DEF123GH"
    }
    ]
    },
    "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
    "created_at": "2026-03-03T10:03:00Z",
    "archived_at": null
    }

    Save the id from the response.

  7. Activate the policy set

    Bind the policy set version as the active policy set for your zone.

    response = requests.patch(
    f"{base}/zones/{zone_id}/policy-sets/{policy_set_id}/versions/{ps_version_id}",
    headers=headers,
    json={"active": True},
    ).json()
    Example response
    {
    "id": "01JKXYZ456DEF123ABC789GH",
    "policy_set_id": "01JKXYZ123ABC789DEF456GH",
    "version": 1,
    "schema_version": "2026-03-16",
    "manifest": {
    "entries": [
    {
    "policy_id": "01JKXYZ456DEF789ABC123GH",
    "policy_version_id": "01JKXYZ789ABC456DEF123GH"
    }
    ]
    },
    "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
    "active": true,
    "created_at": "2026-03-03T10:03:00Z",
    "archived_at": null
    }
    Example response
    {
    "id": "01JKXYZ456DEF123ABC789GH",
    "policy_set_id": "01JKXYZ123ABC789DEF456GH",
    "version": 1,
    "schema_version": "2026-03-16",
    "manifest": {
    "entries": [
    {
    "policy_id": "01JKXYZ456DEF789ABC123GH",
    "policy_version_id": "01JKXYZ789ABC456DEF123GH"
    }
    ]
    },
    "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
    "active": true,
    "created_at": "2026-03-03T10:03:00Z",
    "archived_at": null
    }
  8. Verify

    Confirm the policy set is active for your zone.

    policy_sets = requests.get(
    f"{base}/zones/{zone_id}/policy-sets",
    headers=headers,
    ).json()
    for ps in policy_sets["items"]:
    print(ps["name"], ps.get("active"), ps.get("mode"))
    Example response
    {
    "items": [
    {
    "id": "01JKXYZ123ABC789DEF456GH",
    "zone_id": "01JKXYZ789ABC456DEF123GH",
    "name": "custom-zone-policies",
    "scope_type": "zone",
    "owner_type": "customer",
    "active": true,
    "mode": "active",
    "created_at": "2026-03-03T10:02:00Z",
    "updated_at": "2026-03-03T10:03:00Z"
    }
    ]
    }

    You should see your policy set with "active": true and "mode": "active".

  9. Rollback

    To revert to the previous managed policy set, re-activate the platform-owned policy set version that was active before your custom set replaced it.

    response = requests.patch(
    f"{base}/zones/{zone_id}/policy-sets/{managed_ps_id}/versions/{managed_ps_version_id}",
    headers=headers,
    json={"active": True},
    ).json()
    Example response
    {
    "id": "01JKXYZ999AAA888BBB777CC",
    "policy_set_id": "01JKXYZ789DEF456ABC123GH",
    "version": 2,
    "schema_version": "2026-03-16",
    "active": true,
    "created_at": "2026-03-02T21:41:29Z",
    "archived_at": null
    }

    The managed policy set is now active again, restoring the previous authorization baseline.

    Example response
    {
    "id": "01JKXYZ999AAA888BBB777CC",
    "policy_set_id": "01JKXYZ789DEF456ABC123GH",
    "version": 2,
    "schema_version": "2026-03-16",
    "active": true,
    "created_at": "2026-03-02T21:41:29Z",
    "archived_at": null
    }

    The managed policy set is now active again, restoring the previous authorization baseline.

These examples show common policy patterns for real-world deployments. Each includes the Cedar policy, an explanation of when to use it, and API calls to create it.

Prevent shadow server access by requiring token-based credentials

This policy blocks applications that don’t use token-based credentials from accessing resources. It prevents shadow servers (services using static secrets instead of short-lived tokens) from obtaining upstream access through Keycard.

Applications configured with workload identity federation receive a credential_type of "token", meaning they authenticate with short-lived, verifiable tokens rather than long-lived secrets. This forbid policy denies access to any application that doesn’t meet that requirement.

The credential_type attribute is a Keycard::CredentialType enum entity. Other possible values are Keycard::CredentialType::"password" (client ID & secret), Keycard::CredentialType::"public-key" (key-based assertion), Keycard::CredentialType::"url" (URL-based identity), and Keycard::CredentialType::"public" (no secret). The attribute is optional and may be absent if the application’s credential type is not yet determined. See the entity properties reference for the full list.

import requests
base = "https://api.keycard.ai"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
policy = requests.post(
f"{base}/zones/{zone_id}/policies",
headers=headers,
json={
"name": "require-workload-identity",
"description": "Block applications without token-based credentials to prevent shadow server access",
},
).json()
cedar_policy = """\
@id("require-workload-identity")
forbid (
principal is Keycard::Application,
action,
resource
) unless {
principal has credential_type && principal.credential_type == Keycard::CredentialType::"token"
};"""
version = requests.post(
f"{base}/zones/{zone_id}/policies/{policy['id']}/versions",
headers=headers,
json={
"cedar_raw": cedar_policy,
"schema_version": "2026-03-16",
},
).json()
Example response
{
"id": "01JKXYZ000III111JJJ222KK",
"policy_id": "01JKXYZ999HHH000III111JJ",
"version": 1,
"schema_version": "2026-03-16",
"cedar_raw": "@id(\"require-workload-identity\")\nforbid (\n principal is Keycard::Application,\n action,\n resource\n) unless {\n principal has credential_type && principal.credential_type == Keycard::CredentialType::\"token\"\n};",
"content_sha256": "e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1",
"created_at": "2026-03-03T10:12:00Z",
"archived_at": null
}
Grant access based on upstream IdP groups

This policy maps IdP group membership to resource access in Keycard. You manage who gets access from your existing identity provider (Okta, Entra ID, Google Workspace) rather than duplicating access rules.

When a user authenticates through an upstream IdP, Keycard captures their group claims. This permit policy checks those claims at authorization time, granting access to users who belong to a specific IdP group. In this example, members of the “Engineering” group in Okta receive access to all resources.

import requests
base = "https://api.keycard.ai"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
policy = requests.post(
f"{base}/zones/{zone_id}/policies",
headers=headers,
json={
"name": "permit-idp-engineering-group",
"description": "Grant access to users in the Engineering group from the upstream IdP",
},
).json()
cedar_policy = """\
@id("permit-idp-engineering-group")
permit (
principal is Keycard::User,
action,
resource
) when {
context has subject_claims &&
context.subject_claims has groups &&
context.subject_claims.groups.contains("Engineering")
};"""
version = requests.post(
f"{base}/zones/{zone_id}/policies/{policy['id']}/versions",
headers=headers,
json={
"cedar_raw": cedar_policy,
"schema_version": "2026-03-16",
},
).json()
Example response
{
"id": "01JKXYZ111JJJ222KKK333LL",
"policy_id": "01JKXYZ000III111JJJ222KK",
"version": 1,
"schema_version": "2026-03-16",
"cedar_raw": "@id(\"permit-idp-engineering-group\")\npermit (\n principal is Keycard::User,\n action,\n resource\n) when {\n context has subject_claims &&\n context.subject_claims has groups &&\n context.subject_claims.groups.contains(\"Engineering\")\n};",
"content_sha256": "f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2",
"created_at": "2026-03-03T10:13:00Z",
"archived_at": null
}

Every policy mutation and authorization evaluation emits a structured audit event. These events give you a complete audit trail for compliance and debugging.

Event ActionTrigger
policy:createNew policy created
policy:updatePolicy metadata updated
policy:archivePolicy soft-deleted
policy_version:createNew immutable version created
policy_set:createNew policy set created
policy_set:updatePolicy set metadata updated
policy_set_version:createNew policy set version created
policy_set_version:activateEnable the policy set version
policy_version:archivePolicy version soft-deleted
policy_set:archivePolicy set soft-deleted
policy_set_version:archivePolicy set version soft-deleted
policy_set_version:checkAuthorization decision evaluated
policy_schema:set_defaultDefault schema changed for a zone

Keycard logs each authorization decision with:

  • request_id correlates the decision to the originating request
  • decision is the authorization outcome (allow or deny)
  • determining_policies lists the policy IDs that determined the outcome
  • policy_set_id identifies the active policy set that Keycard evaluated
  • policy_set_version_id identifies the specific version of the policy set that was evaluated
  • evaluation_status indicates whether all policies evaluated successfully (complete or partial)
  • diagnostics contains {policy_id, message} entries for evaluation errors or warnings
  • evaluated_at is the timestamp of when the evaluation occurred
  • manifest_sha is the SHA-256 hash of the policy set manifest for integrity verification

Audit events preserve privacy while maintaining a verifiable audit trail:

  • Keycard never logs Cedar source, only SHA-256 hashes of policy content
  • Entity attributes (email, name, scopes) never appear in plaintext
  • Keycard never logs JWT claims
  • Audit emission failures never block authorization decisions

When a policy blocks a resource during token exchange, Keycard responds in one of two ways depending on whether the blocked resource is the primary resource or a dependency.

Hard denialSoft denial (dependency)
What is blockedThe primary resource in the token requestA dependency resource (e.g., a secondary API the primary resource depends on)
HTTP responseaccess_denied error, no token issuedHTTP 200, token issued successfully
Signalerror and error_description fields in the response bodyThe denied resource is silently dropped from the target claim in the issued token
VisibilityImmediately obvious from the error responseRequires comparing requested resources against the token’s target claim

What you see in the error response (hard denial)

Section titled “What you see in the error response (hard denial)”

A hard policy denial from the token endpoint looks like this:

{
"error": "access_denied",
"error_description": "Access denied by policy. Policy set: ps_abc123. Policy set version: psv_def456. Determining policies: policy-forbid-external-calendar.",
"requestId": "req_a1b2c3"
}

A soft denial returns a normal success response. The only clue is the missing resource in the token:

{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600
}

Decode the access_token and inspect the target claim. If a resource you requested is missing, it was denied by policy.

When your agent requests multiple resources, compare what you asked for against what you received:

import jwt
token = jwt.decode(access_token, options={"verify_signature": False})
requested = ["https://api.google.com/calendar", "https://api.github.com"]
granted = token.get("target", [])
missing = [r for r in requested if r not in granted]
if missing:
print(f"Resources denied by policy: {missing}")
# Use the requestId from the token response to find
# policy_set_version:check audit events with denial details

MCP SDK: handling both hard and soft denials

Section titled “MCP SDK: handling both hard and soft denials”

If you are using the MCP SDK, hard policy denials surface through AccessContext errors. For soft denials, check which resources were actually granted:

# Hard denial — access_denied error
if access_context.has_errors():
errors = access_context.get_errors()
# errors contains the policy denial details
# Soft denial — check granted vs failed resources
successful = access_context.get_successful_resources()
failed = access_context.get_failed_resources()
if failed:
print(f"Resources denied by policy: {failed}")

See the MCP SDK documentation and Authorization for full details on handling access errors.

FieldWhat it tells you
errorAlways access_denied for policy denials
error_descriptionHuman-readable message containing the policy set ID, policy set version ID, and determining policy IDs
requestIdCorrelates this denial with policy_set_version:check audit events for the full evaluation context
ScenarioWhat you seeResolution
No matching permit (default deny)access_denied with no determining policies listedAdd a permit policy that covers the actor, action, and resource.
Explicit forbid matchedaccess_denied with determining policy IDs in the descriptionInspect the listed forbid policies in Console. A forbid always overrides a permit.
Partial evaluationAudit log shows evaluation_status: “partial” with diagnostics entriesCheck diagnostic messages for schema mismatches or malformed entity references. Fix the flagged policies.
No active policy set versionHTTP 422 from the management APINo policy set version is active in the zone. Activate a policy set version first.
Entity not foundHTTP 400 from the token endpointThe actor or resource in the request does not exist in the zone. Verify entity IDs.
Dependency denied by policyToken issued but resource missing from target claimCheck audit log with requestId for the dependency denial. Add a permit policy covering the dependency resource.
  1. Check the error response

    Look at the error_description from the token endpoint. If it lists determining policy IDs, those policies explicitly blocked the action. If no determining policies are listed, this is a default deny because no permit policy matched.

  2. If the token was issued but a resource is missing

    This is a soft dependency denial. The primary resource was allowed, but a dependency was denied by policy. Decode the access token and compare the target claim against the resources you requested. Use the requestId from the token response to find the specific policy_set_version:check audit event showing which dependency was denied and why.

  3. If no determining policies, review your permit policies

    No policy explicitly matched the request. Verify that a permit policy exists for the correct combination of actor, action, and resource. Check that the entity types and IDs match what the policy references.

  4. If determining policies are listed, inspect them in Console

    Those are forbid policies that blocked the action. Open them in Console to review their conditions. Remember that forbid always takes precedence over permit.

  5. Check the audit log for full evaluation details

    Filter audit logs by the requestId from the error response. The policy_set_version:check event contains the complete evaluation context:

    • evaluation_status shows whether all policies evaluated (complete) or some failed (partial)
    • diagnostics contains {policy_id, message} entries for any evaluation errors (schema mismatches, etc.)
    • policy_set_version_id is the exact version that was evaluated
    • manifest_sha is the integrity hash of the policy set manifest
  6. If evaluation was partial, fix the flagged policies

    When evaluation_status is partial, the diagnostics array explains what went wrong. Common causes include schema mismatches, missing entity attributes, or malformed policy syntax. Fix the flagged policies and re-evaluate.