{"id":45764824,"url":"https://github.com/datarocks-ag/keycloak-provisioner","last_synced_at":"2026-04-25T19:01:57.104Z","repository":{"id":339954327,"uuid":"1152915536","full_name":"datarocks-ag/keycloak-provisioner","owner":"datarocks-ag","description":"A Go CLI tool that idempotently provisions Keycloak resources from a YAML config file. Designed as a Docker Compose init container.","archived":false,"fork":false,"pushed_at":"2026-04-25T17:17:55.000Z","size":179,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2026-04-25T17:20:01.713Z","etag":null,"topics":["devops","docker","go","keycloak","provisioner"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/datarocks-ag.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-08T16:23:40.000Z","updated_at":"2026-04-25T15:10:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/datarocks-ag/keycloak-provisioner","commit_stats":null,"previous_names":["datarocks-ag/keycloak-provisioner"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/datarocks-ag/keycloak-provisioner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datarocks-ag%2Fkeycloak-provisioner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datarocks-ag%2Fkeycloak-provisioner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datarocks-ag%2Fkeycloak-provisioner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datarocks-ag%2Fkeycloak-provisioner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/datarocks-ag","download_url":"https://codeload.github.com/datarocks-ag/keycloak-provisioner/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datarocks-ag%2Fkeycloak-provisioner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32273223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"ssl_error","status_checked_at":"2026-04-25T18:29:32.149Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["devops","docker","go","keycloak","provisioner"],"created_at":"2026-02-26T00:27:52.705Z","updated_at":"2026-04-25T19:01:57.093Z","avatar_url":"https://github.com/datarocks-ag.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# keycloak-provisioner\n\n[![CI](https://github.com/datarocks-ag/keycloak-provisioner/actions/workflows/ci.yaml/badge.svg)](https://github.com/datarocks-ag/keycloak-provisioner/actions/workflows/ci.yaml)\n![coverage](https://raw.githubusercontent.com/datarocks-ag/keycloak-provisioner/badges/.badges/develop/coverage.svg)\n\nA Go CLI tool that idempotently provisions Keycloak resources from a YAML config file. Designed as a Docker Compose init container.\n\nRelease notes are maintained in [CHANGELOG.md](CHANGELOG.md).\n\n## Features\n\n- Idempotent provisioning of realms, clients, protocol mappers, realm roles, client roles, users, and service account roles\n- Master realm configuration (SSL, users) without full provisioning\n- `sslRequired` setting on any realm (`external`, `all`, `none`)\n- User management with password setting and realm/client role assignment\n- Service account role mapping for machine-to-machine clients\n- YAML config with `${VAR}` environment variable expansion (and `$${VAR}` escape for literals)\n- Configurable strategy: `update` (default) or `create` (skip existing)\n- `--dry-run` mode that logs all intended changes without applying them\n- Exponential backoff retry for Keycloak connectivity\n- Structured JSON logging via `log/slog`\n- No external Keycloak SDK — pure `net/http`\n\n## Quick Start\n\n```bash\ndocker compose up\n```\n\nThis starts Keycloak and runs the provisioner with the example config.\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `KEYCLOAK_USER` | yes | — | Keycloak admin username |\n| `KEYCLOAK_PASSWORD` | yes | — | Keycloak admin password |\n| `KEYCLOAK_URL` | no | `http://localhost:8080` | Keycloak base URL |\n| `KEYCLOAK_CONFIG_PATH` | no | `./config.yaml` | Path to YAML config |\n| `LOG_LEVEL` | no | `info` | Log level (debug/info/warn/error) |\n\n**Security note:** Use HTTPS in production. The provisioner logs a warning when using plain HTTP.\n\n## Command-line Flags\n\n| Flag | Description |\n|---|---|\n| `--config \u003cpath\u003e` | Path to YAML config; overrides `KEYCLOAK_CONFIG_PATH` |\n| `--dry-run` | Log all intended changes without applying them |\n| `--version` | Print version and exit |\n\n## Strategy\n\nControl whether existing resources are updated or skipped using the `strategy` field:\n\n- `update` (default) — create resources if missing, update if they already exist\n- `create` — create resources if missing, skip if they already exist\n\nStrategy can be set globally or per realm. Per-realm strategy overrides the global setting.\n\n```yaml\nstrategy: \"create\"              # global: skip existing resources\n\nrealms:\n  - realm: \"my-realm\"\n    strategy: \"update\"          # override: always reconcile this realm\n```\n\n## Environment Variable Expansion\n\nString values support `${VAR}` syntax. If the variable is set in the environment, it is replaced; if unset, the placeholder is preserved as-is. Use `$${VAR}` to keep a literal `${VAR}` in the rendered config.\n\n```yaml\nsecret: \"${MY_APP_CLIENT_SECRET}\"     # replaced with env var value at load time\nliteral: \"$${MY_APP_CLIENT_SECRET}\"   # rendered as the literal string ${MY_APP_CLIENT_SECRET}\n```\n\nUnknown YAML fields are rejected at load time so typos surface immediately.\n\n## Dry-Run\n\n```bash\nkeycloak-provisioner --dry-run\n```\n\nIn dry-run mode, every mutating call logs a `DRY-RUN:` message and is skipped. Read calls pass through to Keycloak so drift between current state and config is still observed. Resources that would be created return synthetic IDs internally so the full intended sequence (clients inside a new realm, role assignments to new users, service-account role mappings on new clients) is reported in one run.\n\n## Provisioning Order\n\n1. **Master realm** (if configured) — update `sslRequired`, provision users\n2. For each realm:\n   1. **Realm** — created or updated\n   2. **Clients** — created or updated (matched by `clientId`)\n      - **Protocol mappers** — created or updated (matched by `name`)\n      - **Client roles** — created or updated\n   3. **Realm roles** — created or updated\n   4. **Service account roles** — assigned (additive, after roles exist)\n   5. **Users** — created or updated, passwords set, roles assigned (additive)\n\n## Config Example\n\nSee [config.example.yaml](config.example.yaml) for a full example. The YAML field names mirror Keycloak's realm JSON export format (camelCase), so you can copy-paste from an exported realm.\n\n```yaml\n# Optional: configure the master realm (update-only, never created)\nmasterRealm:\n  sslRequired: external\n  users:\n    - username: \"admin-new\"\n      password: \"${ADMIN_PASSWORD}\"\n      enabled: true\n      roles:\n        realm:\n          - admin\n\nrealms:\n  - realm: \"my-realm\"\n    displayName: \"My Realm\"\n    enabled: true\n    sslRequired: \"external\"\n    clients:\n      - clientId: \"my-app\"\n        secret: \"${MY_APP_CLIENT_SECRET}\"\n        enabled: true\n        protocol: \"openid-connect\"\n        redirectUris:\n          - \"https://myapp.example.com/*\"\n        protocolMappers:\n          - name: \"audience-mapper\"\n            protocolMapper: \"oidc-audience-mapper\"\n            config:\n              \"included.client.audience\": \"my-app\"\n        clientRoles:\n          - name: \"admin\"\n\n      # Service account with role assignments\n      - clientId: \"my-service\"\n        serviceAccountsEnabled: true\n        serviceAccountRoles:\n          realm:\n            - app-admin\n          clients:\n            my-app:\n              - admin\n\n    roles:\n      - name: \"app-admin\"\n        description: \"Application administrator\"\n\n    users:\n      - username: \"service-admin\"\n        password: \"${SERVICE_ADMIN_PASSWORD}\"\n        enabled: true\n        email: \"admin@example.com\"\n        firstName: \"Service\"\n        lastName: \"Admin\"\n        emailVerified: true\n        roles:\n          realm:\n            - app-admin\n          clients:\n            my-app:\n              - admin\n```\n\n## Master Realm\n\nThe `masterRealm` section configures the built-in master realm. Since the master realm always exists, it is update-only — the provisioner will never attempt to create it. This section is deliberately separate from the `realms` list to prevent accidentally applying full provisioning to master.\n\nSupported fields:\n\n- `sslRequired` — set the SSL mode (`external`, `all`, `none`)\n- `users` — create/update users in the master realm (same schema as realm users)\n\n## SSL Required\n\nSet `sslRequired` on any realm (including master) to control whether Keycloak requires SSL:\n\n| Value | Meaning |\n|---|---|\n| `external` | SSL required for external requests (recommended for production) |\n| `all` | SSL required for all requests |\n| `none` | SSL not required (development only) |\n\n## Users\n\nUsers can be provisioned in any realm (including master via `masterRealm.users`). Each user supports:\n\n| Field | Type | Description |\n|---|---|---|\n| `username` | string | **Required.** Username |\n| `password` | string | Permanent password (set on every run via reset-password API). Mutually exclusive with `initialPassword`. |\n| `initialPassword` | string | Temporary password — only set when the user is first created. The user must change it on first login. Ignored on subsequent runs if the user already exists. Mutually exclusive with `password`. |\n| `enabled` | bool | Whether the user is enabled |\n| `email` | string | Email address |\n| `firstName` | string | First name |\n| `lastName` | string | Last name |\n| `emailVerified` | bool | Whether the email is marked as verified |\n| `roles` | object | Role assignments (see below) |\n\n### User Role Assignment\n\nRoles are assigned additively — existing role mappings are never removed. Both realm roles and client roles are supported:\n\n```yaml\nroles:\n  realm:\n    - app-admin          # realm-level role\n  clients:\n    my-app:              # client ID\n      - admin            # client-level role\n```\n\nThe referenced roles and clients must already exist (either defined earlier in the config or pre-existing in Keycloak).\n\n## Service Account Roles\n\nClients with `serviceAccountsEnabled: true` can have roles assigned to their service account user via `serviceAccountRoles`. This uses the same `roles` schema as users:\n\n```yaml\nclients:\n  - clientId: \"my-service\"\n    serviceAccountsEnabled: true\n    serviceAccountRoles:\n      realm:\n        - app-admin\n      clients:\n        another-client:\n          - some-role\n```\n\nValidation enforces that `serviceAccountsEnabled` is `true` when `serviceAccountRoles` is set.\n\n## Connection Retry\n\nOn startup, the tool retries connecting to Keycloak with exponential backoff (1s initial, 30s cap, 15 retries, 5min total timeout). This handles Docker Compose startup ordering without requiring `wait-for-it` scripts.\n\n## Development\n\n```bash\nmake build            # Build binary\nmake test             # Run unit tests\nmake test-integration # Run integration tests (requires Docker)\nmake lint             # Run golangci-lint\nmake vet              # Run go vet\nmake docker           # Build Docker image\n```\n\n## Docker Compose Usage\n\n```yaml\nservices:\n  keycloak:\n    image: keycloak/keycloak:26.0\n    command: [\"start-dev\"]\n    environment:\n      KC_HEALTH_ENABLED: \"true\"\n      KEYCLOAK_ADMIN: admin\n      KEYCLOAK_ADMIN_PASSWORD: adminpass\n    ports: [\"8080:8080\", \"9000:9000\"]\n    healthcheck:\n      test: [\"CMD-SHELL\", \"{ printf 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' \u003e\u00263; grep -q '200 OK' \u003c\u00263; } 3\u003c\u003e/dev/tcp/localhost/9000\"]\n      interval: 5s\n      timeout: 5s\n      retries: 20\n\n  keycloak-provisioner:\n    image: ghcr.io/datarocks-ag/keycloak-provisioner:latest\n    depends_on:\n      keycloak: { condition: service_healthy }\n    environment:\n      KEYCLOAK_USER: admin\n      KEYCLOAK_PASSWORD: adminpass\n      KEYCLOAK_URL: http://keycloak:8080\n      KEYCLOAK_CONFIG_PATH: /config.yaml\n      MY_APP_CLIENT_SECRET: my-secret-value\n    volumes:\n      - ./config.example.yaml:/config.yaml:ro\n\n  app:\n    image: your-app\n    depends_on:\n      keycloak-provisioner:\n        condition: service_completed_successfully\n```\n\n## Container Image\n\n```bash\ndocker pull ghcr.io/datarocks-ag/keycloak-provisioner:latest\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatarocks-ag%2Fkeycloak-provisioner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdatarocks-ag%2Fkeycloak-provisioner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatarocks-ag%2Fkeycloak-provisioner/lists"}