Skip to content

Deploy from scratch

This page takes an empty Kubernetes cluster to a working Elitea deployment with single sign-on, chat, agents, indexing and shared models. Follow it top to bottom, once. It covers the same install as Install with Helm, but in full: what to decide first, the secrets and certificate authorities the chart expects, and the edge in front of the platform. When you are done, GitOps with ArgoCD shows how to keep the same install in git.

It targets the one-chart layout: a release whose docs/UPGRADING.md has the entry “One chart: NATS bundled”. NATS JetStream and its stream bootstrap are part of the elitea chart; earlier releases installed them from two separate charts, nats and nats-bootstrap, which are no longer published. A values file or a guide that still mentions those charts, runtimeRedis, worker.implementation: python or Redis passwords is out of date, and the chart refuses most of it.

Installing on Google Cloud? Read Deploy on Google Cloud (GKE) alongside this page. It covers Cloud SQL, a GCS bucket without keys, the edge on GKE, and platform models authenticated by a service account. Deploy on GKE with Argo CD is the same install as one Argo CD Application, with secrets from Google Secret Manager.

flowchart TB
    Browser["Browser, desktop, MCP clients"] --> Edge["Your edge: one route, / to elitea-main"]
    Edge --> Main["elitea-main"]
    Main -->|"relays /app, /docs"| Web["elitea-web (static SPA)"]
    Main -->|"mTLS /llm hop"| GW["elitea-llm-gateway"]
    GW --> Providers["LLM providers"]
    Worker -->|"gRPC + mTLS :9446"| Main
    Worker -. "mTLS control, output, content" .-> Main
    Worker -->|"mTLS"| Vector["elitea-vector"]
    Vector --> Qdrant[("Qdrant")]
    Main -. "mTLS" .-> NATS[("NATS JetStream")]
    Worker -. "mTLS command bus" .-> NATS
    GW -. "mTLS" .-> NATS
    Sched["elitea-scheduler"] -. "mTLS" .-> NATS
    Main --> PG[("PostgreSQL")]
    GW --> PG
    Sched --> PG
    Worker --> PG
    Main --> S3[("S3 bucket")]
ComponentComes fromNotes
elitea-main, elitea-web, elitea-scheduler, elitea-llm-gateway, elitea-worker, elitea-vector, Qdrant, OTel collectorthe elitea chartone release, one values file, one version
NATS JetStream and its streamsthe elitea chart (nats, natsBootstrap)bundled and on by default; same release, same namespace
PostgreSQL 16+youan existing server, a managed service, or an operator such as CloudNativePG
S3-compatible bucket (or Azure Blob, GCS)youmust exist before the first install
cert-manageryouissues every in-cluster mTLS certificate
Ingress controller or Gateway API implementation, public TLS certificate, DNSyouthe public edge, one route to elitea-main; see The edge
OIDC identity provider (and optionally SAML, SCIM)youKeycloak, Entra ID, Okta, Dex, Google…
Model providersyouOpenAI, Anthropic, Azure OpenAI, Gemini, or a self-hosted OpenAI-compatible server

Every first-party image and chart is public on ghcr.io/elitea-ng, so no image pull secret is needed.

Write the answers down. Every one ends up in a value or a Secret, and several are hard to change later.

DecisionExampleWhere it goesChanging it later
Public hostnameelitea.example.comDEPLOYMENT_URL, OIDC redirect URI, gateway self-origins, the edge routeedit values, re-register the redirect URI
Namespaceeliteaeverything; NATS and its bootstrap are in the same releasereinstall
Chart versionthe latest release--version on the one chartupgrade (read docs/UPGRADING.md first)
PostgreSQL server and its max_connectionspg.internal:5432, 200the database DSN Secrets, postgresql.maxConnectionsedit values; the chart re-checks the connection budget
Bucket and regionelitea-artifacts, eu-west-1STORAGE_* / S3_* envdata does not move by itself
Identity provider and clientissuer URL, client id, client secretOIDC_* env and a Secretedit values
First administratoremail:admin@example.comELITEA_INITIAL_GLOBAL_ADMINSadd entries any time; nobody is ever demoted by this list
Worker identity (SPIFFE id)spiffe://elitea.example.com/ns/elitea/sa/workerthe worker certificate, worker.runtimeSession.expectedIdentityre-mint the worker certificate
Model provider hostsapi.openai.com, 10.0.5.20:8000GATEWAY_EGRESS_ALLOWLISTedit values

Kubernetes

A cluster whose CNI enforces NetworkPolicy (Calico, Cilium, and similar). The chart renders default-deny ingress policies and relies on them. kubectl and helm: Helm 4.3 or newer is recommended, and Helm 3.8 or newer (the first that pulls OCI charts) also works.

cert-manager

Installed cluster-wide before anything else. The bundled NATS, the LLM gateway, elitea-vector and the NATS client identities all render cert-manager Certificate objects.

PostgreSQL 16 or newer

The agent-state migrations use SQL/JSON syntax introduced in 16. No extension is needed: every vector is kept in Qdrant through elitea-vector, not in PostgreSQL.

Object storage

S3-compatible (AWS S3, MinIO, RustFS, Ceph RGW), Azure Blob or GCS. There is no local-disk backend.

An edge

An Ingress controller, or a Gateway API implementation with a Gateway that terminates TLS for your hostname. It needs one route, / to elitea-main, that keeps long streams open.

An OIDC provider

A confidential client with the redirect URI https://<host>/auth/oidc/callback. For GitHub, which has no OIDC sign-in, put Dex in front of it.

The worker dominates. The figures below are requests for the install on this page. Limits are higher.

ComponentReplicasCPU requestMemory requestStorage
elitea-main2 (HPA to 3)100m256Mi—
elitea-web210m32Mi—
elitea-scheduler150m64Mi—
elitea-llm-gateway1250m1Gi—
elitea-worker124Gi—
elitea-vector2250m128Mi—
Qdrant1 (3 for HA)500m2Gi50Gi PVC
OTel collector150m128Mi—
NATS (bundled, one node)1 (3 for HA)100m256Mi10Gi PVC

Plan for about 4 CPU and 10Gi of memory in requests, and at least one node with 2 free CPUs for the worker during a rolling update.

Step 1: Namespace and certificate authorities

Section titled “Step 1: Namespace and certificate authorities”

Three private certificate authorities protect the in-cluster hops. None of them is your public certificate, and they must stay separate:

IssuerSignsCreated by
elitea-nats-caNATS server and every NATS client identitythe elitea chart, automatically
elitea-internal-cathe elitea-main → LLM gateway mTLS hopyou, below
elitea-runtime-caelitea-vector certificates. It must be the same CA as the runtime material you mint in Step 4you, below, from that material
Terminal window
kubectl create namespace elitea

Apply the internal CA. cert-manager generates its key and nothing leaves the cluster:

internal-ca.yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata: {name: elitea-internal-selfsigned, namespace: elitea}
spec: {selfSigned: {}}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata: {name: elitea-internal-ca, namespace: elitea}
spec:
secretName: elitea-internal-ca
isCA: true
commonName: elitea-internal-ca
duration: 43800h
privateKey: {algorithm: ECDSA, size: 256}
issuerRef: {name: elitea-internal-selfsigned, kind: Issuer}
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata: {name: elitea-internal-ca, namespace: elitea}
spec:
ca: {secretName: elitea-internal-ca}
---
# Backed by the runtime CA you mint in Step 4 (Secret elitea-runtime-ca).
apiVersion: cert-manager.io/v1
kind: Issuer
metadata: {name: elitea-runtime-ca, namespace: elitea}
spec:
ca: {secretName: elitea-runtime-ca}
Terminal window
kubectl apply -f internal-ca.yaml

The chart’s defaults name ClusterIssuers. The values in Step 6 switch both references to these namespaced Issuers, which read their CA from the platform’s own namespace and need no access to cert-manager’s namespace.

The platform needs one login role and two databases, both owned by that role:

DatabaseHoldsRead by
elitea_v2 (the platform database)every platform schema (centry, p_<id> per project, elitea_auth, elitea_runtime, gateway, …)elitea-main, its migration Job, the scheduler, the LLM gateway, the workload-session Job
elitea_v2_agentstateagent sessions and graph checkpointsthe worker, the agent-state migration Job

The migration Job runs as the role and creates every schema itself, so the role needs ownership of the platform database and no superuser rights. Nothing is imported: the platform starts empty apart from what the migrations seed.

Run once as an administrator:

CREATE ROLE elitea_v2 LOGIN PASSWORD '<db-password from Step 4>';
CREATE DATABASE elitea_v2 OWNER elitea_v2;
CREATE DATABASE elitea_v2_agentstate OWNER elitea_v2;

Then keep the chart’s database-preparation hook off, and say that the databases exist:

dbInit:
enabled: false
externallyManaged: true

Without externallyManaged: true the chart refuses to render. Otherwise the install would look healthy and then fail every agent turn with “The durable agent checkpoint store is unavailable”.

TLS. Put sslmode=require in both DSNs. The chart has no mount for a server CA bundle, so verify-full is not available yet. The DSN is a plain libpq URL. Generate the password URL-safe, as the Step 4 script does: a / or + in it breaks URL parsing, and the error then names a host or port instead of the password.

PostgreSQL connections run out before CPU does. With the runtime plane on, each elitea-main replica opens one primary pool of 6 plus six isolated runtime pools totalling 36, and elitea-vector’s introspection pool adds 2: 44 connections per replica. The chart adds up every component at its replica ceiling (elitea-main, the gateway, the scheduler, DeepWiki, Inventory and the worker’s agent-state pool: 216 more with every autoscaler at its default ceiling) and refuses to render when the total exceeds postgresql.maxConnections - reservedConnections. The reserve covers the Jobs, backups and interactive psql. Scaling has the formula per component.

max_connections on the serverreservedConnectionsLargest main.autoscaling.maxReplicas (other components at their defaults)
200252
300254
600 (the chart’s default)2510

Read the real value with SHOW max_connections;. The chart cannot read it, so it believes the number in postgresql.maxConnections. A transaction-mode PgBouncer is shipped with the chart (pgbouncer.enabled) when you need more elitea-main replicas than the server can hold. Point elitea-main-db at the pooler’s Service when you turn it on.

Create the bucket before the first install. elitea-main’s readiness probe tolerates a missing bucket. The boot step right after it does not: it writes the bucket’s lifecycle rule. So with no bucket, the release reports success and elitea-main then crash-loops.

The credential elitea-main uses needs:

  • s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket
  • s3:PutLifecycleConfiguration. elitea-main writes the rule elitea-abort-incomplete-multipart-uploads and replaces the bucket’s whole lifecycle configuration. Use a dedicated bucket.
  • s3:GetEncryptionConfiguration. Without it the platform still starts, but the cloud-executor handoff stays off.
Terminal window
kubectl -n elitea create secret generic elitea-main-storage-secrets \
--from-literal=s3-access-key='<access key>' \
--from-literal=s3-secret-key='<secret key>'

On AWS with IRSA, or on any cloud with workload identity, skip the Secret, remove the two S3_* entries from main.secrets in Step 6, and annotate main.serviceAccount. For a self-hosted MinIO, RustFS or Ceph that you administer, objectInit.enabled: true creates the bucket from the same Secret. Azure Blob and GCS use STORAGE_BACKEND: azure or gcs with their own variables. For GCS with Workload Identity, which needs no Secret, see Deploy on Google Cloud. Uploads up to 150 MiB are accepted by default (ARTIFACT_MAX_OBJECT_BYTES).

Nothing secret belongs in a values file: a Helm value renders into a ConfigMap. The chart only takes names of Secrets you create.

The script below writes every generated value into ./elitea-material, owner-only. It needs OpenSSL 3, which is what produces the Ed25519 key. Keep the directory somewhere safe and out of git. Losing secrets-master-key makes every stored credential unreadable.

