Fine-Grained Access Control with GroupBinding¶
GroupBinding is a namespaced CRD that restricts which modules and providers an OIDC-authenticated user may access, based on the groups present in their JWT. It requires OIDC authentication to be enabled.
How It Works¶
When a user authenticates via OIDC, the server extracts the configured groups claim from their JWT and evaluates all GroupBinding resources in the server namespace in alphabetical order by name. The first GroupBinding whose expression evaluates to true for the user's groups is applied. If an expression fails to compile or evaluate, the request is denied with 403 Forbidden rather than skipping to the next binding. The user may then access only the modules whose names match the moduleResources glob patterns and the providers whose type names are listed in providerResources on that GroupBinding.
flowchart TD
A[OIDC JWT received] --> B{Groups claim present?}
B -- No --> C[403 Forbidden]
B -- Yes --> D[Evaluate GroupBindings in alphabetical order]
D --> E{Expression matches?}
E -- No more bindings --> F[403 Forbidden]
E -- Yes --> G[Check resource name against allowed list]
G --> H{Name matches?}
H -- Yes --> I[200 OK]
H -- No --> J[403 Forbidden] First-Match Semantics¶
Only the first binding (alphabetically by name) whose expression is true is applied — later bindings are not evaluated. Use ordered name prefixes (e.g., 01-platform-team, 02-app-teams) when evaluation order matters.
Required Groups Claim¶
The groups claim is required for interactive OIDC users. A valid JWT that does not carry the configured claim is denied with 403 Forbidden. Every OIDC user must have a groups claim in their JWT and match a GroupBinding to access any resource.
The one exception is client credentials tokens. When server.oidc.allowClientCredentials is enabled, the server synthesizes a virtual group from the token's sub claim ("client:<sub>"), so a groups claim is not required in the JWT itself.
Ensure your IdP connector in Dex is configured to emit a groups claim. Common connectors that do this by default include Microsoft (Entra ID) and GitHub (when org is set). See your IdP's Dex connector documentation for the correct scope or claim configuration.
GroupBinding Resource¶
Example¶
apiVersion: opendepot.defdev.io/v1alpha1
kind: GroupBinding
metadata:
name: platform-team-binding
namespace: opendepot-system
spec:
expression: '"platform-team" in groups'
moduleResources:
- "aws-*"
- "gcp-networking"
providerResources:
- "aws"
- "google"
Spec Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
expression | string | Yes | An expr-lang boolean expression evaluated against the user's groups. Must return true or false. |
moduleResources | []string | No | Glob patterns for module names the group may access. Empty list denies access to all modules. |
providerResources | []string | No | Exact provider type names the group may access, or ["*"] to allow all providers. Empty list denies access to all providers. |
Expression Syntax¶
The expression field uses expr-lang syntax. The evaluation environment exposes one variable:
| Variable | Type | Description |
|---|---|---|
groups | []string | Groups extracted from the user's JWT groups claim. |
Examples:
"platform-team" in groups
"platform-team" in groups || "platform-readonly" in groups
len(groups) > 0
Client credentials identities
When server.oidc.allowClientCredentials is enabled, the token's sub claim is mapped to the virtual group "client:<sub>". For Dex client credentials tokens the sub equals the Dex client id (e.g. ci-pipeline → "client:ci-pipeline"). Use this in expressions to grant machine clients scoped access:
sub may be a Dex-internal identifier rather than a human-readable name — inspect the JWT to confirm the exact value before writing the expression. See Client Credentials (Machine-to-Machine) for full setup details.
Warning
A GroupBinding with an unparsable expression fails closed. The server logs a WARN entry and denies the request with 403 Forbidden instead of skipping to the next binding. Check server logs to diagnose expression errors.
Module Resource Glob Patterns¶
moduleResources uses path.Match semantics, supporting the * wildcard:
| Pattern | Matches | Does not match |
|---|---|---|
aws-* | aws-vpc, aws-eks, aws-s3-bucket | gcp-gke |
terraform-aws-* | terraform-aws-eks, terraform-aws-vpc | terraform-google-gke |
* | every module name | — |
my-module | exactly my-module | my-module-v2 |
providerResources takes exact provider type names (e.g., aws, google, azurerm) or the literal "*" to allow all providers. No partial wildcard matching is applied to provider names.
An empty moduleResources or providerResources list denies access to all resources of that type. To allow access to all modules, use ["*"]; to allow access to all providers, use ["*"].
Configuring the Groups Claim Name¶
By default the server reads the groups JWT claim. If your IdP uses a non-standard claim name, set server.oidc.groupsClaim in your Helm values:
This controls the --oidc-groups-claim server flag. Common non-standard names:
| IdP | Claim name |
|---|---|
| AWS Cognito | cognito:groups |
| Okta (custom) | roles |
| Entra ID (via Dex) | groups (standard) |
| GitHub (via Dex) | groups (standard) |
Example: Team-Based Access¶
Two GroupBindings restricting access by team:
Platform team — full access to AWS and Google resources:
apiVersion: opendepot.defdev.io/v1alpha1
kind: GroupBinding
metadata:
name: 01-platform-team
namespace: opendepot-system
spec:
expression: '"platform" in groups || "platform-readonly" in groups'
moduleResources:
- "terraform-aws-*"
- "terraform-google-gke"
providerResources:
- "aws"
- "google"
App teams — access to shared modules only:
apiVersion: opendepot.defdev.io/v1alpha1
kind: GroupBinding
metadata:
name: 02-app-teams
namespace: opendepot-system
spec:
expression: '"developers" in groups'
moduleResources:
- "shared-*"
providerResources:
- "aws"
Apply both:
Note
GroupBindings must be created in the same namespace as the OpenDepot server (default: opendepot-system). The server ServiceAccount is granted get, list, and watch on groupbindings in that namespace by the Helm chart.
Access Logging¶
The server emits structured JSON log entries for every authorization decision. These provide a full audit trail for OIDC access:
| Event | Level | Key Fields |
|---|---|---|
| JWT verified | DEBUG | subject, groups_claim_name, groups |
| GroupBinding matched | INFO | subject, groups, binding_name, expression |
| No GroupBinding matched | WARN | subject, groups |
| Resource access allowed | INFO | subject, binding_name, resource_type, resource_name, namespace |
| Resource access denied (pattern) | WARN | subject, binding_name, resource_type, resource_name, namespace |
| GroupBinding expression invalid | WARN | binding_name, expression, error |
View server logs:
See Also¶
- OIDC Authentication (Dex) — enable OIDC before deploying GroupBindings
- API Reference — GroupBinding — full field reference
- Kubernetes RBAC — cluster-level access control