include::{mod-loc}shared/all-attributes.adoc[]

[id="configuring-registry-security_{context}"]
:_mod-docs-content-type: ASSEMBLY
= Configuring {registry} security options

[role="_abstract"]
You can configure security options for {registry}, including authentication in {keycloak}, Microsoft Azure Active Directory, or Dex, and role-based authorization.

* xref:registry-security-keycloak_{context}[]
* xref:registry-security-azure_{context}[]
* xref:registry-security-dex_{context}[]
* xref:registry-security-settings_{context}[]
* xref:registry-cors-configuration_{context}[]

NOTE: For a list of all available configuration options, see {registry-config-reference}.


// proc-registry-security-keycloak
[id="registry-security-keycloak_{context}"]
:_mod-docs-content-type: PROCEDURE
== Configuring {registry} authentication and authorization with {keycloak}

[role="_abstract"]
You can manually configure authentication and authorization options for {registry} and {keycloak}.

[NOTE]
====
Alternatively, for details on how to configure these settings automatically, 
ifdef::apicurio-registry[]
see the link:https://www.apicur.io/registry/docs/apicurio-registry-operator/{operator-version}/assembly-registry-maintenance.html#registry-security-keycloak[{operator} documentation].
endif::[]
ifdef::rh-service-registry[]
see xref:registry-security-keycloak[]. 
endif::[]
====

The {registry} web console and core REST API support authentication in {keycloak} based on OAuth and OpenID Connect (OIDC). The same {keycloak} realm and users are federated across the {registry} web console and core REST API by using OpenID Connect so that you only require one set of credentials.

{registry} provides role-based authorization for default admin, write, and read-only user roles. {registry} provides content-based authorization at the schema or API level, where only the creator of the registry artifact can update or delete it. {registry} authentication and authorization settings are disabled by default.

.Prerequisites
* {keycloak} is installed and running. For more details, see the 
ifdef::apicurio-registry[]
link:https://www.keycloak.org/documentation[{keycloak} user documentation]. 
endif::[]
ifdef::rh-service-registry[]
link:https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/{keycloak-version}[{keycloak} user documentation].
endif::[]
* {registry} is installed and running.

.Procedure

. In the {keycloak} Admin Console, create a {keycloak} realm for {registry}. By default, {registry} expects a realm name of `registry`. For details on creating realms, see the 
ifdef::apicurio-registry[]
link:https://www.keycloak.org/documentation[{keycloak} user documentation]. 
endif::[]
ifdef::rh-service-registry[]
the link:https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/{keycloak-version}[{keycloak} user documentation].
endif::[]

. Create a {keycloak} client for the {registry} API. By default, {registry} expects the following settings:
** *Client ID*: `registry-api`
** *Client Protocol*: `openid-connect`
** *Access Type*: `bearer-only`
+
You can use the defaults for the other client settings.
+
NOTE: If you use {keycloak} service accounts, the client *Access Type* must be `confidential` instead of `bearer-only`.

. Create a {keycloak} client for the {registry} web console. By default, {registry} expects the following settings:
** *Client ID*: `apicurio-registry`
** *Client Protocol*: `openid-connect`
** *Access Type*: `public`
** *Valid Redirect URLs*: `http://my-registry-url:8080/*`
** *Web Origins*: `+`
+
You can use the defaults for the other client settings.

. In your {registry} deployment on OpenShift, set the following {registry} environment variables to configure authentication by using {keycloak}:
+
.Configuration for {registry} authentication with {keycloak}
[.table-expandable,width="100%",cols="5,6,2,4",options="header"]
|===
|Environment variable
|Description
|Type
|Default
|`QUARKUS_OIDC_TENANT_ENABLED`
|Enables authentication for {registry}. When set to `true`, the environment variables that follow are required for authentication by using {keycloak}.
|String
|`false`
|`QUARKUS_OIDC_AUTH_SERVER_URL`
|The URL of the {keycloak} authentication server. For example, `http://localhost:8080`.
|String
|-
|`QUARKUS_OIDC_CLIENT_ID`
|The client ID for the {registry} REST API.
|String
|`registry-api`
|`APICURIO_UI_AUTH_OIDC_CLIENT_ID`
|The client ID for the {registry} web console.
|String
|`apicurio-registry`
|===
+
TIP: For an example of setting environment variables on OpenShift, see xref:configuring-liveness-readiness-probes_{context}[].

. Set the following option to `true` to enable {registry} user roles in {keycloak}:
+
.Configuration for {registry} role-based authorization
[%header,cols="2,2,1,1"]
|===
|Environment variable
|Java system property
|Type
|Default value
|`APICURIO_AUTH_ROLE_BASED_AUTHORIZATION`
|`apicurio.auth.role-based-authorization`
|Boolean
|`false`
|===

. Assign {registry} users to at least one of the following default user roles in your {keycloak} realm:
+
.Default user roles for registry authentication and authorization
[.table-expandable,width="100%",cols="2,2,2,2,4",options="header"]
|===
|Role
|Read artifacts
|Write artifacts
|Global rules
|Summary
|`sr-admin`
|Yes
|Yes
|Yes
|Full access to all create, read, update, and delete operations.
|`sr-developer`
|Yes
|Yes
|No
|Access to create, read, update, and delete operations, except configuring global rules. This role can configure artifact-specific rules.
|`sr-readonly`
|Yes
|No
|No
|Access to read and search operations only. This role cannot configure any rules.
|===

. Set the following to `true` to enable owner-only authorization for updates to schema and API artifacts in {registry}:
+
.Configuration for owner-only authorization
[.table-expandable,width="100%",cols="2,2,1,1",options="header"]
|===
|Environment variable
|Java system property
|Type
|Default value
|`APICURIO_AUTH_OWNER_ONLY_AUTHORIZATION_LIMIT_GROUP_ACCESS`
|`apicurio.auth.owner-only-authorization.limit-group-access`
|Boolean
|`false`
|===


[role="_additional-resources"]
.Additional resources
* xref:registry-security-settings_{context}[]
* https://github.com/Apicurio/apicurio-registry/tree/{registry-version}.x/distro/docker-compose[Docker Compose example of Apicurio Registry with Keycloak]
ifdef::apicurio-registry[]
* link:https://www.keycloak.org/documentation[Keycloak documentation]
endif::[]
ifdef::rh-service-registry[]
* link:https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/{keycloak-version}/[{keycloak} documentation]
endif::[]



// proc-registry-security-azure
[id="registry-security-azure_{context}"]
:_mod-docs-content-type: PROCEDURE
== Configuring {registry} authentication and authorization with Microsoft Azure Active Directory

[role="_abstract"]
You can manually configure authentication and authorization options for {registry} and Microsoft Azure Active Directory (Azure AD).

The {registry} web console and core REST API support authentication in Azure AD based on OpenID Connect (OIDC) and the OAuth Authorization Code Flow. {registry} provides role-based authorization for default admin, write, and read-only user roles. {registry} authentication and authorization settings are disabled by default.

To secure {registry} with Azure AD, you require a valid directory in Azure AD with specific configuration. You must register the {registry} application in the Azure AD portal with recommended settings and configure environment variables in {registry}.

.Prerequisites
* Azure AD is installed and running. For more details, see the link:https://learn.microsoft.com/en-us/azure[Microsoft Azure AD user documentation].
* {registry} is installed and running.

.Procedure

. Log in to the Azure AD portal by using your email address or GitHub account.

. In the navigation menu, select *Manage > App registrations > New registration*, and complete the following settings:
+
** *Name*: Enter your application name. For example: `apicurio-registry-example`
** *Supported account types*: Click *Accounts in any organizational directory*.
** *Redirect URI*: Select *Single-page application* from the list, and enter your {registry} web console application host. For example: `https://test-registry.com/ui/`
+
IMPORTANT: You must register your {registry} application host as a *Redirect URI*. When logging in, users are redirected from {registry} to Azure AD for authentication, and you want to send them back to your application afterward. Azure AD does not allow any redirect URLs that are not registered.

. Click *Register*. You can view your app registration details by selecting *Manage > App registrations* > *apicurio-registry-example*.

. Select *Manage > Authentication* and verify that the application has your redirect URLs and tokens configured as follows:
** *Redirect URIs*: For example: `https://test-registry.com/ui/`
** *Implicit grant and hybrid flows*: Click *ID tokens (used for implicit and hybrid flows)*

