Pin the identity before you touch anything
Every procedure below needs these facts, and every one of them is read from the cluster rather than remembered. Write them at the top of your incident notes.$RELEASE-0 for brevity, which is right on a
default install. Where yours differs, substitute the pod name from step 3.
Upgrade
A Genesis upgrade is normally an image upgrade: same chart, same release name, same namespace, same PVC. The pod is replaced; the volume reattaches.1. Pull the new image into your mirror
If your nodes pull from a mirror rather than from the Genesis registry, the mirror is the first thing to update — and the tag you are about to deploy must exist in it.crane copies registry-to-registry without a local Docker daemon and preserves the
source digest and every platform in the manifest:
ImagePullBackOff.
2. Run the upgrade
3. Verify by content, not by “Ready”
A Ready pod proves a process started. It does not prove it is the build you shipped:main, latest and any other moving tag resolve to whatever the registry holds at
pull time, and IfNotPresent will happily keep a cached older layer under the same
tag. Verify the digest, and then verify the behaviour you upgraded for.
200; (d) names the same Bound claim as before; and the specific behaviour
you upgraded for is exercised by hand. If (a) and (b) disagree, the node is running
a cached layer — see
The upgrade “worked” but the old behaviour persists.What “version-gated” means
Some Genesis fixes change behaviour that has no switch: the code either does the old thing or the new one. A fix described as version-gated is delivered only by running an image that contains it, and no amount of configuration reproduces it on an older image. That is why step 3(a) reads the digest rather than the tag. When a support answer says “fixed in current builds”, the question it is answering is which image is running — and the only answer that settles it is the digest, checked against the digest of the build the fix landed in.Rollback
Roll the release back
helm rollback restores the rendered manifest of that revision — including the
image reference it carried. That reference is always repository:tag: the chart
takes a tag, not a digest. So if your values file pins a moving tag (main), the
rollback restores the tag and not the build, the node may pull the newest image again,
and you land back where you started with a green rollback to show for it.
The fix is to make the tag itself immutable. Mirror the known-good digest to a tag
of your own that nothing will ever move, then deploy that tag:
imageID must equal the digest you copied. The point is the same in
both directions: deploy a build, not a label.
What survives a rollback, and what does not
helm history shows the target revision
deployed; the running digest is the one you intended; /api/healthcheck returns
200; the PVC is the same Bound claim; and both namespaces still report
resource-policy: keep.Backup and restore
What is on the volume
One PersistentVolumeClaim per deployment (genesis-data-<statefulset-name>-0 by
default), mounted into the backend at four subpaths:
Snapshot the volume
The reliable mechanism is a volume snapshot taken by the storage layer, not akubectl cp. Find the underlying disk, then snapshot it with your cloud’s mechanism
(commands per cloud are in Per-cloud specifics):
fullnameOverride names a claim that does not exist, or worse, one that does and
belongs to something else.
For a file-level copy — useful as a second, portable line of defence, not as a
replacement for volume snapshots — the chart ships one:
Running or /app/.genesis/db is unreadable, and fails with exit 1 on an empty
result. Treat any exit other than 0 as “no backup was taken”.
The restore drill
An untested backup is not a backup. Run this on a schedule — quarterly at the least, and once before the deployment carries anything that matters. Restore to a separate release so the drill can never damage the live one.Checkpoint, snapshot, and record the identity
Create a claim from the snapshot
genesis-drill). Confirm STATUS Bound before going on.Install a second release onto that claim, with the same master key
GENESIS_MASTER_KEY the snapshot was taken under. Supplying a different key is
how a drill “passes” while proving nothing: the server starts, the threads are
there, and every stored credential is unreadable.To exercise the terminal in the drill instead, give it a workspace namespace of
its own (cliTerminal.enabled=true with cliTerminal.namespace=genesis-drill-cli),
never the production one — and own that namespace’s whole lifecycle in Step 5, not
just the release’s. Two releases must never share one workspace namespace.Prove the data came back, by content
Destroy the drill — all of it
Terminating is not deleted — find what is holding it
(kubectl get namespace genesis-drill -o jsonpath='{.status.conditions}') rather
than recording the drill as clean.Finally, prove the production workspace namespace came through untouched:- A claim provisioned from the snapshot reached
Bound - The drill release returned
200, with the same master key - A human confirmed a user, a thread and a provider configuration by looking at them
diff /tmp/ws-baseline.txt /tmp/ws-after.txtshows no PVC line changed in the production workspace namespace — same names, same UIDs. The drill never touched live users’ volumes. (Pod lines may differ; workspaces come and go.)- Nothing drill-owned remains, checked rather than assumed:
helm listdoes not show the drill release, both scratch namespaces areNotFound(notTerminating), and no Role/ClusterRoleBinding carries the drill’s instance label
Secrets
The master key
GENESIS_MASTER_KEY encrypts every credential Genesis stores. It is required — the
chart refuses to render without it — and it is not recoverable: lose it and every
stored connection credential in the database becomes undecryptable ciphertext, which
is a rebuild, not a recovery.
Three supported ways to supply it, in increasing order of operational maturity:
python3 -c "import secrets; print(secrets.token_hex(32))".
LLM and database credentials
genesisConfig path accepts auth_mode: "iam_role" for Bedrock, which authenticates
through the pod’s cloud identity and stores nothing.- The Mintlify documentation signing key (Ed25519, PKCS#8) is accepted only as
an existing Kubernetes Secret — never in
genesisConfig.content,config.extraEnv, a ConfigMap, or a plaintext Helm value. Rotating it is update-the-Secret and roll the workload, because the pod’s environment is what carries it. Give it a named security or platform owner, the same way the master key gets one. - Custom CA certificates are fetched once at pod startup, so rotating one needs a StatefulSet restart. The same is true of any value resolved from a cloud secret manager: after you rotate it there, restart so the pod re-resolves it.
existingSecret* reference is the rotation-friendly shape for all of these,
because the Secret is updated without a helm upgrade and the credential never
appears in the release record. The pod reads these into environment variables at
start, so a rotation takes effect on the next pod restart:
/api/healthcheck returns 200, a chat message gets a reply, and the embedding
endpoint is verified separately (see
Health and verification). Chat and embeddings can use
different credentials and different endpoints; proving one says nothing about the
other.Configuration precedence
Genesis reads one setting from up to five places. When a change “does not take effect”, it is almost always because a higher tier is holding the old value. Highest wins, and the Config Sources page names the tier that actually won for any field — which is the fastest way to end an argument about it:GENESIS__CLI_TERMINAL__ENABLED— prefixGENESIS__, then the config path uppercased with each.replaced by__. This is the force-lock: it is applied when the configuration snapshot is built, so it outranks everything below it. It is never persisted to the database.GENESIS_CLI_TERMINAL_ENABLED— single underscores. This is only a default. For a field declaredenv_leaf, the config-to-env bridge overwrites this variable with the admin’s stored value, live and with no restart — so on its own it does not hold against the panel.
Which fields the chart pins
SettingcliTerminal.enabled=true writes, as container env, both spellings of
three fields:
cliTerminal.maxWorkspaces is deliberately not pinned: lowering the cap in the
panel is safe and a lock would block it to prevent the unsafe direction. See
Scaling for the consequence of raising it there.
Two asymmetries that surprise people:
cliTerminal.enabled=falsepins nothing. The chart emits no lock at all when the feature is off, because deployments that provision the workspace namespace and RBAC outside the chart enable the feature through APP_CONFIG and must keep working. Sofalsemeans “the chart provides no platform”, not “the feature is off everywhere”.- Off ECR, the workspace image is yours and is not locked. With
ecr.enabled, the chart owns the image and pins it, because the pull secret it places authenticates to that registry and nowhere else. Off ECR, repointing the workspace image stays an admin’s to make.
What applies live and what needs a restart
Reset to defaults
“Set default” on a configuration row deletes the stored value and falls back to the tier below. On a current build, for an unlockedcli_terminal.* or
sandbox_runner.* row, that restores the deployment’s own pre-panel value — the
value the environment held before an admin ever touched the panel — rather than the
schema default. The response says so explicitly when it happens, naming the paths that
landed on an environment default rather than a schema one.
For a locked row the reset is refused outright and nothing is deleted, as above.
On a chart-managed install that covers cli_terminal.enabled, .runtime and
.k8s_namespace: they cannot be reset from the panel, by design.
Users: onboarding and offboarding
Onboarding
- The user signs in through your identity provider. Genesis maps their IdP groups to
Genesis roles on each login, from the
groupRoleMappingfile supplied to the chart, so group membership is the thing to change — not anything inside Genesis. - Terminal access is permission-gated, separately from the feature being enabled:
a user needs the
use_cli_terminalpermission. Without it the WebSocket closes with “Access denied”, which is a different failure from the feature being off. - The admin CLI Workspaces tab at
/pulse/cli-workspaceslists running workspace Pods with restart and cancel, and is gated by theview_sandboxespermission.
Offboarding: the step everyone misses
Revoking the IdP group removes access. It does not reclaim disk. Both of a user’s claims carry the ownership metadata needed to find them —genesis.computing/owner-id as a readable annotation, and
genesis.computing/workspace-owner as the selectable digest, because a label value
cannot hold an email address:
Terminating
until the Pod goes.
kubectl -n $WS_NS get pod shows no workspace for them; and
kubectl -n $WS_NS get pvc returns zero claims annotated with their address.
This is destructive and unrecoverable — it is that person’s saved conversations — so
confirm the address in the annotation before deleting, not the digest.Scaling
The capacity formula
The backend is fixed; workspaces scale with people.Nconcurrent is bounded by cliTerminal.maxWorkspaces (default 25) and
Nlifetime by cliTerminal.maxLifetimeUsers (default 50). At those defaults the
feature asks for 12.5 CPU and 100 GiB reserved, bursting to 200 GiB, on top of the
backend’s own 4 CPU / 16 GiB — and up to 1000 GiB of durable disk at the claim
ceiling. A count quota reserves nothing, so the disk ceiling costs only what people
actually claim.
Three facts that make real deployments size differently than expected:
- A workspace outlives the connection. Disconnecting does not delete the Pod; the
idle reaper does, after
cli_terminal.idle_reap_seconds(default 1 hour). Size for the peak number of workspaces that exist, not for the number attached right now. Shortening the reap interval returns capacity sooner and never costs a user work. - One user is one Pod, however many agents they run. Up to
cli_terminal.max_sessions_per_workspace(default 8) sessions share one container’s CPU and memory. More sessions per user is cheaper than more users — and more likely to hit the memory ceiling, which OOM-kills the container and takes every session that user has open. - Reserved is not used. A cluster sitting at 5% observed CPU can correctly refuse
to schedule a workspace. Compare requests against allocatable minus committed
requests, never against
kubectl top.
Raising the cap
Node auto-upgrade will interrupt sessions
A managed node pool upgrade drains and replaces nodes. Workspace Pods are evicted with them: running agent sessions are interrupted, and every volume survives, so users resume on their next attach on a new Pod. Treat node auto-upgrade as a maintenance window, not as a transparent operation. Set the maintenance window to your users’ off-hours, announce it, and prefer draining workspaces yourself beforehand so the interruption happens when you chose it:ReadWriteOnce
and, on zonal storage, bound to one zone — so a regional cluster with plenty of free
capacity in other zones leaves a workspace Pending indefinitely with
didn't match PersistentVolume's node affinity. Shrinking the workspace size does not
fix it; the constraint is the zone. Capacity has to exist where that user’s volume
already lives.Health and verification
Endpoints
failureThreshold: 60 × periodSeconds: 10, because the first boot builds the
database and warms sessions. /api/health/startup tells you whether that time is
being used or wasted — check it before restarting a pod that is simply still working.GET/HEAD
only — so /api/health/detailed authenticates even though /api/health does not. That
matters behind an auth_request-mode ingress: an ingress controller’s auth annotation
applies to the whole Ingress, so an external probe gets a login redirect rather than a
health answer. The chart’s ingress.authExemptHealth block renders a second Ingress
carrying only those exact probe paths, listed with and without a trailing slash, so
health checks read server liveness instead of the proxy’s login page.
Chat working does not mean embeddings working
This is the failure that costs the most trust, because nothing looks broken. Chat and embeddings are separate routes, frequently separate endpoints, and sometimes separate credentials. When embeddings fail, Genesis keeps answering: semantic search silently falls back to graph-only lexical matching, so queries still return results — worse ones — and the data-context tool reportsindex_status: ready_with_warnings rather
than an error.
Both are present-tense: they report whether embedding works now, not whether it
ever did.
1. The AI Providers page, at /config/ai-providers/ (embeddings tab
?tab=embeddings), shows an embedding status chip with its reason. It is fed by
GET /api/config/ai-providers/embedding-health, deliberately as a separate request
from the page overview, so a slow or absent indexing subprocess delays the status alone
rather than the whole page. The chip does not poll — it is a one-shot read with a
30-second cache, so reload the page to re-check rather than watching it.
2. GET_INDEX_STATS, which is an action on the lineage tool, not a REST
endpoint. Over HTTP:
embedding_provider, embedding_model, embedding_status and
embedding_status_reason from one snapshot, so the verdict always describes the
identity printed beside it — alongside index counts. It costs nothing: it reads
recorded outcomes and makes no provider call.
All four embedding fields are present on every successful response, including on an
install with no embedding client at all — that case reports unconfigured. Alert on the
value, and treat an absent embedding_status as a fault in the check itself rather
than as a pass: a response without one came from a Twin too old to carry the field, and
says nothing about whether embedding works.
ok included.
3. Force a real embedding call. The two surfaces above read recorded evidence; when
the verdict is unverified or you have just rotated a credential, make the deployment
actually embed something:
"success": false carries the provider’s own
error text, which is usually the whole diagnosis. This is the only surface that makes a
provider call on demand, and it needs ACCESS_CONFIG_PANEL.
4. The boot-time smoke test leaves a greppable line in the logs, so an incident can
be dated to a restart:
curl -sS -o /dev/null -w '%{http_code}\n' https://<host>/api/healthcheck→200kubectl -n $NS get podsandkubectl -n $WS_NS get pods→ nothing inImagePullBackOff,CrashLoopBackOfforPending- A signed-in user sends a chat message and gets a reply
- The embedding connection test returns
"success": truewith a dimension count — not merely a chip that is not red - A user opens a terminal and gets a shell
The chart’s own validators
The chart ships render-time validators, and they are packaged inside the chart tarball, so an operator who never sees the source can still run them:0:
KUBECONFIG is
pinned to /dev/null inside them. They answer “does this chart version still produce
a correct, data-safe manifest”, which is exactly the question to ask before an upgrade
and after a rollback. They cannot tell you anything about your running deployment; the
gate above does that. Invoke them with bash — files inside a Helm tarball are not
extracted executable.Teardown without data loss
The order is not a style preference. Each step removes something the next step needs.cluster/scripts/… is not in the chart tarball. Unlike the validators, these
scripts live in the Genesis repository, so a deployment installed only from the OCI
chart does not have them — ask Genesis for them before you need them, not during
a teardown.Do not improvise a kubectl delete pod equivalent. The ownership checks described
below are the entire point of these scripts: on a cluster running more than one
Genesis instance, acting on the chart’s default namespace means stopping another
instance’s users mid-turn and, with the wrong flag, destroying their saved
conversations.Drain the workspaces first — while the release is still up
helm get values. It is a separate script
from teardown.sh precisely so it is runnable on a release that is staying up
— which is also what you run before setting cliTerminal.enabled=false.Uninstall the release
helm.sh/resource-policy: keep.Revoke the retained RBAC
genesiscomputing.ai/instance-namespace label rather than on a Helm release name,
so it still works after the release record is gone. It deliberately leaves the
shared, fixed-name sandbox objects alone: other instances bind to them.Retire the data, deliberately and last
Delete or Retain) — check it before assuming either.Retire the workspace namespace, separately and explicitly
helm uninstall, and not
setting cliTerminal.enabled=false. Removal is its own command, and it asks first
unless you pass --force:Ownership is by instance label, never by release name
Both cleanup scripts refuse to act on a workspace namespace they cannot show belongs to this instance, and the reason is worth understanding before you reach for a flag to override it.$INSTANCE_NS: on a release that sets values.namespace, checking $NS here asks
about an identity that never existed and passes while every binding is still standing.Incident triage
This catalogue is the single home for Genesis failure modes. The install runbooks link here rather than restating it, so there is one description to keep current. Each row is symptom → cause → fix; read the symptom column first and do not skip to the fix, because several of these present identically and are cured by opposite actions.The cluster never comes up
Symptom: 0 nodes out of 1 have registered, or kube-system pods stuck ContainerCreating with CSINode at 0 drivers and PVCs unbound
Symptom: 0 nodes out of 1 have registered, or kube-system pods stuck ContainerCreating with CSINode at 0 drivers and PVCs unbound
pause, netd, pdcsi, kube-proxy). The node
then never registers. Nothing in the Genesis chart or your values file is at fault —
and the symptom wears different costumes on different node modes, which is why it
reads as a storage, chart or quota problem and is none of them.Verify — the first minute after the cluster object exists. This is the highest
value minute in the whole install:Running within roughly a minute. A cluster creation
that timed out still leaves a usable kubeconfig behind, so this works on that wreck
too — you do not have to delete and retry to diagnose it.Fix. Grant node egress to the registry before creating the cluster: a NAT
gateway, a private-access path to the registry, or a tag-scoped internet route. The
exact mechanism is per-cloud — see Per-cloud specifics and,
for GCP, the detailed remedies in the
GKE install runbook.And a route is not a permission. A higher-priority egress deny — a VPC firewall
rule, or a policy inherited from a folder or organisation — blocks the pull with a
perfect route in place, and the symptom is identical. Read the effective rules, not
the network’s own.Symptom: the backend pod sits Pending on a volume that never attaches, mid-install
Symptom: the backend pod sits Pending on a volume that never attaches, mid-install
The deployment is up but something is wrong
Symptom: NetworkPolicy applied cleanly and enforces nothing
Symptom: NetworkPolicy applied cleanly and enforces nothing
values.yaml:It is only as good as your CNI. Kubernetes accepts the object whether or not the CNI enforces it. Measured on stock EKS 1.34 with the default VPC CNI: applied cleanly, enforced nothing, workspace reached the public internet. GKE blocked it. Verify.Verify on the cluster, never from the setting — and run the control first. A blocked connection on its own proves nothing: in a locked-down enterprise VPC the cluster may have no egress to that address at all, so a probe that simply fails tells you the destination was unreachable, not that the policy did the blocking. Two Pods, identical except for the one label the policy selects on:
--attach blocks until each container exits and --rm removes the Pod, so there is
nothing to clean up and no half-started Pod to read at the wrong moment.cliTerminal.networkPolicy.enabled is off by default, and that is the intended
posture. See What the network posture actually is.Symptom: the terminal returns 403 or 'disabled' although the chart rendered everything
Symptom: the terminal returns 403 or 'disabled' although the chart rendered everything
1008 and the reason as text. Take it from the browser’s devtools console or
Network → WS on the failed attach, or from the backend log, which records the same
string:kubectl apply of a manifest. That is no longer the path: the chart renders
the namespace, RBAC and quota itself. A hand-applied copy now diverges silently from
the chart’s — only one of the two ever gets updated.Symptom: a Kubernetes 403 on workspace creation for a user who could attach yesterday
Symptom: a Kubernetes 403 on workspace creation for a user who could attach yesterday
cliTerminal.maxWorkspaces renders the
namespace’s count/pods ResourceQuota and seeds the backend’s own cap — but the
backend’s cap is only a default, and an admin who raised it in Advanced Settings
outranks it immediately while the quota stays where it was rendered. The application
then admits a workspace the API server refuses.Symptom: every workspace is ImagePullBackOff, for a correctly configured operator
Symptom: every workspace is ImagePullBackOff, for a correctly configured operator
- The tag is not in the mirror.
image.tagmust be a tag that actually exists in the registry your nodes pull from. The chart’s default falls back to the chart’sappVersion, which is frequently not a tag present in the distribution registry — only a named channel tag is. An upgrade that mirrors the backend image and forgets the workspace image produces exactly this: a working console, no terminals. - The node’s identity cannot pull. A Kubernetes node pulls as its own
service account or instance identity, not as the human who ran
helm. A grant made to your corporate directory does not cover the node.
GENESIS_CLI_TERMINAL_IMAGE at a tag that
exists. For (2), grant the node identity read on the registry — the mechanism is
per-cloud, see Per-cloud specifics.Symptom: chat works, but search results got worse and nobody can say when
Symptom: chat works, but search results got worse and nobody can say when
ready_with_warnings, so the install keeps answering and the degradation is
invisible from chat.Verify the embedding path independently of chat — they are separate routes and
frequently separate endpoints and credentials. In order:POST /api/config/llm-providers/embedding-config/test— forces a real embedding call and returns the dimension, or the provider’s own error text.- Read
embedding_statusandembedding_status_reasonfrom the AI Providers page orGET_INDEX_STATSfor the recorded history behind that one call. kubectl -n $NS logs $RELEASE-0 -c genesis | grep -i 'failed startup smoke test'to date the failure to a restart.
ok without a restart.Symptom: 'reset to defaults' reported success and the feature is still live
Symptom: 'reset to defaults' reported success and the feature is still live
cli_terminal.* or sandbox_runner.* row deleted the stored value and reported
success, while the environment variable the runtime actually reads kept the value the
panel had bridged into it. The panel showed the feature off; the terminal stayed
reachable until the pod restarted.Fix. Upgrade the image — current builds restore the deployment’s own pre-panel
value on reset, and leave a GENESIS__… force-lock winning where one is set. Until
then, never trust the panel after a reset: attempt an attach, or restart the pod, and
read the real state.Symptom: workspaces sit Pending and every attach fails — the feature looks broken
Symptom: workspaces sit Pending and every attach fails — the feature looks broken
Symptom: the upgrade 'worked' but the old behaviour persists
Symptom: the upgrade 'worked' but the old behaviour persists
IfNotPresent is the combination that produces this.Symptom: helm upgrade fails with 'invalid ownership metadata'
Symptom: helm upgrade fails with 'invalid ownership metadata'
cliTerminal.createNamespace=false and leave the namespace where it is,
then delete only the Role, RoleBinding, ResourceQuota and NetworkPolicy so the chart
can render its own. Do not delete the namespace — that cascades to every user’s
home volume.A namespace this chart created re-adopts cleanly, because retention stops the delete
without stripping the ownership annotations Helm wrote. Only a hand-created one is
rejected.What the network posture actually is
State this accurately when asked, because the reassuring version is wrong and the difference is a security finding:- By default, a workspace Pod has exactly the network access any other Pod in the cluster has — the same access the Genesis backend has, because the Genesis namespace carries no NetworkPolicy either. That is deliberate: these are development workspaces, and installing packages and cloning repositories is the point of them.
- The workspace egress lockdown is opt-in (
cliTerminal.networkPolicy.enabled=true), confining workspaces to DNS plus the Genesis backend — at the cost ofgit,gh, npm and pip timing out. - It is only as good as the CNI, and an unenforcing CNI reports success. Verify it on the cluster before describing it to a security reviewer.
Per-cloud specifics
The Genesis release is identical everywhere; the substrate underneath is not. Everything above is cloud-agnostic — these are the rows that differ.VolumeSnapshot API is the portable option where your
cluster has a CSI driver and a snapshot controller installed: one procedure everywhere,
at the cost of a cluster prerequisite. The cloud-native command always works.
Related: Installing Genesis on GKE · Genesis on AWS · Genesis on Azure · Environment strategy and governance

