Docker and Kubernetes deployment
This is the step-by-step guide for running the Web edition (kubron-server) for a team: first with Docker, then on Kubernetes with the Helm chart. It follows the full operations guide in the repository, which remains the reference for advanced topics.
Want to try it on your laptop first? Use the quick start.
Before you begin
| You need | Why |
|---|---|
| PostgreSQL 14+ (external) | Stores users, clusters, audit log, web sessions and shared Helm state. The server applies migrations itself on startup |
| An encryption key | Encrypts kubeconfigs and Argo CD credentials in the database (KUBRON_SECRET_KEY) |
| A TLS-terminating proxy | The server does not serve TLS. Without TLS the session cookie would travel in clear text |
| A DNS name | For example kubron.example.com |
| Kubernetes 1.27+ and Helm 3 | Only for the Helm installation |
Plan for two or more replicas in production. All state is in PostgreSQL, so replicas are interchangeable (see high availability).
Step 1. Prepare PostgreSQL
Create a dedicated role and database. Run as a PostgreSQL superuser:
CREATE ROLE kubron LOGIN PASSWORD 'choose-a-strong-password';
CREATE DATABASE kubron OWNER kubron;The role owns the database, so it can create the tables during migrations. No extensions are required.
Build the connection string. In production use certificate verification:
postgres://kubron:PASSWORD@db.example.com:5432/kubron?sslmode=verify-fullIf you reach PostgreSQL through PgBouncer in transaction mode, also provide a direct address in KUBRON_DB_LISTEN_URL, because LISTEN/NOTIFY does not work through a transaction-mode pooler.
Backups
Schedule PostgreSQL backups now. The key and the database must be backed up separately and stored in different places; see the backup guide.
Step 2. Generate and store the encryption key
The key is id:base64, where the base64 part is exactly 32 random bytes and id is a label (letters, digits, _ . -, up to 32 characters):
echo "k1:$(openssl rand -base64 32)"Store the result in your password manager or KMS before the first start. If the key is lost, every stored kubeconfig and token is unrecoverable. Neither the image nor the chart generates it, precisely for that reason.
Step 3a. Run with Docker
The image is distroless, runs as user 65532, listens on port 8080 and is configured only through environment variables.
docker run -d --name kubron --restart unless-stopped \
-p 127.0.0.1:8080:8080 \
--read-only --tmpfs /home/nonroot:uid=65532,gid=65532 --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges \
-e KUBRON_ALLOW_REMOTE=1 \
-e KUBRON_TRUST_PROXY=1 \
-e KUBRON_ALLOWED_HOSTS=kubron.example.com \
-e KUBRON_SETUP_TOKEN='a-random-token-of-16-or-more-characters' \
-e KUBRON_SECRET_KEY='k1:...' \
-e KUBRON_DATABASE_URL='postgres://kubron:...@db.example.com:5432/kubron?sslmode=verify-full' \
ghcr.io/24dso-labs/kubron:1.0.0What each part does:
KUBRON_ALLOW_REMOTE=1is required because the container listens on0.0.0.0.- Publish the port to
127.0.0.1or an internal network and put your TLS proxy in front. Do not publish it to the internet. KUBRON_TRUST_PROXY=1makes the server trustX-Forwarded-Protofrom the proxy and mark the cookieSecure. Enable it only when the server is reachable only through the proxy.KUBRON_ALLOWED_HOSTSlists the host names the server accepts; otherHostheaders get421.- Pass secrets through the environment or your orchestrator's secrets, never as command-line flags: arguments are visible in the process list.
- Without
KUBRON_DATABASE_URLthe server falls back to file storage in a temporary directory. That is for evaluation only; the data disappears with thetmpfs.
Put a reverse proxy in front. A minimal Caddy example, which obtains the certificate automatically:
kubron.example.com {
reverse_proxy 127.0.0.1:8080
}For nginx, forward Upgrade and Connection headers (WebSocket is used for events and terminals), set a long proxy_read_timeout (for example 3600s) and send X-Forwarded-Proto.
Step 3b. Install with Helm
Create the namespace and the Secret
The chart does not create the key. You create the Secret, and the chart references it:
kubectl create namespace kubron
kubectl -n kubron create secret generic kubron-secrets \
--from-literal=KUBRON_SECRET_KEY='k1:...' \
--from-literal=KUBRON_DATABASE_URL='postgres://kubron:...@db.example.com:5432/kubron?sslmode=verify-full'Write your values
Create values.yaml. This example uses ingress-nginx with TLS and two replicas:
replicaCount: 2
secrets:
existingSecret: kubron-secrets
ingress:
enabled: true
className: nginx
annotations:
# Events and terminals use WebSocket; ingress-nginx closes idle ones after 60 s by default
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-body-size: 5m
# Optional: prefer the same replica for the same browser
nginx.ingress.kubernetes.io/affinity: cookie
nginx.ingress.kubernetes.io/session-cookie-name: kubron-affinity
nginx.ingress.kubernetes.io/session-cookie-samesite: Lax
nginx.ingress.kubernetes.io/session-cookie-secure: "true"
nginx.ingress.kubernetes.io/session-cookie-max-age: "43200"
hosts:
- host: kubron.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: kubron-tls
hosts:
- kubron.example.com
kubron:
sessionTTL: 12hNotes:
- With ingress and TLS enabled, the chart sets
KUBRON_TRUST_PROXY=1automatically and buildsKUBRON_ALLOWED_HOSTSfrom your ingress hosts. - The default
networkPolicyallows incoming traffic only from theingress-nginxnamespace. If your ingress controller lives elsewhere, setnetworkPolicy.ingress.fromto match it, otherwise nothing will reach the pods. values.yamlin the chart is fully commented, andvalues.schema.jsonvalidates your input. Ready-made examples are indeploy/helm/kubron/ci/.
Install
helm install kubron oci://ghcr.io/24dso-labs/charts/kubron \
--version 1.0.0 -n kubron -f values.yamlThe chart version equals the release version (without v), and the image tag defaults to the same version. For a build from source, use helm install kubron deploy/helm/kubron ... and set image.repository and image.tag yourself.
Pin the image by digest in production: add --set image.digest=sha256:... (it overrides the tag).
Check the rollout
kubectl -n kubron rollout status deploy/kubron
kubectl -n kubron get pods
kubectl -n kubron logs deploy/kubron | tailBoth pods should become Ready. Then open https://kubron.example.com.
Step 4. Create the first administrator
Until the first user exists, anyone who could reach the server could claim it. Kubron therefore requires a setup token for this step.
Helm: the chart creates a random token automatically in a Secret named <release>-setup. Read it:
kubectl -n kubron get secret kubron-setup \
-o jsonpath='{.data.KUBRON_SETUP_TOKEN}' | base64 -d; echoOpen the site, enter the token on the Welcome to Kubron screen, and create the administrator.
Docker: use the value you passed as KUBRON_SETUP_TOKEN.
No token, no browser: create the administrator directly in the database. Pass the password on standard input or via KUBRON_ADMIN_PASSWORD, never as an argument:
kubectl -n kubron exec -i deploy/kubron -- \
/kubron-server create-admin --username admin < password.txtAfterwards the token is no longer used. You may delete the <release>-setup Secret and set setup.enabled=false. GitOps users: set setup.existingSecret yourself; Argo CD and helm template render without cluster access, so a generated token would change on every render.
Step 5. Add clusters and users
- Sign in as the administrator.
- Create teams and users and assign roles in the users section. The built-in roles are
user(read-only),operatorandadmin. - Add clusters from the cluster list. The server never runs commands from a kubeconfig: embed credentials, use the sign-in dialog, or paste a token. See Connecting to Kubernetes and the kubeconfig rules.
To manage the cluster that Kubron itself runs in, set inCluster.enabled=true. The server then uses its own ServiceAccount; choose the RBAC profile in the chart's rbac section and grant only what you need.
Optional: browser sign-in and Google accounts
Users sign in to clusters through a dialog (see Connecting to Kubernetes). Two features need server-side settings. Both are optional; without them the paste-the-address and service-account-key methods keep working.
Public URL for browser sign-in
For the OIDC browser-callback flow the server needs the address the identity provider returns the browser to. Set KUBRON_PUBLIC_URL to the public origin of Kubron:
KUBRON_PUBLIC_URL=https://kubron.example.com- Production requires
https.httpis accepted only for local development onlocalhost, a.localhostname or a loopback IP. - It must be an absolute URL with no path, credentials, query or fragment. The server validates it at startup and refuses to start with an unsafe value.
- Register
https://kubron.example.com/auth/cluster/callbackas a redirect URI on the identity provider's web client. - Make sure your proxy passes callback requests through without changing their query parameters.
With Helm, use kubron.extraEnv:
kubron:
extraEnv:
- name: KUBRON_PUBLIC_URL
value: "https://kubron.example.com"Google accounts for GKE
By default GKE clusters sign in with a service-account key. To let users sign in with their own Google account, register an OAuth client in Google Cloud and pass it to the server:
KUBRON_GKE_OAUTH_CLIENT_ID=1234567890-abcdef.apps.googleusercontent.com
KUBRON_GKE_OAUTH_CLIENT_SECRET=... # keep in a Secret, never in valueskubron:
extraEnv:
- name: KUBRON_GKE_OAUTH_CLIENT_ID
value: "1234567890-abcdef.apps.googleusercontent.com"
- name: KUBRON_GKE_OAUTH_CLIENT_SECRET
valueFrom:
secretKeyRef: {name: kubron-gke-oauth, key: client-secret}- With
KUBRON_PUBLIC_URLset, create a Web application client and add<KUBRON_PUBLIC_URL>/auth/cluster/callbackas an authorized redirect URI. Without it, create a Desktop app client and users paste the address Google redirected to. - Kubron requests the
openid,userinfo.emailandcloud-platformscopes.cloud-platformis a sensitive scope: Google may require app verification for external users, and apps in Testing mode have user and refresh-token limits. Check Google's current OAuth consent screen rules before rollout. - The server refuses to start with a client secret but no client ID. Previously saved keys keep working.
What to allow on the identity provider
- HTTPS issuer. In the Web edition the issuer must use HTTPS with a certificate the server trusts.
insecure-skip-tls-verifyis rejected for web sign-in; pass the CA as embedded kubeconfig data. - Device grant (RFC 8628). Enable it on the client if users should sign in by device code. Without it Kubron falls back to the browser method when configured.
- Authorization Code with PKCE (S256) for browser sign-in.
- Refresh tokens. The provider must issue them so users are not asked again at every connection. For Dex, add
--oidc-extra-scope=offline_accessto the kubeconfig. - Network egress. Allow the server to reach the issuer and the endpoints from OIDC discovery (authorization, token, device, JWKS, userinfo, revocation). Set
networkPolicy.egress.idp.portsoridp.cidrsfor non-standard ports. Under thestrictaddress policy, add an internal IdP toKUBRON_CLUSTER_ADDR_ALLOW_CIDRS.
Upgrade
helm upgrade kubron oci://ghcr.io/24dso-labs/charts/kubron \
--version 1.1.0 -n kubron -f values.yaml- Replicas are replaced one by one with a rolling update. Database migrations run automatically on startup. Details for multi-replica operation are in the HA guide.
- Read the changelog first. It lists behaviour changes, for example roles and permissions.
- Take a database backup before every upgrade.
- To roll back:
helm rollback kubron -n kubron. A rollback cannot undo a database migration, so restore from backup if a release changed the schema and you need to go back further.
Uninstall
helm uninstall kubron -n kubronThis removes the workload but not your Secret or the PostgreSQL data. Delete the database and the Secret yourself if you want everything gone.
Verify the image and chart
Every release image and chart is signed with cosign and carries provenance and SBOM attestations. Always verify the digest, not only the tag.
VERSION=1.0.0
IMAGE=ghcr.io/24dso-labs/kubron
CHART=ghcr.io/24dso-labs/charts/kubron
# Provenance: built by this repository's publish workflow
gh attestation verify "oci://$IMAGE:$VERSION" --repo 24DSO-labs/kubron \
--signer-workflow 24DSO-labs/kubron/.github/workflows/publish.yml
# Signature (keyless)
IDENTITY='^https://github\.com/24DSO-labs/kubron/\.github/workflows/publish\.yml@refs/tags/v[0-9].*$'
cosign verify "$IMAGE:$VERSION" \
--certificate-identity-regexp "$IDENTITY" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify "$CHART:$VERSION" \
--certificate-identity-regexp "$IDENTITY" \
--certificate-oidc-issuer https://token.actions.githubusercontent.comThe full set of checks, including SBOM attestations and a Kyverno policy that blocks unsigned images, is in the operations guide.
Hardening checklist
- [ ] TLS terminates at a trusted proxy, and the pod is reachable only through it.
- [ ]
KUBRON_TRUST_PROXY=1only when that is true. - [ ]
KUBRON_ALLOWED_HOSTSis set (the chart does this fromingress.hosts). - [ ]
KUBRON_PUBLIC_URL, if used, is anhttpsorigin that matches the ingress host. - [ ] The encryption key is in a secret manager and backed up separately from the database.
- [ ] PostgreSQL uses
sslmode=verify-full, has backups and is not exposed publicly. - [ ]
networkPolicyallows only your ingress controller; the metrics port is reachable only from monitoring. - [ ] Two or more replicas with a
podDisruptionBudget. - [ ] Terminals, node shell and port-forward are disabled if you do not need them (
kubron.disableExec,kubron.disableNodeShell,kubron.disablePortForward). - [ ] Images are pinned by digest and verified.
Troubleshooting
| Symptom | What to check |
|---|---|
Pods CrashLoopBackOff right after start | kubectl logs: a missing KUBRON_SECRET_KEY, an unreachable database or a key that does not match the stored data stops startup on purpose |
ImagePullBackOff | Wrong image.tag (source charts default to 0.0.0-dev, which is not published) or a private registry without imagePullSecrets |
The site answers 421 | The Host is not in KUBRON_ALLOWED_HOSTS; add it under kubron.allowedHosts |
| Cannot sign in; the cookie is lost | TLS is not terminated before the pod, or trustProxy is off behind an HTTPS proxy |
| Terminals and live updates disconnect after about a minute | The proxy times out idle WebSockets. Raise the read/send timeouts (see the ingress annotations above) |
Nothing reaches the pods when networkPolicy is on | Set networkPolicy.ingress.from to your ingress controller's namespace |
| "Setup token is missing or invalid" | Read the token again from the <release>-setup Secret, or use create-admin |