. Select *Azure AD > Admin > App registrations > Your app > Application (client) ID*. For example: `123456a7-b8c9-012d-e3f4-5fg67h8i901`

. Select *Azure AD > Admin > App registrations > Your app > Directory (tenant) ID*. For example: `https://login.microsoftonline.com/1a2bc34d-567e-89f1-g0hi-1j2kl3m4no56/v2.0`

. In {registry}, configure the following environment variables with your Azure AD settings:
+
.Configuration for Azure AD settings in {registry}
[.table-expandable,width="100%",cols="6,4,6",options="header"]
|===
|Environment variable
|Description
|Setting
|`QUARKUS_OIDC_CLIENT_ID`
|The client application ID for the {registry} REST API
|Your Azure AD Application (client) ID obtained in step 5. For example: `123456a7-b8c9-012d-e3f4-5fg67h8i901`
|`APICURIO_UI_AUTH_OIDC_CLIENT_ID`
|The client application ID for the {registry} web console.
|Your Azure AD Application (client) ID obtained in step 5. For example: `123456a7-b8c9-012d-e3f4-5fg67h8i901`
|`QUARKUS_OIDC_AUTH_SERVER_URL`
|The URL for authentication in Azure AD.
|Your Azure AD Application (tenant) ID obtained in step 6. For example: `https://login.microsoftonline.com/1a2bc34d-567e-89f1-g0hi-1j2kl3m4no56/v2.0`.
|===

. In {registry}, configure the following environment variables for {registry}-specific settings:
+
.Configuration for {registry}-specific settings
[.table-expandable,width="100%",cols="6,5,4",options="header"]
|===
|Environment variable
|Description
|Setting
|`QUARKUS_OIDC_TENANT_ENABLED`
|Enables authentication for {registry}.
|`true`
|`QUARKUS_HTTP_CORS_ORIGINS`
|The host for your {registry} deployment for cross-origin resource sharing (CORS).
|For example: `https://test-registry.com`
|`APICURIO_UI_AUTH_OIDC_REDIRECT_URI`
|The host for your {registry} web console.
|For example: `https://test-registry.com/ui`
|`APICURIO_AUTH_ROLE_BASED_AUTHORIZATION`
|Enables role-based authorization in {registry}.
|`true`
|`QUARKUS_OIDC_ROLES_ROLE_CLAIM_PATH`
|The name of the claim in which Azure AD stores roles.
|`roles`
|===
+
NOTE: When you enable roles in {registry}, you must also create the same roles in Azure AD as application roles. The default roles expected by {registry} are `sr-admin`, `sr-developer`, and `sr-readonly`.


. In {registry} UI, configure the following environment variables:

[.table-expandable,width="100%",cols="6,5,4",options="header"]
|===
|Environment variable
|Description
|Setting
|`REGISTRY_AUTH_TYPE`
|
|`oidc`
|`REGISTRY_AUTH_TOKEN_TYPE`
|The token type to use. Azure AD has non-standard access tokens, so you must use ID tokens.
|`id`
|===

[role="_additional-resources"]
.Additional resources
* xref:registry-security-settings_{context}[]
* link:https://learn.microsoft.com/en-us/azure[Microsoft Azure AD user documentation]



// ref-registry-security-azure-role-mappings
[id="registry-security-azure-role-mappings_{context}"]
:_mod-docs-content-type: REFERENCE
== Multiple role mappings for Azure Entra ID

[role="_abstract"]
When you use Azure Entra ID (formerly Azure AD), you might need to map multiple Azure AD groups or app roles to a single {registry} role. Use multiple role mappings when:

* Interactive users authenticate by using Azure AD groups
* Service principals authenticate by using app roles
* Both must grant the same {registry} permissions

To configure multiple role mappings, use comma-separated values:

[source,properties]
----
# Map multiple Azure AD groups/roles to registry roles
APICURIO_AUTH_ROLES_ADMIN=sr-admin,12345678-aaaa-bbbb-cccc-ddddeeeefffff,AppRole.Admin
APICURIO_AUTH_ROLES_DEVELOPER=sr-developer,87654321-aaaa-bbbb-cccc-ddddeeeefffff,AppRole.Developer
APICURIO_AUTH_ROLES_READONLY=sr-readonly,abcd1234-aaaa-bbbb-cccc-ddddeeeefffff,AppRole.Reader
----

With this configuration, a user with any of the listed roles or group memberships receives the corresponding {registry} authorization level.


// ref-registry-security-azure-oauth-scopes
[id="registry-security-azure-oauth-scopes_{context}"]
:_mod-docs-content-type: REFERENCE
== Client-specific OAuth scopes for Azure Entra ID

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
Azure Entra ID requires that each app registration uses a unique scope. When you have multiple app registrations (for example, one for developers and one for read-only access), you can configure client-specific scopes:

[source,properties]
----
# Default scope for all clients (fallback)
APICURIO_AUTHN_BASIC_SCOPE=api://default-app-id/.default

# Client-specific scope overrides
# Replace dots in client IDs with underscores for environment variables
APICURIO_AUTHN_BASIC_SCOPE_SR_DEVELOPER_CLIENT=api://developer-app-id/.default
APICURIO_AUTHN_BASIC_SCOPE_SR_READONLY_CLIENT=api://readonly-app-id/.default
----

When a client authenticates, {registry} does the following:

1. Check for a client-specific scope configuration matching the client ID
2. If not found, use the default scope
3. If no scope is configured, proceed without a scope parameter

NOTE: You can specify multiple scopes by using comma separation. The scopes are converted to space-separated format per the OAuth2 specification.



// proc-registry-security-dex
[id="registry-security-dex_{context}"]
:_mod-docs-content-type: PROCEDURE
== Configuring {registry} authentication and authorization with Dex

[role="_abstract"]
You can configure authentication and authorization for {registry} by using link:https://dexidp.io/[Dex] as a federated OpenID Connect (OIDC) identity provider.

Dex acts as an OIDC-compliant bridge in front of identity providers that do not fully implement the OIDC specification. Use Dex when your platform uses an OAuth server that does not expose standard OIDC endpoints, such as OpenShift's built-in OAuth server, LDAP directories, or SAML-based providers. Dex has link:https://dexidp.io/docs/connectors/[connectors] for 15+ identity providers including OpenShift, LDAP, SAML, GitHub, GitLab, Google, and Microsoft.

Without an OIDC-compliant provider, the following {registry} features do not work:

.Features requiring OIDC compliance
[.table-expandable,width="100%",cols="3,3,6",options="header"]
|===
|Feature
|Without OIDC bridge
|Why it breaks

|UI login flow
|Not available
|The {registry} web console uses `oidc-client-ts`, which requires OIDC discovery (`/.well-known/openid-configuration`).

|Principal identity
|Not available
|The username cannot be extracted because it might be nested in a non-standard location in the token. {registry} expects flat top-level claims.

|Owner-based authorization
|Not available
|Depends on principal identity, which is empty when the provider does not expose standard claims.
|===

By deploying Dex between {registry} and your identity provider, all three features work because Dex exposes standard OIDC discovery, issues JWTs with standard claims (`sub`, `name`, `email`, `groups`), and provides a standard JWKS endpoint.

.Prerequisites
* A {kubernetes} cluster with Helm 3.x installed.
* {registry} is installed and running.
* Access to the upstream identity provider you want to integrate (for example, OpenShift, LDAP, or GitHub).

.Procedure

. Deploy Dex on your cluster by using Helm:
+
[source,bash]
----
helm repo add dex https://charts.dexidp.io
helm repo update
{kubernetes-client} create namespace dex
----

. Create a Helm values file named `dex-values.yaml` with the following content. Replace the placeholder values with your actual configuration.
+
{registry} needs two OAuth clients: a public client for the browser-based UI (which cannot hold a secret) and a confidential client for the backend and CLI. You must also configure `allowedOrigins` so the UI can fetch the Dex OIDC discovery endpoint by using CORS.
+
[source,yaml]
----
config:
  issuer: https://dex.<cluster-domain>

  storage:
    type: kubernetes
    config:
      inCluster: true

  web:
    http: 0.0.0.0:5556
    # CORS: Required so the Apicurio UI (oidc-client-ts) can fetch
    # /.well-known/openid-configuration from a different origin.
    allowedOrigins:
      - "https://<registry-ui-route>"

  oauth2:
    skipApprovalScreen: true
    responseTypes: [code, token, id_token]

  # Two clients are needed:
  # - A PUBLIC client for the browser UI (oidc-client-ts cannot hold secrets)
  # - A CONFIDENTIAL client for CLI/API token exchange
  staticClients:
    - id: apicurio-registry-ui
      name: Apicurio Registry UI
      public: true
      redirectURIs:
        - "https://<registry-ui-route>/"
        - "https://<registry-ui-route>/dashboard"
    - id: apicurio-registry
      name: Apicurio Registry API
      secret: <GENERATE_A_SECURE_SECRET>
      redirectURIs:
        - "https://<registry-ui-route>/"
        - "https://<registry-app-route>/"

  connectors: []  # Configure in the next step

