Skip to content
Open Source

Architecture

Zeabur shows a Kubernetes cluster as projects and services, and lets people change them, without holding any power of its own. It is a projection: Kubernetes objects are the only state.

  • No power of its own. Every read and write is made with the person's credential: a token issued by Zeabur Identity in the cluster, or their kubeconfig context on their machine. kube-apiserver decides. Zeabur does not impersonate, and its service account can do nothing a person cannot. Member roles are RBAC too: the chart installs three ClusterRoles (zeabur-member, zeabur-project-developer, zeabur-project-viewer), and an admin giving a member a role makes RoleBindings to them in that project's namespace, as the admin (members/access.rs; Members' rights). A Developer may patch their project's Namespace; the chart's ValidatingAdmissionPolicy keeps that patch to the project's details, since labels decide Pod Security admission and NetworkPolicy namespace selectors, and adopting is a label, left to whoever may zeabur-manage the namespace. Every member can list namespaces and so read their annotations: details hold no secrets. A project Zeabur creates enforces, audits, and warns at Pod Security baseline. New services select Strict or Compatible based on the image user or build method. Switching profiles changes only the service and needs no admin rights; Developers still cannot change Namespace labels (Members' rights). When some namespace carries zeabur.com/platform: "true", creating a project also writes the NetworkPolicy zeabur-project (zeabur-model's kernel::isolation): ingress only from the project, the labelled platform namespaces and kube-system, egress open. Without the label nothing tells which namespaces the platform runs in, so no policy is written. On an existing cluster, label them yourself: kubectl label namespace <ns> zeabur.com/platform=true. GET /api/v1/cluster/gaps names the project namespaces without it. The trust boundary is the cluster: everyone who signs in is trusted, and the policy only limits the damage one compromised service can do. A new project's default ServiceAccount has automountServiceAccountToken: false; a workload that calls the Kubernetes API names a ServiceAccount of its own.
  • Kubernetes objects are the only state. No database, no cache of Secret values, and no custom resources for layers L0 to L2. A project is a namespace; creating one is a plan like any other change, and adopting one only adds a label. Removing Zeabur leaves the cluster as it was, but for the member bindings admins made, which carry the label zeabur.com/member.
  • Project, not own. Services are inferred from owner references, selectors, env and volume references, backendRefs, operator conventions and the labels of Helm, Argo CD and Flux. For objects a GitOps tool or Helm manages, Zeabur exports a patch or warns, and never fights over fields.
  • Suggest, then plan. Zeabur detects missing capabilities and writes the helm command, Flux HelmRelease, Argo CD Application or K3s/RKE2 HelmChart. People run the helm command themselves; the objects are created by a plan they review, as the signed-in person.
LayerWhat it addsRuns as
L0read: projects, services, relations, logs, eventszeaburctl ui (local)
L1write: plans, JSON Patch, create and delete with preconditions, releases and rollbackzeaburctl ui (local)
L2the in-cluster server, sign-in through Zeabur Identityzeabur
L3, L4full sign-in, multi-tenancylater

A layer depends only on the layers below it; turning one off leaves those below working.

  • zeabur: the in-cluster server. It serves the API under /api and the embedded web interface, and signs people in as a confidential client of Zeabur Identity.
  • zeaburctl: the command-line tool on a person's machine: ui (the interface with the current kubeconfig context, on a random loopback port) and doctor.

The split follows Zeabur Identity (zeabur-identity/zeabur-identityctl): code only a person's machine runs never ships in the server image.

Everything Zeabur says about a failure is read from the cluster at the time of the request, with the person's rights, and names the objects and fields it rests on:

  • Diagnoses (zeabur-gaps) say what fails, why, how to fix it, where the cause lies (the code, the configuration, the cluster, a component, an upstream registry) and who can fix it.
  • Incidents group what one cluster cause breaks (an ingress controller or cluster DNS down, a node not ready), so the cluster's fault is not shown as each service's own.
  • Traces follow each domain and private address hop by hop, from DNS to a ready pod.
  • Plan previews say, before an apply, which pods are replaced, whether new ones fit the quotas and the nodes, and which data and fields go.
  • Timelines and rollouts read releases, managedFields, events and container statuses; no history of Zeabur's own is kept, so what the cluster does not keep (who made a write, an event older than its TTL) is not shown.
  • Evidence bundles put a service's problems, trace, events, previous logs and objects in one Markdown text, with no Secret and no variable value.

What cannot be read is named (unreadable), and a check that cannot be made is unknown, never ok.

A terminal is pods/exec as the person, relayed over a WebSocket. Two plans prepare one, each applied like any other plan and alone in it:

  • A debug container (debugContainer), for a container without a shell: an ephemeral container written through pods/ephemeralcontainers, with a pinned busybox image, stdin and tty, targetContainerName set so it shares the target's process namespace, and the target's user with every capability dropped. Kubernetes keeps it until the pod is replaced; the preview says so. Zeabur's earlier one is reused, matched by its name prefix zeabur-debug-.
  • A tools pod (clusterTools): kubectl and helm acting as the person, for what Zeabur has no page for.

The tools pod holds the person's Zeabur Identity token, which Zeabur otherwise never lets out of its process. So:

  • In the terminal namespace (zeabur-terminals), after a plan that says what runs, as whom, until when, and that it is deleted on close or at the deadline. Zeabur first asks kube-apiserver whether the person may create pods/exec in the project (for the cluster scope: whether they are a cluster administrator), then creates, execs into and deletes the pod with its own ServiceAccount. The chart binds that account to a Role with exactly those rights in that one namespace. Members have no rights there, so no one can list or exec into another person's tools pod. kubectl in it starts in the project.
  • Nothing of Zeabur's in it: automountServiceAccountToken: false, no service links.
  • Locked down: non-root, read-only root filesystem, no privilege escalation, every capability dropped, seccomp RuntimeDefault; small requests and limits; activeDeadlineSeconds (30 minutes by default); labels naming it Zeabur's and whose it is.
  • The token never rests in the API: not in the pod spec, an environment variable, a ConfigMap or a Secret. Each terminal writes a kubeconfig through exec stdin into an emptyDir with medium: Memory at /home/user/.kube, readable only by the pod's user. The kubeconfig reaches kube-apiserver at kubernetes.default.svc with the cluster CA from kube-root-ca.crt.
  • Bounded by the token: Zeabur Identity's tokens live minutes and cannot be revoked. The terminal closes when the person's token expires; kubectl and helm refuse with a plain message after that time; reopening writes a fresh token.
  • Gone after use: when the last terminal closes, the kubeconfig is removed, and the pod is deleted after a grace for reconnects (60 seconds by default). Pods left behind (Zeabur restarted, the token expired before the deletion) stop at their deadline, and the person's next tools request deletes them.

What remains: until the token expires, Zeabur itself, cluster administrators and whoever administers the pod's node can read it. Nobody else should get rights in the terminal namespace.

CPU and memory use come from metrics-server (metrics.k8s.io). Everything else measured comes from the metrics store, a Service serving the Prometheus HTTP API, read through kube-apiserver's service proxy as the person (get on its services/proxy). With Zeabur Proxy, history from the default store goes through the proxy instead, held to the project's namespace (get on metrics.proxy.zeabur.com):

  • History: range queries over the series the store keeps.
  • Node disk: node-exporter's node_filesystem_size_bytes, node_filesystem_avail_bytes and node_filesystem_free_bytes for the filesystem holding /var/lib/kubelet (the kubelet's nodefs, which disk-pressure eviction watches), else /. A series counts for a node by its node label, else by its instance host matching the node's name or one of its addresses.
  • Volume use: the kubelets' kubelet_volume_stats_* by namespace and persistentvolumeclaim.

