keycloak

package
v0.23.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Index

Constants

View Source
const NetBirdAPIScopeName = "api"

NetBirdAPIScopeName is the ClientScope created in the realm and assigned as a default scope on netbird-client + netbird-backend. It carries the audience ProtocolMapper that asserts the token audience matches NetBird Mgmt's expected `aud` claim.

Exported because the kubernetes client created by ReconcileKubernetes inherits this scope (so kubelogin tokens share the NetBird audience mapping) — callers pass this in KubernetesSpec.DefaultScopes.

View Source
const NetBirdGroupsScopeName = "groups"

NetBirdGroupsScopeName is the ClientScope that carries a Group Membership ProtocolMapper, surfacing the user's Keycloak group memberships as a `groups` claim on every issued token. NetBird Mgmt's "Enable JWT Group Sync" Dashboard toggle reads this claim to drive group-based access policies — the toggle is a no-op without this scope being attached.

Exported because ReconcileKubernetes inherits it too, so kubelogin tokens carry the same groups claim and downstream RBAC can key off the same Keycloak groups. Caller passes this in KubernetesSpec.DefaultScopes alongside NetBirdAPIScopeName.

Variables

This section is empty.

Functions

This section is empty.

Types

type ClientScopeSpec

type ClientScopeSpec struct {
	Name        string
	Protocol    string // typically "openid-connect"
	Description string

	// ConsentScreenText is the display label Keycloak shows on the
	// "Grant Access to <client>" consent page in lieu of the raw
	// scope Name. When unset Keycloak falls back to Name verbatim,
	// which is fine for things like "email" but produces lowercase
	// fragments like "api" / "groups" next to neatly cased built-in
	// scopes ("Email address", "User profile") — set this to
	// "API" / "Groups" / etc. to keep the consent screen tidy.
	//
	// Stored under the `consent.screen.text` attribute on the
	// ClientScope representation, not a top-level field.
	ConsentScreenText string

	// IncludeInTokenScope controls whether the scope's name appears
	// in the issued token's `scope` claim. NetBird's docs require
	// this on for the `groups` scope so the operator's Keycloak
	// groups land in the JWT — Keycloak stores it as the
	// `include.in.token.scope` attribute on the ClientScope
	// representation, not a top-level field.
	IncludeInTokenScope bool
}

ClientScopeSpec describes a Keycloak client scope. Only Name and Protocol are required; description is optional.

type ClientSpec

type ClientSpec struct {
	// ClientID is the OIDC client_id (also Keycloak's display name
	// in the admin UI's Clients list). Required.
	ClientID string

	// PublicClient = true means PKCE / no client secret (browser
	// SSO, kubelogin). false means confidential — Keycloak issues
	// (or accepts a pre-set) client secret.
	PublicClient bool

	// ServiceAccountsEnabled = true makes Keycloak auto-create a
	// service-account user for this confidential client. Only
	// valid when PublicClient is false.
	ServiceAccountsEnabled bool

	// StandardFlowEnabled controls authorization-code (browser)
	// flow. DirectAccessGrantsEnabled controls Resource Owner
	// Password Credentials. Default both off; callers opt in.
	StandardFlowEnabled       bool
	DirectAccessGrantsEnabled bool

	// RedirectURIs / WebOrigins are required for browser flows
	// (PKCE). Ignored for purely backend confidential clients.
	RedirectURIs []string
	WebOrigins   []string

	// Secret pre-sets the confidential client's secret on creation.
	// When empty, Keycloak generates a random one. Ignored for
	// PublicClient = true.
	Secret string

	// DeviceAuthorizationGrantEnabled controls whether the client can
	// initiate the OAuth 2.0 Device Authorization Grant flow
	// (RFC 8628). Required for headless CLI tools — NetBird's
	// `netbird up`, for instance — that can't host an
	// authorization-code redirect URI. Keycloak stores this as the
	// `oauth2.device.authorization.grant.enabled` client attribute,
	// not a top-level Client field, so buildClient translates the
	// bool onto the Attributes map.
	DeviceAuthorizationGrantEnabled bool
}

ClientSpec describes a Keycloak client (OIDC application) in a shape that's natural for callers; ReconcileClient maps it onto gocloak's pointer-everywhere Client representation.

type KubernetesSpec