ingress:
  enabled: true
  hosts:
    - host: dex.<cluster-domain>
      paths:
        - path: /
          pathType: Prefix
----
+
IMPORTANT: The UI client must be `public: true` because the browser-based authorization code flow with PKCE cannot securely store a client secret. If you use a single confidential client for both UI and backend, the UI login fails with `invalid_client` errors. The `/dashboard` redirect URI is needed because the {registry} UI redirects there after login.
+
NOTE: When generating the client secret, avoid special characters such as `+`, `/`, or `=` that can cause URL encoding mismatches. Use `openssl rand -base64 32 | tr -d '+/=' | head -c 32` to generate a safe secret.
+
ifdef::apicurio-registry[]
On OpenShift, create a Route instead of an Ingress:
+
[source,bash]
----
oc create route edge dex --service=dex --port=5556 --hostname=dex.<cluster-domain> --namespace=dex
----
endif::[]

. Install Dex by using Helm:
+
[source,bash]
----
helm install dex dex/dex -n dex -f dex-values.yaml
----

. Verify that OIDC discovery is available:
+
[source,bash]
----
curl -sk https://dex.<cluster-domain>/.well-known/openid-configuration | jq .issuer
# Expected: "https://dex.<cluster-domain>"
----

. Configure a connector for your upstream identity provider. Add the connector configuration to the `connectors` section in your `dex-values.yaml` file. See xref:registry-security-dex-connectors_{context}[Dex connector examples] for configuration examples.

. Upgrade the Dex Helm release after updating the connector configuration:
+
[source,bash]
----
helm upgrade dex dex/dex -n dex -f dex-values.yaml
----

. In your {registry} deployment, set the following environment variables to configure authentication by using Dex:
+
.Configuration for {registry} authentication with Dex
[.table-expandable,width="100%",cols="5,6,2,4",options="header"]
|===
|Environment variable
|Description
|Type
|Default

|`QUARKUS_OIDC_TENANT_ENABLED`
|Enables authentication for {registry}. When set to `true`, the environment variables that follow are required.
|String
|`false`

|`QUARKUS_OIDC_AUTH_SERVER_URL`
|The URL of the Dex OIDC server. For example, `\https://dex.example.com`.
|String
|-

|`QUARKUS_OIDC_CLIENT_ID`
|The client ID for the {registry} REST API. Must match the public client ID (`apicurio-registry-ui`) so the backend accepts tokens issued by Dex for the UI.
|String
|`registry-api`

|`APICURIO_UI_AUTH_OIDC_CLIENT_ID`
|The client ID for the {registry} web console. Must match the public Dex static client.
|String
|`apicurio-registry`

|`QUARKUS_OIDC_AUTHENTICATION_SCOPES`
|The OIDC scopes to request. Must include `groups` so that Dex includes group membership in the token.
|String
|`openid`

|`QUARKUS_OIDC_ROLES_ROLE_CLAIM_PATH`
|The JWT claim path where roles are stored. Dex passes upstream group membership in the `groups` claim.
|String
|-

|`QUARKUS_OIDC_TOKEN_PRINCIPAL_CLAIM`
|The JWT claim to use as the principal identity for artifact ownership. Set to `email` because Dex populates this as a flat top-level claim.
|String
|`sub`

|`APICURIO_UI_AUTH_OIDC_SCOPE`
|The OIDC scopes requested by the {registry} web console. Must include `groups` so that the UI token contains group membership for role-based authorization.
|String
|`openid profile email`
|===
+
For example, when you use Dex, set these values:
+
[source,properties]
----
QUARKUS_OIDC_TENANT_ENABLED=true
QUARKUS_OIDC_AUTH_SERVER_URL=https://dex.example.com
QUARKUS_OIDC_CLIENT_ID=apicurio-registry-ui
APICURIO_UI_AUTH_OIDC_CLIENT_ID=apicurio-registry-ui
QUARKUS_OIDC_AUTHENTICATION_SCOPES=openid,email,profile,groups
QUARKUS_OIDC_ROLES_ROLE_CLAIM_PATH=groups
QUARKUS_OIDC_TOKEN_PRINCIPAL_CLAIM=email
APICURIO_UI_AUTH_OIDC_SCOPE=openid profile email groups
----
+
IMPORTANT: Both `QUARKUS_OIDC_CLIENT_ID` and `APICURIO_UI_AUTH_OIDC_CLIENT_ID` must be set to the public client ID (`apicurio-registry-ui`). If they do not match, the backend rejects tokens issued for a different audience with `InvalidJwtSignatureException` errors.

. Configure role-based authorization. Dex passes upstream group membership in the `groups` claim. Map your upstream groups to {registry} roles:
+
.Configuration for {registry} role-based authorization with Dex
[.table-expandable,width="100%",cols="5,6,2,4",options="header"]
|===
|Environment variable
|Description
|Type
|Default

|`APICURIO_AUTH_ROLE_BASED_AUTHORIZATION`
|Enables role-based authorization in {registry}.
|Boolean
|`false`

|`APICURIO_AUTH_ROLE_SOURCE`
|Where to get user roles. Set to `token` to read from the JWT.
|String
|`token`

|`APICURIO_AUTH_ROLES_ADMIN`
|The upstream group name that grants admin access.
|String
|`sr-admin`

|`APICURIO_AUTH_ROLES_DEVELOPER`
|The upstream group name that grants developer access.
|String
|`sr-developer`

|`APICURIO_AUTH_ROLES_READONLY`
|The upstream group name that grants read-only access.
|String
|`sr-readonly`
|===
+
The mapping between upstream provider groups and {registry} roles works as follows:
+
.Group-to-role mapping example
[.table-expandable,width="100%",cols="3,3,3,3",options="header"]
|===
|Upstream group
|Dex passes as
|CR role config
|{registry} role

|`registry-admins`
|`registry-admins`
|`admin: registry-admins`
|Admin

|`registry-developers`
|`registry-developers`
|`developer: registry-developers`
|Developer

|`registry-readers`
|`registry-readers`
|`readOnly: registry-readers`
|Read-Only
|===

. Enable owner-based authorization. Because Dex provides proper principal identity through the `email` claim, owner-based authorization works correctly:
+
[source,properties]
----
APICURIO_AUTH_OWNER_ONLY_AUTHORIZATION=true
----
+
With this setting, only the user who created an artifact can modify or delete it. For more details on owner-only authorization, see xref:registry-security-obac-enabled_{context}[].

. If you use the {registry} Operator on {kubernetes}, configure all of the preceding settings in the `ApicurioRegistry3` custom resource. The `spec.app.auth` section covers the standard OIDC settings, and you must set Dex-specific properties by using `spec.app.env`. Use `spec.app.ingress` with TLS annotations instead of the bare `host` field:
+
[source,yaml]
----
apiVersion: registry.apicur.io/v1
kind: ApicurioRegistry3
metadata:
  name: apicurio-registry
  namespace: apicurio-registry
