Skip to content

Keycloak Airgap CRLs

In connected environments, Keycloak can use OCSP (Online Certificate Status Protocol) to check whether a client certificate has been revoked. In a true airgap, OCSP responders are unreachable, so you must either:

  • disable revocation checks (not recommended), or
  • rely on CRLs (Certificate Revocation Lists) that you download before entering the airgap and load locally into Keycloak.

This guide documents a repeatable workflow to:

  1. Collect one or more CRL files (*.crl).
  2. Package them as a small OCI data image and wrap that into a Zarf package.
  3. Deploy that Zarf package before Keycloak.
  4. Mount the OCI data image into the Keycloak pod via Kubernetes ImageVolume.
  5. Configure Keycloak’s X.509 authenticator to read CRLs from the generated CRL path list.

You do not need to build a custom Keycloak image.


Mounting CRL files via Kubernetes ImageVolume requires:

  • Kubernetes 1.31–1.34 — supported, but the ImageVolume feature gate must be explicitly enabled on both the API server and kubelet.
  • Kubernetes 1.35+ImageVolume is enabled by default; no feature gate configuration needed.

This requirement applies to all Kubernetes distributions (EKS, GKE, RKE2, k3s, etc.). For k3s/k3d-specific configuration see Enable ImageVolume on uds-k3d in the bundle configuration section below.


The intent of this workflow is that users run one script to fetch (or accept) a CRL bundle, filter it, build an OCI “data image”, and emit everything you need to wire Keycloak up.

When you run create-keycloak-crl-oci-volume-package.sh, it will:

  1. Acquire CRLs as a ZIP

    • If you provide --crl-zip <path>, it uses that ZIP.
    • Otherwise it downloads the DoD “ALL CRL ZIP” from DISA.
  2. Extract and filter CRL files

    • Unzips and finds all *.crl.
    • By default it excludes CRLs whose filenames start with:
      • DODEMAIL* (email) and
      • DODSW* (software)
    • You can include them with flags (below).
  3. Stage the CRLs into an OCI data image

    • Copies the selected CRLs into a staging directory.
    • Builds a tiny OCI image (FROM scratch) that contains only the CRL files.
  4. Generate the Keycloak “CRL Path” string

    • Sorts the CRL filenames.
    • Builds a single string of relative paths using ## as the delimiter.
    • Writes it to ./keycloak-crls/keycloak-crl-paths.txt.
  5. Create a Zarf package that delivers the image

    • Creates a keycloak-crls Zarf package that contains the OCI image.
    • Writes the package to ./keycloak-crls/zarf-package-keycloak-crls-*.tar.zst.

Run the script on a machine that has:

  • bash, curl, unzip, find, sort
  • docker (to build the OCI data image)
  • uds (so the script can run uds zarf package create)

From the repo root (or wherever the script lives):

Terminal window
bash scripts/keycloak-crl-airgap/create-keycloak-crl-oci-volume-package.sh

That will:

  • download the DISA CRL ZIP

  • filter out DODEMAIL* and DODSW*

  • output:

    • ./keycloak-crls/keycloak-crl-paths.txt
    • ./keycloak-crls/zarf-package-keycloak-crls-*.tar.zst
Section titled “Use a pre-downloaded ZIP (recommended when preparing an airgap transfer)”
Terminal window
bash scripts/keycloak-crl-airgap/create-keycloak-crl-oci-volume-package.sh \
--crl-zip /path/to/crls.zip

This is the usual flow when you:

  1. download the ZIP on a connected machine,
  2. move it into the airgap (or a build system inside the enclave), then
  3. run the script locally to produce the package.
Terminal window
bash scripts/keycloak-crl-airgap/create-keycloak-crl-oci-volume-package.sh --include-email
Terminal window
bash scripts/keycloak-crl-airgap/create-keycloak-crl-oci-volume-package.sh --include-sw

After the script finishes, you should have:

  • CRL path list (paste into Bundle Keycloak config):

    • ./keycloak-crls/keycloak-crl-paths.txt
  • Zarf package to add to your bundle/deploy:

    • ./keycloak-crls/zarf-package-keycloak-crls-*.tar.zst