type KubernetesSpec struct {
	// Realm is the Keycloak realm name (typically derived from the
	// public DNS — see cluster.keycloak.realm).
	Realm string

	// ClusterName is the Kubernetes cluster's name (cluster.name).
	// The OIDC client created in Keycloak is named
	// `kubernetes-<ClusterName>`.
	ClusterName string

	// RedirectURIs are the redirect URIs for kubelogin's localhost
	// callback. Defaults to defaultKubeloginRedirectURIs when empty.
	RedirectURIs []string

	// DefaultScopes are extra ClientScope names to assign as defaults
	// on the kubernetes client (e.g., the NetBird api scope so the
	// audience mapper covers kubelogin tokens too). May be empty.
	DefaultScopes []string
}

KubernetesSpec describes the OIDC client a Kubernetes cluster needs in a customer's Keycloak realm so kubelogin can authenticate kubectl users against that cluster's kube-apiserver.

One Keycloak realm typically hosts multiple Kubernetes clusters (the VPN cluster plus each workload cluster joined to it), each with its own `kubernetes-<ClusterName>` client.

type NetBirdSpec

type NetBirdSpec struct {
	// Realm is the Keycloak realm name (typically derived from the
	// public DNS — see cluster.keycloak.realm).
	Realm string

	// NetBirdMgmtURL is the public URL of the NetBird Management
	// UI (e.g., https://netbird.<vpn-dns>). Used as netbird-client's
	// redirect URI base (browser SSO + dashboard) and Web Origin.
	// NOT the JWT audience — that's the netbird-client client_id,
	// keyed on a separate netBirdClientID const so the value can
	// also surface in the api scope's audience ProtocolMapper.
	NetBirdMgmtURL string

	// NetBirdBackendSecret is the pre-generated client secret to
	// assign to `netbird-backend` on create. Pre-setting avoids a
	// second git push: kubeaid-cli renders the SealedSecret with
	// this value before ever calling the Keycloak admin API, then
	// hands the same value to ReconcileClient via spec.Secret.
	NetBirdBackendSecret string
}

NetBirdSpec describes the realm-side resources NetBird needs in the customer's Keycloak realm. Populated from cluster.keycloak.* + the pre-generated netbird-backend secret kubeaid-cli also templates into the SealedSecret consumed by NetBird Mgmt.

type ProtocolMapperSpec

type ProtocolMapperSpec struct {
	Name           string
	Protocol       string // "openid-connect"
	ProtocolMapper string // e.g. "oidc-audience-mapper"
	Config         map[string]string
}

ProtocolMapperSpec describes a single protocol mapper attached to a client scope. Config carries the mapper-type-specific keys (e.g. "included.client.audience" for an audience mapper).

type Reconciler

type Reconciler struct {
	// contains filtered or unexported fields
}

Reconciler issues idempotent admin-API calls against a running Keycloak. Construct via NewReconciler with admin credentials; each Reconcile* method either creates the resource if missing or no-ops when it already exists.

func NewReconciler

func NewReconciler(ctx context.Context, baseURL, adminUser, adminPassword string) (*Reconciler, error)

NewReconciler logs in as admin against Keycloak's master realm and returns a Reconciler holding the resulting access token. baseURL is Keycloak's HTTP root — kubeaid-cli's bootstrap passes the cluster's public https://<cluster.Keycloak.DNS>/auth (the keycloakx chart serves Keycloak under /auth).

func (*Reconciler) AssignClientDefaultScopes

func (r *Reconciler) AssignClientDefaultScopes(ctx context.Context, realm, clientID string, scopeNames []string) error

AssignClientDefaultScopes ensures every name in scopeNames is set as a default scope on the client identified by clientID (the user-facing OIDC client_id). Existing assignments are preserved; missing ones are added. Unknown scope names return an error rather than silently being ignored.

func (*Reconciler) AssignClientServiceAccountRole

func (r *Reconciler) AssignClientServiceAccountRole(
	ctx context.Context,
	realm, srcClientID, targetClientID, roleName string,
) error

AssignClientServiceAccountRole grants the named role from targetClientID's role catalogue to srcClientID's auto-created service-account user. Both client IDs are user-facing (clientId), not Keycloak's internal id.

Idempotent: when the service-account user already holds the role, no API write is performed.

Typical use: granting `view-users` from the built-in `realm-management` client to `netbird-backend`'s service-account so NetBird can resolve user identities through Keycloak.

func (*Reconciler) EnsureClientDeviceAuthorizationGrant

func (r *Reconciler) EnsureClientDeviceAuthorizationGrant(
	ctx context.Context, realm, clientID string, enabled bool,
) error