spec:
  app:
    ingress:
      host: apicurio-registry-app-apicurio-registry.apps.<cluster-domain>
      # On OpenShift, this annotation enables edge TLS termination on the Route,
      # so the app is served over HTTPS with automatic HTTP -> HTTPS redirect.
      annotations:
        route.openshift.io/termination: edge
    auth:
      enabled: true
      # Both must point to the PUBLIC Dex client so the backend accepts
      # tokens with aud=apicurio-registry-ui issued by the browser flow.
      appClientId: apicurio-registry-ui
      uiClientId: apicurio-registry-ui
      authServerUrl: "https://dex.<cluster-domain>"
      # Explicit redirect URI prevents oidc-client-ts from using the current
      # page URL (which may contain stale ?code=...&state=... query params),
      # fixing silent token refresh failures.
      redirectUri: "https://<registry-ui-route>/"
      anonymousReadsEnabled: true
      tls:
        tlsVerificationType: "none"
      authz:
        enabled: true
        readAccessEnabled: true
        ownerOnlyEnabled: true
        groupAccessEnabled: false
        roles:
          source: token
          admin: "registry-admins"
          developer: "registry-developers"
          readOnly: "registry-readers"
        adminOverride:
          enabled: true
          from: token
          type: role
          role: "registry-admins"
    env:
      - name: QUARKUS_OIDC_AUTHENTICATION_SCOPES
        value: "openid,email,profile,groups"
      - name: QUARKUS_OIDC_ROLES_ROLE_CLAIM_PATH
        value: "groups"
      - name: QUARKUS_OIDC_TOKEN_PRINCIPAL_CLAIM
        value: "email"
      # The UI scope defaults to "openid profile email" which does NOT include
      # groups. Without this, Dex won't include group membership in the token
      # and RBAC will deny all write operations.
      - name: APICURIO_UI_AUTH_OIDC_SCOPE
        value: "openid profile email groups"
  ui:
    ingress:
      host: apicurio-registry-ui-apicurio-registry.apps.<cluster-domain>
      annotations:
        route.openshift.io/termination: edge
    env:
      # The operator hardcodes http:// for REGISTRY_API_URL.
      # Override it to use HTTPS since we enabled edge TLS on the app route.
      - name: REGISTRY_API_URL
        value: "https://apicurio-registry-app-apicurio-registry.apps.<cluster-domain>/apis/registry/v3"
----
+
Note the following important details in this CR:
+
*Two-client architecture* — Both `appClientId` and `uiClientId` are set to `apicurio-registry-ui` (the public Dex client). The browser-based UI uses `oidc-client-ts`, which cannot securely hold a client secret. If you use a confidential client for the UI, the token exchange fails with `Invalid client credentials`. The backend validates JWT signatures by using the JWKS endpoint and does not need a client secret. The separate confidential client (`apicurio-registry`) is kept for CLI/API token exchange by using curl or scripts.
+
*UI scope with `groups`* — The `APICURIO_UI_AUTH_OIDC_SCOPE` env var adds `groups` to the scope the UI requests from Dex. Without this, the default scope (`openid profile email`) does not include groups, and Dex does not put group membership in the token. RBAC then denies all write operations because it cannot determine the user's role. Verified on a live cluster with Dex v2.45.1 and the OpenShift connector: tokens include the `groups` claim with the user's OpenShift group memberships, and {registry} successfully resolves roles for RBAC authorization.
+
*Explicit `redirectUri`* — Without this, `oidc-client-ts` uses `window.location.href` as the redirect URI for silent token refresh. After the initial login, the URL might still contain `?code=...&state=...` query parameters, which do not match any registered redirect URI in Dex, causing a 400 error.
+
NOTE: A complete example CR is available in the link:{attachmentsdir}/apicurio-registry-dex-cr.yaml[Dex example CR].

. Verify that all features work correctly:
+
[source,bash]
----
# Verify OIDC discovery
curl -sk https://dex.<cluster-domain>/.well-known/openid-configuration | jq .issuer

# Verify anonymous read access (if enabled)
curl -sk https://<registry-app-route>/apis/registry/v3/system/info

# Verify UI login — open in browser and authenticate via your upstream provider
# https://<registry-ui-route>

# Verify principal identity — artifacts should have an owner
curl -sk https://<registry-app-route>/apis/registry/v3/groups/my-group \
  -H "Authorization: Bearer $TOKEN" | jq .owner
# Expected: the user's email address, NOT empty
----

// ref-registry-security-dex-connectors
[id="registry-security-dex-connectors_{context}"]
:_mod-docs-content-type: REFERENCE
== Dex connector examples

[role="_abstract"]
Dex supports many upstream identity providers by using pluggable connectors. The following examples show how to configure the most common connectors.

ifdef::apicurio-registry[]
.OpenShift connector

First, create an OAuthClient for Dex on your OpenShift cluster:

[source,bash]
----
CLIENT_SECRET=$(openssl rand -base64 32 | tr -d '=' | head -c 32)

cat <<EOF | oc apply -f -
apiVersion: oauth.openshift.io/v1
kind: OAuthClient
metadata:
  name: dex
grantMethod: auto
secret: "$CLIENT_SECRET"
redirectURIs:
  - "https://dex.<cluster-domain>/callback"
EOF
----

Then add the connector to your `dex-values.yaml`:

[source,yaml]
----
connectors:
  - type: openshift
    id: openshift
    name: OpenShift
    config:
      issuer: https://api.<cluster-domain>:6443
      clientID: dex
      clientSecret: "<CLIENT_SECRET>"
      redirectURI: https://dex.<cluster-domain>/callback
      groups:
        - registry-admins
        - registry-developers
        - registry-readers
      insecureCA: true  # Set to false in production and configure proper CA
----
endif::[]

.LDAP connector

[source,yaml]
----
connectors:
  - type: ldap
    id: ldap
    name: LDAP
    config:
      host: ldap.example.com:636
      bindDN: cn=admin,dc=example,dc=com
      bindPW: <bind-password>
      userSearch:
        baseDN: ou=users,dc=example,dc=com
        filter: "(objectClass=person)"
        username: uid
        idAttr: uid
        emailAttr: mail
        nameAttr: cn
      groupSearch:
        baseDN: ou=groups,dc=example,dc=com
        filter: "(objectClass=groupOfNames)"
        userMatchers:
          - userAttr: DN
            groupAttr: member
        nameAttr: cn
----

.GitHub connector

[source,yaml]
----
connectors:
  - type: github
    id: github
    name: GitHub
    config:
      clientID: <github-oauth-app-client-id>
      clientSecret: <github-oauth-app-client-secret>
      redirectURI: https://dex.<cluster-domain>/callback
      orgs:
        - name: your-organization
          teams:
            - registry-admins
            - registry-developers
----

.Microsoft Azure AD connector

[source,yaml]
----
connectors:
  - type: microsoft
    id: microsoft
    name: Microsoft
    config:
      clientID: <azure-app-client-id>
      clientSecret: <azure-app-client-secret>
      redirectURI: https://dex.<cluster-domain>/callback
      tenant: <azure-tenant-id>
      groups:
        - <registry-admins-group-uuid>
        - <registry-developers-group-uuid>
----

.SAML 2.0 connector

[source,yaml]
----
connectors:
  - type: saml
    id: saml
    name: Corporate SSO
    config:
      ssoURL: https://idp.example.com/sso
      ca: /etc/dex/saml-ca.crt
      redirectURI: https://dex.<cluster-domain>/callback
      usernameAttr: name
      emailAttr: email
      groupsAttr: groups
----

NOTE: Dex supports multiple connectors simultaneously. You can federate across multiple identity providers so that users from different organizations or systems authenticate to {registry} through a single Dex instance.

// ref-registry-security-dex-m2m
[id="registry-security-dex-m2m_{context}"]
:_mod-docs-content-type: REFERENCE
== Machine-to-machine access and Kafka SerDes with Dex

[role="_abstract"]
The preceding configuration covers the browser-based UI flow. For CI/CD pipelines, Kafka SerDes, and services that need to interact with the {registry} API without a browser, additional considerations apply.

.Dex client_credentials support

Dex added `client_credentials` support in link:https://github.com/dexidp/dex/pull/4583[PR #4583] (merged March 3, 2026), but this feature is *not included in Dex v2.45.1* — the latest stable release at time of writing. Expect it to ship in v2.46.0 or later.

NOTE: To test before a stable release, use the `dexidp/dex:master` image from Docker Hub (rebuilt daily by Dex CI). For production, wait for v2.46.0+.

Enable it by setting an environment variable on the Dex deployment:

[source,yaml]
----
# In the Dex Deployment spec (requires Dex master or >= v2.46.0)
env:
  - name: DEX_CLIENT_CREDENTIAL_GRANT_ENABLED_BY_DEFAULT
    value: "true"
----

Then use the confidential client to obtain a token:

[source,bash]
----
TOKEN_RESPONSE=$(curl -s -X POST "https://dex.<cluster-domain>/token" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "client_id=apicurio-registry" \
  --data-urlencode "client_secret=<CLIENT_SECRET>" \
  --data-urlencode "scope=openid profile")

ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r .access_token)
----

IMPORTANT: Even with `client_credentials` working, the tokens *do not include the `groups` claim*. The `groups` scope is accepted but no groups are populated because there is no upstream connector or user in the `client_credentials` flow — the token is built solely from the static client definition, which has no group membership. Because {registry} uses `groups` for RBAC, M2M clients authenticating with `client_credentials` have no assigned role.

