Configuring per-resource authorization

Per-resource authorization controls access to individual groups and artifacts, for example "Alice can write artifacts in group team-a and read shared, but cannot see `team-b`". Access is defined in a grants file and applies to every Apicurio Registry API, including search and list results.

Overview

Apicurio Registry applies two layers of authorization:

  • Role-based access control (RBAC): coarse-grained global roles (sr-readonly, sr-developer, sr-admin), typically assigned by an identity provider such as Keycloak.

  • Per-resource authorization: fine-grained access to individual groups and artifacts, defined in a grants file and evaluated after RBAC.

A request must pass both layers. Grants restrict within what RBAC allows; they never grant more than the user’s role permits.

Grants are evaluated by an implementation of the Kroxylicious Authorizer API, the authorization contract shared with Kroxylicious and StreamsHub, so the same grants file can describe access across those systems.

How it works

  • Point access (reading, writing, or deleting a specific group or artifact): the user’s identity and roles are matched against the grants for the addressed resource.

  • Search and lists: the user’s grants are translated into the storage query, so results contain exactly the resources the user can read, and page sizes and total counts are accurate. A resource never appears in search results unless direct read access to it would also be allowed.

Configuration

Per-resource authorization requires authentication (OIDC, HTTP basic, or proxy header authentication) to be enabled.

Property Default Description

apicurio.auth.resource-based-authorization.enabled

false

Enables per-resource authorization.

apicurio.auth.resource-based-authorization.grants.path

(none)

Path to the JSON grants file. Required when per-resource authorization is enabled. Apicurio Registry fails to start if the file is missing or invalid.

apicurio.auth.resource-based-authorization.grants.reload-every

5s

How often to check the grants file for changes. Must be a duration, for example 5s or 1m.

apicurio.auth.resource-based-authorization.grants.reload-enabled

true

Reloads the grants file when it changes. Set to false to load it only at startup.

apicurio.auth.resource-based-authorization.owner-bypass.enabled

true

Lets the recorded owner of an artifact or group access it regardless of grants.

Example configuration
environment:
  apicurio.auth.resource-based-authorization.enabled: "true"
  apicurio.auth.resource-based-authorization.grants.path: "/opt/apicurio/authz/grants.json"

In Kubernetes, store the grants file in a ConfigMap and mount it as a volume. Mount the ConfigMap as a directory rather than with subPath, because subPath mounts do not receive ConfigMap updates.

A complete Docker Compose example with Keycloak, including a script that verifies the expected access, is available in distro/docker-compose/in-memory-with-authz-grants/.

Grants file format

The grants file is a JSON object with an optional config section and a required grants array.

{
  "config": {
    "admin_roles": ["sr-admin"]
  },
  "grants": [
    {
      "principal": "alice",
      "operation": "write",
      "resource_type": "artifact",
      "resource_pattern_type": "prefix",
      "resource_pattern": "team-a/"
    },
    {
      "principal_role": "sr-developer",
      "operation": "read",
      "resource_type": "artifact",
      "resource_pattern_type": "prefix",
      "resource_pattern": "public/"
    }
  ]
}

config.admin_roles lists roles whose users have full access to all resources; no grants are evaluated for them. RBAC administrators (the sr-admin role, or the role configured as the RBAC admin role) always bypass grants, so admin_roles is only needed for additional roles. It must be an array of non-empty strings; any other value rejects the file, like an invalid grant.

Grant fields

Field Required Description

principal

One of principal or principal_role

User name to match, from the authenticated identity. Never matches unauthenticated requests.

principal_role

One of principal or principal_role

Role to match. Any user with this role receives the grant.

operation

Yes

read, write, or admin.

resource_type

Yes

artifact or group.

resource_pattern_type

No

prefix matches names that start with the pattern. exact, or omitting the field, matches the name exactly.

resource_pattern

Yes

The name or prefix to match. * matches every resource of the type, whatever the pattern type.

deny

No

true makes the grant deny the matched access. Deny rules take precedence over allow rules. Default: false.

Every grant must be valid: exactly one of principal and principal_role, a known operation and resource_pattern_type, and a boolean deny. A file with an invalid grant is rejected as a whole, because skipping an invalid deny rule would grant access you meant to forbid: Apicurio Registry does not start, or keeps the previous grants on reload.

