How to Create the Apicurio Migration Client

This guide shows you how to create the apicurio-migration Keycloak client and its migration-admin realm role for the Apicurio Registry v2 to v3 upgrade, in the Admin Console or with a script, and how to verify the client.

Type

How-to guide

Goal

Give the upgrade a Keycloak client that keeps register and export access on Apicurio Registry v2 while every other client is read-only.

Audience

Platform Operator with admin access to the Keycloak Admin Console for the Apicurio realm.

When to use

Use this guide before you freeze schema registration in How to Migrate Schemas to Apicurio v3.

Prerequisites

Confirm the following before you begin.

Access and permissions required

You need the following access and permissions:

  • Admin access to the Keycloak Admin Console for the realm set in your Apicurio values under security.authentication.keycloak.realm.

  • For the script: an admin user in the Keycloak master realm who can manage that realm.

Tools and versions required

You need the following tools for the script and the verification:

  • curl >= 7.76 (the script uses --fail-with-body)

  • jq

Resources that must exist before starting

The following resources must already exist before you start:

Create the client in the Admin Console

The apicurio-migration client is the only principal that keeps register and export access on v2 during the registration freeze. It holds the migration-admin role, which becomes the sole admin role on v2 while every other client, including the platform’s apicurio-api, is read-only.

Log in to the Keycloak Admin Console for the realm set in your Apicurio values under security.authentication.keycloak.realm, then follow these steps:

  1. Create the migration-admin realm role: Navigate to Realm roles, click Create role, enter migration-admin as the role name, and save.

  2. Create the apicurio-migration client: Navigate to Clients and click Create client. Set the Client ID to apicurio-migration, then apply these settings on the Capability config step:

    Setting Value

    Client authentication

    On

    Authorization

    Off

    Standard flow

    unchecked

    Implicit flow

    unchecked

    Direct access grants

    unchecked

    Service accounts roles

    checked

    OAuth 2.0 Device Authorization Grant

    unchecked

    OIDC CIBA Grant

    unchecked

    Save the client and copy the client secret from the Credentials tab. You need it for MIGRATION_CLIENT_SECRET in Step 1: Set shell variables.

  3. Assign the role to the service account: Open the apicurio-migration client, go to the Service account roles tab and click Assign role. In the filter dropdown (top-left of the dialog), switch from Filter by clients to Filter by realm roles, then select migration-admin and click Assign.

Create the client with a script

Instead of clicking through the Admin Console, you can run the three steps in Create the client in the Admin Console as a single script against the Keycloak Admin REST API. The script is idempotent: it skips creating the role or client when either already exists, but it always (re-)assigns the migration-admin role to the service account.

Edit the four variables in the # --- Config --- block at the top of the script before running it:

  • KC: the base Keycloak URL. Append /auth if your Keycloak deployment serves its Admin REST API under that path. Older WildFly-based Keycloak distributions do this by default, as does a Quarkus-based Keycloak explicitly configured with KC_HTTP_RELATIVE_PATH=/auth. A newer Quarkus-based Keycloak (17+, default configuration) serves the API at the root and needs no /auth. The upgrade guides always reach Keycloak at https://<APICURIO_KEYCLOAK_HOST>/auth/realms/…​; (see Step 1: Set shell variables). If those curl calls work unmodified against your Keycloak, set KC to the full URL of the same host with /auth appended, including the scheme, for example KC="https://apicurio.example.com/auth".

  • REALM: the realm from your Apicurio values' security.authentication.keycloak.realm, the same realm used in the manual steps.

  • ADMIN_USER: the username of an admin user in the master realm who can manage REALM. An admin defined only inside the Apicurio realm can’t log in through this script.

  • ADMIN_PASS: the password for that admin. The script exchanges it for a token with a password grant against the master realm’s admin-cli client, not a service-account token.

#!/usr/bin/env bash
set -euo pipefail

# --- Config ---
KC=""
REALM=""
ADMIN_USER=""
ADMIN_PASS=""

ROLE_NAME="migration-admin"
CLIENT_ID="apicurio-migration"
# --------------

echo "==> Getting admin token"
TOKEN=$(curl -sS --fail-with-body -X POST "$KC/realms/master/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=admin-cli" \
  --data-urlencode "username=$ADMIN_USER" \
  --data-urlencode "password=$ADMIN_PASS" \
  | jq -r '.access_token')

if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
  echo "ERROR: failed to obtain admin token" >&2
  exit 1
fi