.M2M access with Dex `client_credentials`
[.table-expandable,width="100%",cols="4,2,6",options="header"]
|===
|Scenario
|Works?
|Why

|SerDes reading schemas (with `anonymousReadsEnabled: true`)
|Yes
|Anonymous reads bypass authentication entirely.

|SerDes registering schemas
|No
|No role in token means write access is denied.

|CI/CD creating artifacts
|No
|No role in token means write access is denied.
|===

.Recommended approach for Kafka SerDes

For the most common Kafka use case — *consumers fetching schemas* — you do not need `client_credentials` at all. Set `anonymousReadsEnabled: true` in the {registry} CR and the SerDes can read schemas without any token. Anonymous reads are the recommended approach for read-heavy workloads.

For *schema registration* (typically done by developers or CI/CD, not by Kafka producers at runtime), use the authorization code flow with the confidential client and a service user that has the right groups.

.Upcoming fix: groups on static clients

The Dex project tracks the lack of groups in `client_credentials` tokens in link:https://github.com/dexidp/dex/issues/4690[dexidp/dex#4690], and a proposed fix exists in link:https://github.com/dexidp/dex/pull/4691[dexidp/dex#4691]. The fix adds a `clientCredentialsClaims` sub-struct to static client definitions, keeping identity attributes separate from core client fields:

[source,yaml]
----
staticClients:
  - id: apicurio-registry
    secret: "..."
    clientCredentialsClaims:
      groups: ["registry-admins"]   # included in client_credentials tokens
----

Track the PR for when this lands in a stable Dex release.

.When to use {keycloak} instead

If your deployment requires M2M *write access* with proper RBAC — for example, if Kafka producers register schemas at runtime, or if you have many CI/CD pipelines that need different permission levels — use {keycloak} (or Auth0, Azure AD) instead of Dex. These providers support service account roles natively, so `client_credentials` tokens include all the claims {registry} needs.

Dex is the right choice when:

* Your upstream IdP is non-OIDC-compliant (OpenShift OAuth, LDAP, SAML)
* You primarily need user-facing auth (UI login, principal identity, owner-based authz)
* M2M workloads are read-only (SerDes consumers with anonymous reads)

// ref-registry-security-dex-production
[id="registry-security-dex-production_{context}"]
:_mod-docs-content-type: REFERENCE
== Dex production considerations

[role="_abstract"]
When running Dex in production, consider the following:

* *TLS*: Configure Dex with proper TLS by using Ingress termination or its own certificate. In the {registry} CR, set `spec.app.auth.tls.tlsVerificationType` to `certificate` and provide a truststore if using a private CA.
* *High availability*: Run multiple Dex replicas. For better HA, switch Dex's storage backend from Kubernetes CRDs to PostgreSQL.
* *Network policies*: Restrict access so that only {registry} pods and the ingress controller can reach Dex. Ensure Dex can reach your upstream identity providers.
* *Secret management*: Store the Dex client secret securely by using {kubernetes} Secrets or a secrets manager such as HashiCorp Vault.

// ref-registry-security-dex-troubleshooting
[id="registry-security-dex-troubleshooting_{context}"]
:_mod-docs-content-type: REFERENCE
== Dex troubleshooting

[role="_abstract"]
* *"Invalid client credentials" on UI login*: The UI (`oidc-client-ts`) runs in the browser and cannot send a client secret. Use a *public* Dex client (`public: true`, no secret) for the UI. A confidential client always fails.
* *403 Forbidden on API calls after UI login*: Two common causes: (1) *Audience mismatch* — the UI gets tokens with `aud: "apicurio-registry-ui"` but the backend expects `aud: "apicurio-registry"`. Fix: set both `appClientId` and `uiClientId` to the same public client ID. (2) *Missing `groups` scope* — the default UI scope does not include `groups`. Fix: set `APICURIO_UI_AUTH_OIDC_SCOPE` to `openid profile email groups`.
* *Groups not appearing in the token*: Verify that both `QUARKUS_OIDC_AUTHENTICATION_SCOPES` (backend) and `APICURIO_UI_AUTH_OIDC_SCOPE` (UI) include `groups`. Decode the JWT to inspect: `echo $TOKEN | cut -d. -f2 | base64 -d | jq .`
* *"Unregistered redirect_uri" on page refresh*: The UI redirects to `/dashboard` after login. Add `\https://<registry-ui-route>/dashboard` to the Dex client's redirect URIs.
* *Silent token refresh returns 400*: `oidc-client-ts` uses the current page URL (with stale `?code=...&state=...` params) as the redirect URI. Fix: set `redirectUri` explicitly in the {registry} CR auth config.
* *"No end session endpoint" on logout*: Dex does not implement OIDC RP-Initiated Logout. The local session is still cleared, but `signoutRedirect` fails. The error is a known Dex limitation and is non-blocking.
* *CORS errors in browser console*: The UI cannot fetch `/.well-known/openid-configuration` from Dex. Add the UI route origin to `web.allowedOrigins` in the Dex config.
* *`InvalidJwtSignatureException` after Dex restart*: Dex generates new signing keys on restart. Old tokens become invalid. Users must re-authenticate. For production, configure persistent signing keys.
* *`invalid_client` when exchanging authorization code*: If the client secret contains special characters (`+`, `/`, `=`), use `--data-urlencode` instead of `-d` in curl.
* *Owner field is empty*: Check that `QUARKUS_OIDC_TOKEN_PRINCIPAL_CLAIM` is set to a claim that Dex populates, such as `email`, `name`, or `sub`.
* *UI redirects to Dex but gets an error*: Check Dex logs (`{kubernetes-client} logs -n dex deploy/dex`). The cause is usually a redirect URI mismatch or connector misconfiguration.
* *`client_credentials` returns `unsupported_grant_type`*: Dex added this grant type in link:https://github.com/dexidp/dex/pull/4583[Dex PR #4583] (merged March 3, 2026) but did not include it in v2.45.1 or earlier. You need Dex >= v2.46.0 or a build from `master`.
* *"OIDC Server is not available"*: The {registry} pod cannot reach Dex. Check DNS resolution, network policies, and TLS trust configuration.

[role="_additional-resources"]
.Additional resources
* xref:registry-security-settings_{context}[]
* link:https://dexidp.io/docs/connectors/[Dex connectors documentation]
* link:https://dexidp.io/docs/[Dex documentation]





// ref-registry-security-settings
[id="registry-security-settings_{context}"]
:_mod-docs-content-type: REFERENCE
== {registry} authentication and authorization configuration options

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
{registry} provides authentication options for OpenID Connect with {keycloak} and HTTP basic authentication.

{registry} provides authorization options for role-based and content-based approaches:

* Role-based authorization for default admin, write, and read-only user roles.
* Content-based authorization for schema or API artifacts, where only the owner of the artifacts or artifact group can update or delete artifacts.

IMPORTANT: All authentication and authorization options in {registry} are disabled by default. Before enabling any of these options, you must first set the `QUARKUS_OIDC_TENANT_ENABLED` option to `true`.

You can configure the following options:

* xref:registry-security-authn-keycloak_{context}[{registry} authentication by using OpenID Connect with {keycloak}]
* xref:registry-security-authn-http_{context}[{registry} authentication by using HTTP basic]
* xref:registry-security-rbac-enabled_{context}[{registry} role-based authorization]
* xref:registry-security-obac-enabled_{context}[{registry} owner-only authorization]
* xref:registry-security-auth-read_{context}[{registry} authenticated read access]
* xref:registry-security-anon-read_{context}[{registry} anonymous read-only access]

[id="registry-security-authn-keycloak_{context}"]
=== {registry} authentication by using OpenID Connect with {keycloak}

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
You can set the following environment variables to configure authentication for the {registry} web console and API with {keycloak}:

.Configuration for {registry} authentication with {keycloak}
[.table-expandable,width="100%",cols="5,6,2,4",options="header"]
|===
|Environment variable
|Description
|Type
|Default
|`QUARKUS_OIDC_TENANT_ENABLED`
|Enables authentication for {registry}. When set to `true`, the environment variables that follow are required for authentication by using {keycloak}.
|String
|`false`
|`QUARKUS_OIDC_AUTH_SERVER_URL`
|The URL of the {keycloak} authentication server. For example, `http://localhost:8080`.
|String
|-
|`QUARKUS_OIDC_CLIENT_ID`
|The client ID for the {registry} REST API.
|String
|`registry-api`
|`APICURIO_UI_AUTH_OIDC_CLIENT_ID`
|The client ID for the {registry} web console.
|String
|`apicurio-registry`

