From 82ae2f701b8cd6a23d2dfe33b3af616e508d25b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 12 Dec 2025 18:46:17 +0100 Subject: [PATCH] Setup SAML locally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .gitignore | 1 + GNUmakefile | 42 ++++-- compose.yaml | 29 +++- compose/keycloak/probo-realm.json.tmpl | 194 +++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 16 deletions(-) create mode 100644 compose/keycloak/probo-realm.json.tmpl diff --git a/.gitignore b/.gitignore index 3477fef13..cdf9b92cf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ sbom-docker.json *.pem *.crt *.key +compose/keycloak/probo-realm.json diff --git a/GNUmakefile b/GNUmakefile index a717b2d97..9fa271dba 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,13 +1,17 @@ -NPM?= npm -NPX?= npx -PRETTIER?= $(NPX) prettier -GO?= go -DOCKER?= docker -SYFT ?= syft -GRYPE ?= grype -CP ?= cp -MKDIR ?= mkdir -p -MKCERT ?= mkcert +CAT ?= cat +CP ?= cp +DOCKER ?= docker +GO ?= go +GRYPE ?= grype +MKCERT ?= mkcert +MKDIR ?= mkdir -p +NPM ?= npm +NPX ?= npx +OPENSSL ?= openssl +PRETTIER ?= $(NPX) prettier +SED ?= sed +SYFT ?= syft +TAIL ?= tail DOCKER_BUILD_FLAGS?= DOCKER_BUILD= DOCKER_BUILDKIT=1 $(DOCKER) build $(DOCKER_BUILD_FLAGS) @@ -95,8 +99,8 @@ test-e2e-coverage: bin/probod-coverage ## Run e2e tests with coverage .PHONY: coverage-combined coverage-combined: coverage-report test-e2e-coverage ## Generate combined coverage report (unit + e2e) - @cat coverage.out > coverage-combined.out - @tail -n +2 coverage-e2e.out >> coverage-combined.out + @$(CAT) coverage.out > coverage-combined.out + @$(TAIL) -n +2 coverage-e2e.out >> coverage-combined.out $(GO) tool cover -html=coverage-combined.out -o=coverage-combined.html .PHONY: build @@ -206,7 +210,7 @@ clean: ## Clean the project (node_modules and build artifacts) $(RM) -rf coverage/ .PHONY: stack-up -stack-up: compose/pebble/certs/rootCA.pem ## Start the docker stack as a deamon +stack-up: compose/pebble/certs/rootCA.pem compose/keycloak/probo-realm.json ## Start the docker stack as a deamon $(DOCKER_COMPOSE) up -d .PHONY: stack-down @@ -230,9 +234,19 @@ goreleaser-check: ## Check goreleaser configuration goreleaser check compose/pebble/certs/rootCA.pem: - @$(MKDIR) -p compose/pebble/certs + @$(MKDIR) compose/pebble/certs $(MKCERT) -cert-file compose/pebble/certs/pebble.crt \ -key-file compose/pebble/certs/pebble.key \ localhost 127.0.0.1 ::1 pebble $(CP) "$$($(MKCERT) -CAROOT)/rootCA.pem" compose/pebble/certs/rootCA.pem $(CP) "$$($(MKCERT) -CAROOT)/rootCA-key.pem" compose/pebble/certs/rootCA-key.pem + +compose/keycloak/certs/cert.pem: + $(MKDIR) ./compose/keycloak/certs + $(OPENSSL) req -x509 -newkey rsa:2048 -keyout compose/keycloak/certs/private-key.pem -out compose/keycloak/certs/cert.pem -days 3650 -nodes -subj "/CN=keycloak-saml-signing" + +compose/keycloak/probo-realm.json: compose/keycloak/probo-realm.json.tmpl compose/keycloak/certs/cert.pem + $(SED) \ + -e "s|CERTIFICATE_PLACEHOLDER|$$(awk 'NR==1 {printf "%s", $$0; next} {printf "\\\\n%s", $$0}' compose/keycloak/certs/cert.pem)|g" \ + -e "s|PRIVATE_KEY_PLACEHOLDER|$$(awk 'NR==1 {printf "%s", $$0; next} {printf "\\\\n%s", $$0}' compose/keycloak/certs/private-key.pem)|g" \ + $@.tmpl > $@ diff --git a/compose.yaml b/compose.yaml index fcc228fff..9e719cfb8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -19,7 +19,7 @@ services: interval: 5s timeout: 5s retries: 5 - + minio: image: "quay.io/minio/minio" entrypoint: "sh" @@ -33,7 +33,7 @@ services: environment: MINIO_ROOT_USER: "probod" MINIO_ROOT_PASSWORD: "thisisnotasecret" - + grafana: image: "grafana/grafana:latest" ports: @@ -129,9 +129,34 @@ services: - "8056:8056" # Management API command: -dns01 ":8053" -http01 ":8055" -management ":8056" + keycloak: + image: quay.io/keycloak/keycloak:latest + user: root + environment: + KC_HOSTNAME: localhost + KC_HOSTNAME_PORT: 8082 + KC_HOSTNAME_STRICT: false + KC_HOSTNAME_STRICT_HTTPS: false + + KC_LOG_LEVEL: info + KC_METRICS_ENABLED: true + KC_HEALTH_ENABLED: true + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + command: + - "start-dev" + - "--import-realm" + - "--verbose" + ports: + - 8082:8080 + volumes: + - "./compose/keycloak:/opt/keycloak/data/import:ro" + - "keycloak-data:/opt/keycloak/data/h2:rw" + volumes: postgres-data: minio-data: grafana-data: prometheus-data: tempo-data: + keycloak-data: diff --git a/compose/keycloak/probo-realm.json.tmpl b/compose/keycloak/probo-realm.json.tmpl new file mode 100644 index 000000000..dfb0669ad --- /dev/null +++ b/compose/keycloak/probo-realm.json.tmpl @@ -0,0 +1,194 @@ +{ + "id": "probo", + "realm": "probo", + "displayName": "Probo", + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "bruteForceProtected": true, + "roles": { + "realm": [ + { + "name": "admin", + "description": "Administrator role" + }, + { + "name": "member", + "description": "Regular member role" + }, + { + "name": "viewer", + "description": "Read-only viewer role" + } + ] + }, + "defaultRoles": ["member"], + "clients": [ + { + "clientId": "http://localhost:5173/connect/saml/metadata", + "name": "Probo Console", + "description": "Probo GRC Platform - SAML Service Provider", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "frontchannelLogout": true, + "attributes": { + "saml.assertion.signature": "true", + "saml.force.post.binding": "true", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "saml.server.signature": "true", + "saml.server.signature.keyinfo.ext": "false", + "saml.signature.algorithm": "RSA_SHA256", + "saml_force_name_id_format": "true", + "saml_name_id_format": "email", + "saml.client.signature": "false", + "saml.authnstatement": "true", + "saml_single_logout_service_url_post": "http://localhost:5173/connect/saml/logout", + "saml_single_logout_service_url_redirect": "http://localhost:5173/connect/saml/logout", + "saml.onetimeuse.condition": "false" + }, + "rootUrl": "http://localhost:5173", + "baseUrl": "/", + "adminUrl": "http://localhost:5173/connect/saml/consume", + "redirectUris": ["http://localhost:5173/*"], + "webOrigins": ["http://localhost:5173"], + "protocolMappers": [ + { + "name": "email", + "protocol": "saml", + "protocolMapper": "saml-user-property-mapper", + "consentRequired": false, + "config": { + "attribute.nameformat": "Basic", + "user.attribute": "email", + "friendly.name": "email", + "attribute.name": "email" + } + }, + { + "name": "firstName", + "protocol": "saml", + "protocolMapper": "saml-user-property-mapper", + "consentRequired": false, + "config": { + "attribute.nameformat": "Basic", + "user.attribute": "firstName", + "friendly.name": "firstName", + "attribute.name": "firstName" + } + }, + { + "name": "lastName", + "protocol": "saml", + "protocolMapper": "saml-user-property-mapper", + "consentRequired": false, + "config": { + "attribute.nameformat": "Basic", + "user.attribute": "lastName", + "friendly.name": "lastName", + "attribute.name": "lastName" + } + }, + { + "name": "role", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "true", + "attribute.nameformat": "Basic", + "friendly.name": "role", + "attribute.name": "role" + } + } + ] + } + ], + "components": { + "org.keycloak.keys.KeyProvider": [ + { + "name": "custom-saml-signing", + "providerId": "rsa", + "config": { + "priority": ["200"], + "enabled": ["true"], + "active": ["true"], + + "certificate": ["CERTIFICATE_PLACEHOLDER"], + "privateKey": ["PRIVATE_KEY_PLACEHOLDER"] + } + } + ] + }, + "users": [ + { + "username": "owner@example.com", + "email": "owner@example.com", + "emailVerified": true, + "enabled": true, + "firstName": "Owner", + "lastName": "User", + "credentials": [ + { + "type": "password", + "value": "owner@example.com", + "temporary": false + } + ], + "realmRoles": ["owner"] + }, + { + "username": "admin@example.com", + "email": "admin@example.com", + "emailVerified": true, + "enabled": true, + "firstName": "Admin", + "lastName": "User", + "credentials": [ + { + "type": "password", + "value": "admin@example.com", + "temporary": false + } + ], + "realmRoles": ["admin"] + }, + { + "username": "member@example.com", + "email": "member@example.com", + "emailVerified": true, + "enabled": true, + "firstName": "Member", + "lastName": "User", + "credentials": [ + { + "type": "password", + "value": "member123", + "temporary": false + } + ], + "realmRoles": ["member"] + }, + { + "username": "viewer@example.com", + "email": "viewer@example.com", + "emailVerified": true, + "enabled": true, + "firstName": "Viewer", + "lastName": "User", + "credentials": [ + { + "type": "password", + "value": "viewer@example.com", + "temporary": false + } + ], + "realmRoles": ["viewer"] + } + ] +}