Resource names

  • Artifacts: {groupId}/{artifactId}, for example team-a/my-schema. Artifacts in the default group are named default/{artifactId}.

  • Groups: the group ID, for example team-a. The default group is named default.

In artifact names, the characters % and / in the group ID are written as %25 and %2F, so the first / always separates the group from the artifact ID. For example, artifact x in group team-a/sub is named team-a%2Fsub/x, while artifact sub/x in group team-a is named team-a/sub/x. Artifact IDs and group names are not escaped.

Patterns are matched against the complete name. For example, the prefix team-a/ matches every artifact in group team-a (but not in group team-a/sub), and the prefix team-a/internal- matches only the artifacts in team-a whose ID starts with internal-.

Matching is case-sensitive on every storage variant, in direct access and in search results. Some databases, such as MySQL and SQL Server, look up group and artifact IDs case-insensitively, so a request for TEAM-A/My-Schema can address the stored artifact team-a/my-schema. Such a request is authorized against the stored IDs, so changing the case of an ID never gets around a grant or deny rule.

Operations

admin implies write, and write implies read. The operation required by each API operation matches its RBAC level: reads require read, and changes, including deletes, require write. A few operations require admin, such as dropping Iceberg namespaces, tables, and views.

Group grants and artifact grants

  • Group grants control operations on the group itself: reading group metadata, listing artifacts in it, and creating, updating, or deleting the group.

  • Artifact grants control operations on artifacts and their versions, content, comments, and references.

Some operations need both:

  • Creating an artifact requires write on the group and on the new artifact’s name, so a deny on an artifact name also prevents creating it. This applies to every API that creates artifacts, including registering a new subject with the Confluent compatibility API, publishing a new MCP server, and renaming an Iceberg table.

  • Deleting a group, or all artifacts in a group, requires write on the group and on every artifact in it.

Most deployments need both. For example, to give a user full access to team-a:

{"principal": "alice", "operation": "write", "resource_type": "group", "resource_pattern_type": "exact", "resource_pattern": "team-a"},
{"principal": "alice", "operation": "write", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "team-a/"}

Deny rules

Deny rules use the same fields with "deny": true. If a deny rule matches, access is denied even if an allow rule also matches. Deny rules apply to search results as well as direct access.

{"principal": "alice", "operation": "write", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "team-a/"},
{"principal": "alice", "operation": "read", "resource_type": "artifact", "resource_pattern_type": "exact", "resource_pattern": "team-a/secret-schema", "deny": true}

Alice can read and write all artifacts in team-a except team-a/secret-schema.

A deny rule denies its operation and every operation that implies it:

  • Denying read blocks all access, and hides the resource from search results.

  • Denying write blocks changes but keeps read access, which makes the resource read-only for the user.

  • Denying admin blocks only operations that require admin.

Reloading the grants file

Apicurio Registry checks the grants file for changes at the interval set by apicurio.auth.resource-based-authorization.grants.reload-every and applies changes without a restart. If the changed file is not valid JSON, is not an object, or has no grants array, Apicurio Registry logs an error and keeps using the previously loaded grants.

References and generated artifacts

Some operations read or write artifacts other than the one addressed by the request. These artifacts are authorized as if the caller accessed them directly:

  • Resolving references: dereferencing content (references=DEREFERENCE, or dereference=true in the v2 API), returning canonical content, exporting a Protobuf version as a ZIP file, and the Confluent format=resolved option all include the content of referenced artifacts. They require read on every referenced artifact, including references of references; otherwise the request fails with 403. Without these options, content is returned as stored, with its references unresolved.

  • Registering references: creating an artifact or version whose content references other artifacts requires read on every referenced artifact, through any API, including draft versions.

  • Listing references: inbound reference lists, including the Confluent referencedby endpoint, contain only readable artifacts. The outbound reference graph shows that a reference exists but hides the metadata of unreadable targets.

  • Generated artifacts: some artifact types make Apicurio Registry write additional artifacts in the same group. Embedded schemas of MODEL_SCHEMA and PROMPT_TEMPLATE artifacts are registered as {artifactId}-input-schema and {artifactId}-output-schema, and an OpenAPI artifact with an x-agent-card extension generates {artifactId}-agent-card. Writing the artifact requires write on each artifact it would generate and, if a generated artifact does not exist yet, write on the group, as for any artifact creation. Embedded schemas are also subject to owner-only authorization. These checks, and the reference checks above, run before anything is written, so a request rejected with 403 leaves no generated artifacts or versions behind.