|`QUARKUS_OIDC_TLS_TRUST_STORE_FILE`
| The file path to the TLS trust store that Quarkus uses to secure OpenID Connect (OIDC) communications. Populate the trust store with the trusted certificates that are needed to establish secure TLS connections with the OIDC provider.
|String
|-

|`QUARKUS_OIDC_TLS_TRUST_STORE_PASSWORD`
|The password required to access the TLS trust store file.
|String
|-

|`APICURIO_AUTH_ROLE_BASED_AUTHORIZATION`
|Enables or disables role-based authorization.
|Boolean
|False

|===

[id="registry-security-authn-http_{context}"]
=== {registry} authentication by using HTTP basic

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
By default, {registry} supports authentication by using OpenID Connect. Users or API clients must obtain an access token to make authenticated calls to the {registry} REST API. However, because some tools do not support OpenID Connect, you can also configure {registry} to support HTTP basic authentication by setting the following configuration options to `true`:

.Configuration for {registry} HTTP basic authentication
[%header,cols="4,4,1,1"]
|===
|Environment variable
|Java system property
|Type
|Default value
|`QUARKUS_OIDC_TENANT_ENABLED`
|`quarkus.oidc.tenant-enabled`
|Boolean
|`false`
|`APICURIO_AUTHN_BASIC_CLIENT_CREDENTIALS_ENABLED`
|`apicurio.authn.basic-client-credentials.enabled`
|Boolean
|`false`
|===

.{registry} HTTP basic client credentials cache expiry

You can also configure the HTTP basic client credentials cache expiry time. By default, when you use HTTP basic authentication, {registry} caches JWT tokens and does not issue a new token when there is no need. You can configure the cache expiry time for JWT tokens, which is set to 10 mins by default.

When you use {keycloak}, set this configuration to your {keycloak} JWT expiry time minus one minute. For example, if you have the expiry time set to `5` mins in {keycloak}, set the following configuration option to `4` mins:

.Configuration for HTTP basic client credentials cache expiry
[%header,cols="4,4,1,1"]
|===
|Environment variable
|Java system property
|Type
|Default value
|`APICURIO_AUTHN_BASIC_CLIENT_CREDENTIALS_CACHE_EXPIRATION`
|`apicurio.authn.basic-client-credentials.cache-expiration`
|Integer
|`10`
|===


[id="registry-security-rbac-enabled_{context}"]
=== {registry} role-based authorization

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
You can set the following options to `true` to enable role-based authorization in {registry}:

.Configuration for {registry} role-based authorization
[%header,cols="4,4,1,1"]
|===
|Environment variable
|Java system property
|Type
|Default value
|`QUARKUS_OIDC_TENANT_ENABLED`
|`quarkus.oidc.tenant-enabled`
|Boolean
|`false`
|`APICURIO_AUTH_ROLE_BASED_AUTHORIZATION`
|`apicurio.auth.role-based-authorization`
|Boolean
|`false`
|===

You can then configure role-based authorization to use roles included in the user's authentication token (for example, granted when authenticating by using {keycloak}), or to use role mappings managed internally by {registry}.

.Using roles assigned in {keycloak}

To use roles assigned in {keycloak}, set the following environment variables:

.Configuration for {registry} role-based authorization by using {keycloak}
[id="registry-security-rbac-keycloak-settings_{context}",.table-expandable,width="100%",cols="6,6,2,3",options="header"]
|===
|Environment variable
|Description
|Type
|Default
|`APICURIO_AUTH_ROLE_SOURCE`
| When set to `token`, user roles are taken from the authentication token.
|String
|`token`
|`APICURIO_AUTH_ROLES_ADMIN`
|The name of the role that indicates a user is an admin.
|String
|`sr-admin`
|`APICURIO_AUTH_ROLES_DEVELOPER`
|The name of the role that indicates a user is a developer.
|String
|`sr-developer`
|`APICURIO_AUTH_ROLES_READONLY`
|The name of the role that indicates a user has read-only access.
|String
|`sr-readonly`
|===

When you configure {registry} to use roles from {keycloak}, you must assign {registry} users to at least one
of the following user roles in {keycloak}. However, you can configure different user role names by using the environment variables in xref:registry-security-rbac-keycloak-settings_{context}[].

.{registry} roles for authentication and authorization
[.table-expandable,width="100%",cols="2,2,2,2,4",options="header"]
|===
|Role name
|Read artifacts
|Write artifacts
|Global rules
|Description
|`sr-admin`
|Yes
|Yes
|Yes
|Full access to all create, read, update, and delete operations.
|`sr-developer`
|Yes
|Yes
|No
|Access to create, read, update, and delete operations, except configuring global rules and import/export. This role can configure artifact-specific rules only.
|`sr-readonly`
|Yes
|No
|No
|Access to read and search operations only. This role cannot configure any rules.
|===

.Managing roles directly in {registry}

To use roles managed internally by {registry}, set the following environment variable:

.Configuration for {registry} role-based authorization by using internal role mappings
[.table-expandable,width="100%",cols="6,6,2,3",options="header"]
|===
|Environment variable
|Description
|Type
|Default
|`APICURIO_AUTH_ROLE_SOURCE`
| When set to `application`, user roles are managed internally by {registry}.
|String
|`token`
|===

When you use internally managed role mappings, you can assign a role to users by using the `/admin/roleMappings`
endpoint in the {registry} REST API. For more details, see {registry-rest-api}.

You can grant each user exactly one role: `ADMIN`, `DEVELOPER`, or `READ_ONLY`. Only admin
users can grant access to other users.


.{registry} admin-override configuration

Because {registry} has no default admin users, you can configure an alternative way to identify admins. Configure the admin-override feature by using the following environment variables:

.Configuration for {registry} admin-override
[.table-expandable,width="100%",cols="6,6,2,3",options="header"]
|===
|Environment variable
|Description
|Type
|Default
|`APICURIO_AUTH_ADMIN_OVERRIDE_ENABLED`
| Enables the admin-override feature.
|String
|`false`
|`APICURIO_AUTH_ADMIN_OVERRIDE_FROM`
|Where to look for admin-override information. Only `token` is currently supported.
|String
|`token`
|`APICURIO_AUTH_ADMIN_OVERRIDE_TYPE`
|The type of information used to determine whether a user is an admin. Values depend on the value of the FROM variable, for example, `role` or `claim` when FROM is `token`.
|String
|`role`
|`APICURIO_AUTH_ADMIN_OVERRIDE_ROLE`
|The name of the role that indicates a user is an admin.
|String
|`sr-admin`
|`APICURIO_AUTH_ADMIN_OVERRIDE_CLAIM`
|The name of a JWT token claim to use for determining admin-override.
|String
|`org-admin`
|`APICURIO_AUTH_ADMIN_OVERRIDE_CLAIM_VALUE`
|The value that the JWT token claim indicated by the CLAIM variable must be for the user to be granted admin-override.
|String
|`true`
|===

For example, you can use this admin-override feature to assign the `sr-admin` role to a single user
in {keycloak}, which grants that user the admin role. That user can then use the `/admin/roleMappings`
REST API (or associated UI) to grant roles to additional users (including additional admins).

[id="registry-security-obac-enabled_{context}"]
=== {registry} owner-only authorization

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
You can set the following options to `true` to enable owner-only authorization for updates to artifacts or artifact groups in {registry}:

.Configuration for owner-only authorization
[%header,cols="4,4,1,1"]
|===
|Environment variable
|Java system property
|Type
|Default value

|`QUARKUS_OIDC_TENANT_ENABLED`
|`quarkus.oidc.tenant-enabled`
|Boolean
|`false`

|`APICURIO_AUTH_OWNER_ONLY_AUTHORIZATION`
|`apicurio.auth.owner-only-authorization`
|Boolean
|`false`

|`APICURIO_AUTH_OWNER_ONLY_AUTHORIZATION_LIMIT_GROUP_ACCESS`
|`apicurio.auth.owner-only-authorization.limit-group-access`
|Boolean
|`false`
|===

When owner-only authorization is enabled, only the user who created an artifact can modify or delete that artifact.