auth=(-H "Authorization: Bearer $TOKEN")

# --- 1. Create the realm role ---
echo "==> Creating realm role: $ROLE_NAME"
if curl -s -o /dev/null -w '%{http_code}' "${auth[@]}" \
     "$KC/admin/realms/$REALM/roles/$ROLE_NAME" | grep -q '^200$'; then
  echo "    role already exists, skipping"
else
  curl -sS --fail-with-body -X POST "$KC/admin/realms/$REALM/roles" "${auth[@]}" \
    -H "Content-Type: application/json" \
    -d "{\"name\": \"$ROLE_NAME\"}"
  echo "    created"
fi

# --- 2. Create the client ---
# UI mapping (Capability config):
#   Client authentication On          -> publicClient: false
#   Authorization Off                 -> authorizationServicesEnabled: false
#   Standard flow            unchecked -> standardFlowEnabled: false
#   Implicit flow            unchecked -> implicitFlowEnabled: false
#   Direct access grants     unchecked -> directAccessGrantsEnabled: false
#   Service accounts roles     checked -> serviceAccountsEnabled: true
#   OAuth2 Device Grant      unchecked -> attr oauth2.device.authorization.grant.enabled: false
#   OIDC CIBA Grant          unchecked -> attr oidc.ciba.grant.enabled: false
echo "==> Creating client: $CLIENT_ID"
INTERNAL_ID=$(curl -sS --fail-with-body "${auth[@]}" \
  "$KC/admin/realms/$REALM/clients?clientId=$CLIENT_ID" | jq -r '.[0].id // empty')

if [[ -n "$INTERNAL_ID" ]]; then
  echo "    client already exists (id=$INTERNAL_ID), skipping"
else
  curl -sS --fail-with-body -X POST "$KC/admin/realms/$REALM/clients" "${auth[@]}" \
    -H "Content-Type: application/json" \
    -d "{
      \"clientId\": \"$CLIENT_ID\",
      \"enabled\": true,
      \"publicClient\": false,
      \"authorizationServicesEnabled\": false,
      \"standardFlowEnabled\": false,
      \"implicitFlowEnabled\": false,
      \"directAccessGrantsEnabled\": false,
      \"serviceAccountsEnabled\": true,
      \"attributes\": {
        \"oauth2.device.authorization.grant.enabled\": \"false\",
        \"oidc.ciba.grant.enabled\": \"false\"
      }
    }"
  INTERNAL_ID=$(curl -sS --fail-with-body "${auth[@]}" \
    "$KC/admin/realms/$REALM/clients?clientId=$CLIENT_ID" | jq -r '.[0].id')
  echo "    created (id=$INTERNAL_ID)"
fi

# --- 3. Assign realm role to the service account ---
echo "==> Resolving service-account user"
SA_USER_ID=$(curl -sS --fail-with-body "${auth[@]}" \
  "$KC/admin/realms/$REALM/clients/$INTERNAL_ID/service-account-user" | jq -r '.id')

echo "==> Fetching role representation"
ROLE_JSON=$(curl -sS --fail-with-body "${auth[@]}" "$KC/admin/realms/$REALM/roles/$ROLE_NAME")

echo "==> Assigning $ROLE_NAME to service account $SA_USER_ID"
curl -sS --fail-with-body -X POST "$KC/admin/realms/$REALM/users/$SA_USER_ID/role-mappings/realm" \
  "${auth[@]}" \
  -H "Content-Type: application/json" \
  -d "[$ROLE_JSON]"

echo
echo "==> Done."

The script doesn’t create or print a client secret. After it finishes, open the apicurio-migration client in the Keycloak Admin Console and copy the secret from the Credentials tab, as in the manual flow.

Verify the migration client

Check that the client can get a token, which proves it exists, is confidential and has service accounts enabled.

Replace every <VALUE> placeholder with your actual value before running the command. Drop /auth from the URL if your Keycloak serves its API at the root, as described for KC above.
curl -skS --fail-with-body -X POST \
  "https://<APICURIO_KEYCLOAK_HOST>/auth/realms/<APICURIO_REALM>/protocol/openid-connect/token" \
  -d "grant_type=client_credentials&client_id=apicurio-migration&client_secret=<MIGRATION_CLIENT_SECRET>" \
  | jq -r '.access_token' | cut -c1-20

The command prints the first characters of a JSON Web Token, starting with eyJ. Then open the apicurio-migration client in the Admin Console and confirm that the Service account roles tab lists migration-admin.