Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example for using SPIRE for mTLS with Keycloak #248

Merged
merged 8 commits into from
Feb 19, 2024
81 changes: 81 additions & 0 deletions examples/keycloak-config-cli-using-spire/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# keycloak-config-cli using spire

moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved
This example shows how to leverage Spire in establishing an mTLS connection
moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved
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
```

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 `java-spiffe-helper` properties. These will be needed for the `keycloak` installation in the next step

```shell
kubectl apply -f java-spiffe-helper.yaml
```

5. 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
```

6. Install `ghostunnel` (this creates the mTLS tunnel to Keycloak using Spire-issued certificates)

```shell
kubectl apply -f ghostunnel.yaml
```

7. Install `keycloak-config-cli`

```shell
kubectl apply -f keycloak-config-cli.yaml
```

8. Verify the realm config at the bottom of [keycloak-config-cli.yaml](./keycloak-config-cli.yaml) has been created!
9. 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`
moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved
> job again, you need to make sure Keycloak is also restarted/provided with non-expired certificates.

### Ghostunnel as separate deployment

The idea of `keycloak-config-cli` is to run only once. A `Job` resource is the perfect fit. However, a `Job` needs the
main process to exit to become `Completed`. `ghostunnel` does not complete. So for the purpose of this example it runs
within its own deployment. In a more practical example one would bake the `ghostunnel` into the main container and run
it as a background job to the main process.
moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved

### 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
moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved
common name section of the certificate, so make sure you can somehow extract the username from it.
66 changes: 66 additions & 0 deletions examples/keycloak-config-cli-using-spire/ghostunnel.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
apiVersion: v1
kind: Service
metadata:
name: ghostunnel
spec:
selector:
app: ghostunnel
ports:
- protocol: TCP
port: 8080
name: listen
targetPort: listen
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ghostunnel
labels:
app: ghostunnel
spec:
replicas: 1
selector:
matchLabels:
app: ghostunnel
template:
metadata:
labels:
app: ghostunnel
app.kubernetes.io/instance: ghostunnel # This needs to match the podSelector for the SpiffeID
spec:
containers:
- name: ghostunnel
image: ghostunnel/ghostunnel:v1.7.3
imagePullPolicy: IfNotPresent
args:
- client
- --use-workload-api-addr
- unix:///run/spire/agent-sockets/spire-agent.sock # The filename depends on what the spire-agent uses
- --listen
- 0.0.0.0:8080 # Listen on local http
- --target
- keycloak:8443 # Tunnel via https
- --status
- http://0.0.0.0:6060
- --unsafe-listen # Required to listen on 0.0.0.0. This (and listening to 0.0.0.0) is not needed when running the process as a sidecar or as process baked into the main image
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
restartPolicy: Always
volumes:
- name: spire-sockets
hostPath:
path: /run/spire/agent-sockets # This needs to match the path mounted by the spire-agent
type: DirectoryOrCreate
15 changes: 15 additions & 0 deletions examples/keycloak-config-cli-using-spire/java-spiffe-helper.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved
apiVersion: v1
kind: ConfigMap
metadata:
name: java-spiffe-helper
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
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
spec:
containers:
- name: keycloak-config-cli
image: adorsys/keycloak-config-cli:latest
imagePullPolicy: IfNotPresent
env:
- name: KEYCLOAK_URL
value: "http://ghostunnel: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
---
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
}
119 changes: 119 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,119 @@
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:
moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved
- 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
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
configMap:
name: java-spiffe-helper
- 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
}
]
}
35 changes: 35 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,35 @@
spiffe-oidc-discovery-provider:
tools:
kubectl:
image:
tag: v1.26.10 # Only required if the rancher/kubectl image doesn't exist for your k8s version
moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved
spire-server:
tools:
kubectl:
image:
tag: v1.26.10 # Only required if the rancher/kubectl image doesn't exist for your k8s version
moritzschmitz-oviva marked this conversation as resolved.
Show resolved Hide resolved
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
ghostunnel:
spiffeIDTemplate: spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: default
podSelector:
matchLabels:
app.kubernetes.io/instance: ghostunnel
dnsNameTemplates:
- keycloak-config-cli # This is the common name used for the certificate. In this case, the username