When owner-only authorization and group owner-only authorization are both enabled, only the user who created an artifact group has write access to that artifact group, for example, to add or remove artifacts in that group.

[id="registry-security-auth-read_{context}"]
=== {registry} authenticated read access

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
When the authenticated read access option is enabled, {registry} grants at least read-only access to requests from any authenticated user in the same organization, regardless of their user role.

To enable authenticated read access, you must first enable role-based authorization, and then ensure that the following options are set to `true`:

.Configuration for authenticated read access
[%header,cols="4,4,1,1"]
|===
|Environment variable
|Java system property
|Type
|Default value
|`QUARKUS_OIDC_TENANT_ENABLED`
|`quarkus.oidc.tenant-enabled`
|Boolean
|`false`
|`APICURIO_AUTH_AUTHENTICATED_READ_ACCESS_ENABLED`
|`apicurio.auth.authenticated-read-access.enabled`
|Boolean
|`false`
|===

For more details, see xref:registry-security-rbac-enabled_{context}[].

[id="registry-security-anon-read_{context}"]
=== {registry} anonymous read-only access

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
In addition to the two main types of authorization (role-based and owner-based authorization), {registry}
supports an anonymous read-only access option.

To allow anonymous users, such as REST API calls with no authentication credentials, to make read-only
calls to the REST API, set the following options to `true`:

.Configuration for anonymous read-only access
[%header,cols="4,4,1,1"]
|===
|Environment variable
|Java system property
|Type
|Default value
|`QUARKUS_OIDC_TENANT_ENABLED`
|`quarkus.oidc.tenant-enabled`
|Boolean
|`false`
|`APICURIO_AUTH_ANONYMOUS_READ_ACCESS_ENABLED`
|`apicurio.auth.anonymous-read-access.enabled`
|Boolean
|`false`
|===


[role="_additional-resources"]
.Additional resources
* xref:configuring-liveness-readiness-probes_{context}[]
* https://quarkus.io/guides/security-openid-connect-web-authentication[Quarkus Open ID Connect documentation]



// con-registry-cors-configuration
[id="registry-cors-configuration_{context}"]
:_mod-docs-content-type: CONCEPT
== Configuring {registry} Cross-Origin Resource Sharing (CORS)

[role="_abstract"]
To allow your browser-based application (React, Angular, Vue, or plain JavaScript) to call the {registry} REST API from a different origin, you must configure Cross-Origin Resource Sharing (CORS).
Without the correct CORS configuration on the {registry} server, the browser blocks API calls from your application, even though the same requests succeed from a non-browser client such as `curl`.
{registry} includes built-in CORS support based on the Quarkus HTTP CORS filter that you can configure for your deployment.

* xref:registry-cors-understanding_{context}[]
* xref:registry-cors-defaults_{context}[]
* xref:registry-cors-env-vars_{context}[]
* xref:registry-cors-local-dev_{context}[]
* xref:registry-cors-production_{context}[]
* xref:registry-cors-authentication_{context}[]
* xref:registry-cors-operator_{context}[]
* xref:registry-cors-troubleshooting_{context}[]
* xref:registry-cors-diagnosing_{context}[]


// con-registry-cors-understanding
[id="registry-cors-understanding_{context}"]
:_mod-docs-content-type: CONCEPT
== Understanding CORS

[role="_abstract"]
Browsers enforce a same-origin policy that prevents a web page served from one origin (for example, `https://my-app.example.com`) from making HTTP requests to a different origin (for example, `https://registry.example.com`).
CORS is the mechanism by which a server explicitly allows cross-origin requests by including specific HTTP response headers such as `Access-Control-Allow-Origin`.

Consider a typical scenario where your browser application runs at `https://my-app.example.com` and {registry} runs at `https://registry.example.com`.
When your JavaScript code calls the {registry} REST API:

[source,javascript]
----
fetch("https://registry.example.com/apis/registry/v3/groups")
----

The browser first sends a preflight `OPTIONS` request to {registry}, asking whether {registry} allows the cross-origin request.
{registry} must respond with the correct headers for the browser to allow the request:

* `Access-Control-Allow-Origin` matching your application's origin
* `Access-Control-Allow-Methods` listing the HTTP methods you use
* `Access-Control-Allow-Headers` listing any custom headers your request includes

If {registry} does not respond with these headers, the browser blocks the request and you see an error similar to this example:

----
Access to fetch at 'https://registry.example.com/apis/registry/v3/groups'
from origin 'https://my-app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
----


// ref-registry-cors-defaults
[id="registry-cors-defaults_{context}"]
:_mod-docs-content-type: REFERENCE
== {registry} CORS defaults

[role="_abstract"]
{registry} ships with CORS enabled by default, configured for the bundled {registry} web console.
The default settings from `application.properties` are:

.Default {registry} CORS configuration properties
[.table-expandable,width="100%",cols="4,6",options="header"]
|===
|Property
|Default value

|`quarkus.http.cors`
|`true`

|`quarkus.http.cors.origins`
|`http://localhost:8888,http://127.0.0.1:8888`

|`quarkus.http.cors.methods`
|`GET,PUT,POST,PATCH,DELETE,OPTIONS`

|`quarkus.http.cors.headers`
|`x-registry-name`, `x-registry-name-encoded`, `x-registry-description`, `x-registry-description-encoded`, `x-registry-version`, `x-registry-artifactid`, `x-registry-artifacttype`, `x-registry-hash-algorithm`, `x-registry-content-hash`, `access-control-request-method`, `access-control-allow-credentials`, `access-control-allow-origin`, `access-control-allow-headers`, `authorization`, `content-type`, `content-encoding`, `user-agent`
|===

The default allowed origins (`localhost:8888` and `127.0.0.1:8888`) match the bundled {registry} web console.
If your application runs on a different host, port, or protocol, {registry} rejects preflight requests from your origin.
You must add your application's origin to the allowed list.


// ref-registry-cors-env-vars
[id="registry-cors-env-vars_{context}"]
:_mod-docs-content-type: REFERENCE
== CORS environment variables

[role="_abstract"]
You can override the default CORS settings by using environment variables.
Environment variables are the recommended approach because they let you configure {registry} without modifying configuration files.

.{registry} CORS environment variables
[.table-expandable,width="100%",cols="3,5,4",options="header"]
|===
|Environment variable
|Description
|Example

|`QUARKUS_HTTP_CORS`
|Enables or disables the CORS filter.
|`true`

|`QUARKUS_HTTP_CORS_ORIGINS`
|A comma-separated list of allowed origins, or `*` to allow all origins.
|`https://my-app.example.com,https://other.example.com`

|`QUARKUS_HTTP_CORS_METHODS`
|A comma-separated list of allowed HTTP methods.
|`GET,PUT,POST,PATCH,DELETE,OPTIONS`

|`QUARKUS_HTTP_CORS_HEADERS`
|A comma-separated list of allowed request headers.
|`authorization,content-type`
|===

NOTE: Setting `QUARKUS_HTTP_CORS_ORIGINS` by using an environment variable completely overrides the default origins from `application.properties`. The default methods and headers still apply unless you also override them.

For the full set of Quarkus CORS options, see the link:https://quarkus.io/guides/http-reference#cors-filter[Quarkus HTTP Reference - CORS Filter].


ifndef::service-registry-downstream[]
// ref-registry-cors-local-dev
[id="registry-cors-local-dev_{context}"]
:_mod-docs-content-type: REFERENCE
== CORS configuration for local development

[role="_abstract"]
During local development, you can allow all origins by using `*` as the value for `QUARKUS_HTTP_CORS_ORIGINS`.

Use a Docker Compose configuration such as this example to run {registry} with CORS open for all origins:

[source,yaml]
----
services:
  apicurio-registry:
    image: apicurio/apicurio-registry:latest-snapshot
    environment:
      apicurio.rest.deletion.group.enabled: "true"
      apicurio.rest.deletion.artifact.enabled: "true"
      apicurio.rest.deletion.artifact-version.enabled: "true"
      QUARKUS_HTTP_CORS_ORIGINS: "*"
    ports:
      - "8080:8080"

  apicurio-registry-ui:
    image: apicurio/apicurio-registry-ui:latest-snapshot
    ports:
      - "8888:8080"
    depends_on:
      - apicurio-registry
----

Start the deployment by running:

[source,bash]
----
docker compose -f docker-compose-no-auth.yaml up
----

With {registry} running on `localhost:8080`, you can call the API from any origin:

