The following tools are required to perform this demo:
All the content of the YAML files will be displayed in each step, and can be applied with the related
kubectl command, except for the KinD configuration. The kubectl commands should be executed in
the same namespace. For simplicity, this demo uses the default namespace in the following instructions.
You will find the same YAML files inside the demo/ directory for inspection and/or to use them.
The demonstration can only be performed on Kubernetes 1.33.x or above, this due
to the ImageVolume feature that is required to mount the extension/module images
inside the pods. Image volumes are enabled by default starting with Kubernetes 1.35.
If you are using Kubernetes 1.33 or 1.34 with Kind, you need the kind-config.yaml file, which serves as the configuration to set up the local Kubernetes cluster with the required feature to run this demo.
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
# any feature gate can be enabled here with "Name": true
# or disabled here with "Name": false
# not all feature gates are tested, however
"ImageVolume": trueCreate the cluster with the following command:
kind create cluster --name pg-imagevolumeFor Kubernetes 1.33 or 1.34:
kind create cluster --name pg-imagevolume --config=./demo/kind-config.yamlThis demonstration requires the following operators:
All of them can be installed with the following list of commands:
kubectl apply --server-side \
-f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.28/releases/cnpg-1.28.0.yaml
kubectl apply \
-f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.5.1/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
kubectl apply \
-f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.5.1/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
kubectl apply \
-f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.5.1/kubernetes/kubernetes.yml
kubectl apply \
-f https://github.com/cert-manager/cert-manager/releases/download/v1.19.2/cert-manager.yamlNote
The -n default parameter is omitted in each of the above commands, as it is
the default namespace of the KinD cluster.
Note: it is required to wait for a few minutes to make sure everything up and running. You can check and monitor these resources with the following command:
kubectl get pods -AwDeploy all the required objects to set up a working Keycloak service.
Using CloudNativePG deploy the required PostgreSQL database to be used by Keycloak with the following file:
kubectl apply -f ./demo/keycloak-db.yamlapiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: keycloak-db
spec:
instances: 3
bootstrap:
initdb:
database: keycloak
owner: keycloak
storage:
size: 1GiThe name of our database is keycloak, and the name of the cluster keycloak-db. This information will
be useful in future steps.
The keycloak server will require certificates to serve the content over HTTPS, which will be used by PostgreSQL.
First create the issuer:
kubectl apply -f ./demo/certificates-issuer.yamlapiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: keycloak-issuer
spec:
selfSigned: {}Then generate the certificate with the following dnsNames:
keycloak-app-service- which is the default servicekeycloak-app-service.svc.cluster.local- default service with FQDNkeycloak-app- a simplified version of the domain for future testing
kubectl apply -f ./demo/keycloak-certificates.yamlapiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: keycloak-certificate
spec:
dnsNames:
- keycloak-app-service
- keycloak-app-service.svc.cluster.local
- keycloak-app
subject:
countries:
- IT
localities:
- Prato
organizations:
- cert-manager
duration: 2160h0m0s
issuerRef:
kind: ClusterIssuer
name: keycloak-issuer
privateKey:
algorithm: RSA
encoding: PKCS1
rotationPolicy: Always
secretName: keycloak-certificateOnce you have the required database and certificates, you can set up the Keycloak instance.
Deploy the Keycloak server, named keycloak-app:
kubectl apply -f ./demo/keycloak-server.yamlapiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: keycloak-app
spec:
instances: 1
db:
vendor: postgres
host: keycloak-db-rw
usernameSecret:
name: keycloak-db-app
key: username
passwordSecret:
name: keycloak-db-app
key: password
additionalOptions:
- name: log-console-output
value: json
- name: metrics-enabled
value: 'true'
http:
tlsSecret: keycloak-certificate
hostname:
strict: false
proxy:
headers: xforwarded # double check your reverse proxy sets and overwrites the X-Forwarded-* headersSome important points:
- the
tlsSecretpoints to the secret with the certificates generated by cert-manager - the
dbsection uses the default secrets generated by CloudNativePG to allow Keycloak instance to access the database service - to have a more flexible environment the
hostname.strictis set tofalse, to not enforce only one specific domain for everything
Create a generic service to access from everywhere with a simplified name:
kubectl apply -f ./demo/keycloak-service.yamlapiVersion: v1
kind: Service
metadata:
labels:
app: keycloak
app.kubernetes.io/instance: keycloak-app
app.kubernetes.io/managed-by: keycloak-operator
name: keycloak-app
spec:
ports:
- name: https
port: 443
protocol: TCP
targetPort: 8443
selector:
app: keycloak
app.kubernetes.io/instance: keycloak-app
app.kubernetes.io/managed-by: keycloak-operator
type: ClusterIPNote: this will not be used right away, but later. It serves to create more base examples in the future or to access the Keycloak instance from everywhere exposed with an Ingress.
Check for the keycloak-app-0 pod to be up and running:
kubectl get pod keycloak-app-0Enable the port forwarding with:
kubectl port-forward services/keycloak-app-service 8443And in another terminal get the temporary admin password, that will be used during the whole demonstration:
kubectl get secrets keycloak-app-initial-admin -ojsonpath='{.data.password}' | base64 -dNow access the Keycloak server in your browser from the following URL:
https://localhost:8443/admin/master/console/
Note: You will have to accept that it's a self-signed certificate in your browser.
- Username: temp-admin
- Password: (the one from the previous step)
Once you verified the Keycloak interface is accessible, you can proceed.
In this demonstration we choose a fixed realm with the name demo and a couple of pre-settings for
Clients, scopes, policies and users.
The kecloak-realm.yaml can be downloaded and applied in the following way:
kubectl apply -f ./demo/keycloak-realm.yamlThis will create a KeycloakRealmImport that will connect to our Keycloak instance and load
all the required settings. This procedure will create a Kubernetes Job that can be deleted once
completed.
Check for the job completion with:
kubectl get job realm-demo Once the Job is in Completed state, verify that the realm demo was created from the web interface.
Now it's time to create the CloudNativePG cluster that will use Keycloak for the authentication and authorization method.
First create the initial SQL ConfigMap called pg-init-sql that contains some examples of the required
roles and permissions:
kubectl apply -f ./demo/init_sql-configmap.yamlapiVersion: v1
kind: ConfigMap
metadata:
name: pg-init-sql
data:
init.sql: |
-- Create the 'users' table
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create the 'orders' table
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
product VARCHAR(100),
amount INTEGER,
ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample users
INSERT INTO users (username, email) VALUES
('alice', 'alice@example.com'),
('bob', 'bob@example.com');
-- Insert sample orders
INSERT INTO orders (user_id, product, amount) VALUES
(1, 'Widget', 3),
(2, 'Gadget', 5);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_readonly') THEN
CREATE ROLE app_readonly LOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_readwrite') THEN
CREATE ROLE app_readwrite LOGIN;
END IF;
END$$;
REVOKE CONNECT ON DATABASE appdb FROM PUBLIC;
GRANT CONNECT ON DATABASE appdb TO app_readonly, app_readwrite;
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO app_readonly, app_readwrite;
GRANT CREATE ON SCHEMA public TO app_readwrite;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readwrite;
GRANT INSERT ON ALL TABLES IN SCHEMA public TO app_readwrite;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_readonly;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO app_readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO app_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT ON TABLES TO app_readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_readwrite;Create the CloudNativePG cluster with the required configurations:
kubectl apply -f ./demo/pg_oauth-db.yamlapiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-oauth
annotations:
cnpg.io/validation: disabled
spec:
instances: 3
#logLevel: debug
env:
- name: "PGOAUTHDEBUG"
value: "UNSAFE"
- name: "SSL_CERT_DIR"
value: "/projected/certificate/"
- name: "CURL_CA_BUNDLE"
value: "/projected/certificate/ca.crt"
- name: "PGOAUTHCAFILE"
value: "/projected/certificate/ca.crt"
# Bootstrap from scratch and run our init SQL from a ConfigMap
bootstrap:
initdb:
database: appdb
owner: app
postInitApplicationSQLRefs:
configMapRefs:
- name: pg-init-sql
key: init.sql
projectedVolumeTemplate:
sources:
- secret:
name: keycloak-certificate
items:
- key: ca.crt
path: certificate/ca.crt
- key: tls.crt
path: certificate/tls.crt
managed:
roles:
- name: app_readonly
ensure: present
comment: readonly user
login: true
- name: app_readwrite
ensure: present
comment: readwrite user
login: true
storage:
size: 1Gi
postgresql:
extensions:
- name: kc-validator
image:
reference: ghcr.io/cloudnative-pg/postgres-keycloak-oauth-validator-testing:18-dev-trixie
pullPolicy: Always
ld_library_path:
- system
parameters:
oauth_validator_libraries: "kc_validator"
kc.token_endpoint: "https://keycloak-app-service:8443/realms/demo/protocol/openid-connect/token"
kc.audience: "postgres-resource"
kc.resource_name: "appdb"
kc.client_id: "postgres-resource"
kc.http_timeout_ms: "2000"
#kc.expected_issuer: "https://keycloak-app/realms/demo"
kc.debug: "true"
kc.log_body: "on"
log_min_messages: "debug1"
pg_hba:
- host all all 0.0.0.0/0 oauth issuer="https://keycloak-app-service:8443/realms/demo" scope=db_access validator="kc_validator" delegate_ident_mapping=1Where:
env: contains the required environment variablesPGAUTHDEBUG: allows you to use the next variablePGAUTHCAFILE: accepts the self-signed certificate generated using the cert-managerSSL_CERT_DIR: is the directory path containing the certificatesCURL_CA_BUNDLE: is the path to the actual CA bundle
projectedVolumeTemplate: mounts the generated certificate inside the container as a projected volumeboostrap: is required to execute the initial SQL commandspostInitApplicationSQLRefs: specifies the reference to retrieve the actual SQL configmap created in the previous step
postgresql.extensions: mounts the specified image inside the container and to make thekc_validatorextension available to be loaded inside PostgreSQLparameters: contains the full section of the PostgreSQL GUC related to the OAuth validation.oauth_validator_libraries: loads thekc_validatorto configure it using the GUCs prefixed withkc.
pg_hba: contains the HBA entry to match the client OAuth requests and how these request are handled
Let's test the connection to our PostgreSQL instance using Keycloak for the OAuth part.
For simplicity, create a pod inside the same namespace to access everything from the same network:
kubectl run debian --image=debian:unstable -- sleep 100000Note: it is important to use the unstable release since it contains the PostgreSQL version 18. The previous versions
do not support the OAuth feature.
Get into the pod:
kubectl exec -ti debian -- bashTrigger the following command:
apt update && apt dist-upgrade -y && apt install -y postgresql-client libpq-oauth
These commands will install psql and the required libraries to support libpq-oauth.
From another terminal, get the CA for the certificates previously generated for Keycloak, otherwise, you will not be able to verify the certificates:
kubectl get secrets keycloak-certificate -ojsonpath='{.data.ca\.crt}' | base64 -dPlace the content in a file inside the debian pod:
echo "<content....>" > /root/ca.crt
Try the first login:
PGOAUTHDEBUG=UNSAFE PGOAUTHCAFILE=/root/ca.crt psql "host=pg-oauth-rw user=app_readonly dbname=appdb oauth_issuer=https://keycloak-app-service:8443/realms/demo oauth_client_id=appA oauth_client_secret=XyIXBUgsLhgvJJO4EQrcp8iJvHqaJIjm oauth_scope='db_access'"
This will offer a URL that needs to be open in your browser, but first you need to replace the shown domain
with localhost. This requires the port-forward command to still be running to expose the Keycloak service.
Enter the requested data:
- Username: peggie
- Password: 123123
Once the authorization is done, your psql command should be already logged in.