Users, permissions and the settings administrators manage in the panel.
Users are User resources in the panel namespace; every user owns the namespace
<panel namespace>-user-<username> (default kubedactyl-user-<username>) that holds its game servers.
Passwords are stored as Argon2id hashes (PHC format), never in clear text.
First start: while no active administrator exists, every page redirects to /setup, where the
first administrator chooses username and password and is signed in. The setup needs a one-time
setup token: the panel logs a link on start (open the setup page with this one-time link url=…/setup?token=…) or uses KUBEDACTYL_SETUP_TOKEN, so nobody who merely reaches the page can take
over the panel. For automated installations KUBEDACTYL_ADMIN_PASSWORD creates admin directly
instead. After the setup the page answers 409.
New users: administrators add users on the Users page in two ways:
- New user with a start password. Must choose a new password after signing in (on by default,
mustChangePassword) sends the user to/change-passwordafter the sign-in; until the password is replaced every request except the own account, the password change and signing out answers 403, for sessions and API tokens alike. Administrators can set the switch again for any user. - Invite: a one-time link (
/invite?token=…), shown once as QR code and as text to send in a messenger. The invited person chooses username (unless the administrator fixed one), display name and password and is signed in. A link works once, for 7 days; open invites are rows of the users table, where Renew replaces the link with a new one valid for 7 days again (also after it expired; the old link stops working) and the trash can revokes it. Invites areInviteresources in the panel namespace; only a SHA-256 hash of the link is stored. Failed link checks count against the client like failed sign-ins. - Password reset link (key icon of a local account): the same kind of link, for an existing account. Whoever opens it sets a new password; every session and API token of the account ends. Accounts linked to the identity provider have none: linking deletes open reset links, and a link of a disabled account does not work.
Subusers: every user may invite friends who see only the servers the user shares with them, for example for a
LAN party. The Invite button of a server (next to the power buttons) opens the invite on the Account page with
that server picked; more servers can be added. Per server a subuser gets View (status, address, console output)
or Control (also start, stop and console commands); files, backups, schedules, settings and diagnostics stay
with the owner. The owner changes the access later in the server's Settings (card Subusers), and manages
subusers and their pending invites on the Account page (delete, password reset link). A subuser is a User with
spec.owner: it has no namespace, cannot own servers or become an administrator, signs in only with a password
(not while its owner is disabled) and is deleted together with its owner. Shares are spec.sharedWith of the server; a transfer clears them.
Administrators see all subusers in their own table on the Users page. Settings → Security → Subusers per user
limits accounts and open invites per user (default 10, 0 turns subusers off).
Single sign-on: users can also sign in through an OpenID Connect identity provider (Keycloak), whose groups grant access and the role; see Single sign-on.
Authentication
- Web UI:
POST /api/auth/loginsets the HttpOnly, SameSite=Strict cookiekd_session(HMAC-signed). The signing key lives in the secretkubedactyl-auth. - API clients:
Authorization: Bearer <token>with the session token from the login response or a personal API token (kdt_…, created under Account, only a SHA-256 hash is stored, shown once; at most 50 per account). - Lifetimes (Settings → Security): a sign-in lasts 12 hours, an API token at most 90 days (1-720 hours / 1-3650 days). Both are checked on every request, so a shorter value also ends older sessions and tokens (counted from their creation); tokens without an expiry no longer exist.
- Changing or resetting a password and disabling a user invalidates all sessions of that user;
Account → Sign out everywhere (
POST /api/auth/logout-all) does the same on request (API tokens stay valid). A password change also revokes the own API tokens unless Also revoke my API tokens is switched off (revokeTokens); an administrator's password reset always revokes them. API tokens cannot create tokens (403); only a session can. - Failed logins: 5 per client IP and username within 5 minutes, 20 per client IP and 30 per username within
15 minutes → HTTP 429. Attempts count before the password is checked, so parallel requests get no more tries.
A browser in which the user signed in before (cookie
kd_device, 180 days, sent only to the sign-in) is exempt from the per-username limit: failed attempts from elsewhere cannot lock its user out.
| Area | User | Admin |
|---|---|---|
| Own servers: view, power, console, commands, files, backups, stats, reinstall | ✓ (not while suspended) | ✓ (all servers) |
| Subusers: invite, share own servers (view or control), password reset link, delete | ✓ | ✓ |
| Suspend / unsuspend servers | - | ✓ |
| Transfer servers to another user | - | ✓ |
| Migrate servers to another storage class | - | ✓ |
| Server settings: display name, crash restart, egg image, egg startup command, editable egg variables, load balancer pool (enabled pools, or none when allowed) | ✓ | ✓ |
| Server resources, ports, IP, own startup command, stop timeout, custom image | - | ✓ |
| Create / delete servers (with owner) | - | ✓ |
| Eggs: list / view (hidden variables and install script stripped) | ✓ | ✓ full |
| Eggs: import / delete, egg library | - | ✓ |
| Users: create, invite, edit, disable, reset password, delete (with servers, data, namespace, subusers) | - | ✓ |
| Cluster page: nodes, hardware, capacity, connection and permissions | - | ✓ |
| Panel settings: external domain, enabled storage classes and pools (read: everyone) | - | ✓ |
| Own account: password, API tokens | ✓ | ✓ |
Servers of other users answer 404 (their existence is not revealed). Variables an egg marks as not user-viewable are removed from responses for users. The last active administrator cannot be deleted, demoted or disabled, and nobody can delete the own account.
Several installations can share a cluster (different --namespace): each one only reconciles and
shows objects in its own namespace and its own <namespace>-user-* namespaces.
Settings (admins) is stored in the PanelSettings resource panel in the panel namespace
(kubectl -n kubedactyl get kdsettings).
-
Egg library: git repositories (
https://<host>/<owner>/<repo>, at most 20) whose eggs the Library tab of Eggs lists. Each URL is checked while it is typed: the field shows whether the panel can download the repository and how many eggs it found. -
Kube API limit: requests per second the panel sends to the Kubernetes API at most (5-1000, default 50, bursts of twice that) and requests per second one user may send to the panel (Per user, 1-200, default 10). Protects the API server from overload; both apply at once, without a restart. All clients of the panel share one limiter (
kube.RateLimiter). A request is refused before it starts, never in the middle: 429 when its user sent too many, 503 ("The panel is busy") while the calls already waiting for the limit would take longer than 3 s (administrators are not refused as busy but wait in the queue, so they can always raise the limit). So no long queue builds up and no change is left half done. Both answers carryRetry-After; the web interface shows a notice and keeps polling. The footer shows the user's requests per second against the per-user limit (and, for administrators, the panel's Kubernetes API calls against the panel limit) with a green, yellow (from 50 %) or red (from 90 %) light: averages over 5 s fromGET /api/request-rates, which does not count itself. Measured with the default: 50 users with an open server page answer in ~55 ms (a server page costs about one API request per second); one user flooding the panel gets ~10 requests per second through and other users do not notice it. -
Legal notice & privacy policy: Markdown (max. 20,000 characters each, live preview), linked in the footer of every page including sign-in and setup; served publicly by
GET /api/legal(only these two texts). Rendered with react-markdown: no raw HTML, unsafe links are dropped. -
Server notice: plain text (max. 2000 characters) that users see as a dialog every time they open one of their servers (switching tabs of the same server does not show it again; admins do not get it).
-
External domain: users see
domain:portinstead of the load balancer IP; the API replacesstatus.addresswith the domain (and drops a fixed IP andstatus.addresses) in responses for users. Admins still see the IPs. The domain must resolve or be forwarded to the IPs of the enabled pools. -
Storage classes: every StorageClass of the cluster is listed (provisioner, reclaim policy, binding, expansion); the checked ones appear in the storage dropdown of New server, one is the default. Migrate storage class in the danger zone of a server moves it to another enabled class (see Storage migration).
-
Load balancer pools: every
CiliumLoadBalancerIPPool(cilium.io/v2, falls back tov2alpha1) with blocks, free IPs per IP family and its service selector. Cilium reports one count for IPv4 and IPv6 together, so the panel counts each family itself: the addresses of the blocks, and as used the load balancer IPs of all services in the cluster plus the first and last IP of CIDR blocks when the pool setsallowFirstLastIPs: No. A server stores the pool name; the controller sets thematchLabelsof the pool'sserviceSelectoron the service (e.g. poolgeneral-pool→lb.cilium.io/pool=general) and removes the labels of the previous pool when it moves (annotationkubedactyl.io/pool-labels). Pools that are disabled, usematchExpressionsor select by service namespace/name cannot be enabled. Users can move their servers between enabled pools (the address changes; Cilium releases the old IP and assigns a new one, verified live). A fixed IP must be inside the pool's blocks and is cleared when the pool changes; give one per IP family, separated by a comma, because a server with fixed IPs gets only these (a transfer fixes all of them). The service asks only for fixed IPs of its own IP families: Cilium would assign a fixed IPv6 address to a service without IPv6 too, where it does not work. -
IPv6: a switch per server (admins). It sets
ipFamilyPolicy: PreferDualStackon the service: the API server keeps only the families the cluster has, so on a single stack cluster nothing changes. On a dual stack cluster Cilium assigns an IPv6 address when the pool has an IPv6 block (the IPv4 address does not depend on it). The game must listen on IPv6: eggs that bind to{{SERVER_IP}}(0.0.0.0) listen only on IPv4. The server shows every address (status.addresses); with an external domain, add an AAAA record for the IPv6 addresses. Pools with an IPv6 block get the badge "IPv6 not available in cluster" (and the switch a warning) when theServiceCIDRobjects (Kubernetes 1.33+) have no IPv6 range orkube-system/cilium-confighasenable-ipv6: false. -
ClusterIP: the switch "Allow ClusterIP" below the pools (
allowClusterIP). Owners can then pick "ClusterIP" instead of a pool: the service becomesClusterIP(spec.serviceType), without pool labels, fixed IPs or traffic policy, and Cilium releases the load balancer IP. The server keeps its pool for when it gets a load balancer again. Its address is the service's DNS name<server>.<namespace>.svc, shown to users instead of the external domain; diagnostics skip the DNS and router checks. With network isolation on, it is reachable from the owner's other servers (e.g. a proxy server) and from namespaces that are not user namespaces (e.g. a gateway), not from other users' servers. -
OpenID Connect: identity provider, client and groups; see Single sign-on.
-
Panel updates: see Self-upgrades.
On the first start the settings are created from --storage-class and --lb-pool (pool name or the
value of its lb.cilium.io/pool selector label), and servers without a storage class / pool are
filled in from their existing PVC and service.
Migrate storage class in the danger zone of a server's Settings tab (POST /api/servers/{server}/migrate) moves
its files, backups included, to a volume of another enabled storage class. The server is stopped (as with Stop) and
locked for everyone until the migration has ended; the server page shows the step and the files copied.
- The controller deletes the game and files pods and creates the claim
<server>-migrateof the new class. - The pod
<server>-migrate(helper image, root with onlyCHOWN,DAC_OVERRIDE,FOWNER,FSETID) mounts the data volume read-only and copies everything with owners, modes and links, then compares the number of entries and bytes of both volumes. - Both volumes are set to
Retain, the claims are deleted and the new volume is bound to<server>-data(annotationkubedactyl.io/moved-volume). - Once that claim is bound, the old volume is set to
Deleteand removed by Kubernetes.
The state lives in status.migration and spec.storageClass / status.storageClass (the class of the bound
volume), so a restart of the panel continues where it stopped. A failed copy deletes the new volume and leaves
the server on its old one (spec.storageClass is set back, the error shows in the danger zone). Cancel on
the server page (POST /api/servers/{server}/migrate/cancel) works until the switch in step 3. The server
stays stopped afterwards unless Start the server after the migration was switched on (startOnCompletion,
off by default): then it starts as soon as the migration has finished. A failed or cancelled migration always
leaves it stopped. Changing spec.storageClass with kubectl starts a migration too.
Cluster (admins) shows every node with roles, status, pressure conditions, taints, OS / kernel / kubelet / runtime, capacity, live usage (metrics-server) and the resources requested by running pods, plus the totals of the cluster.
Kubernetes does not know the CPU model or the machine. The panel reads it with a short-lived
probe pod per ready node (kubedactyl-probe-… in the panel namespace): unprivileged
(UID 65534, read-only root filesystem, all capabilities dropped, tolerates every taint), it prints
/proc/cpuinfo (model, cores, threads, max clock, hypervisor flag) and /sys/class/dmi/id
(vendor, product, BIOS) and is deleted right away. Results are cached for 24 h in the ConfigMap
kubedactyl-node-hardware (nodes that left the cluster are dropped); Re-read hardware probes again
(POST /api/cluster/nodes/probe). Nodes that are not ready are skipped. Probe pods left by an earlier panel
are removed when the panel first reads the nodes.
Health checks (GET /api/cluster/health, cached 30 s unless a read failed, checked again at once after a
settings change) are shown in the sidebar cluster card above the user menu (green / amber / red; problems are
always listed with their explanation, hovering the card also lists the passing checks in green): Kubedactyl CRDs
served, all permissions granted (a check the API server did not answer is a warning), nodes ready, metrics-server available, Cilium LB IPAM installed and the enabled pools
present with free addresses, the enabled storage classes present, and (in the cluster) the admission policy
that limits the panel's permissions ("Permission guard").
The Access section shows how the panel connects: kubeconfig file, context, cluster, user entry and
all context names (or the in-cluster service account and its token expiry), the authentication
method (client certificate subject/expiry, masked token, exec plugin with credential arguments
redacted), TLS/CA, the identity the API server reports (SelfSubjectReview) and a
SelfSubjectAccessReview for every permission the panel needs.