KubeUser

KubeUser is a Kubernetes-native way to manage users, certificates, RBAC, and kubeconfigs declaratively β without running an external identity provider.
Overview
Managing Kubernetes access often means manually creating kubeconfigs, handling certificates, and keeping RBAC in sync. This quickly becomes error-prone, hard to audit, and unfriendly to GitOps workflows.
KubeUser solves this by managing Kubernetes users through declarative custom resources. It automatically generates and rotates certificates, applies RBAC bindings, and produces ready-to-use kubeconfigs using native Kubernetes APIs.
Designed for small teams and self-managed clusters that want Kubernetes-native, GitOps-friendly access control without a full IAM or OIDC stack. Not a replacement for enterprise identity providers.
Architecture
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β User CRD βββββΆβ User Controller βββββΆβ RBAC Resources β
β (Custom Res.) β β (Reconciler) β β (Roles/Bindings)β
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β
βΌ
βββββββββββββββββββ
β Certificate & β
β Kubeconfig Gen β
βββββββββββββββββββ
Features
β
Implemented
- Declarative User CRD β status tracking, conditions, and finalizers for clean resource lifecycles
- Automatic Certificate Generation β seamless integration with the Kubernetes CSR API
- Stateful Rotation Engine β resumable, multi-step rotation via the Shadow Secret pattern; survives controller restarts
- Atomic Secret Updates β zero-downtime credential flip with rollback on failure
- Dynamic RBAC Reconciliation β automatic RoleBinding and ClusterRoleBinding management
- Production-Grade Webhooks β TLS-secured mutating and validating webhooks with cert-manager CA injection
- Managed K8s Support β configurable CSR signers for EKS, GKE, and vanilla clusters
- Prometheus Metrics & Alerting β rotation counters, duration histograms, expiry gauges, pre-built Grafana dashboard, and shipped PrometheusRule alerts
- Kubernetes-native Observability β structured events via
kubectl describe user and standard Ready, Renewing, AutoRenewal status conditions
π§ Planned
- x509 Group Membership β
O= field support for RBAC group bindings
- Certificate Revocation Notification β admission warning and event on User deletion
- Audit Log β immutable record of every certificate issuance and rotation event
- Short-Lived Certificates (< 24h) β sub-24h TTL for ephemeral, zero-trust access
- ECDSA Key Support β configurable key algorithm via
spec.auth.keyAlgorithm
- OpenTelemetry Tracing β end-to-end traces across reconcile and rotation paths
- Predefined Role Templates β curated library for common access patterns
Security Considerations
Deleting a User does NOT invalidate issued certificates.
When deleting a User:
- RBAC bindings are removed immediately (access revoked)
- Secrets are deleted
- Certificates remain cryptographically valid until natural expiry
Plan your TTL accordingly. For short-lived access, use a short ttl and autoRenew: false.
Installation
Prerequisites
- Kubernetes v1.28+
- kubectl with cluster-admin permissions
- cert-manager (required for webhook certificates)
Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.2/cert-manager.yaml
kubectl wait --for=condition=ready pod -l app=cert-manager -n cert-manager --timeout=60s
Option 1: Helm (Recommended)
helm repo add kubeuser https://openkube-hub.github.io/KubeUser
helm repo update
export KUBERNETES_API_SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
helm upgrade --install kubeuser kubeuser/kubeuser \
--create-namespace \
--namespace kubeuser \
--version <version> \
--set env.KUBERNETES_API_SERVER="$KUBERNETES_API_SERVER"
# Verify
kubectl get pods -n kubeuser
kubectl get certificates -n kubeuser
All resource names are prefixed by the Helm release name. Use helm search repo kubeuser --versions to list available versions.
Option 2: Kustomize
git clone https://github.com/openkube-hub/KubeUser.git
cd KubeUser
kubectl create namespace kubeuser
kubectl apply -k config/default
kubectl wait --for=condition=ready pod -l control-plane=controller-manager -n kubeuser --timeout=120s
Option 3: Local Development (kind)
make docker-build
kind load docker-image ghcr.io/openkube-hub/kubeuser-controller:latest --name <cluster-name>
kubectl apply -k config/default
kubectl patch deployment kubeuser-controller-manager -n kubeuser \
-p '{"spec":{"template":{"spec":{"containers":[{"name":"manager","imagePullPolicy":"Never"}]}}}}'
Usage
How Defaults Work
KubeUser uses a mutating admission webhook to persist defaults into the User spec at creation time:
# You submit:
spec:
auth:
type: x509
# Webhook persists:
spec:
auth:
type: x509
ttl: "2160h" # from KUBEUSER_DEFAULT_TTL
autoRenew: true # from KUBEUSER_DEFAULT_AUTORENEW
Verify applied defaults: kubectl get user <name> -o yaml
Customize defaults via Helm:
helm upgrade --install kubeuser kubeuser/kubeuser \
--set authDefaults.ttl=720h \
--set authDefaults.autoRenew=false
Important: authDefaults changes only apply to users created after the upgrade. Existing users retain their persisted defaults.
Basic User (Namespace-Scoped Access)
apiVersion: auth.openkube.io/v1alpha1
kind: User
metadata:
name: alice
spec:
auth:
type: x509 # REQUIRED: currently only 'x509' is supported
ttl: "72h" # Optional: default 2160h (90 days)
autoRenew: false # Optional: default true
roles:
- namespace: "development"
existingRole: "developer"
- namespace: "staging"
existingRole: "viewer"
User with Cluster-wide Access
apiVersion: auth.openkube.io/v1alpha1
kind: User
metadata:
name: bob-admin
spec:
auth:
type: x509
ttl: "2160h"
autoRenew: true
clusterRoles:
- existingClusterRole: "cluster-admin"
Mixed Permissions
apiVersion: auth.openkube.io/v1alpha1
kind: User
metadata:
name: contractor-jane
spec:
auth:
type: x509
ttl: "720h" # 30 days
autoRenew: true
renewBefore: "72h" # Renew 3 days before expiry (overrides 33% rule)
roles:
- namespace: "project-x"
existingRole: "developer"
- namespace: "monitoring"
existingClusterRole: "view" # ClusterRole bound to a specific namespace
clusterRoles:
- existingClusterRole: "view"
Retrieve a User's Kubeconfig
kubectl get secret <username>-kubeconfig -n kubeuser \
-o jsonpath='{.data.config}' | base64 -d > /tmp/kubeconfig
kubectl --kubeconfig /tmp/kubeconfig get pods -n dev
Field Reference
| Field |
Type |
Required |
Description |
spec.auth |
AuthSpec |
Yes |
Authentication configuration |
spec.auth.type |
string |
Yes |
Auth method β only x509 is supported |
spec.auth.ttl |
string |
No |
Certificate lifetime (default: 2160h) |
spec.auth.autoRenew |
boolean |
No |
Enable automatic renewal (default: true) |
spec.auth.renewBefore |
string |
No |
Renew this duration before expiry. Cannot exceed 90% of TTL |
spec.roles |
[]RoleSpec |
No |
Namespace-scoped role bindings |
spec.roles[].namespace |
string |
Yes |
Target namespace |
spec.roles[].existingRole |
string |
No* |
Existing Role in the namespace |
spec.roles[].existingClusterRole |
string |
No* |
ClusterRole to bind to the namespace |
spec.clusterRoles |
[]ClusterRoleSpec |
No |
Cluster-wide role bindings |
spec.clusterRoles[].existingClusterRole |
string |
Yes |
Existing ClusterRole |
Either existingRole or existingClusterRole must be specified per role entry.
Managed Kubernetes Support
AWS EKS:
helm install kubeuser kubeuser/kubeuser \
--set signerName="beta.eks.amazonaws.com/app-client" \
--set rbac.signerResourceNames[0]="beta.eks.amazonaws.com/app-client"
GKE / AKS: Discover your signer name, then configure:
kubectl get csr -o jsonpath='{.items[0].spec.signerName}'
helm install kubeuser kubeuser/kubeuser \
--set signerName="<your-signer-name>" \
--set rbac.signerResourceNames[0]="<your-signer-name>"
Configuration
Certificate Duration Limits
| Limit |
Value |
Notes |
| Minimum TTL |
24h |
Enforced by validating webhook β prevents thundering herd loops |
| Maximum TTL |
8760h (1 year) |
Based on Kubernetes default --cluster-signing-duration |
| Default TTL |
2160h (90 days) |
Applied by mutating webhook; configurable via authDefaults.ttl |
Environment Variables
| Variable |
Default |
Description |
KUBERNETES_API_SERVER |
https://kubernetes.default.svc |
API server address written into generated kubeconfigs |
CLUSTER_DOMAIN |
cluster.local |
Cluster DNS domain |
KUBEUSER_DEFAULT_TTL |
2160h |
Default certificate TTL |
KUBEUSER_DEFAULT_AUTORENEW |
true |
Default auto-renewal behaviour |
KUBEUSER_SIGNER_NAME |
kubernetes.io/kube-apiserver-client |
CSR signer name |
Observability
# View all users with expiry and next renewal
kubectl get users -o custom-columns=NAME:.metadata.name,EXPIRY:.status.expiryTime,NEXT_RENEWAL:.status.nextRenewalAt
# Detailed status and events for a specific user
kubectl describe user <username>
# Check Ready and Renewing conditions
kubectl get user <username> -o json | jq '.status.conditions'
For Prometheus metrics, Grafana dashboards, and alerting rules see docs/metrics.md.
Troubleshooting
Controller Pod Not Starting
kubectl get pods -n kubeuser
kubectl logs -n kubeuser deployment/kubeuser-controller-manager
kubectl get events -n kubeuser --sort-by=.lastTimestamp
Common causes: missing cert-manager, webhook certificate not ready, image pull issues.
Webhook Certificate Issues
kubectl get certificates -n kubeuser
kubectl describe certificate kubeuser-webhook-cert -n kubeuser
kubectl logs -n cert-manager deployment/cert-manager
User Creation Fails
kubectl describe user <username>
kubectl logs -n kubeuser deployment/kubeuser-controller-manager | grep -i error
Common causes: referenced Role/ClusterRole does not exist, target namespace does not exist, webhook validation failure.
Certificate Generation Issues
kubectl get csr -l auth.openkube.io/user=<username>
kubectl describe csr <csr-name>
kubectl auth can-i create certificatesigningrequests \
--as=system:serviceaccount:kubeuser:kubeuser-controller-manager
Documentation
π€ Contributing
We welcome contributions of all kinds β bug reports, features, documentation, and tests.
See CONTRIBUTING.md for the full guide: prerequisites, local setup,
code style, commit format, testing, and PR checklist.
If you find KubeUser useful, please consider giving it a β on GitHub!