Zeabur never reads the kubelets' own API (nodes/proxy): that right also lets someone run commands in any pod on the node. Where the store is not set up, refuses, does not answer, or holds no series for a node or claim, the answer says which, and the interface says it in one line instead of a number; requests and requested sizes are still shown.

GET /api/v1/projects/{project}/storage lists the cluster's StorageClasses and, for each of the project's PersistentVolumeClaims, its class and the node its PersistentVolume is kept on. Both are cluster-scoped and read as the person; the chart's member role grants list on StorageClasses and get on PersistentVolumes, and a refusal is named in unreadable instead of failing the answer.

A class is shown by the kind of disk its provisioner gives:

KindProvisionersWhat it means
localrancher.io/local-path, openebs.io/local, kubernetes.io/no-provisionerA directory or disk of one node; the PersistentVolume's node affinity pins its pods there
replicateddriver.longhorn.ioCopies on several nodes; a volume follows its pod
otherany otherUp to its driver

A new volume, or a database's storage, goes on the class chosen for it; the default sends no storageClassName, so the cluster's default applies. Growing a volume is offered only when its class has allowVolumeExpansion; the plan refuses it otherwise (expansion_not_allowed).

Zeabur holds no credentials and reads none of the issuer's objects. The Account page shows what kube-apiserver says:

  • Who you are: the user name and groups of a SelfSubjectReview.
  • Your role in each project: one SelfSubjectRulesReview per adopted project, summarised as Admin, Developer or Viewer by a few key rights (crates/zeabur-api/src/account.rs lists them). A cluster administrator, found with one SelfSubjectAccessReview for every verb on every resource, is Admin everywhere and no project is reviewed. A project whose review fails says its access could not be read.
  • Sign-in methods: a link to the issuer's own page, from issuer.accountUrl, else the manage URL of the issuer's /.well-known/passkey-endpoints (W3C Passkey Endpoints), read with a 3-second deadline and kept for 10 minutes, else no link.

Members (/members) is Zeabur Identity's admin view and still reads and changes its objects as the signed-in person.

The sign-in settings on the Settings page are the ConfigMap zeabur-identity-settings in Zeabur Identity's namespace, read and written as the person; Zeabur Identity applies them at every sign-in and refresh (Sign-in settings).

A sign-in session (crates/zeabur-auth/src/session.rs) ends at the shorter of Zeabur's idle limit (1 hour) and the idle time Zeabur Identity gives the refresh token (refresh_expires_in), and 12 hours after sign-in at the latest. Requests the browser marks as background, sent after 60 seconds without input, renew the tokens but do not count as activity, so a page left open lets the session go idle.