From 43a72a22ff12454b6ca7dff5667c006fd68ce267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schmitz=20von=20H=C3=BClst?= Date: Mon, 19 Feb 2024 13:33:01 +0100 Subject: [PATCH] Add example for using SPIRE for mTLS with Keycloak (#248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add example for using Spire for mTLS with Keycloak Signed-off-by: Moritz Schmitz von Hülst * Minor improvement to the README.md Signed-off-by: Moritz Schmitz von Hülst * But I still need to learn GitHub Markdown format Signed-off-by: Moritz Schmitz von Hülst * Make it more obvious that it works without a (correct) password Signed-off-by: Moritz Schmitz von Hülst * Add warning for Kubernetes 1.29+ feature Signed-off-by: Moritz Schmitz von Hülst * Move ghostunnel into an initContainer with restartPolicy=Always Signed-off-by: Moritz Schmitz von Hülst * Apply suggestions from code review Co-authored-by: kfox1111 Signed-off-by: Moritz Schmitz von Hülst * Move java-spiffe-helper-properties into extraDeploy of the Keycloak chart and pin node version to it has a matching rancher/kubectl image Signed-off-by: Moritz Schmitz von Hülst --------- Signed-off-by: Moritz Schmitz von Hülst Co-authored-by: kfox1111 --- .../keycloak-config-cli-using-spire/README.md | 67 +++++++++ .../keycloak-config-cli.yaml | 84 +++++++++++ .../keycloak-values.yaml | 134 ++++++++++++++++++ .../spire-values.yaml | 26 ++++ 4 files changed, 311 insertions(+) create mode 100644 examples/keycloak-config-cli-using-spire/README.md create mode 100644 examples/keycloak-config-cli-using-spire/keycloak-config-cli.yaml create mode 100644 examples/keycloak-config-cli-using-spire/keycloak-values.yaml create mode 100644 examples/keycloak-config-cli-using-spire/spire-values.yaml diff --git a/examples/keycloak-config-cli-using-spire/README.md b/examples/keycloak-config-cli-using-spire/README.md new file mode 100644 index 0000000..ca18cda --- /dev/null +++ b/examples/keycloak-config-cli-using-spire/README.md @@ -0,0 +1,67 @@ +# keycloak-config-cli using spire + +> [!WARNING] +> This example uses +> the [`SidecarContainers`](https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/#enabling-sidecar-containers) +> feature. This is only enabled by default in Kubernetes 1.29+. + +This example shows how to leverage SPIRE in establishing an mTLS connection +between [Keycloak](https://www.keycloak.org/) and [keycloak-config-cli](https://github.com/adorsys/keycloak-config-cli), +a tool to configure Keycloak. + +## Setup + +1. Create a local cluster for testing + +```shell +kind create cluster --image kindest/node:v1.29.0 +``` + +2. Install CRDs + +```shell +helm upgrade --install -n spire-server spire-crds ../../charts/spire-crds --create-namespace +``` + +3. Install `spire-server` + +```shell +helm upgrade --install -n spire-server spire ../../charts/spire --create-namespace -f spire-values.yaml +``` + +4. Install `keycloak` (this also configures Keycloak for client certificate authentication) + +```shell +helm upgrade --install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak -f keycloak-values.yaml +``` + +5. Install `keycloak-config-cli` + +```shell +kubectl apply -f keycloak-config-cli.yaml +``` + +6. Verify the realm config at the bottom of [keycloak-config-cli.yaml](./keycloak-config-cli.yaml) has been created! +7. Cleanup + +```shell +kind delete cluster +``` + +## Notes + +### java-spiffe-helper as Keycloak initContainer + +This example uses [java-spiffe-helper](https://github.com/spiffe/java-spiffe/tree/main/java-spiffe-helper) as an +initContainer for Keycloak. It fetches the certificates from the `spire-agent` and conveniently provides them to +Keycloak in `pkcs12` format. + +> [!IMPORTANT] +> Keycloak does not rotate the certificates like SPIRE does. If you want to run the `keycloak-config-cli` +> job again, you need to make sure Keycloak is also restarted/provided with non-expired certificates. + +### Common name as username + +This example is configured to read the username from the common name (`CN`) from the client certificate. Keycloak has +some options there, this looked like the easiest one. SPIRE joins the values from `dnsNameTemplates` in the +common name section of the certificate, so make sure you can somehow extract the username from it. diff --git a/examples/keycloak-config-cli-using-spire/keycloak-config-cli.yaml b/examples/keycloak-config-cli-using-spire/keycloak-config-cli.yaml new file mode 100644 index 0000000..bce260d --- /dev/null +++ b/examples/keycloak-config-cli-using-spire/keycloak-config-cli.yaml @@ -0,0 +1,84 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: keycloak-config-cli + labels: + app: keycloak-config-cli +spec: + backoffLimit: 1 + template: + metadata: + labels: + app: keycloak-config-cli + app.kubernetes.io/instance: keycloak-config-cli # This needs to match the podSelector for the SpiffeID + spec: + initContainers: + - name: ghostunnel + image: ghostunnel/ghostunnel:v1.7.3 + imagePullPolicy: IfNotPresent + restartPolicy: Always + args: + - client + - --use-workload-api-addr + - unix:///run/spire/agent-sockets/spire-agent.sock # The filename depends on what the spire-agent uses + - --listen + - localhost:8080 # Listen on local http + - --target + - keycloak:8443 # Tunnel via https + - --status + - http://0.0.0.0:6060 + ports: + - containerPort: 8080 + name: listen + protocol: TCP + - containerPort: 6060 + name: readiness + protocol: TCP + readinessProbe: + httpGet: + path: /_status + port: readiness + volumeMounts: + - name: spire-sockets + mountPath: /run/spire/agent-sockets + readOnly: true + containers: + - name: keycloak-config-cli + image: adorsys/keycloak-config-cli:latest + imagePullPolicy: IfNotPresent + env: + - name: KEYCLOAK_URL + value: "http://127.0.0.1:8080" + - name: KEYCLOAK_USER + value: "keycloak-config-cli" + - name: KEYCLOAK_PASSWORD + value: "doesn't matter, since we are authenticated via the client certificate" + - name: KEYCLOAK_CLIENTID + value: "keycloak-config-cli" # This is the client created on bootstrapping Keycloak via the keycloak-config-cli sidecar + volumeMounts: + - name: realm + mountPath: /config + restartPolicy: OnFailure + volumes: + - name: realm + configMap: + name: keycloak-config-cli + - name: spire-sockets + hostPath: + path: /run/spire/agent-sockets # This needs to match the path mounted by the spire-agent + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: keycloak-config-cli + labels: + app: keycloak-config-cli +data: + keycloak-config-cli.json: | + { + "id": "keycloak-config-cli", + "realm": "keycloak-config-cli", + "enabled": true + } diff --git a/examples/keycloak-config-cli-using-spire/keycloak-values.yaml b/examples/keycloak-config-cli-using-spire/keycloak-values.yaml new file mode 100644 index 0000000..6fb83e8 --- /dev/null +++ b/examples/keycloak-config-cli-using-spire/keycloak-values.yaml @@ -0,0 +1,134 @@ +extraDeploy: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: java-spiffe-helper-properties + data: + java-spiffe-helper.properties: | + keyStorePath=/certs/keystore.p12 + keyStorePass=password + keyPass=password + trustStorePath=/certs/truststore.p12 + trustStorePass=password + keyStoreType=pkcs12 + keyAlias=spiffe + spiffeSocketPath=unix:/run/spire/agent-sockets/spire-agent.sock +service: + extraPorts: + - name: https + port: 8443 + targetPort: 8443 +extraEnvVars: + - name: KC_HTTPS_CLIENT_AUTH + value: "request" + - name: KC_HTTPS_KEY_STORE_FILE + value: "/certs/keystore.p12" + - name: KC_HTTPS_KEY_STORE_PASSWORD + value: "password" + - name: KC_HTTPS_KEY_STORE_TYPE + value: "pkcs12" + - name: KC_HTTPS_TRUST_STORE_FILE + value: "/certs/truststore.p12" + - name: KC_HTTPS_TRUST_STORE_PASSWORD + value: "password" + - name: KC_HTTPS_TRUST_STORE_TYPE + value: "pkcs12" +initContainers: + - name: java-spiffe-helper + image: ghcr.io/spiffe/java-spiffe-helper:0.8.5 + imagePullPolicy: IfNotPresent + restartPolicy: Always + readinessProbe: + exec: + command: + - ls + - /certs/truststore.p12 + volumeMounts: + - name: java-spiffe-helper-properties + mountPath: /app/java-spiffe-helper.properties + subPath: java-spiffe-helper.properties + - name: spire-sockets + mountPath: /run/spire/agent-sockets + readOnly: true + - name: certs + mountPath: /certs +extraVolumeMounts: + - name: certs + mountPath: /certs +extraVolumes: + - name: java-spiffe-helper-properties + configMap: + name: java-spiffe-helper-properties + - name: spire-sockets + hostPath: + path: /run/spire/agent-sockets + type: DirectoryOrCreate + - name: certs + emptyDir: {} +auth: + adminPassword: "password" +keycloakConfigCli: + enabled: true + configuration: + master.json: | + { + "id": "master", + "realm": "master", + "enabled": true, + "users": [ + { + "username": "keycloak-config-cli", + "enabled": true, + "realmRoles": [ + "admin" + ], + "credentials": [ + { + "type": "password", + "value": "it-really-doesn't-matter-what-you-put-here" + } + ] + } + ], + "authenticationFlows": [ + { + "alias": "direct grant x509", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticatorConfig": "username", + "authenticator": "direct-grant-auth-x509-username", + "requirement": "REQUIRED", + "priority": 0 + } + ] + } + ], + "authenticatorConfig": [ + { + "alias": "username", + "config": { + "x509-cert-auth.regular-expression": "CN=(keycloak-config-cli)", + "x509-cert-auth.mapper-selection": "Username or Email", + "x509-cert-auth.mapping-source-selection": "Match SubjectDN using regular expression" + } + } + ], + "clients": [ + { + "clientId": "keycloak-config-cli", + "name": "keycloak-config-cli", + "enabled": true, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": true, + "publicClient": true, + "authenticationFlowBindingOverrides": { + "direct_grant": "direct grant x509" + }, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0 + } + ] + } diff --git a/examples/keycloak-config-cli-using-spire/spire-values.yaml b/examples/keycloak-config-cli-using-spire/spire-values.yaml new file mode 100644 index 0000000..b1f401d --- /dev/null +++ b/examples/keycloak-config-cli-using-spire/spire-values.yaml @@ -0,0 +1,26 @@ +spire-server: + controllerManager: + identities: + clusterSPIFFEIDs: + default: + enabled: false + keycloak: + spiffeIDTemplate: spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }} + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: default + podSelector: + matchLabels: + app.kubernetes.io/instance: keycloak + dnsNameTemplates: + - keycloak + keycloak-config-cli: + spiffeIDTemplate: spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }} + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: default + podSelector: + matchLabels: + app.kubernetes.io/instance: keycloak-config-cli + dnsNameTemplates: + - keycloak-config-cli # This is the common name used for the certificate. In this case, the username