EnsureClientDeviceAuthorizationGrant sets/unsets the OAuth 2.0 Device Authorization Grant (RFC 8628) capability on the named client. Idempotent — no-op when the attribute is already in the desired state.

Keycloak stores this as the `oauth2.device.authorization.grant.enabled` client attribute; there's no top-level Client field for it, so ReconcileClient's create path uses the Attributes map and existing clusters retro-fit the attribute through this method.

Called from ReconcileNetBird for the netbird-client OIDC client — NetBird's CLI (`netbird up`) uses this flow to authenticate from headless contexts. Without the attribute Keycloak rejects /protocol/openid-connect/auth/device with "Client is not allowed to initiate OAuth 2.0 Device Authorization Grant. The flow is disabled for the client."

func (*Reconciler) ReconcileClient

func (r *Reconciler) ReconcileClient(ctx context.Context, realm string, spec ClientSpec) (string, error)

ReconcileClient ensures a client matching spec.ClientID exists in the realm. For confidential clients the returned string is the effective client secret (either spec.Secret if pre-set on create, or the secret Keycloak generated and we read back). Public clients return "".

func (*Reconciler) ReconcileClientScope

func (r *Reconciler) ReconcileClientScope(ctx context.Context, realm string, spec ClientScopeSpec) error

ReconcileClientScope ensures a client scope with spec.Name exists in the realm. Idempotent: if a scope with the same name already exists the function no-ops.

func (*Reconciler) ReconcileKubernetes

func (r *Reconciler) ReconcileKubernetes(ctx context.Context, spec KubernetesSpec) error

ReconcileKubernetes upserts the `kubernetes-<ClusterName>` PUBLIC PKCE OIDC client in the given realm and assigns any DefaultScopes the caller asks for. Idempotent — calling with the same spec a second time is a no-op.

The realm and any DefaultScopes referenced here must already exist; this function does not create them. The typical caller runs ReconcileNetBird (which creates the realm + the netbird `api` ClientScope) immediately before, then passes that scope's name in DefaultScopes here.

func (*Reconciler) ReconcileNetBird

func (r *Reconciler) ReconcileNetBird(ctx context.Context, spec NetBirdSpec) error

ReconcileNetBird creates everything NetBird (and the VPN cluster's own kube-apiserver) needs in the customer's Keycloak realm. Idempotent end-to-end: every step delegates to a Reconcile* / Assign* helper that no-ops when the resource already exists.

Order matters — clients depend on the realm; the api scope must exist before it can be assigned to clients; the audience mapper must be on the scope before kube-apiserver tokens get the expected `aud` claim; the service-account role grant requires netbird-backend to exist (so its service-account user has been auto-created).

func (*Reconciler) ReconcileProtocolMapperOnClientScope

func (r *Reconciler) ReconcileProtocolMapperOnClientScope(
	ctx context.Context,
	realm, clientScopeName string,
	spec ProtocolMapperSpec,
) error

ReconcileProtocolMapperOnClientScope ensures a protocol mapper with spec.Name exists on the client scope identified by name and that its config matches spec.Config. Idempotent on Name + Config — missing mapper is created, drifted mapper is updated, otherwise no-op. Only the config keys the spec explicitly sets are diffed, so unrelated keys (operator customizations, Keycloak defaults) aren't overwritten.

func (*Reconciler) ReconcileRealm

func (r *Reconciler) ReconcileRealm(ctx context.Context, name string) error

ReconcileRealm ensures a realm with the given name exists. The realm is created enabled (Realm.Enabled = true) — Keycloak's default leaves it disabled, which would silently break OIDC discovery and token issuance.

func (*Reconciler) ReconcileUser

func (r *Reconciler) ReconcileUser(
	ctx context.Context,
	realm string,
	spec UserSpec,
	initialPassword string,
) error

ReconcileUser ensures a user with spec.Username exists in the realm. When the user is created and initialPassword is non-empty, the password is set as a non-temporary credential. On a re-run (user already present) the existing password is preserved — kubeaid-cli is not in the password-rotation business.

type UserSpec

type UserSpec struct {
	Username  string
	Email     string
	FirstName string
	LastName  string

	// Enabled defaults to true. Set explicitly for clarity in
	// callers, so a zero-value spec doesn't silently create a
	// disabled user.
	Enabled bool
}

UserSpec describes a Keycloak user in the realm. Username is required; the rest are optional.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL