Skip to content

Commit

Permalink
Add example for using SPIRE for mTLS with Keycloak (#248)
Browse files Browse the repository at this point in the history
* Add example for using Spire for mTLS with Keycloak

Signed-off-by: Moritz Schmitz von Hülst <[email protected]>

* Minor improvement to the README.md

Signed-off-by: Moritz Schmitz von Hülst <[email protected]>

* But I still need to learn GitHub Markdown format

Signed-off-by: Moritz Schmitz von Hülst <[email protected]>

* Make it more obvious that it works without a (correct) password

Signed-off-by: Moritz Schmitz von Hülst <[email protected]>

* Add warning for Kubernetes 1.29+ feature

Signed-off-by: Moritz Schmitz von Hülst <[email protected]>

* Move ghostunnel into an initContainer with restartPolicy=Always

Signed-off-by: Moritz Schmitz von Hülst <[email protected]>

* Apply suggestions from code review

Co-authored-by: kfox1111 <[email protected]>
Signed-off-by: Moritz Schmitz von Hülst <[email protected]>

* 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 <[email protected]>

---------

Signed-off-by: Moritz Schmitz von Hülst <[email protected]>
Co-authored-by: kfox1111 <[email protected]>
  • Loading branch information
moritzschmitz-oviva and kfox1111 authored Feb 19, 2024
1 parent cc8ec89 commit 43a72a2
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 0 deletions.
67 changes: 67 additions & 0 deletions examples/keycloak-config-cli-using-spire/README.md
Original file line number Diff line number Diff line change
@@ -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.
84 changes: 84 additions & 0 deletions examples/keycloak-config-cli-using-spire/keycloak-config-cli.yaml
Original file line number Diff line number Diff line change
@@ -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
}
134 changes: 134 additions & 0 deletions examples/keycloak-config-cli-using-spire/keycloak-values.yaml
Original file line number Diff line number Diff line change
@@ -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
}
]
}
26 changes: 26 additions & 0 deletions examples/keycloak-config-cli-using-spire/spire-values.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 43a72a2

Please sign in to comment.