Before deploying, configure your bundle to:

  1. deploy the CRL package before Keycloak
  2. mount the CRL data image via ImageVolume
  3. configure Keycloak X.509 settings to use the generated CRL Path
  4. add policy exemptions if needed

Add the following to your bundle’s Keycloak values:

keycloak:
keycloak:
values:
- path: realmInitEnv
value:
X509_OCSP_FAIL_OPEN: "false"
X509_OCSP_CHECKING_ENABLED: "false"
X509_CRL_CHECKING_ENABLED: "true"
X509_CRL_ABORT_IF_NON_UPDATED: "false"
X509_CRL_RELATIVE_PATH: "<paste keycloak-crl-paths.txt contents here>"
- path: extraVolumes
value:
- name: ca-certs
configMap:
name: uds-trust-bundle
optional: true
- name: keycloak-crls
image:
reference: keycloak-crls:local # Common Zarf registry address; adjust for your environment
pullPolicy: Always
- path: extraVolumeMounts
value:
- name: ca-certs
mountPath: /tmp/ca-certs
readOnly: true
- mountPath: /tmp/keycloak-crls
name: keycloak-crls
readOnly: true

Add CRL package to bundle (deployment order)

Section titled “Add CRL package to bundle (deployment order)”

Make sure the CRL package deploys before Keycloak.

packages:
- name: core-base
ref: x.x.x
- name: keycloak-crls
path: ./keycloak-crls/zarf-package-keycloak-crls-<arch>-<tag>.tar.zst
ref: x.x.x
- name: core-identity-authorization
ref: x.x.x

ImageVolumes are currently blocked by UDS Policies (future support will be added), so add an exemption targeting Keycloak pods:

uds-exemptions:
uds-exemptions:
values:
- path: exemptions.custom
value:
- name: keycloak-imagevolume-exemption
exemptions:
- policies:
- RestrictVolumeTypes
matcher:
namespace: keycloak
name: "^keycloak-.*"
kind: pod
title: "Allow Keycloak ImageVolume for CRLs"
description: "Allow Keycloak pods to mount CRLs via Kubernetes ImageVolume (OCI-backed)."

If running on the dev/demo bundles (uds-k3d) with Kubernetes < 1.35, enable the feature gate via uds-config.yaml:

variables:
uds-k3d-dev:
k3d_extra_args: >-
--k3s-arg --kube-apiserver-arg=feature-gates=ImageVolume=true@server:0
--k3s-arg --kubelet-arg=feature-gates=ImageVolume=true@server:0

Deploy the fully configured bundle.

Terminal window
# For Slim Dev Bundle
UDS_CONFIG=bundles/k3d-slim-dev/uds-config.yaml uds deploy bundles/k3d-slim-dev/uds-bundle-k3d-core-slim-dev-amd64-*.tar.zst --confirm --no-progress

Terminal window
uds zarf package list | grep keycloak-crls
Terminal window
kubectl exec -n keycloak keycloak-0 -c keycloak -- ls -la /tmp/keycloak-crls

Confirm the realm’s X.509 configuration uses the CRL Path value you generated (the contents of keycloak-crl-paths.txt).

Use your normal mTLS/browser client cert flow and confirm Keycloak validates certificates without CRL-related errors.


CRLs have an expiry window (driven by nextUpdate). Treat refresh as an operational requirement:

  1. Re-download all CRLs on a connected machine.
  2. Validate nextUpdate is in the future.
  3. Rebuild and redeploy the CRL Zarf package.
  4. Restart Keycloak if needed to ensure caches are refreshed.

”Volume has a disallowed volume type of ‘image’”

Section titled “”Volume has a disallowed volume type of ‘image’””

Your UDS exemption was not applied (or did not match). Verify:

  • The exemption is included in your bundle and deployed
  • It targets the right namespace (keycloak) and pod matcher (^keycloak-.*)

The CRL image is missing or the reference is wrong. Verify:

  • CRL package is deployed before Keycloak
  • extraVolumes.image.reference matches the image reference available in the cluster registry

Keycloak logs: “Unable to load CRL from …”

Section titled “Keycloak logs: “Unable to load CRL from …””

Verify:

  • CRL files exist in the container at /tmp/keycloak-crls
  • X509_CRL_RELATIVE_PATH exactly matches keycloak-crl-paths.txt
  • CRLs are not expired (nextUpdate still in the future)