Covered APIs

Per-resource authorization applies to every API that addresses or lists groups and artifacts:

API Behavior

Core REST API v3 and v2

Group and artifact operations are checked against the addressed resource. Searches and lists, including group lists, artifact and version searches, and inbound references, return only readable resources. Creating a group requires write on that group.

Content and global IDs (/ids/…​)

A global ID resolves to its artifact. Content can be shared by several artifacts, so a content ID or hash is readable if at least one artifact that uses it is readable.

Confluent Schema Registry compatibility API

A subject is an artifact in the group given by the X-Registry-GroupId header (or, with apicurio.ccompat.group-concat.enabled, in the group encoded in the subject name). Schema IDs resolve as content IDs, or as global IDs in legacy ID mode. Subject and schema lists, and referencedby lists, contain only readable subjects. Registering a schema under a new subject also requires write on its group.

Iceberg REST catalog

A namespace is a group and a table or view is an artifact in it. The catalog prefix in the URL does not affect authorization. Renaming requires write on the source, on the destination, and on the destination namespace.

MCP Registry API

A server name namespace/server is the artifact namespace/server. Publishing requires write on it, and publishing a new server also requires write on the namespace group. Server lists contain only readable servers.

Discovery endpoints (/.well-known/…​)

Agent, AI catalog, and MCP tool listings contain only readable artifacts.

Common use cases

Multi-team isolation

Each team sees and modifies only its own schemas:

{
  "config": { "admin_roles": ["sr-admin"] },
  "grants": [
    {"principal_role": "team-a-developers", "operation": "write", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "team-a/"},
    {"principal_role": "team-a-developers", "operation": "write", "resource_type": "group", "resource_pattern_type": "exact", "resource_pattern": "team-a"},
    {"principal_role": "team-b-developers", "operation": "write", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "team-b/"},
    {"principal_role": "team-b-developers", "operation": "write", "resource_type": "group", "resource_pattern_type": "exact", "resource_pattern": "team-b"}
  ]
}

Shared schemas with selective write access

All developers read shared schemas; only the platform team modifies them:

{"principal_role": "sr-developer", "operation": "read", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "shared/"},
{"principal_role": "sr-developer", "operation": "read", "resource_type": "group", "resource_pattern_type": "exact", "resource_pattern": "shared"},
{"principal_role": "platform-team", "operation": "write", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "shared/"},
{"principal_role": "platform-team", "operation": "write", "resource_type": "group", "resource_pattern_type": "exact", "resource_pattern": "shared"}

Read-only service account

{"principal": "monitoring-service", "operation": "read", "resource_type": "artifact", "resource_pattern": "*"},
{"principal": "monitoring-service", "operation": "read", "resource_type": "group", "resource_pattern": "*"}

CI/CD pipeline with scoped write access

{"principal": "ci-pipeline", "operation": "write", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "production/"},
{"principal": "ci-pipeline", "operation": "write", "resource_type": "group", "resource_pattern_type": "exact", "resource_pattern": "production"}

Group access with excluded artifacts

{"principal": "contractor", "operation": "read", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "team-a/"},
{"principal": "contractor", "operation": "read", "resource_type": "group", "resource_pattern_type": "exact", "resource_pattern": "team-a"},
{"principal": "contractor", "operation": "read", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "team-a/internal-", "deny": true}

Interaction with other authorization features

Requests are authorized in this order:

Authentication
  → Admin override                     (bypasses everything below)
  → RBAC                                (role check; skipped for reads when anonymous or
                                         authenticated read access applies)
  → Owner-only authorization (OBAC)     (when enabled)
  → Per-resource authorization          (RBAC admins, and by default resource owners, bypass it)

RBAC

