Setting up Keycloak 26.8 as an EUDI credential issuer and verifier
Which Keycloak 26.8 features to enable for EUDI credential issuance and verification, which settings to tighten, and which fixes your build must include.
Keycloak 26.8 can issue verifiable credentials over OpenID4VCI and, for the first time, verify them over OpenID4VP. Many EUDI teams already run Keycloak for login, so it is a natural place to start. The default settings are built for a demo, though, and the two features are at different maturity levels. This article lists what we enable, what we tighten and what we check before a Keycloak issuer or verifier leaves the lab.
Everything below was checked against Keycloak 26.8.1 (released 13 October 2026), its release notes and the compiled 26.8.1 server classes. Attribute names and defaults can change between minor versions, so check them again when you upgrade.
know the maturity of each feature
Keycloak 26.8.0 promoted OpenID4VCI issuance from experimental to preview. The related flags have different statuses:
oid4vc-vci: credential issuance (OpenID4VCI). Preview.oid4vc-vp: Keycloak as an OpenID4VP verifier. Experimental.oid4vc-vci-preauth-code: pre-authorized code grant. Experimental.oid4vc-vci-rest-credential-offer: REST endpoint for creating offers. Experimental.oid4vc-mdoc: mdoc credential format. Experimental.client-auth-abca: attestation-based client authentication. Experimental.
Preview means the feature works but its configuration and APIs may still change. Experimental means it is not ready for production. Enable features one by one, for example --features=oid4vc-vci, rather than --features=preview, which turns on every preview feature at once. Leave the experimental flags off until someone has accepted the risk for a specific use case.
give issuance its own realm and posture
Create a dedicated realm instead of enabling credentials in an existing customer realm. Issuance needs different session lengths, user profile rules and audit settings from ordinary login. A separate realm also keeps the blast radius of a misconfiguration small. If the same organisation also verifies credentials, put the verifier in a third realm or cluster. The issuer signs with keys the verifier must never hold, and the two have different operators.
Set verifiableCredentialsEnabled on the realm to expose the issuer metadata and credential endpoints. We review these settings in every deployment, with the 26.8.1 defaults in parentheses:
sslRequired(defaultexternal): set toallbehind a TLS-terminating proxy.credentialOfferLifespanSrealm attribute (default 300 seconds): shorten it if offers are shown as a QR code in the same session.vc.c-nonce-lifetime-secondsrealm attribute (default 60 seconds): already short. Keep it short.oid4vci.time.claims.strategyrealm attribute (not set by default):roundorrandomizetime claims to reduce linkability.- Access token lifespan: keep it short for credential requests.
registrationAllowed(default off): keep it off. Holders should come from your system of record.- Brute force detection (default off): turn it on if holders sign in with passwords.
- Login and admin events (default off): turn them on, and ship them somewhere you retain them.
The time claims setting deserves attention. Exact iat and nbf values in a credential can link presentations of the same credential back to the moment of issuance. Keycloak 26.8 can round these values to a unit (oid4vci.time.round.unit: second, minute, hour or day) or shift them within a random window (oid4vci.time.randomize.window.seconds). Pick one and document why. Rounding to the hour or day is easy to explain to a privacy reviewer.
lock the attributes that become credential claims
A credential is only as reliable as the user attributes behind it. In 26.8.0 Keycloak fixed a case where a user-editable did attribute became the credential subject, and another where a user-editable mapped attribute could extend an SD-JWT credential's lifetime. The general rule outlasts those fixes: no attribute that feeds a credential should be editable by its holder.
In the declarative user profile, give every attribute that a credential mapper reads edit: ["admin"] permissions. Holders may still view their own data. Set the unmanaged attribute policy to ADMIN_EDIT so attributes outside the profile cannot be edited by holders either. Disable editUsernameAllowed if the username or anything derived from it appears in a credential.
Better still, have the attributes written only by the integration that imports them from the authoritative source. We cover that boundary in turning enterprise records into credential issuance.
map only the claims the credential needs
Each credential type is a client scope with the oid4vc protocol. The scope carries the format (vc.format, default dc+sd-jwt), the type (vc.verifiable_credential_type), the signing algorithm and the expiry (vc.expiry_in_seconds). Add one protocol mapper per claim, and only for the claims the credential definition requires. A mapper added "for later" is a disclosure you will have to explain later.
Keycloak's issuer metadata also advertises built-in example configurations such as oid4vc_natural_person_sd. Remove or exclude any configuration you do not issue. Wallets and verifiers read the metadata as a statement of what you are prepared to sign.
The generic scope and mappers are enough to issue an SD-JWT credential with PID-like claims. They do not by themselves give you the Person Identification Data rulebook from the EUDI Architecture and Reference Framework, an mdoc PID or registration in an official trust list. Treat those as separate work items.
On the client side, use confidential clients with PKCE (S256), exact redirect URIs and no wildcards. Disable direct access grants and the implicit flow on every client that takes part in issuance. Device flow, CIBA and token exchange should stay off unless a documented flow needs them.
prepare signing keys that satisfy HAIP
This is where a working demo most often fails once the configuration gets stricter. Keycloak's SD-JWT signer in 26.8.1 checks the certificate chain it places in the x5c header against the OpenID4VC High Assurance Interoperability Profile. If the signing certificate is self-signed, signing fails with HAIP-6.1.1 violation: signing certificate MUST NOT be self-signed. Keycloak also strips a trailing trust anchor from the chain before sending it.
In practice:
- Issue the signing certificate from a CA you operate (or a test CA in the lab), not from a key Keycloak generated for itself.
- Use an EC P-256 key and
ES256unless your credential profile says otherwise. - Load the key through a
java-keystorekey provider with the leaf and intermediates, in order. Keycloak rejects a chain in the wrong order withInvalid certificate chain. Check the order of certificates. - Place the keystore file under the realm's directory. Keycloak 26.8.1 refuses other locations with
Keystore file '...' is not under the realm directory '...'. - Point each credential scope at the key with
vc.signing_key_id, so a key rotation for login tokens does not silently change the credential signer.
In production, the CA key, keystore passwords and database credentials belong in a vault or HSM, not in files next to the server.
require proof of the holder's key
Set vc.binding_required on each credential scope so Keycloak only issues credentials bound to a wallet key. For high-assurance credentials, also set vc.key_attestations_required, with the key storage and user authentication levels your profile demands. Without a key attestation you know the wallet holds a key, but not where it is stored.
Two 26.8.0 fixes apply here. Before them, proofs without a key attestation were accepted even when one was required, and attestation certificate chains were validated against the system trust store without EKU or revocation checks. Configure the attestation trust material explicitly rather than relying on the JVM's default CA list.
keep the riskier issuance paths off
The authorization code flow, started from an authenticated session, is the safest default for issuance. 26.8 added an application initiated action so a signed-in user can request a credential without a separately delivered offer.
The pre-authorized code grant is useful when the user is identified elsewhere, for example at a branch counter. It is also where several 26.8.0 fixes landed: unbounded offer expiry, refreshing a targeted offer to bypass offer-required issuance and role name confusion in targeted offers. If you enable it, keep offer lifetimes short, require a transaction code and deliver that code over a different channel from the offer itself.
If you issue JSON-LD credentials, note that before 26.8.0 the signer fetched remote @context URLs over HTTP with no allowlist. Restrict outbound traffic from the Keycloak hosts so the server can only reach the JWKS, context, CRL and OCSP endpoints you expect.
treat the native verifier as experimental
With oid4vc-vp enabled, Keycloak can act as an OpenID4VP verifier through an identity provider of type oid4vp. Its configuration takes a DCQL query (dcqlQuery), trusted issuer keys (trustedIssuerJwks) or trust material from other providers (trustMaterialIdps), a response mode (responseMode) and a wallet scheme (walletScheme, default openid4vp://).
A minimal query for an SD-JWT credential with three claims looks like this:
{
"credentials": [
{
"id": "pid",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["urn:example:pid:1"] },
"claims": [
{ "path": ["given_name"] },
{ "path": ["family_name"] },
{ "path": ["birthdate"] }
]
}
]
}
Request only what the relying party's purpose needs. If an age check is the goal, ask for an age-over attribute where the credential has one, not the full date of birth.
Prefer the direct_post.jwt response mode, which encrypts the wallet's response and is what HAIP expects. Pinning issuer keys in trustedIssuerJwks works for a pilot with one issuer. It becomes a manual rotation task once you accept credentials from several issuers, and it does not cover ETSI trust lists or issuer DIDs. Plan your trust model before the verifier goes live, and test the session binding cases we list in OpenID4VP replay protection.
harden the server around the features
None of the credential settings above help if the server around them is exposed. For any deployment beyond a laptop:
- Run
kc.sh start, notstart-dev, with TLS andsslRequiredset toall. - Set an explicit hostname (
--hostname) and a separate admin hostname (--hostname-admin). Do not publish the admin console on the internet. - Behind a reverse proxy, set
--proxy-headersto match the proxy and make sure clients cannot inject those headers themselves. - Rotate or remove the bootstrap admin once a real administrator exists.
- Give issuer operators and verifier operators separate, narrowly scoped admin roles instead of
realm-admin.
make sure your build includes the fixes
Run 26.8.1 or a later patch release. The 26.8.0 release fixed the OpenID4VCI issues linked above, plus missing ownership checks when deleting issued credentials and the acceptance of RSA1_5 for encrypted credential requests. The 26.8.1 release fixed nineteen CVEs. Several touch identity brokering and supporting services that EUDI deployments rely on:
- CVE-2026-18569: OIDC brokers accepted unsigned backchannel logout tokens when signature validation was disabled. Signature checks are now mandatory.
- CVE-2026-15943: identity provider updates could reuse a masked client secret after the token URL changed.
- CVE-2026-103884: a client-supplied X.509 CRL distribution point path could escape the configuration directory.
- CVE-2026-107121: SMTP with
starttls=trueallowed a plaintext downgrade. Credential offer emails go through this path.
The 26.8.1 upgrading guide describes the behaviour changes these fixes bring. Read it before you upgrade rather than after.
check before you go live
We use a short acceptance list before an issuer or verifier receives real users:
- Issuer metadata at
/.well-known/openid-credential-issuerlists only the credential configurations you intend to issue. - Signing succeeds with the CA-issued key, and the
x5cheader contains the leaf and intermediates but no root. - A holder cannot change any attribute that appears in a credential, through the account console or the account API.
- A credential request without a holder proof, or without a required key attestation, is rejected.
- An expired offer and an expired
c_nonceare rejected. - Issued
iatandnbfvalues follow the time claims strategy you chose. - The verifier rejects a credential from an untrusted issuer and a response bound to another request.
- Login, admin and broker events reach your log store, and the issuer and verifier realms have separate operators.
- The running version is 26.8.1 or later, and the experimental flags that are on have a named owner.
Our identity engineering practice sets up and reviews Keycloak issuers and verifiers for EUDI projects. If you are moving a lab setup towards production, send us your current configuration and target credential profile and we will scope the review together.
Working on something similar? Talk to us