[source,javascript]
----
// List all groups in the registry
const response = await fetch("http://localhost:8080/apis/registry/v3/groups");
const data = await response.json();
console.log(data);
----

WARNING: Using `QUARKUS_HTTP_CORS_ORIGINS: "*"` is acceptable for local development but must not be used in production. A wildcard origin means any website can make requests to your {registry} instance.
endif::[]


// ref-registry-cors-production
[id="registry-cors-production_{context}"]
:_mod-docs-content-type: REFERENCE
== CORS configuration for production

[role="_abstract"]
In production, explicitly list the origins that are allowed to access the {registry} REST API.

TIP: If you deploy {registry} by using the {kubernetes} Operator, see xref:registry-cors-operator_{context}[]. The operator includes automatic CORS origin detection based on your Ingress configuration, and you often do not need to configure CORS manually.

To allow a single origin:

[source,yaml]
----
services:
  apicurio-registry:
    image: apicurio/apicurio-registry:latest-snapshot
    environment:
      QUARKUS_HTTP_CORS_ORIGINS: "https://my-app.example.com"
    ports:
      - "8080:8080"
----

To allow multiple origins, separate them with commas:

[source,yaml]
----
services:
  apicurio-registry:
    image: apicurio/apicurio-registry:latest-snapshot
    environment:
      QUARKUS_HTTP_CORS_ORIGINS: "https://my-app.example.com,https://admin.example.com"
    ports:
      - "8080:8080"
----



// ref-registry-cors-authentication
[id="registry-cors-authentication_{context}"]
:_mod-docs-content-type: REFERENCE
== CORS configuration with authentication

[role="_abstract"]
When you use OIDC authentication (for example, {keycloak}), CORS and authentication work together.
Your browser application must send an `Authorization` header with a bearer token, and the {registry} CORS configuration must allow that header for authenticated cross-origin requests to succeed.

The `authorization` header is already included in the default allowed headers, so you typically only need to configure the allowed origins.

Use a Docker Compose configuration such as this example to run {registry} with {keycloak} and CORS configured for browser-based applications:

[source,yaml]
----
services:
  keycloak:
    image: quay.io/keycloak/keycloak:23.0.7
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_HOSTNAME: "localhost"
      KC_HOSTNAME_PORT: "8080"
      KC_HOSTNAME_STRICT_BACKCHANNEL: "false"
    command:
      - start-dev
      - --import-realm
    ports:
      - "8080:8080"
    volumes:
      - ./realm.json:/opt/keycloak/data/import/realm.json

  apicurio-registry:
    image: apicurio/apicurio-registry:latest-snapshot
    environment:
      apicurio.rest.deletion.group.enabled: "true"
      apicurio.rest.deletion.artifact.enabled: "true"
      apicurio.rest.deletion.artifact-version.enabled: "true"
      QUARKUS_OIDC_TENANT_ENABLED: "true"
      QUARKUS_OIDC_AUTH_SERVER_URL: "http://keycloak:8080/realms/registry"
      QUARKUS_OIDC_CLIENT_ID: "registry-api"
      APICURIO_UI_AUTH_OIDC_CLIENT_ID: "apicurio-registry"
      QUARKUS_OIDC_TLS_VERIFICATION: "none"
      QUARKUS_HTTP_CORS_ORIGINS: "http://localhost:8888,http://localhost:3000"
      QUARKUS_PROFILE: "prod"
    ports:
      - "8081:8080"
    depends_on:
      - keycloak

  apicurio-registry-ui:
    image: apicurio/apicurio-registry-ui:latest-snapshot
    environment:
      REGISTRY_API_URL: "http://localhost:8081/apis/registry/v3"
      REGISTRY_AUTH_TYPE: "oidc"
      REGISTRY_AUTH_URL: "http://localhost:8080/realms/registry"
    ports:
      - "8888:8080"
    depends_on:
      - apicurio-registry
      - keycloak
----

When you configure CORS with authentication, keep these points in mind:

* The default CORS allowed headers already include the `authorization` header. You do not need to add it manually unless you override `QUARKUS_HTTP_CORS_HEADERS`.
* If you override `QUARKUS_HTTP_CORS_HEADERS`, make sure to include `authorization` in your list.
* When you use `QUARKUS_HTTP_CORS_ORIGINS: "*"` with credentials, some browsers reject the request. Use explicit origins when authentication is involved.

NOTE: Authentication is a complex topic with many possible configurations ({keycloak}, Microsoft Entra ID, and other OIDC providers). For a complete guide to configuring authentication, see xref:registry-security-settings_{context}[].


// ref-registry-cors-operator
[id="registry-cors-operator_{context}"]
:_mod-docs-content-type: REFERENCE
== CORS configuration with the {kubernetes} Operator

[role="_abstract"]
When you deploy {registry} by using the {kubernetes} Operator, you can set CORS environment variables in the `ApicurioRegistry3` custom resource.

To configure explicit origins:

[source,yaml]
----
apiVersion: registry.apicur.io/v1
kind: ApicurioRegistry3
metadata:
  name: my-registry
spec:
  app:
    env:
      - name: QUARKUS_HTTP_CORS_ORIGINS
        value: "https://my-app.example.com,https://admin.example.com"
----

The {registry} Operator includes built-in logic for automatic CORS origin detection. In most {kubernetes} deployments, you do not need to configure CORS manually:

. If you set `QUARKUS_HTTP_CORS_ORIGINS` in the `spec.app.env` section, those origins are used (manual override).
. If you do not configure origins and you have an Ingress for the registry UI, the operator automatically detects the Ingress host and adds both `http://` and `https://` variants of that host to the allowed origins.
. If you do not configure origins and no Ingress is found, the operator defaults to `*` (allow all origins).

You only need to configure `QUARKUS_HTTP_CORS_ORIGINS` manually if your browser application runs on a different origin than the one auto-detected from your Ingress.

NOTE: This automatic CORS detection works whether your networking layer uses {kubernetes} Ingress or OpenShift Routes. CORS is configured at the application level, not at the networking layer.


// ref-registry-cors-troubleshooting
[id="registry-cors-troubleshooting_{context}"]
:_mod-docs-content-type: REFERENCE
== Troubleshooting CORS errors

:_mod-docs-content-type: REFERENCE

[role="_abstract"]
.Common CORS errors and fixes
[%header,cols="3,3,4"]
|===
|Error
|Cause
|Fix

|"No Access-Control-Allow-Origin header is present on the requested resource"
|Your application's origin is not in the {registry} allowed origins list.
|Add your application's origin to `QUARKUS_HTTP_CORS_ORIGINS`. Include the protocol and port (for example, `https://my-app.example.com:3000`, not only the hostname `my-app.example.com`).

|Preflight request fails with 403
|The HTTP method or a request header is not allowed by the CORS configuration.
|Check that `QUARKUS_HTTP_CORS_METHODS` includes the HTTP method your application uses, and that `QUARKUS_HTTP_CORS_HEADERS` includes any custom headers your application sends.

|CORS works without authentication but fails with authentication enabled
|You might have overridden `QUARKUS_HTTP_CORS_HEADERS` without including `authorization`.
|Make sure `authorization` is in your `QUARKUS_HTTP_CORS_HEADERS` list.

|Credentials or cookies are not sent
|When you use `Access-Control-Allow-Origin: *`, some browsers do not send credentials (cookies, authorization headers).
|Use explicit origins instead of `*` when your application needs to send credentials.
|===

// proc-registry-cors-diagnosing
[id="registry-cors-diagnosing_{context}"]
:_mod-docs-content-type: PROCEDURE
== Diagnosing CORS errors with browser developer tools

:_mod-docs-content-type: PROCEDURE

[role="_abstract"]
You can use your browser developer tools to identify the cause of CORS errors.

.Prerequisites

* {registry} is installed and running.

.Procedure

. Open your browser developer tools (F12).
. Go to the *Network* tab.
. Look for the failed request, which often shows as `(blocked:cors)` or has a red status.
. Look for a preceding `OPTIONS` request (the preflight) and inspect its response headers.
. Check whether `Access-Control-Allow-Origin` is present and matches your application's origin.
. Check whether `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` include the values your application needs.

[role="_additional-resources"]
.Additional resources
* link:https://quarkus.io/guides/http-reference#cors-filter[Quarkus HTTP Reference - CORS Filter]
* link:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[MDN: Cross-Origin Resource Sharing (CORS)]
* xref:registry-security-settings_{context}[]