#!/usr/bin/env bash
# mint-elitea-material.sh — every generated value the chart needs.
set -euo pipefail
NS="${NS:-elitea}"
DIR="${DIR:-$PWD/elitea-material}"
DAYS="${DAYS:-825}"
WORKER_ID="${WORKER_ID:-spiffe://elitea.example.com/ns/elitea/sa/worker}"
KEY_ID="${KEY_ID:-elitea-runtime-v1}"
DB_HOST="${DB_HOST:-pg.internal}"
DB_ROLE="${DB_ROLE:-elitea_v2}"
DB_NAME="${DB_NAME:-elitea_v2}"
umask 077; mkdir -p "$DIR"; cd "$DIR"
if [ -f runtime-ca.crt ]; then
echo "material exists in $DIR; delete it to rotate"; exit 0
fi
token() { openssl rand -base64 32 | tr -d '\n=' ; }
urltoken() { openssl rand -base64 32 | tr '+/' '-_' | tr -d '\n=' ; }
sans() { # every name a client may dial the Service by
local s="$1"
printf 'DNS:%s,DNS:%s.%s.svc,DNS:%s.%s.svc.cluster.local' \
"$s" "$s" "$NS" "$s" "$NS"
}
sign() { # stem, extensions: sign <stem>.csr with the runtime CA
openssl x509 -req -in "$1.csr" -out "$1.crt" -days "$DAYS" \
-CA runtime-ca.crt -CAkey runtime-ca.key -CAcreateserial \
-extfile <(printf '%b' "$2") 2>/dev/null
}
SERVER_EXT='extendedKeyUsage=serverAuth
keyUsage=critical,digitalSignature,keyEncipherment'
CLIENT_EXT='extendedKeyUsage=clientAuth
keyUsage=critical,digitalSignature'
server_cert() { # stem, service
openssl req -newkey rsa:2048 -nodes -subj "/CN=$2" \
-keyout "$1.key" -out "$1.csr" 2>/dev/null
sign "$1" "subjectAltName=$(sans "$2")\n$SERVER_EXT"
}
# The runtime CA signs elitea-main's runtime listeners (the platform
# listener on 9446 reuses content-server), the worker's identity, and
# (via cert-manager) elitea-vector.
openssl req -x509 -newkey rsa:2048 -nodes -days "$DAYS" \
-subj "/CN=elitea-runtime-ca" \
-keyout runtime-ca.key -out runtime-ca.crt \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" 2>/dev/null
for stem in control-server output-server content-server; do
server_cert "$stem" elitea-main
done
# The worker's client certificate: exactly ONE URI SAN, nothing else.
openssl req -newkey rsa:2048 -nodes -subj "/CN=elitea-agent-worker" \
-keyout agent-worker-client.key -out agent-worker-client.csr 2>/dev/null
sign agent-worker-client "subjectAltName=URI:$WORKER_ID\n$CLIENT_EXT"
# (No DeepWiki/Inventory callback client certificate: the chart issues it
# from the elitea-runtime-ca Issuer.)
rm -f ./*.csr ./*.srl
# The Ed25519 command-signing pair; the keyring holds its public half.
openssl genpkey -algorithm ed25519 -out command-signing-key.pem
PUB="$(openssl pkey -in command-signing-key.pem -pubout -outform DER \
| tail -c 32 | base64 | tr -d '\n')"
cat > command-signing-keyring.json <<JSON
{
"schema_version": "elitea.runtime-ed25519-keyring.v1",
"keys": [{"key_id": "$KEY_ID", "public_key_base64": "$PUB"}]
}
JSON
# Independent random values. Never derive two of them from one seed: the
# platform refuses two files with equal bytes.
openssl rand 32 > auth-attempt-key # 32 raw bytes
printf '%s' "$(token)" > auth-pat-signing-key # text, no newline
openssl rand 32 > worker-output-spool-key # exactly 32 raw bytes
printf '%s' "$(token)" > gateway-identity-secret
printf '%s' "$(token)" > application-secret-key
openssl rand -hex 32 | tr -d '\n' > qdrant-api-key
printf '%s' "$(token)" > deepwiki-identity-secret
printf '%s' "$(token)" > inventory-identity-secret
# Fernet key: 44 base64url characters, padding included, no newline.
openssl rand -base64 32 | tr '+/' '-_' | tr -d '\n' > secrets-master-key
# URL-safe, because it is embedded in a postgres:// URL.
printf '%s' "$(urltoken)" > db-password
DSN="$DB_ROLE:$(cat db-password)@$DB_HOST:5432"
printf 'postgres://%s/%s?sslmode=require' \
"$DSN" "$DB_NAME" > database-url
printf 'postgresql://%s/%s_agentstate?sslmode=require' \
"$DSN" "$DB_NAME" > agent-checkpoint-connection
# The Form users file. The authentication document requires it even
# though nobody signs in with a password: Form sign-in is off by default.
cat > auth-form-users.json <<JSON
{"users": [{"login": "unused", "email": "unused@example.invalid",
"password": "$(token)"}]}
JSON
chmod 600 ./*
echo "material written to $DIR"
Terminal window
WORKER_ID=spiffe://elitea.example.com/ns/elitea/sa/worker \
DB_HOST=pg.internal bash mint-elitea-material.sh

Use the generated db-password when you create the role in Step 2.

Terminal window
cd elitea-material
k() { kubectl -n elitea "$@" --dry-run=client -o yaml \
| kubectl apply -f -; }
f() { for name; do printf -- '--from-file=%s ' "$name"; done; }
k create secret generic elitea-main-db $(f database-url)
k create secret generic elitea-llm-gateway-secrets \
$(f identity-secret=gateway-identity-secret secrets-master-key)
k create secret generic elitea-main-llm-gateway-secrets \
$(f gateway-identity-secret)
k create secret generic elitea-main-sso $(f application-secret-key) \
--from-literal=oidc-client-secret='<client secret from your IdP>'
k create secret generic elitea-main-auth-material \
$(f auth-attempt-key auth-pat-signing-key auth-form-users.json)
k create secret generic elitea-runtime-material \
$(f runtime-ca.crt command-signing-key.pem) \
$(f command-signing-keyring.json) \
$(f control-server.crt control-server.key) \
$(f output-server.crt output-server.key) \
$(f content-server.crt content-server.key)
k create secret generic elitea-worker-material \
$(f runtime-ca.crt agent-worker-client.crt agent-worker-client.key) \
$(f command-signing-keyring.json worker-output-spool-key) \
$(f agent-checkpoint-connection)
k create secret tls elitea-runtime-ca \
--cert=runtime-ca.crt --key=runtime-ca.key
k create secret generic elitea-qdrant-api $(f api-key=qdrant-api-key)
k create secret generic elitea-deepwiki-secrets \
$(f identity-secret=deepwiki-identity-secret)
k create secret generic elitea-inventory-secrets \
$(f identity-secret=inventory-identity-secret)

The key names are part of the contract. The chart and the binaries look files up by these exact names, and nothing renames them for you.

SecretKeysRead byRules
elitea-main-dbdatabase-urlmain, migrations, scheduler, gateway, workload-session JobDSN of the platform database
elitea-llm-gateway-secretsidentity-secret, secrets-master-keygateway; main also reads secrets-master-key from herethe master key wraps every project’s vault key. Never rotate it casually: a new key cannot read what the old one wrote
elitea-main-llm-gateway-secretsgateway-identity-secretmainsame value as identity-secret above, or every /llm call fails
elitea-main-ssoapplication-secret-key, oidc-client-secretmainelitea-main refuses to start with single sign-on configured and no application secret key
elitea-main-storage-secretss3-access-key, s3-secret-keymain, objectInitomit when using workload identity
elitea-main-auth-materialauth-attempt-key, auth-pat-signing-key, auth-form-users.jsonmainthe three must hold different bytes. Rotating the PAT key invalidates every personal access token
elitea-runtime-materialruntime CA, Ed25519 private key, keyring, three listener keypairsmainthe keyring’s key_id must equal main.runtime.signingKeyId
elitea-worker-materialruntime CA, worker client keypair, keyring, spool key, agent-state DSNworker, agent-state migration Job, workload-session Jobsame CA and keyring as the runtime material
elitea-runtime-catls.crt, tls.keythe elitea-runtime-ca Issuerthe runtime CA itself
elitea-qdrant-apiapi-keyelitea-vector, Qdrantgenerate once; a value that changes on every sync locks elitea-vector out
elitea-deepwiki-secretsidentity-secretmain, DeepWikisigns and verifies the identity headers elitea-main sends DeepWiki
elitea-inventory-secretsidentity-secretmain, Inventorythe same, for Inventory; a different value from DeepWiki’s

Every name above is the chart’s fixed default, so the values file names none of them. The chart creates the remaining certificate Secrets itself through cert-manager. These include the NATS client certificates, the gateway server and client certificates, the elitea-vector certificates, Qdrant’s TLS certificate (elitea-qdrant-tls) and the DeepWiki and Inventory callback client certificates (elitea-deepwiki-callback-client, elitea-inventory-callback-client, one DNS SAN elitea-subapp-callback, from the elitea-runtime-ca Issuer).

Coming from an older install script?

Scripts written before 4.7 also mint redis-* passwords, a Redis server keypair, a redis-users.acl file and an elitea-runtime-redis-material Secret. Nothing reads any of them now; drop them. Scripts written before the platform listener (#1359) also mint a platform-edge keypair and an elitea-platform-edge-tls Secret, and set worker.platformEdge; delete all three. Scripts from the first platform-listener release also mint a main-platform-client keypair; elitea-main no longer reads it, so drop it. The redis-auth-password and runtime-ca.crt keys in the auth material Secret are ignored too. If your old script put the master key in a separate elitea-secrets-master Secret, either keep it there and point both main.secrets.SECRETS_MASTER_KEY and llmGateway.secrets.SECRETS_MASTER_KEY at it, or move it. Two different keys would put two incompatible formats in one table.

NATS is the platform’s bus for live updates, presence, LLM budget counters and the runtime command bus. It runs mTLS-only, with one account per plane, and it is part of the elitea release: there is nothing to install separately. The chart renders the server (the upstream NATS chart, a pinned dependency that the published chart carries), its CA (elitea-nats-ca), its certificates, a NetworkPolicy that admits port 4222 only from the platform’s NATS clients, and the elitea-nats-bootstrap hook Job that creates every stream, KV bucket and durable consumer. elitea-main refuses to start until they exist; until the Job has run, it restarts and then settles.

The defaults are one node with JetStream on a 10Gi volume. To change them, set the upstream chart’s values under nats in your values file:

nats:
config:
jetstream:
fileStore:
pvc:
size: 20Gi
storageClassName: <your class>

For three nodes, layer the chart’s values-nats-ha.yaml (-f values-nats-ha.yaml -f values-prod.yaml, so the storage settings in your own file still win). It sets the bootstrap’s replica count (natsBootstrap.replicas) to 3 as well; the chart refuses a bootstrap whose replica count does not match the cluster.

This is a complete production values file for the install on this page. Every elitea.example.com and every value in angle brackets is yours to replace. The chart checks the combinations while it renders and names the field when something is missing or contradictory.

The file states each URL-derived value in full, so you can see every one. A shorter file sets global.publicUrl: https://elitea.example.com and global.trustedProxyCidrs once, and leaves out DEPLOYMENT_URL, ELITEA_TRUSTED_PROXY_CIDRS, OIDC_REDIRECT_URI, public_origin, trusted_proxy_cidrs, GATEWAY_SELF_LLM_ORIGINS, worker.runtime.publicBaseUrl and the ingress hosts; the chart fills each one that is empty. It derives the worker identity as spiffe://<host>/ns/<namespace>/sa/agent-worker, so keep worker.runtimeSession.expectedIdentity when your worker certificate carries another SAN (the Step 4 script mints …/sa/worker). See Configuration.

values-prod.yaml
platform:
aiProjectId: "1" # the seeded "Default Project"; see Step 8
postgresql:
existingSecret: elitea-main-db
key: database-url
maxConnections: 200 # SHOW max_connections on YOUR server
reservedConnections: 25
# NATS: bundled (nats.enabled, on by default); nothing to set. See Step 5.
dbInit: # see Step 2 for the other option
enabled: false
externallyManaged: true
# The worker's session and checkpoint tables live in the agent-state
# database and come from a separate migration lineage. Without this Job
# every agent turn fails on a missing table while every pod reports Ready.
agentStateMigrations:
enabled: true
databaseSecret:
name: elitea-worker-material
key: agent-checkpoint-connection
networkPolicies:
enabled: true
main:
# The data-plane pods of YOUR Gateway implementation (Step 7).
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: envoy-gateway-system
podSelector:
matchLabels:
app.kubernetes.io/name: envoy
main:
replicaCount: 2
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 3 # 3 x ~50 connections fits 200 - 25
env:
DEPLOYMENT_URL: "https://elitea.example.com"
ELITEA_TRUSTED_PROXY_CIDRS: "10.0.0.0/8" # your pod network
ELITEA_CONFIGURATIONS_ENABLED: "true"
ELITEA_PROJECT_INFO_ENABLED: "true"
ELITEA_INDEX_TYPES_ENABLED: "true"
ELITEA_APPLICATION_SKILLS_ENABLED: "true"
ELITEA_CONFIGURATIONS_MUTATION_ENABLED: "false"
ELITEA_ARTIFACTS_ENABLED: "true"
ELITEA_ALLOW_PROJECT_OWN_LLMS: "true" # "false": shared models only
# Single sign-on
OIDC_ISSUER_URL: "https://login.example.com/realms/elitea"
OIDC_CLIENT_ID: "elitea"
OIDC_REDIRECT_URI: "https://elitea.example.com/auth/oidc/callback"
OIDC_DISPLAY_NAME: "Company SSO"
ELITEA_INITIAL_GLOBAL_ADMINS: "email:admin@example.com"
# Object storage
STORAGE_BACKEND: "s3"
STORAGE_CONTAINER: "elitea-artifacts"
S3_REGION: "eu-west-1"
# Empty for AWS; http(s)://host:port for MinIO, RustFS, Ceph.
S3_ENDPOINT_URL: ""
S3_FORCE_PATH_STYLE: "false" # "true" for MinIO, RustFS, Ceph
# The /llm hop to the gateway: gRPC over mutual TLS to the headless
# Service elitea-llm-gateway-grpc (port 8443). Port 8083 on
# elitea-llm-gateway-svc serves health and metrics only.
LLM_GATEWAY_URL: "https://elitea-llm-gateway-grpc:8443"
LLM_GATEWAY_CLIENT_CERT: "/run/elitea-llm-gateway/tls.crt"
LLM_GATEWAY_CLIENT_KEY: "/run/elitea-llm-gateway/tls.key"
LLM_GATEWAY_CA_FILE: "/run/elitea-llm-gateway/ca.crt"
secrets:
APPLICATION_SECRET_KEY:
secretName: elitea-main-sso
key: application-secret-key
optional: false
OIDC_CLIENT_SECRET:
secretName: elitea-main-sso
key: oidc-client-secret
optional: false
S3_ACCESS_KEY:
secretName: elitea-main-storage-secrets
key: s3-access-key
optional: false
S3_SECRET_KEY:
secretName: elitea-main-storage-secrets
key: s3-secret-key
optional: false
fileConfig:
llmGatewayClientMaterial:
enabled: true
secretName: elitea-main-gateway-client-tls # issued by the chart
mountPath: /run/elitea-llm-gateway
# Production authentication. Required by the runtime plane and personal
# access tokens, even when nobody signs in with a password.
authConfig:
enabled: true
material:
mountPath: /run/elitea-auth
secretName: elitea-main-auth-material
document:
schema_version: elitea.auth.form.v2
# Your public origin, the same as DEPLOYMENT_URL: it scopes the
# Form session cookie. No edge authenticates for elitea-main.
public_origin: https://elitea.example.com
trusted_proxy_cidrs:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
redirects:
direct_access_denied: /app/
main_access_denied: /auth/login
default_login: /app/
default_logout: /app/
cookie:
name: elitea_browser_auth
same_site: lax
lifetime_seconds: 86400
credentials:
attempt_key_file: /run/elitea-auth/auth-attempt-key
pat_signing_key_file: /run/elitea-auth/auth-pat-signing-key
credential_headers: []
mappers:
contract: elitea.auth_mappers.tracked.v1
authorization:
main_configured_public_rules: []
identity:
# Empty: ELITEA_INITIAL_GLOBAL_ADMINS applies.
initial_global_admins: []
provider:
kind: form
form:
users_json_file: /run/elitea-auth/auth-form-users.json
# The runtime plane: chat, agents, pipelines, indexing.
runtime:
enabled: true
commandStream: "ELITEA_RT_V1_VALIDATE"
maxOutstanding: 64
toolkitDiscovery:
enabled: true
agentExecutionDispatch:
enabled: true
commandStream: "ELITEA_RT_V1_AGENT"
indexIngestDispatch:
enabled: true
commandStream: "ELITEA_RT_V1_AGENT"
signingKeyId: "elitea-runtime-v1" # = KEY_ID in the mint script
material:
mountPath: /run/elitea-runtime
secretName: elitea-runtime-material
llmGateway:
replicaCount: 1
env:
# LLM_BUDGET_EXPECTED_REPLICAS is derived by the chart from the
# gateway's replica ceiling; leave it unset.
# Every HTTP origin that reaches /llm, public and in-cluster. A provider
# credential pointing back here is refused, so a loop is impossible.
# Not https://elitea-main:9446: the worker and the engines reach the
# model pipeline there over gRPC (ModelService), which no credential's
# api_base can name.
GATEWAY_SELF_LLM_ORIGINS: >-
https://elitea.example.com/llm/v1,http://elitea-main:8080/llm/v1
GATEWAY_EGRESS_ALLOWLIST: >-
api.openai.com,api.anthropic.com,*.openai.azure.com
egressPosture: "allowlist"
secrets:
GATEWAY_IDENTITY_SECRET:
secretName: elitea-llm-gateway-secrets
key: identity-secret
optional: false
SECRETS_MASTER_KEY:
secretName: elitea-llm-gateway-secrets
key: secrets-master-key
optional: false
mtls:
enabled: true
issuerRef:
name: elitea-internal-ca
kind: Issuer
worker:
enabled: true
materialSecretName: elitea-worker-material
runtime:
publicBaseUrl: https://elitea.example.com
# Writes the worker's row in elitea_runtime.workload_sessions after each
# migration, and a CronJob renews it. Must equal the worker
# certificate's SAN. Required: it is also the identity elitea-main's
# ModelService admits for the worker's model calls.
runtimeSession:
enabled: true
expectedIdentity: "spiffe://elitea.example.com/ns/elitea/sa/worker"
# platform_origin defaults to https://elitea-main:9446, elitea-main's
# mTLS platform listener.
# The vector store the Rust indexing runtime, DeepWiki and Inventory write
# to (on by default). Its Qdrant API key is the Secret elitea-qdrant-api.
vector:
replicaCount: 2
mtls:
issuerRef:
name: elitea-runtime-ca
kind: Issuer
mainClient:
enabled: true
# The bundled Qdrant (the upstream qdrant chart, on by default): one node,
# REST and gRPC over TLS from vector.mtls.issuerRef. For three nodes, add the
# chart's values-qdrant-ha.yaml. qdrant.enabled: false is bring-your-own,
# at vector.qdrant.external.url.
qdrant:
persistence:
size: 50Gi
storageClassName: "" # "" = the cluster's default StorageClass
# DeepWiki and Inventory (on by default). Cloning is denied until you list
# the git hosts; the chart copies each list to elitea-main and derives the
# rest of elitea-main's side. This guide's Issuers are namespaced (Step 1).
deepwiki:
env:
ELITEA_DEEPWIKI_GIT_ALLOWLIST: "github.com,gitlab.example.com"
mtls:
issuerRef: {name: elitea-internal-ca, kind: Issuer}
callbackClient:
certManager:
issuerRef: {name: elitea-runtime-ca, kind: Issuer}
inventory:
env:
ELITEA_INVENTORY_GIT_ALLOWLIST: "github.com,gitlab.example.com"
mtls:
issuerRef: {name: elitea-internal-ca, kind: Issuer}
callbackClient:
certManager:
issuerRef: {name: elitea-runtime-ca, kind: Issuer}
otelCollector:
enabled: true

Choices in this file you may want to change

Section titled “Choices in this file you may want to change”
Model egress: allowlist or public-unrestricted

The chart refuses to render until you choose. With allowlist, the gateway calls only the hosts in GATEWAY_EGRESS_ALLOWLIST, and governance rows added later can only extend that list. With public-unrestricted, any public host is allowed until the first governance row narrows it. In both modes a private address is reachable only through an entry that names it, by IP or CIDR with an optional port (for example 10.0.5.20:8000). A host name alone never opens a private address. For a self-hosted vLLM or Ollama, list its address and port.

Shared models only, or project credentials too

ELITEA_ALLOW_PROJECT_OWN_LLMS: "false" limits every project to the models an administrator shares from the public project. Use it for a public or demo deployment, where a project credential could otherwise point anywhere the allowlist permits. The value must be exactly "true" or "false".

Outbound calls made by elitea-main itself

Toolkit connection checks, MCP servers, MCP OAuth and webhooks have their own allowlists on elitea-main (ELITEA_TOOLKIT_CHECK_ALLOWLIST, ELITEA_MCP_EGRESS_ALLOWLIST, ELITEA_MCP_OAUTH_EGRESS_ALLOWLIST, ELITEA_WEBHOOK_EGRESS_ALLOWLIST). Their defaults refuse every private address. To reach a self-hosted GitLab, Jira or MCP server on a private network, list both its name and its range, for example gitlab.corp.example,10.20.0.0/16. When a toolkit’s Test connection is refused, its message names the refused host and ELITEA_TOOLKIT_CHECK_ALLOWLIST. The message is the same whether the host is not on the list, does not resolve, or resolves to a refused address, so that it tells a credential author nothing about internal names. The warning toolkit_check: egress refused the destination in the elitea-main log gives the reason, the resolved addresses and their class, and the remedy. A private address (private) is reachable when the list names its address or range. Cloud metadata, link-local, multicast, reserved and unspecified addresses (forbidden) are never reachable, whatever the list says.

Indexing sources and the document extractor

The worker’s index runs reach a default set of public SaaS hosts (GitHub, GitLab, Bitbucket, Azure DevOps, Figma, Zephyr and Xray cloud). Self-hosted sources need worker.runtime.indexEgressAllowlist. PDF and Office extraction needs worker.extractor.enabled: true, which adds a sidecar of about 2.5Gi to the worker pod. Without it, those files are skipped as unsupported.

The values above install the platform with every default on: the runtime plane, the worker, elitea-vector with Qdrant, DeepWiki and Inventory. Each of those is one switch (main.runtime.enabled, worker.enabled, vector.enabled with qdrant.enabled, deepwiki.enabled, inventory.enabled); set it to false to leave the feature out, and the render tells you if another feature still needs it. The components below are off by default. Turn each one on after the base install works, one at a time, and render after each change.

ComponentWhat it addsTurn it on when
Document extractor (worker.extractor)PDF and Office text extraction, as a sidecar of about 2.5Gi in the worker podusers index or attach PDF, Word, Excel or PowerPoint files. Without it, those files are skipped as unsupported
Cloud executor (sandboxKubernetes)agent workspaces run in gVisor-isolated pods in a separate namespaceusers choose Send to cloud
Egress proxy (egressProxy)per-project host allowlists for cloud-executor workspacesyou run the cloud executor and projects restrict its network to named hosts
PgBouncer (pgbouncer)a transaction-mode pooler in front of elitea-mainyou need more elitea-main replicas than max_connections allows
Document extractor
worker:
extractor:
enabled: true

It needs the indexing runtime, which the values above already turn on. No Secret is needed.

DeepWiki and Inventory: what the one switch derives

DeepWiki and Inventory are on by default, and the values file above sets everything they need: the git allowlists and the namespaced Issuers. Each one is a separate Deployment that elitea-main calls over mTLS. It writes its own schema (deepwiki_build, inventory_graph) into the platform database. The database owner from Step 2 already has the rights it needs. Their dense vectors go to elitea-vector, so vector.enabled: false beside either one fails the render.

From deepwiki.enabled and inventory.enabled the chart derives elitea-main’s side: ELITEA_DEEPWIKI_ENABLED / ELITEA_INVENTORY_ENABLED, the git allowlists (copied from the provider’s env), the client certificate paths and their mounts (main.fileConfig.deepwikiClientMaterial / inventoryClientMaterial, issued by the chart), and ELITEA_DEEPWIKI_PLATFORM_GRPC_TARGET / ELITEA_INVENTORY_PLATFORM_GRPC_TARGET (elitea-<app>-grpc:9443, a headless Service that elitea-main balances over with round_robin; the certificate is still checked against elitea-<app>-svc). A main.env value that agrees is kept; one that disagrees fails the render. The old *_BASE_URL keys are refused. A provider with more than one replica also needs its NATS client certificate (elitea-deepwiki / elitea-inventory, issued by the chart when NATS TLS is on) so a cancel reaches the replica running the job.

The providers call elitea-main only over gRPC on its private listener (port 9446): the engines’ model calls (elitea.llm.v1.ModelService), artifact uploads and reads from both containers (elitea.runtime.v1.ArtifactObjectService), and Inventory’s investigate source-tool calls (elitea.runtime.v1.InternalToolService). So DeepWiki and Inventory need the runtime plane (main.runtime.enabled), and their callback client certificates, which the chart issues from the elitea-runtime-ca Issuer. The chart mounts those and the runtime CA into both containers of each provider pod, sets ELITEA_DEEPWIKI_ARTIFACT_SERVICE_URL and ELITEA_DEEPWIKI_MODEL_SERVICE_URL (and the ELITEA_INVENTORY_ pair) to https://elitea-main:9446, and adds dns:elitea-subapp-callback to the identities elitea-main admits for each service. No provider pod reaches elitea-main on port 8080. The old ELITEA_DEEPWIKI_CALLBACK_BASE_URL and ELITEA_INVENTORY_CALLBACK_BASE_URL keys and deepwiki.callbackViaPlatformListener are refused.

To bring your own callback certificate instead, set callbackClient.certManager.enabled: false and create the Secret (tls.crt, tls.key, one DNS SAN elitea-subapp-callback, signed by the runtime CA).

Cloud executor and egress proxy

Follow Cloud executor on Kubernetes. It mints the sandbox material, creates the run-token key pair for the egress proxy, and gives the full values.

The egress proxy is needed only for cloud-executor workspaces whose project limits network access to named hosts. A NetworkPolicy cannot match host names, so those workspaces reach the internet only through the proxy, with a run token signed by elitea-main. If it is off, a turn in such a project is refused with workspace_run.egress_proxy_unavailable, and its network access is never widened instead. The proxy does not affect the LLM gateway, toolkits or the worker. Those have their own allowlists, described in the accordions above.

The chart refuses egressProxy.enabled without both key Secrets, with the same Secret used for both, with fewer than 2 replicas, or without the execution namespace and your cluster’s pod and service CIDRs.

PgBouncer
Terminal window
k create secret generic elitea-pgbouncer-credentials \
--from-literal=user=elitea_v2 --from-file=password=db-password
# elitea-main's own DSN, pointing at the pooler; same role and database.
printf 'postgres://elitea_v2:%s@elitea-pgbouncer:6432/elitea_v2?sslmode=disable' \
"$(cat db-password)" > database-url-pooled
k create secret generic elitea-main-db-pooled $(f database-url-pooled)
pgbouncer:
enabled: true
postgresHost: pg.internal
database: elitea_v2
credentialsSecretName: elitea-pgbouncer-credentials
main:
secrets:
DATABASE_URL: # wins over postgresql.existingSecret for main only
secretName: elitea-main-db-pooled
key: database-url-pooled
optional: false

Only elitea-main’s pods go through the pooler. The migration Job, dbInit, the scheduler and the gateway keep the direct DSN in elitea-main-db. The database name in the pooled DSN must equal pgbouncer.database.

Terminal window
VERSION=<chart version> # an exact release; charts have no latest tag
helm template elitea oci://ghcr.io/elitea-ng/charts/elitea \
--version "$VERSION" \
-n elitea -f values-prod.yaml > /dev/null && echo "renders"

Each refusal names the field and tells you what to do. Fix them all here, where they cost nothing.

Terminal window
helm install elitea oci://ghcr.io/elitea-ng/charts/elitea \
--version "$VERSION" \
-n elitea -f values-prod.yaml --timeout 15m

Do not add --wait to this first install: the NATS bootstrap is a post-install hook, so --wait would wait for workloads that wait for the streams the hook creates. With Helm 4, --atomic is spelled --rollback-on-failure.

The hooks run in a fixed order: the database preparation (when on), then the platform and agent-state migrations, then the workload-session row. Then the Deployments and the NATS StatefulSet roll out, and the elitea-nats-bootstrap Job creates the streams. elitea-main can restart a few times while cert-manager issues its NATS and gateway certificates and until the bootstrap has run; that settles by itself. Watch it with:

Terminal window
kubectl -n elitea get pods,jobs,certificates -w

elitea-main is the browser edge. It serves the API, redirects / to /app/, relays /app and /docs to elitea-web, and serves its MCP endpoint (/app/<project id>/mcp) and icon files under /app itself. So the public edge is one route: / to elitea-main:8080. No rule names elitea-web, and none needs a regular expression.

  1. Send / to elitea-main, port 8080, and nothing to elitea-web directly. Its NetworkPolicy admits elitea-main only.
  2. Keep streams open. Chat, the events stream, /llm and MCP tools/call are server-sent events, and a model answer can stream for several minutes. elitea-main writes a comment into an idle stream every 25 seconds, so an idle timeout above 60 seconds is enough; disable the per-request timeout and turn off response buffering. /llm/v1/realtime needs WebSocket upgrades.
  3. Accept large uploads, at least 150 MiB, or whatever ARTIFACT_MAX_OBJECT_BYTES is set to.

You do not need to strip identity headers or hide /readyz and /startupz: elitea-main deletes client-supplied identity headers itself before any route runs, and answers the two probe paths to a loopback caller only (its probes run inside the container).

Let the chart render it: main.ingress.enabled: true renders an Ingress, or, with gatewayApi: true, an HTTPRoute, each a single / rule to elitea-main.

main:
ingress:
enabled: true
gatewayApi: true
hosts: [elitea.example.com]
gateway:
name: <your-gateway>
namespace: <its-namespace>
sectionName: https

The chart’s HTTPRoute also removes the client identity headers (main.ingress.stripIdentityHeaders), as defence in depth. It sets no timeout, so raise your implementation’s route and idle timeouts where it has them. For a route that has to live with the Gateway rather than with the release, apply the reviewed copy in deploy/gateway-api/httproute.yaml after adding your parentRefs and hostnames; you can add timeouts: {request: 0s} to its rule there.

networkPolicies.main.ingressFrom in the values file must select the pods that actually forward traffic, meaning your gateway’s or ingress controller’s data-plane pods, not its controller. Find their namespace and labels:

Terminal window
# Envoy Gateway:
kubectl get pods -A -l app.kubernetes.io/name=envoy --show-labels

If the selector is wrong, the gateway gets connection timeouts from elitea-main while everything in the namespace looks healthy. A cloud load balancer with no pods (the GKE Ingress, container-native) is admitted by ipBlock instead: 35.191.0.0/16 and 130.211.0.0/22 on GKE.

Step 8: First sign-in, the public project and models

Section titled “Step 8: First sign-in, the public project and models”
  1. Register the redirect URI https://elitea.example.com/auth/oidc/callback with the identity provider. The provider must issue an email claim.
  2. Sign in at https://elitea.example.com/ as the account named in ELITEA_INITIAL_GLOBAL_ADMINS. That account becomes an administrator during the sign-in, and nothing needs to be run by hand. Your personal project and an auto-issued personal access token are created in the same step. The worker needs that token to act for you in chat.
    • email:<address> matches only when the identity provider marks the address verified (email_verified: true).
    • oidc:<sub> always matches. After a first sign-in, open Settings › Profile and copy the Sign-in reference row. Add it, and sign in again.
    • The list never demotes anybody. Remove administrators in the admin console instead.
  3. Check the public project. The first migration seeds project 1, “Default Project”, which platform.aiProjectId: "1" names. Its shared models are what every other project can use. Keep it, or name another existing project. The chart refuses a values file in which the copies of this id disagree.
  4. Add a model provider. In the admin console (/admin/app), go to Configuration › LLM Proxy › Providers & models. Add a credential for OpenAI, Anthropic, Azure OpenAI, Vertex AI, Amazon Bedrock, EPAM AI DIAL or an OpenAI-compatible server (vLLM, Ollama). Then publish the models you want shared, and choose the platform default model on the same tab. LLM Proxy has the details. A provider whose host is not in GATEWAY_EGRESS_ALLOWLIST is refused at request time. On GKE, Vertex AI and AI DIAL can authenticate as the gateway’s Google service account instead of with a key; see Platform models with a service account.
  5. Optional: SAML and SCIM. SAML providers are configured only in the admin console (Configuration › Authentication), so sign in through OIDC first. Because OIDC is configured in the values file, the sign-in routes already exist, and a SAML provider added later needs no restart. Give the identity provider the metadata URL https://elitea.example.com/auth/saml/metadata. SCIM accepts only a SCIM client credential created on the same page; personal access tokens are refused there. Bind directory groups to a project and a role on the same page. Authentication describes every field.

Then open a chat, pick a shared model and send a message. That one exchange exercises the edge, elitea-main, NATS, the command bus, the worker, the platform listener, the agent-state database, the LLM gateway and the provider.

  1. Every pod is Ready and every Job completed

    Terminal window
    kubectl -n elitea get pods,jobs
    kubectl -n elitea get certificates # every one READY=True
  2. The edge reaches elitea-main

    Terminal window
    curl -si https://elitea.example.com/healthz \
    | grep -i -e '^HTTP' -e '^vary'
    # Expect HTTP/2 200 and "vary: Origin".
    # elitea-main sets that header; a gateway 404 does not.
    curl -si https://elitea.example.com/.well-known/elitea-client \
    | grep -i '^vary'
  3. The web app and the MCP endpoint come through elitea-main

    Terminal window
    curl -si https://elitea.example.com/ | grep -i '^location' # /app/
    curl -si -X POST https://elitea.example.com/app/1/mcp | head -5
    # Expect a JSON 401 from elitea-main.
    # An HTML page means the edge sends /app to elitea-web; route / to elitea-main.
  4. The web app and admin console load

    https://elitea.example.com/app/ and https://elitea.example.com/admin/app, signed in as the administrator.

  5. The gateway sees its configuration

    GET /api/v2/admin/gateway/status as an administrator reports the egress allowlist and whether rate limits are enforceable. The latter needs NATS.

  6. A chat turn completes

    The test that matters. If it hangs or fails, use the table below.

SymptomLikely causeFix
helm template refusesa missing or contradictory valuethe message names the field; fix it there
elitea-main CrashLoopBackOff right after a green installthe bucket does not exist, or the credential cannot set its lifecycle rulecreate the bucket; grant s3:PutLifecycleConfiguration
elitea-main exits naming the presence bucket or NATSthe elitea-nats-bootstrap hook Job has not succeeded yet, or natsBootstrap.enabled is offwait for the Job and read its logs; a first helm install --wait never lets it run
elitea-main exits: master key invalidnot 44 base64url characters, or a trailing newlineregenerate with the Step 4 command; never echo it into a file
elitea-main exits naming the application secret keySSO configured and elitea-main-sso missing or emptycreate the Secret
FATAL: sorry, too many clients already in any podthe connection budget is wrongset postgresql.maxConnections to the server’s real value and lower main.autoscaling.maxReplicas
Every /llm call is refused by the gatewaythe two gateway identity secrets differput the same value in both Secrets and restart main and the gateway
/llm answers 503 llm_gateway_not_configuredLLM_GATEWAY_URL is emptyset the four LLM_GATEWAY_* values
Chat sends but nothing ever answers; the worker is Readyno valid workload session, or the worker certificate’s SAN differs from expectedIdentitycheck the workload-session Job’s logs; re-mint with the right WORKER_ID
Chat fails: “durable agent checkpoint store is unavailable” or a missing relationthe agent-state database or its migrations are missingcreate the database; set agentStateMigrations.enabled: true
Chat fails at actor_pat_issuancethe account has no active personal access tokensign out and in again; one is issued at sign-in
Model picker is emptyno shared models in the public project, or aiProjectId names another projectStep 8.4; check the id
A provider is refused although its key is right; Test connection says The LLM gateway refused the host <host>the host is on neither egress list (governance entries and GATEWAY_EGRESS_ALLOWLIST), or it is private and only named by host. Vertex AI needs its regional host <location>-aiplatform.googleapis.comadd the named host in Admin → LLM Governance, type Egress allowlist, or to GATEWAY_EGRESS_ALLOWLIST; for a private address, add its IP or CIDR too
OIDC login answers 409 this identity is already linked to another account. If your identity provider re-created your user…the identity provider re-created the user, so its sub changedrelink with elitea-main oidc-relink; see Relink an account
OIDC login answers another 409 (SAML account, SCIM account, address of another account)the body names the causesee When an OIDC login answers 409
OIDC login answers 403 email claim is not verifiedthe id_token has "email_verified": false (always refused), or no claim on a first login while e-mail verification is requiredmark the address verified at the identity provider; see OIDC sign-in rules
The gateway gets timeouts from elitea-mainnetworkPolicies.main.ingressFrom does not select the gateway’s data-plane podsfix the selector
Login loops back to the sign-in pageCOOKIE_SECURE on behind plain HTTP, or a redirect URI mismatchserve HTTPS end to end; match the URI exactly
Streams stop after 15 to 60 secondsan edge request or idle timeoutStep 7, item 2
  • Upgrades. Bump the one chart. Read the entries in docs/UPGRADING.md between your version and the new one first. The chart refuses values that a release made obsolete, so render with helm template before you upgrade. Migrations run as a hook on every upgrade.
  • Verify what you install. Charts and images are signed with cosign:
    Terminal window
    cosign verify "ghcr.io/elitea-ng/charts/elitea:$VERSION" \
    --certificate-identity-regexp \
    '^https://github.com/elitea-ng/elitea-platform/' \
    --certificate-oidc-issuer \
    https://token.actions.githubusercontent.com
    Build provenance, SBOMs and an admission policy that enforces the signature are in Verify release artifacts.
  • Certificate expiry. The minted runtime material is valid for 825 days by default. cert-manager renews its own certificates; nothing renews the minted ones. Re-mint the runtime CA, the listener, edge and worker certificates, and the elitea-runtime-ca Secret before they expire. Keep the master key, the PAT signing key and the database password, which the script would otherwise regenerate.
  • Rotation costs. Rotating auth-pat-signing-key invalidates every personal access token. Rotating application-secret-key signs everyone out. secrets-master-key cannot be rotated by replacing it.
  • Backups. Back up both databases, the bucket, the Qdrant volume (or plan to re-index) and the elitea-material directory. NATS state is rebuildable: budget counters restart for the current period.
  • GitOps. The same install as one Argo CD Application is in GitOps with ArgoCD; the chart orders NATS, its bootstrap and the platform itself with sync waves. Keep cert-manager and the Issuers of Step 1 outside it, created first. Fill the Secrets from your secret manager with the chart’s externalSecrets block rather than by hand; Deploy on GKE with Argo CD shows it end to end.

Continue with Post-install configuration and Day-2 operations.