RBAC administrators bypass per-resource authorization. A user with the sr-readonly role can never write, whatever their grants. To control write access with grants, give users the sr-developer role and use grants to choose which resources they can change.

Resource owners

By default, the recorded owner of an artifact or group has access to it whatever the grants, whether or not owner-only authorization (apicurio.auth.owner-only-authorization) is enabled. Set apicurio.auth.resource-based-authorization.owner-bypass.enabled to false to make grants the only per-resource authority. Owners also see their own resources in search results and can read content used by their artifacts. With the Elasticsearch search index, searches that require the index (content and structure searches) include at most 1000 of the caller’s owned artifacts that are not otherwise covered by grants. A resource without a recorded owner does not grant this exception to anyone.

When owner-only authorization is enabled, it applies as well to resources named in a request body, such as Iceberg rename targets and published MCP servers: only the owner can change them, even if other users have matching grants.

The creator of an artifact becomes its owner, but creating it requires write on both the group and the artifact name, so the owner bypass cannot be used to get around a deny on that name.

Anonymous and authenticated read access

These settings skip the RBAC check for reads; per-resource authorization still applies:

  • With apicurio.auth.authenticated-read-access.enabled, any authenticated user can read, even without a registry role, but only the resources their grants allow.

  • With apicurio.auth.anonymous-read-access.enabled, unauthenticated callers get the role $anonymous. They can read only what grants allow to that role, for example:

    {"principal_role": "$anonymous", "operation": "read", "resource_type": "artifact", "resource_pattern_type": "prefix", "resource_pattern": "public/"}

    Other reads by unauthenticated callers are answered with 401.

Registry-wide reports

Reports that cover the whole registry and cannot be filtered per resource, such as the GitOps status and validation tasks, are available only to callers who are not restricted by grants (administrators).

Admin override and trusted proxy authorization

Users identified as administrators by the admin override (apicurio.auth.admin-override.*) bypass per-resource authorization. When apicurio.authn.proxy-header.trust-proxy-authorization is true, requests authenticated by proxy headers bypass all local authorization, including grants.

Monitoring and audit logging

Metrics

When OpenTelemetry metrics are enabled, Apicurio Registry exports the counter apicurio.authz.decisions, with the attributes:

Attribute Values

apicurio.registry.result

allow or deny

apicurio.registry.entity_type

artifact, group, or content

apicurio.registry.operation

read, write, or admin

Audit log

Denied point-access decisions are logged at INFO level to the io.apicurio.registry.audit.authz logger:

authz.denied user="alice" operation="write" resource_type="artifact" resource="team-b/secret-schema"

To disable these messages, set quarkus.log.category."io.apicurio.registry.audit.authz".level=OFF.

Troubleshooting

A user is denied access although a grant matches

Check, in order:

  1. RBAC: does the user’s role allow the operation? sr-readonly users cannot write.

  2. Operation: does the grant’s operation cover the request? Changes and deletes require write.

  3. Resource type: operations on a group itself, such as listing its artifacts or creating an artifact in it, need a group grant.

  4. Deny rules: does a deny rule match? Deny rules take precedence.

  5. Referenced and generated artifacts: dereferencing, or registering content with references, requires read on every referenced artifact; writing an artifact that generates other artifacts requires write on them.

  6. Resource name: artifacts are named {groupId}/{artifactId}; the default group is default. For the Confluent compatibility API, check which group the request addresses.

  7. Audit log: the authz.denied message shows the exact resource name and operation that were evaluated.

A user can see more than expected

Check whether the user is an RBAC administrator or has a role listed in config.admin_roles, whether the user owns the resources (see owner-bypass.enabled), and, for unauthenticated callers, what the $anonymous role is granted.

Apicurio Registry does not start

Apicurio Registry fails to start when per-resource authorization is enabled and the grants file path is not set, the file does not exist, or the file is not valid grants data. The startup error names the file and the problem.

Grants file changes do not take effect

  1. Check that apicurio.auth.resource-based-authorization.grants.reload-enabled is not false.

  2. Check the logs for Grants data reloaded successfully or for a reload error. If the new file is invalid, the previous grants stay in effect.

  3. In Kubernetes, check that the ConfigMap is not mounted with subPath.