Building Access Management for ISO 27001 on Keycloak
How we built a lightweight IAM solution on Keycloak to manage permissions across all our tools - driven by ISO 27001, not by budget for an enterprise product.
We're a smallish company (~80 employees) going through ISO 27001 certification. One of the requirements: proper access management with documented processes and protocols around who has access to what. We didn't want to maintain a massive Excel spreadsheet. So we built something on top of Keycloak instead.
Turns out, that was harder than you'd naively assume.
The Problem
We have a lot of tools. On the engineering side there's GitHub, ArgoCD, Grafana, SonarQube, and more. But it's not just dev tooling - there's Float for people management, Personio for HR, and a whole bunch of others across the company. Each tool has its own permission model, its own admin UI, its own way of managing who can do what.
For day-to-day work, this was honestly fine. At our size, you mostly know who has access to what, and when someone needs something, you just grant it. It wasn't a huge problem.
But ISO 27001 doesn't care about "mostly fine." It wants documented access management. It wants to know that you have a process for granting, reviewing, and revoking permissions - across everything. And when we sat down to actually map out all permissions across all tools for all people, we realized the landscape was more complex than we thought.
Some permissions are global: "this person can use Grafana." Others are project-scoped: "this person can push to these GitHub repos" or "this person can deploy this service in ArgoCD." And a single project might span resources across half a dozen tools.
We needed one place to document and manage all of this. Something more structured than a spreadsheet, but less than an enterprise IAM suite we'd never fully use.
Why Not Just Buy Something?
I briefly looked at Okta, which is probably the most well-known commercial IAM platform. It's a proprietary, cloud-based solution with thousands of pre-built integrations - and for a SaaS-heavy, US-centric stack, it might be a good fit. But we're a German company, and a fair number of the tools we use (Float, Personio, and others) are either Germany-specific or niche enough that there aren't pre-built integrations. So Okta would have taken some of the pain away on the well-known dev tools, but we'd still be doing custom work for everything else - while paying a per-user subscription on top.
More generally, the commercial options we came across either solved a much bigger problem than we had (and came with a price tag that made no sense for 80 people), or they imposed a permission model that didn't map to how we actually work.
What we needed was conceptually simple:
- Tools (GitHub, ArgoCD, Grafana, SonarQube, Float, Personio, ...) have permissions
- Some permissions are global - you can use the tool at all
- Some permissions are project-scoped - you have a specific role in a specific project's resources within that tool
- Projects are cross-cutting - a single project spans repos in GitHub, dashboards in Grafana, namespaces in ArgoCD, and so on
We needed a model that could represent all of this. Keycloak gave us the building blocks.
The Model
The Building Blocks: Groups and Roles
Keycloak gives you two core primitives: groups and roles. The trick is deciding what each one represents in your domain. Here's what we landed on.
Groups = who you are
Groups represent organizational structure - what kind of employee you are and which projects you're on:
/projects
/project-alpha
/project-beta
/developers
/consultants
/hr
/management
A person can be in multiple groups. A developer working on Project Alpha and Project Beta would be in /developers, /projects/project-alpha, and /projects/project-beta.
Client roles = what you can do, and where
In Keycloak, a "client" maps to an application. Each client has its own set of roles. This is where it gets interesting, because some tools only have global permissions while others have project-scoped ones.
A tool like Float (our people management tool) is simple - you're either a user or an admin:
float (client)
├── float-user
└── float-admin
But a tool like GitHub has both global roles and project-scoped roles:
github (client)
├── github-org-member (global - you're in the org)
├── github-org-admin (global - org-level admin)
├── github-user-project-alpha (project-scoped)
├── github-admin-project-alpha (project-scoped)
├── github-user-project-beta (project-scoped)
└── github-admin-project-beta (project-scoped)
The naming convention encodes the scope right into the role name: {tool}-{permission}-{project} for scoped roles, {tool}-{permission} for global ones. It's not the most elegant thing in the world, but it's explicit and it works.
How It Comes Together
The data model alone doesn't do much - what makes it work is the layer we built on top of Keycloak to make it self-service.
Tools are enabled per project. Our internal IDP has a permissions section where project managers can toggle which tools their project uses. When a PM enables GitHub for Project Alpha, the corresponding roles (github-user-project-alpha, github-admin-project-alpha) get created automatically and mapped to the project's group. No manual Keycloak fiddling required.
Users are always assigned to groups, never to roles directly. Roles flow from group membership. When you're added to /projects/project-alpha, you inherit whatever tool roles that project has enabled. There's also a base employees group that grants a handful of global roles everyone needs - the kind of stuff you get on day one regardless of which project you're on.
Ownership is distributed. Project managers own their project groups - they can add people, and team members can request to join (with PM approval). For non-project groups, ownership follows the org chart: the head of HR owns the /hr group, and so on. This means we don't have a single IAM admin bottleneck. The people who know who should have access are the ones granting it.
In practice, onboarding someone to a project looks like this:
- Team member requests to join Project Alpha (or PM adds them directly)
- PM approves the request
- User is added to
/projects/project-alpha - All tool roles enabled for that project are automatically inherited
- Done - no tickets, no manual role assignments, no spreadsheet updates
The IDP itself is a custom-built internal platform - I actually wrote about that journey separately in Why Backstage Might Not Be Worth It. The short version: I spent months trying to make Backstage work, scrapped it, and built our own IDP in days. The permissions management you're reading about here is one of the features that lives inside that platform, talking to Keycloak's admin API under the hood.
The Tricky Part: Keycloak Is Not the Tool
Here's the thing that's easy to overlook: assigning someone a role in Keycloak doesn't actually give them access to anything. Keycloak is the source of truth for who should have what, but the actual tools - GitHub, ArgoCD, Float - still need to be updated. Someone has to go into GitHub and add that user to the right repos.
This is where provisioning comes in, and it's arguably the most important part of the whole system.
When a role is assigned, a provisioning task is generated. Each Keycloak client (tool) has its own provisioning logic. When a developer joins Project Alpha and inherits github-user-project-alpha, a task is automatically created and assigned to the person responsible for GitHub, telling them: "Add this user to the Project Alpha repositories."
The same happens for every other tool the project uses. Enable ArgoCD for a project, add a user, and the ArgoCD owner gets a provisioning task too.
Some of this is automated, some isn't. For tools with good APIs - like GitHub - we were able to automate the provisioning entirely. Role gets assigned, API call fires, user has access. For others, it's still a manual task that someone picks up. The important thing is that either way, it's tracked: you can see what's been provisioned, what's pending, and who's responsible.
This is also where the ISO 27001 story comes full circle. The certification doesn't just want you to know who has access - it wants you to have a documented process for granting and revoking it. The provisioning tasks give us exactly that: an auditable trail from "PM adds user to project" to "user actually has access in the tool."
What Made This Hard (and What Didn't)
Honestly, no single part of this was particularly difficult in isolation. Keycloak's admin API is... not great, let's put it that way. And coming up with the right conceptual model - how to map our messy reality of tools, projects, and permission levels onto groups and roles - took more thinking than I expected. But once the model clicked, the implementation was relatively straightforward.
The real complexity is in the breadth: tens of tools, each with their own permission model, some project-scoped, some global, some with automatable APIs, some without. It's not a hard computer science problem. It's a hard organizing-the-real-world problem.
The Two Integration Patterns
In practice, we ended up with two ways tools connect to this system:
Pattern 1: OIDC-integrated tools. Some tools (like ArgoCD or Grafana) support Keycloak as an OIDC provider natively. These can read group memberships or roles directly from the token. For these, the Keycloak role is the access - no separate provisioning needed beyond configuring the tool to trust Keycloak.
Pattern 2: API-provisioned or manually provisioned tools. Other tools (like GitHub, or more niche tools like Float) don't use Keycloak for authorization. They might still use Keycloak as an SSO provider for login, but the actual permissions within the tool aren't driven by Keycloak roles. For these, Keycloak is the source of truth for what access someone should have, and that access is synced via provisioning - either automated through the tool's API or via manual tasks assigned to tool owners.
What We Gained
Nothing revolutionary, but exactly what we needed:
Visibility. We can open Keycloak and see who has access to what, across every tool and every project. Before this, that answer lived in a dozen different admin UIs - or in someone's head.
Easier offboarding. When someone leaves, we know exactly what to revoke. Remove them from their groups, provisioning tasks get generated for the tools, done. No more "did we remember to remove them from that one SonarQube instance?"
Auditability. Every permission is documented, every change is tracked, every provisioning task has a paper trail. Which is exactly what ISO 27001 wants to see.
What I'd Do Differently
If I started over today, I'd probably look at Authentik or a similar alternative instead of Keycloak. We went with Keycloak because we already had it running for another project and were familiar with it - not because we did a thorough comparison. It worked, but it wasn't always pleasant.
The admin API is fine, but we actually had to connect to the Keycloak database directly for some read operations because the API was way too slow. That's not a great sign. Keycloak is a powerful tool, but it sometimes feels like it was designed for a different era of software - one where you configure things through an admin console, not through an API-first workflow.
That said, the concepts in this post aren't Keycloak-specific. The model of groups, client roles, and provisioning tasks would work just as well with a different identity provider underneath.
Wrapping Up
If you're going through ISO 27001 (or just tired of not knowing who has access to what), you don't need an enterprise IAM product. You need a clear model, a bit of automation, and the discipline to actually use it.
We built ours on Keycloak, plugged it into our custom IDP, and haven't regretted it yet. It's not perfect, but it's ours - and it fits how we actually work, which is more than I can say for most off-the-shelf solutions.
If you're in a similar boat, I hope this gives you a useful starting point. Feel free to reach out if you have questions about the approach - I'm happy to talk through it.