The OpenEng Kubernetes app is a full cluster console — dashboard, resource explorer, topology, nodes, an interactive pod terminal, and live events — split across two halves that trust each other and almost nothing else: a local sealed engine bound to 127.0.0.1:50054 that does all cluster access, and a thin web UI at k8s.openeng.app that only renders panels and issues operations. Your kube credentials — tokens, client certs, exec-credential plugins — are resolved entirely inside the engine, on your machine, from your own kubeconfig. They never cross the wire to the browser, and they never egress to OpenEng.
Overview
The browser cannot reach a Kubernetes API server directly, and you would not want it to hold your cluster credentials if it could. The app resolves that by keeping the two concerns physically apart:
The engine (local)
A native binary that binds loopback 127.0.0.1:50054. It reads ~/.kube/config plus any imported fragments, builds an authenticated client per context using the kube and k8s-openapi crates (no kubectl shell-out), and executes every operation — list, get, apply, logs, exec, metrics — against the API server itself. Credential resolution happens here and only here.
The UI (browser)
A static single-page app served from k8s.openeng.app. It is a pure client: it renders the clusters / resources / pods / logs / events / metrics / nodes panels and sends gRPC-Web calls to the local engine. It never reads your ~/.kube/config: cluster auth for kubeconfig contexts is resolved by the engine. It does handle a kubeconfig you paste (sent once to the engine), a connector's keys (echoed back to pre-fill the re-auth dialog), and any resource YAML you open, which for a Secret includes its data.
Every call between them travels the same single encrypted boundary. The transport is gRPC + gRPC-Web over loopback HTTPS, with each frame sealed to the public key the other side sent in CreateSession (the client sends client_pub, the engine returns engine_pub). When the client offered a key, every subsequent RPC is sealed per frame with a fresh ephemeral X25519 key, HKDF-SHA256 and ChaCha20-Poly1305; the key exchange isn't authenticated, and a request frame that fails to unseal is passed on as it arrived. A default-deny auth interceptor guards the port, and the browser pairs by fetching a 0600 handshake token the engine mints at boot. The security properties are covered under Security model and engine updates.
Note
One engine per machine. The engine holds a spawn lock, advertises its endpoint only after a successful bind, and serves every browser on the host from the same local state — so preferences, caches, and the current context are consistent no matter which browser you open.
Install and pair the engine
Kubernetes is free. It is one of the five engineering apps on the OpenEng platform and shares your OpenEng sign-in, but it runs on its own engine. The engine talks to the Kubernetes API itself, so you do not need kubectl installed. The app is a direct client for your own clusters and does not run agents; to let an agent work with a cluster, see Reusing a cloud cluster in Chat or OpenEng AI.
-
Install the engine. On Linux (x86_64 or ARM64) or macOS (Apple silicon or Intel; for now the macOS build is the previous release, see Platforms), run:
bashcurl -fsSL https://openeng.app/engine/kubernetes/install.sh | bashThe installer downloads the binary for your platform, checks it against its published SHA-256 checksum when it can (it prints
checksum verified; see Check a download yourself), and installsopeneng-kubernetesinto~/.local/bin(setOPENENG_BIN_DIRto use another directory), adding that directory to yourPATHif needed. There is no native Windows build; on Windows, use WSL. -
Sign in. Open k8s.openeng.app and sign in with your Google account.
-
Start the engine. The pairing screen (
/pair) shows a start command with a one-time key, ready to copy:bashopeneng-kubernetes serve --oauth=<one-time key>Run it on the same machine. The app connects as soon as the engine is up (or when you click Retry connection) and opens the page you were heading to.
Note
The one-time key expires after about five minutes, a refresh reuses a key that is still valid, and Regenerate mints a new one. The OpenEng API mints at most five unused keys an hour per account, shared by all OpenEng apps (a key an engine has used no longer counts), and each app also counts the keys it mints in your browser. The engine reads your existing ~/.kube/config, so your kubeconfig contexts appear as soon as it connects. Install, platforms and updates covers updating and uninstalling.
Using the app
This section walks through the console at k8s.openeng.app. The sections after it are the reference for what the engine does underneath.
The workspace
- Left menu: Dashboard, Explorer, Topology, Nodes, Terminal, and Events, with Settings at the bottom. Each section has its own URL (
/overview,/explorer,/topology,/nodes,/terminal,/events,/settings), and the active cluster (?ctx=), namespace (?ns=), and open resource (?name=) ride along in the query string, so a refresh or a shared link opens the same view. Any other path opens/overview. - Header: a button that hides or shows the side panel, the cluster switcher, a status dot for the engine, a search button for the command palette, and your account menu. The cluster switcher shows each cluster's health and nodes ready, marks cloud clusters and read-only clusters, and ends with a Settings row. Switching clusters resets the namespace to all namespaces and closes any open resource.
- Every section except Settings needs a connected cluster; without one, it asks you to pick one.
Adding and switching clusters
Settings → Clusters lists every cluster the engine knows: your kubeconfig contexts and any cloud clusters. The active one is marked Connected, and every other row has a Connect button. A cloud cluster shows a read-only or read-write badge and a remove button; kubeconfig contexts cannot be removed here.
- Add a kubeconfig: enter a cluster name, paste the kubeconfig YAML, and click Add kubeconfig. See Importing a kubeconfig.
- Add a cloud cluster: enter a name, the connector URL, and its API key. Tick Use a separate auth token to send a different bearer token, or Configure a write key (grants write access) to enter a write key. The result reads "Connected read-only." or "Connected read-write." A write key that the connector does not accept still adds the cluster, read-only. See Cloud clusters via connector and Connectors.
Dashboard
Dashboard (/overview) is a live view of the active cluster: stat cards for CPU, Memory, Pods, Nodes, Workloads, and Alerts; CPU Usage and Memory Usage charts; workload health; per-node capacity; recent cluster events; load per namespace; the top CPU consumers; and recent Deployment rollouts. The 5M, 15M, 1H, 3H, 6H, and 24H buttons set the charts' time window. CPU and memory figures come from the cluster's metrics-server; without one, the dashboard shows zeros and empty states instead of failing.
Explorer and the resource drawer
- Explorer lists resource kinds in its side panel (on a kubeconfig cluster, every kind the API server reports, custom resources included), with a namespace picker on top (one namespace, or all). Picking a kind shows its resources in a table you can filter; the trash button on a row deletes it after you confirm. On kubeconfig clusters the table updates live as resources are added, changed, or removed; on connector clusters it refreshes by listing again (see Generic resource operations).
- Click a row to open the resource drawer. Every kind has the overview (details, containers, conditions, and labels), yaml, events, and actions tabs. Pods and nodes also get metrics; pods get logs and files; pods and workloads (Deployments, StatefulSets, DaemonSets, ReplicaSets, Jobs, and CronJobs) get env. The open tab is part of the URL. Esc closes the drawer.
- yaml: the live manifest. Switch to edit mode, change it (an "edited" badge appears), and click Apply to server-side apply it.
- logs: a live stream of a container's log. Choose the container (init containers included), search, wrap long lines, refresh, or download the output as a
.logfile. - metrics: live CPU and memory charts. Network and disk I/O show as unavailable, because metrics-server does not report them.
- env and files: the container's environment variables (the running values when exec is allowed, otherwise the values declared in the pod spec) and a browser for the container's file system.
- actions: Edit YAML, Scale, Restart Rollout, Port Forward, View Logs, Cordon and Drain (nodes only), and Delete. Drain and Delete ask you to confirm first.
Topology
Topology draws the active cluster's Services, workloads (Deployments, StatefulSets, and DaemonSets), and Pods as a graph, built in your browser from the resource lists. It has a namespace filter, a refresh button, a health legend, a count of workloads and pods, and pan, zoom, and minimap controls.
Nodes
Nodes shows a card per node with its readiness, a cordoned marker, roles, kubelet version, CPU and memory capacity, taints, and conditions. Cordon and Uncordon ask you to confirm, and Drain opens a dialog that streams the drain's progress line by line.
Pod terminal
Terminal lists pods. Pick one (and a container, if it has several) to open a shell. On a kubeconfig cluster this is a live interactive terminal. On a connector cluster each command runs on its own through the connector, with the working directory and environment carried from one command to the next, and the app provides ↑/↓ history and line editing (see Interactive exec). The engine keeps what you saw, so it comes back when you return to the pod, refresh, or reconnect.
Events
Events (/events) is the full-screen live event stream for the current namespace scope. Filter it by All, Warning, or Normal, search across reason, message, object, and namespace, and click Pause to hold the list still while you read. When you resume, events that arrived during the pause are not added back.
Read-only clusters and re-authentication
- On a read-only cloud cluster, every control that would change the cluster is disabled, with the tooltip "Read-only cluster — actions are disabled", and a banner says so above each affected pane. Reading stays available: logs, search, refresh, and View Logs keep working. A pod that matches the connector's force-exec allowlist still gets a terminal; other pods show "Read-only — exec not available". See Scopes: read-only, write, and gating.
- If the active cloud cluster rejects its stored credentials (HTTP 401 or 403), the app opens a dialog pre-filled with the stored API key, where you can correct the key or add an auth token and click Save & retry. If you cancel, the cluster is marked inaccessible, and a bar offers Re-enter credentials to reopen the dialog. Kubeconfig clusters never show this dialog.
Command palette and account menu
⌘+K (Ctrl+K on Windows and Linux), or the search button, opens a palette that jumps to any section or switches the namespace (namespace: <name>); switching the namespace closes any open resource. The account menu (your avatar) shows your email, the engine and app versions, a Release notes link, and Sign out.
Connections and clusters
The engine presents a single flat list of clusters via ListClusters, blending two sources into one uniform Cluster row. The active one is the current context; you switch with SetContext and the header cluster picker.
Kubeconfig contexts
Every context in your merged kubeconfig (~/.kube/config plus imported fragments) becomes a row. These are backed by a real local kube::Client: the engine resolves the context's token / cert / exec-credential locally when it first builds and caches the client. Watch, port-forward and drain are available; Helm is engine-only for now (see Helm releases).
Connector clusters
A cluster whose source is an HTTPS connector rather than a local client, identified by the context id connector:<name>. The engine fetches resources by POSTing tool calls to the connector instead of talking to an API server itself. Ideal for managed clusters reachable only from inside a VPC. See Cloud clusters via connector.
Switching and connecting
The active context is URL-addressable: ?ctx= in the UI drives it, and the header switcher writes it. Switching to a context runs a small pipeline — SetContext, then ConnectCluster (which builds/validates the client, or for a connector calls cluster_info), then discovery (ListNamespaces + ListResourceKinds). The relevant RPCs:
ListClusters list every kubeconfig context + connector cluster
GetContext the engine's current context
SetContext switch the active context (validated against known contexts)
ConnectCluster build + verify the client; returns the API server version
ClusterHealth api reachable? server version, nodes ready / total
Both kinds switch identically from the UI's point of view — the header picker, the Settings clusters list, and a deep-linked ?ctx all funnel through the same switch.
Importing a kubeconfig
You do not have to depend on whatever is already in ~/.kube/config. In Settings → Clusters, paste a kubeconfig YAML with a name and click Add kubeconfig; the UI calls AddKubeconfig. The engine:
- Parses the YAML and rejects anything with no contexts (or invalid syntax).
- Writes the fragment
0600under~/so it survives restarts — the file may carry credentials, so it is never logged.. openeng/ kubernetes/ kubeconfigs/ - Merges it into the live registry and returns the newly available context names, which appear immediately in
ListClusters.
Tip
On boot the engine folds every imported fragment on top of ~/.kube/config. A broken fragment is skipped rather than fatal, so one bad paste never stops the engine from serving your other clusters.
Cloud clusters via connector
A connector cluster is a cluster reached through an OpenEng cloud-Kubernetes connector — a deployed service (e.g. a Lambda) that runs inside your own cloud/VPC, speaks the OpenEng tools protocol over HTTPS, and reaches the API server from there. The engine never holds the cluster's credentials; it only holds your key to your connector, and it sees only the connector's trimmed JSON responses.
Adding one
Settings → Clusters → Add a cloud cluster calls AddConnectorCluster. The form fields map directly onto the request:
| Field | Wire field | Meaning |
|---|---|---|
| Name | name | Assigned the context id connector:<name>. |
| Connector URL | url | The connector base URL (must start with http:// or https://; trailing slashes trimmed). |
| API key | api_key | Your connector key, sent as the x-api-key header. |
| Auth token (optional) | auth_token | An explicit Authorization: Bearer value. Empty ⇒ the API key is reused as the bearer. |
| Write key (optional) | write_key | A plaintext write key sent as x-write-key. Empty ⇒ read-only. See Scopes. |
On add, the engine probes the connector's GET /manifest to (a) validate reachability and the credentials and (b) detect whether the connector is serving read-only. It then persists the connector 0600 under ~/ and returns the assigned context plus the detected read_only flag. A 401/403 comes back stamped with the CLUSTER_ sentinel so the UI can say "check the API key" rather than "unreachable".
How reads are served
For a connector context the engine routes each operation to a connector tool over POST / (NDJSON), reducing the streamed frames back into one result for unary RPCs. An older connector that predates streaming (which 404s /call-stream) is transparently retried over the buffered POST /call, so a not-yet-redeployed connector is never broken. A connector exposes no discovery API, so the engine advertises a static set of common kinds for the Explorer.
ListNamespaces -> list_namespaces ConnectCluster/Health -> cluster_info
ListResources -> list_<plural> GetResource/Describe -> get_<kind>
ListPods -> list_pods GetPodLogs (stream) -> pod_logs
ListNodes -> list_nodes GetMetrics -> node_metrics | pod_metrics
ExecPod -> pod_exec (per-command) ApplyResource -> apply_manifest
DeleteResource -> delete_resource Cordon/Uncordon -> node_cordon | node_uncordon
Re-authentication
When a connector later rejects the stored credentials mid-session (any RPC returning CLUSTER_), the UI raises a re-auth dialog — but only if that connector is still the active cluster, so a stale late 401 from a cluster you just left cannot pop a dialog. GetConnector reads the stored api_key, auth_token, and write_key back to the local UI to pre-fill the dialog; you edit, save, and retry. That echo stays strictly on the loopback seal boundary — the same trust domain that entered the values — and echoing the write key back means a re-auth save never silently drops your write access. Remove a connector with RemoveConnectorCluster (it deletes the persisted file).
Scopes: read-only, write, and gating
Local kubeconfig clusters carry no OpenEng-level scope — whatever your kube RBAC allows, the engine allows; RBAC is your authority. Connector clusters, by contrast, have an explicit read-only / write scope enforced at two layers.
Read-only by default
A connector added with no write key attaches read-only. The engine's guard_read_only hook refuses every mutation routed to a read-only connector with permission_ ("cluster is read-only") — ApplyResource, DeleteResource, CordonNode, UncordonNode. The connector itself also hides and blocks mutating tools, so the guard is defense in depth, not the only line.
Write via the write key
Supply a write key and the engine sends it as x-write-key; the connector grants write access when it matches its configured WRITE_KEY hash. The Settings row then shows a read-write badge instead of read-only. Generate the key at connectors..
Live read-only re-probe
Read-only status is not frozen at add time. Every ListClusters re-probes each connector's /manifest, so a connector that flips read-only ⇄ write (e.g. its env changed) takes effect on the next refresh — the UI enables or disables mutating actions immediately, with no re-add.
Force-exec prefixes
A read-only connector can still be allowed to exec into specific pods via its K8S_FORCE_EXEC allowlist — a set of pod-name prefixes surfaced on each cluster row as force_exec (refreshed on every ListClusters manifest re-probe). The UI uses these to decide, per pod, whether to offer an exec console even on a read-only cluster. Because the connector enforces the allowlist itself and refuses non-matching pods, the engine deliberately does not apply guard_read_only to connector exec — a refused pod comes back as an error in PtyDone.status.
Note
Metrics is always a read and is never gated by read-only, on either kind of cluster.
The workspace
Once a cluster is active, the console is organized into sections; each (except Settings) requires a connected context and otherwise prompts you to pick one.
Overview
The dashboard — cluster health, node readiness, namespace rollups, recent rollouts, top consumers, and metric sparklines.
Explorer
The generic resource browser: pick a kind and namespace, list rows, and open the detail drawer for YAML / describe / edit / delete.
Topology
A visual map of workloads and their relationships across the selected scope.
Nodes
Node inventory with readiness, roles, kubelet version, capacity, taints, and cordon / uncordon / drain actions.
Terminal
The pod exec console and per-pod interactive shell.
Events
A live stream of cluster events (Normal / Warning) for the current namespace scope.
Settings is reachable without a connected cluster — it hosts the local engine status and the clusters management (kubeconfig import + connector add / switch / remove). The current context (?ctx) and namespace (?ns) are carried across sections in the URL; per-view state like the open drawer is dropped when you navigate.
Namespaces and resources
Discovery is two RPCs: ListNamespaces for the namespace switcher and ListResourceKinds for the kind picker (group / version / kind / plural / namespaced). A local cluster enumerates kinds from the API server's discovery; a connector advertises a static common set.
Generic resource operations
Everything in the Explorer runs through a small, kind-agnostic set of RPCs that work for any kind via the dynamic API:
| RPC | Returns | Purpose |
|---|---|---|
ListResources | summaries | Rows for a kind in a namespace: status, humanized age, and kind-specific columns (Ready, Restarts, IP, …). |
GetResource | full YAML | The complete object manifest, for viewing and editing. |
DescribeResource | pretty text | A kubectl describe-style rendering with status, conditions, and events. |
ApplyResource | ok | Server-side apply from edited YAML (write-gated on connectors). |
DeleteResource | ok | Delete by kind / api-version / name / namespace (write-gated on connectors). |
WatchResources | stream | Live table updates: ADDED / MODIFIED / DELETED events feed the row list. |
Watch is local-only
WatchResources is served through a local kube::Client and is not routed to connector tools. On a connector cluster the resource tables refresh by re-listing rather than by a live watch stream.
Pods, logs, and exec
ListPods returns the compact pod shape — phase, node, IP, restarts, ready/total, age, containers — for the pods pane. Two richer surfaces sit on top.
Streaming logs
GetPodLogs streams LogEvents. It takes container, follow (tail), previous (the crashed container's prior instance), tail_lines, and since (e.g. 1h, 10m). On a connector the logs stream over /call-stream, forwarding each NDJSON text chunk into the same LogEvent stream as it arrives, with the same tail semantics.
Interactive exec
The exec surface mirrors the Terminal engine's PTY shape: a server stream of PtyEvent (started / output / done) for output, and unary RPCs for input — WriteExec, ResizeExec, CloseExec. There are two execution modes depending on the cluster kind:
Local: a live PTY
ExecPod opens a real interactive PTY into the container over the kube client. The container's own shell handles line editing and its own tab-completion. Full-screen programs, cursor keys, and Ctrl-C behave as expected.
Connector: per-command exec
The connector transport is stateless per request, so instead of a live shell the engine runs each command via the connector's pod_exec and streams its output. It round-trips the shell state (cwd + env) the connector returns back into the next command — keyed per (context, namespace, pod, container) — so cd and export persist command to command. The resulting cwd is surfaced as PtyDone.cwd and rendered in the prompt.
Tab-completion for the connector console
Because a connector console has no live shell to complete paths, the engine serves completion explicitly via CompleteExec. Given the input line up to the cursor it resolves the trailing path token against the pod's filesystem with a single ls run under the session's stored cwd/env — deliberately without advancing that state (completion must never mutate the session it inspects). It returns the directory's full listing (dirs end in /, which the UI caches and filters by prefix), the fragment being completed, and the current cwd. A first bare word is treated as a command name and completed by the UI from its own catalog + history. Failures come back as a message, never a hard error — a completion that can't answer is a no-op at the prompt, not a broken console.
Port-forwarding and events
Port-forward
StartPortForward opens a local listener that tunnels local_ through the engine; ListPortForwards enumerates active tunnels and StopPortForward tears one down. Port-forward is served through a local kube::Client, so it is available on kubeconfig clusters only.
Events
WatchEvents streams cluster Events for a namespace — type (Normal / Warning), reason, message, involved object, last-seen, and count — feeding the Events pane and the Overview's recent-activity views. Like watch and port-forward, the events stream runs through the local client and is not routed to connectors.
Helm releases
The engine manages Helm releases through four RPCs: HelmList, HelmInstall and HelmUpgrade (each a ProgressEvent stream of live output lines), and HelmUninstall. Install/upgrade take release, chart, version, and values; uninstall takes an optional keep_history. The current console has no Helm section, so these RPCs are implemented in the engine but not exposed in the app yet.
Listing needs no helm binary. Helm 3 stores each release as a Secret of type helm. (labeled owner=helm). HelmList reads those Secrets directly, decodes each one, and returns the latest revision of every release (name, namespace, revision, status, chart, app version, and last deploy time), for one namespace or across all of them.
Install, upgrade, and uninstall need a local helm binary
The shipped engine drives these by shelling out to a helm binary resolved on PATH, streaming its stdout/stderr as progress. If helm is absent the operation returns a clean "helm binary not found on PATH" rather than failing opaquely. Helm operations run against local kubeconfig contexts only — they are not routed to connector clusters. The engine does not pass --kube-context to helm, so install, upgrade (run as helm upgrade --install), and uninstall act on the context your kubeconfig's current-context names.
Nodes and metrics
Nodes
ListNodes returns each node's readiness, schedulability, roles, kubelet version, age, CPU/memory capacity, taints, and conditions. Node operations:
CordonNode/UncordonNode— mark a node un/schedulable. Routed tonode_cordon/node_uncordonon a connector, and write-gated byguard_read_onlythere.DrainNode— evict pods, streaming progress. Served through the local client only (kubeconfig clusters).
Metrics
GetMetrics takes a scope of node or pod and returns CPU cores, memory bytes, and their percentages per entity — powering the Overview sparklines, top-consumers, and node-capacity views. On a local cluster it reads the metrics API (a metrics-server must be present); on a connector it routes to node_metrics / pod_metrics. Metrics is a read and is never read-only-gated.
Local vs connector capability matrix
Because a connector reaches your cluster through a tool API rather than a live client, a handful of streaming/tunneling operations are available only on kubeconfig clusters. The full picture:
| Capability | Kubeconfig context | Connector cluster |
|---|---|---|
| Namespaces, kinds, resource list/get/describe | Yes | Yes (static kind set) |
| Apply / Delete | Yes (RBAC) | Yes, if read-write |
Live resource watch (WatchResources) | Yes | No — re-list instead |
| Pods list & streaming logs | Yes | Yes |
| Pod exec | Interactive live PTY | Per-command (state round-trip + CompleteExec) |
| Port-forward | Yes | No |
Events (WatchEvents) | Yes | No |
| Helm | Engine only, not in the app yet (listing needs no binary; install, upgrade, and uninstall need helm on PATH) | No |
| Nodes list, cordon / uncordon | Yes | Yes (cordon/uncordon write-gated) |
| Drain node | Yes | No |
| Metrics | Yes (metrics-server) | Yes |
Reusing a cloud cluster in Chat
A connector cluster you add here is not private to this app. Because connectors are persisted by the engine under ~/ and exposed through the standard ListClusters RPC, the OpenEng Chat app's coding agent can drive the very same cluster — without ever re-entering the credentials.
Chat's engine opens its own sealed loopback session to the Kubernetes engine (the same 127.0.0.1:50054, addressed as engine. pinned to loopback, SNI + Host engine.) and calls the typed RPCs on your behalf: ListClusters, ListNamespaces, ListPods, DescribeResource, GetPodLogs, ListResources, and so on. A Chat group's config carries a connection_id that is exactly the Kubernetes context id — a plain context like prod, or a connector like connector:my-cloud — and an empty id means the K8s engine's current context.
The key property: the connector's API key, bearer, and write key never enter Chat. Chat asks the Kubernetes engine by context id; the Kubernetes engine is the sole holder of those credentials and performs the connector call itself. The read-only / write scope you configured here is enforced there too — if the connector is read-only, the agent's mutations are refused the same way the UI's are.
Tip
Add and validate a cloud cluster once, in this app's Settings. It then shows up automatically in Chat's connection picker, already scoped, already authenticated.
Security model and engine updates
What crosses the wire
The proto that defines this boundary has no field for an OpenEng secret or product prompt. A schema test scans it for fields named like a secret, credential, API key, password, private key or token, with the marked exceptions below exempted. Cluster auth from the kubeconfigs on your machine is resolved locally by kube and doesn't cross the wire. The rest of the payload is your own cluster data — resource YAML, logs, exec bytes.
There are two deliberate exceptions, both your own credentials, and they reach only your engine over the sealed loopback:
- A connector cluster's
api_key/auth_token/write_key, explicitly markedconnector-credentialand exempted from the test. That is your key for your connector — handed to the engine onAddConnectorCluster, stored locally0600, and echoed back to the local UI (over the loopback seal) to pre-fill the re-auth dialog. It is not a product secret leaking out. - A kubeconfig you paste into the app. Its YAML, which may carry tokens and client keys, travels to the engine in
AddKubeconfigRequest, and the engine stores it0600.
Boundary hardening
Sealed loopback HTTPS
Every frame after CreateSession is sealed with a fresh ephemeral key when the client offered a key at CreateSession (the exchange isn't authenticated, and a request frame that fails to unseal is passed on as it arrived). The engine serves TLS on loopback using the publicly-trusted engine. certificate (DNS-pinned to 127.0.0.1) so an https:// web app avoids Mixed-Content blocking, with a persisted self-signed cert as fallback.
Origin + Host allowlists
In the current Linux builds, CORS is not a wildcard: only the app's own origins — https:// and its Firebase default domains https:// and https:// (and localhost in dev) — may reach the engine from a browser, and the allowed origin is reflected, not starred. A separate Host/authority allowlist (engine., localhost, 127.0.0.1, ::1) defeats DNS rebinding — a request whose Host is an attacker domain is rejected outright even if the Origin trick would make it look same-origin. The macOS builds are older releases pending a rebuild and don't enforce the exact-origin allow-list yet; a build without it answers any website, so on macOS don't browse untrusted sites while the engine runs, or run the engine on a Linux machine until the rebuild ships.
Default-deny auth
The port sits behind a default-deny interceptor. The browser pairs by fetching a 0600 handshake token the engine mints at boot from OS randomness (vended at an auth-exempt GET / to the app's origin and to any local program that sends no Origin, so other users' programs on a shared machine can fetch it too); a world-readable token is refused. Private Network Access preflights are answered so the public app can legitimately reach the loopback engine.
Local-only persistence
Imported kubeconfigs (~/) and connectors (~/) are written 0600 and never logged. Generic UI preferences and SWR caches are engine-persisted (GetClientState/SetClientState, CacheGet/CacheSet) so every browser on the machine shares the same last-known state — never as a credential carrier.
Mandatory engine auto-update
The engine self-updates at boot and cannot be skipped. While it is serving, a newer stable build is downloaded, verified, and staged in the background; CreateSession and GetUpdateStatus report the running version plus any staged_version awaiting a restart. The UI surfaces a "restart to update" banner that calls RestartEngine — the engine re-execs in place into the staged binary, carrying the live session internally so you never re-pair, and the UI re-probes and reconnects to the new version. The one exception, from the release notes: restarting this way when updating from Kubernetes 0.0.4 asks for a fresh one-time key once; updating at the next start needs nothing. No credential rides either update RPC; they carry only version strings.