Lightweight Kubernetes user management with a web UI.
Creates x509 users via the Kubernetes CSR API, issues kubeconfigs, and manages RBAC bindings — no LDAP, no OIDC, no Dex.
- Create Kubernetes users (x509 / CSR API)
- Assign preset roles (
cluster-admin,admin,edit,view) or define custom RBAC rules (API groups, resources, verbs) - Multi-namespace scoped bindings per user
- Groups — manage k8s Group subjects with their own RBAC; users added to a group via x509 O field inherit permissions automatically
- Download / view generated kubeconfigs
- Graph view — visualise any user's full access tree: cluster-wide role and per-namespace bindings
- Multi-cluster support — manage users and groups across multiple Kubernetes clusters; switch between clusters via the sidebar; each cluster has its own isolated users/groups
- PostgreSQL as source of truth — all user state (RBAC config, cert PEM, private key) stored in postgres; Sync button recreates any missing k8s objects from DB
- Kubeconfig API server address configurable per cluster
- Private keys stored in cluster Secrets and postgres — never logged or exposed raw
- Simple username/password auth backed by PostgreSQL
| Users | Groups |
|---|---|
![]() |
![]() |
| Graph — all users | Graph — group filter |
|---|---|
![]() |
![]() |
cmd/server/ # entrypoint
internal/
api/ # HTTP handlers (Gin)
auth/ # JWT
cert/ # x509 key + CSR generation
config/ # env config
db/ # PostgreSQL (pgx)
k8s/ # CSR, RBAC, Secret helpers
kubeconfig/ # kubeconfig builder
models/ # shared types
web/ # Vue 3 frontend (embedded in binary)
charts/kubevalet/ # Helm chart (includes bundled PostgreSQL)
Feature branches must follow the format x.x.x-suffix, e.g.:
0.3.13-dev
0.3.13-feature-oidc
0.3.13-fix-login
This is enforced by a pre-push git hook. Activate it once after cloning:
make hooks-setupAfter that it activates automatically on every make commit and make release — no need to remember.
git checkout -b 0.3.13-dev
# iterate freely — no version bumps, no file changes needed
make commit MSG="add feature X"
make commit MSG="fix bug Y"Every push to a branch matching x.x.x-* builds a Docker image tagged with the branch name:
truebad0ur/kubevalet:0.3.13-dev
truebad0ur/kubevalet:0.3.13-feature-oidc
Deploy a branch image to test before releasing:
KUBECONFIG=~/.kube/local-config helm upgrade kubevalet ./charts/kubevalet \
-n kubevalet --reuse-values --set image.tag=0.3.13-dev- Open a PR —
pr-validateruns automatically on every push: go build, tests, helm lint, docker build. - When all checks pass,
pr-validateautomatically adds theok-to-testlabel. pr-imagetriggers and pushes a test image tagged<next-version>-<branch-name>to DockerHub (e.g.0.3.19-my-feature).- On the next push the label is re-cycled automatically — no manual action needed.
- When satisfied — squash merge.
Requires: a
PAT_TOKENsecret in repo Settings → Secrets → Actions (classic PAT withreposcope). This is needed because GitHub does not fire webhook events for labels added by the built-inGITHUB_TOKEN.
pr-validateruns automatically — go build, tests, helm lint, docker build (no push). No secrets, safe for forks.- Review the code. If it looks good, add the label
ok-to-testmanually. pr-imagetriggers and pushes the test image to DockerHub.- If the contributor pushes new commits,
ok-to-testis removed automatically — re-review and re-label to build again. - When satisfied — squash merge.
Version lives only in the git tag — no version bumping in files. CI reads the tag and injects it into the Docker image and Helm chart at build time.
-
Open a PR → squash merge to
main. -
On
mainafter merge — tag and trigger CI:
make release VER=0.3.13 # patch
make release VER=0.4.0 # minor — new feature set, no breaking changes
make release VER=1.0.0 # major — breaking changesCI always injects whatever version you pass. The decision of patch/minor/major is yours — it has no effect on the build process itself.
GitHub Actions builds in parallel:
- Docker image
truebad0ur/kubevalet:0.3.13+latest→ DockerHub - Helm chart
0.3.13→ ghcr.io → Artifact Hub
- Deploy:
KUBECONFIG=~/.kube/local-config helm upgrade kubevalet ./charts/kubevalet \
-n kubevalet --reuse-values --set image.tag=0.3.13
KUBECONFIG=~/.kube/local-config kubectl rollout status deployment/kubevalet -n kubevaletPrerequisites: Docker with buildx, a builder instance.
Set your own image repo in Makefile:
IMAGE := your-dockerhub-user/kubevaletThen build & push:
# One-time buildx setup
make buildx-setup
# Build & push multi-arch image (linux/amd64 + linux/arm64)
TAG=0.1.0 make docker-buildx-pushAnd point the Helm chart at your image:
helm install kubevalet ./charts/kubevalet \
--set image.repository=your-dockerhub-user/kubevalet \
--set image.tag=0.1.0 \
...Single-arch local build:
make web-build # build Vue frontend
make build # compile Go binary → bin/kubevalethelm install kubevalet ./charts/kubevalet \
--namespace kubevalet --create-namespace \
--set cluster.server=https://<your-api-server>:6443 \
--set auth.adminPassword=changemeUpgrade:
helm upgrade kubevalet ./charts/kubevalet --namespace kubevaletAccess UI:
kubectl port-forward svc/kubevalet 8080:80 -n kubevalet
# http://localhost:8080| Value | Default | Description |
|---|---|---|
image.tag |
0.3.16 |
Image tag |
cluster.server |
https://kubernetes.default.svc.cluster.local |
API server URL embedded in kubeconfigs — set to the external address users will connect to (can also be changed at runtime in Settings UI) |
cluster.name |
kubernetes |
Cluster name in kubeconfig context |
auth.adminPassword |
admin |
Initial admin password |
auth.jwtSecret |
(auto-generated) | JWT signing secret — auto-generated on first install, preserved across upgrades |
postgres.persistence.enabled |
false |
Enable PVC for PostgreSQL (requires a StorageClass) |
ingress.enabled |
false |
Expose via Ingress |
ingress.host |
kubevalet.example.com |
Ingress hostname |
The binary is configured via environment variables. For production use Helm values instead — they map to the same vars automatically.
export POSTGRES_DSN=postgres://kubevalet:pass@localhost:5432/kubevalet
export JWT_SECRET=changeme
export CLUSTER_SERVER=https://your-api:6443 # URL that goes into kubeconfigs
export ADMIN_USERNAME=admin
export ADMIN_PASSWORD=changeme
# optional:
export KUBECONFIG=/path/to/kubeconfig # defaults to in-cluster service account
export NAMESPACE=kubevalet # namespace where key Secrets are stored
export TOKEN_TTL=24h
make build
./bin/kubevaletkubevalet can manage users and groups across multiple Kubernetes clusters from a single UI.
- The default cluster is the one where kubevalet itself runs (in-cluster service account). It is created automatically on startup.
- Additional clusters are added via the Clusters page (admin only) by pasting a kubeconfig with cluster-admin permissions.
- Users and groups are isolated per cluster — switching clusters in the sidebar reloads all views for that cluster.
- Kubeconfigs generated for users embed the
api_serverURL configured for each cluster (set at add time, editable in Settings).
Security note: Kubeconfigs for external clusters are stored as plaintext in PostgreSQL. Encrypt the database at rest before using in production.
- Go to Clusters in the sidebar.
- Click + Add Cluster.
- Fill in:
- Name — identifier (DNS label format, e.g.
prod-eu) - API Server — external URL that will be embedded in generated kubeconfigs (e.g.
https://api.prod.example.com:6443) - Kubeconfig — paste a kubeconfig with cluster-admin access to the target cluster
- Name — identifier (DNS label format, e.g.
- After adding, select the cluster from the sidebar dropdown to start managing its users and groups.
The sidebar shows a cluster dropdown when more than one cluster is configured. Selecting a cluster reloads Users, Groups, Graph, and Settings for that cluster. Templates are global across all clusters.
Each rule covers exactly one API group. Resources from different groups must be split into separate rules:
| Resource | API Group |
|---|---|
pods, secrets, configmaps, services |
`` (empty = core) |
deployments, replicasets, statefulsets, daemonsets |
apps |
cronjobs, jobs |
batch |
ingresses |
networking.k8s.io |
clusterroles, roles, rolebindings |
rbac.authorization.k8s.io |
Wrong — pods won't work because it's not in the apps group:
API Groups: apps Resources: pods, deployments
Correct — two separate rules:
Rule 1 — API Groups: (empty) Resources: pods
Rule 2 — API Groups: apps Resources: deployments
-
Cluster kubeconfigs stored in plaintext. Multi-cluster support stores kubeconfigs (including credentials) in PostgreSQL without encryption. Anyone with read access to the database can extract cluster credentials. Encrypt the database at rest (e.g. via LUKS, cloud provider encryption, or pgcrypto) before using this in production. Application-level encryption may be added in a future release.
-
JWT role changes take effect only after token expiry. When an admin demotes another admin to viewer (or changes any role), the existing JWT is not invalidated — the affected user retains their previous role until their token expires (default TTL: 24 h). To force immediate effect, the user must log out and log in again. This is an inherent trade-off of stateless JWT auth; adding server-side token blacklisting would require a shared revocation store.
- Screenshots in README
- RBAC for local users (is_admin flag → admin can manage all users/passwords, regular users can only change own password)
- Keycloak / OIDC integration
- Multi-cluster support
- Audit log (who created/deleted which user and when)
- User expiry / certificate rotation reminders
- Role templates (save and reuse custom RBAC configs)
- LDAP sync
- CVE scanning in CI (Trivy)
- CI workflow for external PRs:
pull_request(go build + test + helm lint + docker build --no-push, no secrets) andpull_request_targetgated byok-to-testlabel (build + pushpr-{N}image to DockerHub, auto-remove label on new commits) - Write tests for all




