Production Installation
This is the procedure a customer follows to deploy Plexicus on a Kubernetes cluster they already operate. It is manual, uses standard tooling (helm, kubectl, openssl, curl), and takes about an hour once the items below are in hand. Examples assume chart 1.2.43 or later.
Before you start
Everything you must obtain, provide, or decide is on this one screen. Nothing below step 1 asks for something that is not listed here.
What you receive from Plexicus
Your delivery package, handed over by Plexicus when your deployment was set up. It contains:
| File | What it is | First used in |
|---|---|---|
keys.json | GAR service-account key — the registry reader credential | steps 2 and 4 |
license.jwt | your signed Plexicus licence (plus license-key.pem if your plan includes one) | step 5 |
plexicus-eval.env | your domain, your AI key, and PLEXICUS_CHART_VERSION | steps 5, 7 and 8 |
values.yaml (production packages) | a values overlay already seeded with your domain | step 7 |
github-app.pem (optional) | a GitHub App private key — only present if Plexicus registered a GitHub App for you. Not needed to install. | after install — SCM Connection Reference |
README.txt | the handover notes for your package | — |
What you provide
| Requirement | Detail |
|---|---|
| A Kubernetes cluster, v1.25+ | Any CNCF-conformant distribution. AKS, EKS, GKE and kubeadm-style on-premises clusters are validated; k3s works; OpenShift, RKE2 and other hardened distributions are not in the validation matrix yet — contact engineering@plexicus.ai first. Provisioning the cluster — CNI, nodes, control plane — is yours; Plexicus never installs or changes it. |
| An operator workstation | kubectl pointed at the cluster (kubectl config current-context), helm 3.8+ (OCI support), openssl. Every command runs here, none on the nodes. |
| A domain and two DNS records | plexicus.<your-domain> and api.plexicus.<your-domain>, both A records pointing at your ingress controller's external IP, before step 7 — Let's Encrypt cannot issue a certificate for a name that does not resolve. |
| An ingress controller | Traefik (default) or nginx, already installed. Only ports 80 and 443 on it need to be reachable by users. |
| TLS | cert-manager with a ClusterIssuer, or certificates you supply. Installing cert-manager does not create an issuer — you do, in step 6. |
A StorageClass with reclaimPolicy: Retain | For MongoDB, PostgreSQL and object storage. Delete destroys the volume with the PVC; node-local classes lose the data with the node. kubectl get storageclass. |
| Node capacity | Sizing depends on whether you run the infrastructure in-cluster — see Infrastructure Prerequisites. |
| Outbound egress | europe-west3-docker.pkg.dev (chart and images), your ACME endpoint, your AI endpoints, your SMTP relay, your SCM host, and services.nvd.nist.gov if you use an NVD key. Nothing else; the platform sends no telemetry. Fully disconnected clusters: Air-Gapped Installation. |
| A Cloudflare account | For Turnstile keys. The chart ships with test keys that disable bot protection; a production install must replace them (step 7). |
| An SMTP relay (recommended) | Host, username, password. It is what makes the first-admin bootstrap and every later invitation a normal in-product flow. You can run without email, but then the first admin needs a one-time database write (step 10). |
Decide now
| Decision | Options | Where it lands |
|---|---|---|
| Infrastructure | Let the chart install MongoDB, Redis, MinIO, PostgreSQL and Temporal as bundled subcharts, or point it at services you operate | step 6 |
Configure SMTP (standard), or declare deliveryMode: "disabled" | step 7, step 10 | |
| AI key | Use the key in your delivery package, or bring your own OpenAI-compatible provider | step 5 |
Not performed by the product
Plexicus is a workload on your cluster. Cluster provisioning, host hardening, DNS, TLS trust, network policy, backup and monitoring are yours, before or alongside this procedure. The full boundary is in Appendix B.
1. Unpack Your Delivery Package
You already have your delivery package — the keys.json, license.jwt, plexicus-eval.env and, for a production package, values.yaml listed above — from your Plexicus onboarding. If you do not, or if any file is missing, contact engineering@plexicus.ai with your organization name, target environment and domain and it will be re-issued.
Save every file to one working directory and stay in it for the whole install:
mkdir -p ~/plexicus-deploy && cd ~/plexicus-deploy
# copy keys.json, license.jwt, plexicus-eval.env, and values.yaml / github-app.pem if present, into this directory
keys.json and license.jwt are credentials. Never commit them to version control, share them over unencrypted channels, or store them in plain text on shared systems.
Older packages and email threads may call keys.json by its former name, sa-key.json. It is the same file.
2. Authenticate Helm Against the Registry
Pin the chart version from your package, then log Helm in to the OCI registry:
export CHART_VERSION=$(grep '^PLEXICUS_CHART_VERSION=' plexicus-eval.env | cut -d= -f2)
echo "$CHART_VERSION" # must print a version, e.g. 1.2.43 — if empty, ask engineering@plexicus.ai for the current one
cat keys.json | helm registry login \
europe-west3-docker.pkg.dev \
--username _json_key \
--password-stdin
A successful login prints Login Succeeded. Always install a pinned --version; an unpinned install resolves to whatever is newest at that moment and is not reproducible. $CHART_VERSION persists for the rest of your shell session — re-export it if you open a new one.
3. Create the Kubernetes Namespace
kubectl create namespace plexicus
4. Create the Image Pull Secret
The chart expects this Secret to be named gar-secret:
kubectl create secret docker-registry gar-secret \
--docker-server=europe-west3-docker.pkg.dev \
--docker-username=_json_key \
--docker-password="$(cat keys.json)" \
--namespace plexicus
If your naming policy requires a different name, set global.imagePullSecrets[0].name to match. If you rotate the key later, re-create this Secret and restart the deployments.
5. Create Application Secrets
Sensitive values — passwords, API keys, OAuth secrets — are never written to values.yaml. Each service reads them from its own Kubernetes Secret via existingSecret, and every Secret must exist before the chart is installed. The script below creates all nine.
Two inputs come from your delivery package: license.jwt, and the AI key with its provider, endpoint and model ids in plexicus-eval.env. Everything else you generate here with openssl.
When you choose DB_PASS, REDIS_PASS, and MINIO_PASS, use only letters and digits (e.g. openssl rand -hex 24). pymongo refuses to URL-encode special characters (@, :, /, ?, #, %, !, $) in connection URIs and will fail with Username and password must be escaped according to RFC 3986. Redis and MinIO have similar quoting traps in their clients.
# === Shared values (set once, used across multiple Secrets) ===
DB_PASS='<choose-a-strong-mongo-root-password>'
REDIS_PASS='<choose-a-redis-password>'
MINIO_USER=$(openssl rand -hex 12) # object-storage access key id — never leave a well-known default
MINIO_PASS='<choose-a-minio-password>' # object-storage secret access key
SECRET_KEY=$(openssl rand -hex 32) # Plexicus signing key
PLEXALYZER_SECRET=$(openssl rand -hex 32) # Plexalyzer worker handshake
NUXT_SECRET=$(openssl rand -hex 32) # Nuxt session secret
# AI key: use the one Plexicus provided with your deployment, or your own
# (BYOAI) if you want the traffic billed to your account.
AI_API_KEY='<the AI key from plexicus-eval.env, or your own provider key>'
AI_SCAN_KEY='<chat LLM key for the AI SAST scan engine — can reuse AI_API_KEY>'
AI_SCAN_EMBEDDING_KEY='<API key for an endpoint that serves BAAI/bge-m3 embeddings>'
# === Optional integration secrets — leave empty if not used ===
GH_OAUTH_SECRET="" # GitHub OAuth client_secret (App or OAuth App)
GL_OAUTH_SECRET="" # GitLab OAuth client_secret
BB_OAUTH_SECRET="" # Bitbucket OAuth client_secret
EMAIL_PASS="" # SMTP password — required for the standard first-admin path (step 10)
TURNSTILE_SECRET="" # Cloudflare Turnstile secret key — required for production (step 7)
# === SSO / SAML / OIDC secrets — required only when SSO is enabled ===
# Generate strong random values; safe to populate even if SSO is off
# (they're only consumed when SAML/OIDC handlers are invoked).
BREAK_GLASS=$(openssl rand -hex 32) # Emergency-admin bypass key
SSO_RELAY=$(openssl rand -hex 32) # HMAC for SAML RelayState signing
SSO_ENCRYPT=$(openssl rand -hex 32) # AES-256 key for OIDC client_secret encryption at rest
# === Internal service-to-service tokens — REQUIRED, and each is a SHARED value ===
# Generate each ONCE and use the same value in every Secret listed below. These
# are not optional: the chart sets ENVIRONMENT=production, which engages the
# mandatory-token startup guards in the worker and the exporter.
WORKER_CP_SECRET=$(openssl rand -hex 32) # fastapi <-> worker control plane
EXPORTER_TOKEN=$(openssl rand -hex 32) # worker <-> exporter internal API
# === Secrets for all currently-enabled Plexicus services (AI Pentest disabled by default; no secret needed until enabled) ===
kubectl -n plexicus create secret generic plexicus-fastapi \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
--from-literal=REDIS_PASSWORD="$REDIS_PASS" \
--from-literal=SECRET_KEY="$SECRET_KEY" \
--from-literal=PLEXALYZER_SECRET_KEY="$PLEXALYZER_SECRET" \
--from-literal=OBJECT_STORAGE_ACCESS_KEY="$MINIO_USER" \
--from-literal=OBJECT_STORAGE_SECRET_KEY="$MINIO_PASS" \
--from-literal=AI_REMEDIATION_API_KEY="$AI_API_KEY" \
--from-literal=AI_VALIDATION_API_KEY="$AI_API_KEY" \
--from-literal=GITHUB_APP_PRIVATE_KEY="" \
--from-literal=GITHUB_OAUTH_CLIENT_SECRET="$GH_OAUTH_SECRET" \
--from-literal=GITLAB_OAUTH_CLIENT_SECRET="$GL_OAUTH_SECRET" \
--from-literal=BITBUCKET_OAUTH_CLIENT_SECRET="$BB_OAUTH_SECRET" \
--from-literal=EMAIL_PASSWORD="$EMAIL_PASS" \
--from-literal=WORKER_CONTROL_PLANE_SECRET="$WORKER_CP_SECRET" \
--from-literal=BREAK_GLASS_SECRET_KEY="$BREAK_GLASS" \
--from-literal=SSO_RELAY_STATE_SECRET="$SSO_RELAY" \
--from-literal=SSO_ENCRYPTION_KEY="$SSO_ENCRYPT"
kubectl -n plexicus create secret generic plexicus-worker \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
--from-literal=REDIS_PASSWORD="$REDIS_PASS" \
--from-literal=SECRET_KEY="$SECRET_KEY" \
--from-literal=PLEXALYZER_SECRET_KEY="$PLEXALYZER_SECRET" \
--from-literal=GITHUB_APP_PRIVATE_KEY="" \
--from-literal=OBJECT_STORAGE_ACCESS_KEY="$MINIO_USER" \
--from-literal=OBJECT_STORAGE_SECRET_KEY="$MINIO_PASS" \
--from-literal=PLEXALYZER_TOKEN="$PLEXALYZER_SECRET" \
--from-literal=AI_REMEDIATION_API_KEY="$AI_API_KEY" \
--from-literal=AI_VALIDATION_API_KEY="$AI_API_KEY" \
--from-literal=PLEXICUS_AI_SCAN_API_KEY="$AI_SCAN_KEY" \
--from-literal=PLEXICUS_AI_SCAN_EMBEDDING_API_KEY="$AI_SCAN_EMBEDDING_KEY" \
--from-literal=WORKER_CONTROL_PLANE_SECRET="$WORKER_CP_SECRET" \
--from-literal=EXPORTER_INTERNAL_TOKEN="$EXPORTER_TOKEN" \
--from-literal=EMAIL_PASSWORD="$EMAIL_PASS"
kubectl -n plexicus create secret generic plexicus-frontend \
--from-literal=NUXT_SECRET_KEY="$NUXT_SECRET" \
--from-literal=NUXT_INTERNAL_SHARED_SECRET="$WORKER_CP_SECRET" \
--from-literal=NUXT_TURNSTILE_SECRET_KEY="$TURNSTILE_SECRET"
kubectl -n plexicus create secret generic plexicus-analysis-scheduler \
--from-literal=DATABASE_PASSWORD="$DB_PASS"
kubectl -n plexicus create secret generic plexicus-codex-remedium \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
--from-literal=REDIS_PASSWORD="$REDIS_PASS"
kubectl -n plexicus create secret generic plexicus-exporter \
--from-literal=DATABASE_PASSWORD="$DB_PASS" \
--from-literal=AI_ENRICHMENT_API_KEY="$AI_API_KEY" \
--from-literal=EXPORTER_INTERNAL_TOKEN="$EXPORTER_TOKEN"
for s in plexalyzer-code plexalyzer-prov; do
kubectl -n plexicus create secret generic plexicus-$s \
--from-literal=PLEXALYZER_SECRET_KEY="$PLEXALYZER_SECRET"
done
# Licence — license.jwt is in your delivery package. Without this Secret the
# fastapi and worker pods never start (FailedMount, no logs). If your package
# also contains license-key.pem, add: --from-file=license-key.pem=license-key.pem
kubectl -n plexicus create secret generic plexicus-license \
--from-file=license.jwt=license.jwt
# Verify
kubectl -n plexicus get secrets | grep ^plexicus-
# Expected: 9 plexicus-* secrets (8 service + 1 license)
If your delivery package contains github-app.pem, or you operate your own GitHub App, replace the empty GITHUB_APP_PRIVATE_KEY="" with GITHUB_APP_PRIVATE_KEY="$(cat github-app.pem)" in both plexicus-fastapi and plexicus-worker. The nine Secrets above are the complete set for a standard install; the key catalog in Appendix A lists every key each one accepts.
DATABASE_PASSWORD, REDIS_PASSWORD, OBJECT_STORAGE_*, PLEXALYZER_SECRET_KEY, SECRET_KEY, WORKER_CONTROL_PLANE_SECRET and EXPORTER_INTERNAL_TOKEN are each one credential read by several services. The script above sets each once and reuses it; if you create the Secrets any other way, keep that property. A mismatched WORKER_CONTROL_PLANE_SECRET stops the worker from starting; the others fail at runtime with authentication errors. Detail in Appendix A.
6. Provide the Infrastructure
Plexicus runs on MongoDB, Redis, an S3-compatible object store, PostgreSQL and Temporal. You decided above whether the chart installs them or you do.
If the chart installs them, add this block to your values overlay in step 7 — the passwords must be the ones you chose in step 5:
mongodb:
enabled: true
fullnameOverride: "mongodb"
auth:
rootPassword: "<the DB_PASS you chose in step 5>"
redis:
enabled: true
fullnameOverride: "redis"
auth:
password: "<the REDIS_PASS you chose in step 5>"
minio:
enabled: true
fullnameOverride: "minio"
auth:
rootUser: "<the MINIO_USER you chose in step 5>"
rootPassword: "<the MINIO_PASS you chose in step 5>"
temporal-postgresql:
enabled: true
fullnameOverride: "temporal-postgresql"
auth:
postgresPassword: "<choose a PostgreSQL password>"
temporal:
enabled: true
fullnameOverride: "temporal"
If you run them yourself — existing databases, a cloud object store, your own Temporal — follow Infrastructure Prerequisites. It has the sizing, the StorageClass and ClusterIssuer manifests, and the exact install commands for each service.
Before you continue
Three things must be true before step 8, whichever path you took:
- A
StorageClasswithreclaimPolicy: Retainis set asglobal.storageClass. ADeletepolicy destroys the volume when a PVC is removed, and node-local classes such aslocal-pathlose the data with the node. Check withkubectl get storageclass. - A
ClusterIssuerexists, if you use cert-manager. Installing cert-manager creates the controller and CRDs only — never an issuer. Without one, certificates stayPendingwithClusterIssuer "letsencrypt-prod" not found. The manifest is on the Infrastructure page. - All infrastructure pods are
Running—kubectl -n plexicus get pods.
7. Prepare Your Values File
If your delivery package contains values.yaml, it is your overlay, already seeded with your domain:
cp values.yaml my-values.yaml
Otherwise start from the canonical customer overlay that ships inside the chart:
helm pull oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version "$CHART_VERSION" --untar
cp plexicus/values-customer.yaml.example my-values.yaml
Either way, open my-values.yaml and confirm every value below. The ones marked REQUIRED have no usable default — the schema rejects a placeholder before anything reaches the cluster.
This is the overlay for a standard install. If your package contains values.yaml, it already looks like this with your domain filled in:
global:
# Your root domain — all service URLs are derived from this.
domain: plexicus.yourdomain.com # REQUIRED
scheme: "https"
wsScheme: "wss"
# Ingress controller class applied to the fastapi and frontend Ingress resources.
ingressClassName: "traefik" # also: "nginx", "gce", "alb"
# When enabled, the chart injects cert-manager.io/cluster-issuer on every
# Ingress that has TLS configured. Set enabled: false if you manage TLS yourself.
certManager:
enabled: true
clusterIssuer: "letsencrypt-prod"
imagePullSecrets:
- name: gar-secret
required:
ai:
# REQUIRED by values.schema.json — the render fails without them.
openAiDeploymentNameFree: "<model-id>"
openAiDeploymentSwe: "<model-id>"
# Outbound email (standard first-admin path). The password is EMAIL_PASSWORD
# in the plexicus-fastapi Secret. To run without email instead, remove
# server/username/port and set deliveryMode: "disabled" — see step 10.
smtp:
server: "<smtp-relay-host>"
username: "<smtp-username>"
port: 587
# Cloudflare Turnstile — REQUIRED for production. siteKey is public; the
# secret key is NUXT_TURNSTILE_SECRET_KEY in the plexicus-frontend Secret.
# The widget's domain allowlist in Cloudflare must include your domain.
turnstile:
siteKey: "<your-turnstile-site-key>"
fastapi:
ingress:
enabled: true
hosts:
- host: api.plexicus.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: plexicus-fastapi-tls
hosts:
- api.plexicus.yourdomain.com
existingSecret: plexicus-fastapi
frontend:
ingress:
enabled: true
hosts:
- host: plexicus.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: plexicus-frontend-tls
hosts:
- plexicus.yourdomain.com
existingSecret: plexicus-frontend
worker:
existingSecret: plexicus-worker
envs:
# REQUIRED. The worker reads its AI configuration from these; without them
# every AI call fails. Copy the values from plexicus-eval.env in your
# delivery package, or use your own provider's. Change all three per channel together.
AI_VALIDATION_PROVIDER: "<provider>"
AI_VALIDATION_BASE_URL: "<endpoint>"
AI_VALIDATION_MODEL: "<model-id>"
AI_REMEDIATION_PROVIDER: "<provider>"
AI_REMEDIATION_BASE_URL: "<endpoint>"
AI_REMEDIATION_MODEL: "<model-id>"
# REQUIRED for AI SAST scans. The embedding endpoint must serve BAAI/bge-m3
# at 1024 dimensions (Appendix A, "AI SAST scan credentials").
PLEXICUS_AI_SCAN_BASE_URL: "<endpoint>"
PLEXICUS_AI_SCAN_MODEL: "<model-id>"
PLEXICUS_AI_SCAN_EMBEDDING_BASE_URL: "<embedding-endpoint>"
analysis-scheduler:
existingSecret: plexicus-analysis-scheduler
codex-remedium:
existingSecret: plexicus-codex-remedium
exporter:
existingSecret: plexicus-exporter
plexalyzer-code:
existingSecret: plexicus-plexalyzer-code
plexalyzer-prov:
existingSecret: plexicus-plexalyzer-prov
plexalyzer-code, plexalyzer-prov, codex-remedium, exporter and AI Pentest ship enabled: false (and the first three also replicaCount: 0) on purpose. The worker spawns the scanners and the remediation engine as ephemeral Kubernetes Jobs, one per scan or remediation, and deletes them when the run ends — see create_job() in worker/utils/kubernetes.py. The existingSecret entries above are still required: the Job inherits those credentials.
So after helm install you will see no plexalyzer-* or codex-remedium Pod in kubectl get pods, and that is the correct, healthy state. They appear only while a scan is running.
Do not set enabled: true to "fix" the missing Pods. That renders a second, idle Deployment which never serves a scan, consumes memory, and is not the path the product uses. The image tag the Jobs actually pull comes from global.required.scanJobImages.*, not from these blocks.
global.required.ai.openAiDeploymentNameFree and openAiDeploymentSwe are both required by values.schema.json, which also rejects placeholder values by pattern. Omitting them, or leaving a placeholder in place, fails at helm install / helm template time with an error naming the path — nothing reaches the cluster.
Set both to a real model id at your configured endpoint:
global:
required:
ai:
openAiDeploymentNameFree: "<model-id>"
openAiDeploymentSwe: "<model-id>"
For what each value does, what the schema enforces, and the values derived from global.domain, see Appendix A. Two production requirements are easy to skip and are checked in Definition of done: replace the Turnstile test keys, and keep OAUTHLIB_INSECURE_TRANSPORT unset or "0".
8. Install the Chart
helm upgrade --install plexicus \
oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version $CHART_VERSION \
--namespace plexicus \
--values my-values.yaml \
--timeout 20m
The installation takes a few minutes while all pods and dependencies initialize. upgrade --install is idempotent — re-running it after fixing a values typo does the right thing instead of erroring with release plexicus already exists.
Helm's default timeout is 5 minutes, which the infrastructure subcharts (notably Temporal, step 6) can exceed on a cold cluster — hence --timeout 20m. It matters most if you also pass --wait or --atomic, where a premature timeout rolls back a release that was merely still settling.
A values.schema.json violation is caught before any resource is created and names the offending path, so a schema failure leaves nothing to clean up — fix the overlay and re-run.
9. Verify the Installation
# All pods should reach Running or Completed status
kubectl get pods -n plexicus
# Ingress should have an external address assigned
kubectl get ingress -n plexicus
Pods typically reach a healthy state within 3–5 minutes. If a pod is stuck in CrashLoopBackOff or Error, inspect its logs:
kubectl logs -n plexicus <pod-name> --previous
Common causes are missing secret keys or incorrect connection details in my-values.yaml. If the symptom is a specific error rather than a stuck pod, see Troubleshooting.
9.1 Confirm the licence loaded
kubectl -n plexicus logs deploy/fastapi | grep "License OK"
# License OK — customer='…' plan=… state=valid
state=valid is what you want. A CrashLoopBackOff on fastapi or worker with a clean container start means the licence is invalid or expired — see licence failures.
9.2 Confirm the AI model ids that were rendered
The schema catches a placeholder in your overlay, but it cannot see a value injected some other way. Confirm what the pod actually received:
kubectl -n plexicus get deploy fastapi -o \
jsonpath='{range .spec.template.spec.containers[0].env[?(@.name=="AI_VALIDATION_MODEL")]}{.value}{"\n"}{end}'
If this prints a placeholder rather than a real model id, fix your overlay before going further. Repeat for AI_REMEDIATION_MODEL on the worker Deployment.
A first end-to-end smoke test from outside the cluster:
curl -s -o /dev/null -w "Frontend %{http_code}\n" https://<your-domain>/
curl -s -o /dev/null -w "API/health %{http_code}\n" https://api.<your-domain>/health
# Frontend 200 (the SPA shell — it renders the login view client-side)
# API/health 200 (FastAPI is running — liveness only; does not probe MongoDB / Redis / MinIO / Temporal)
A trusted-TLS verification (no -k needed when using a public ClusterIssuer):
curl -s -o /dev/null -w "TLS verify=%{ssl_verify_result}\n" https://<your-domain>/
# TLS verify=0 (publicly trusted certificate)
/health does not prove very muchGET /health returns a static {"status": "ok"}. It is a liveness probe — it does not check the database, the licence, or any dependency. A 200 tells you the process is up and nothing more. Use the licence endpoint above for a real readiness signal.
10. Bootstrap the First Admin User
The chart ships with an empty database. Plexicus does not seed an initial admin during install — every account is created through the registration API. The default registration flow expects email verification via SMTP; on a brand-new install the very first admin therefore has to be either:
- The standard path, and the one to use in production — registered through the regular UI flow once SMTP credentials (
global.required.smtp.*values plus theEMAIL_PASSWORDSecret key — all surfaced in-container asEMAIL_*) are wired intoplexicus-fastapi. The user clicks the verification link in the welcome email; nothing else is needed and steps 10.1 and 10.2 can be skipped entirely. - A one-time operator fallback, only when there is no mail server — registered via the API and then flagged
is_verified: truedirectly on theUsersMongoDB collection. This is a database maintenance action performed by the cluster operator, not a product feature; see 10.2 before you use it.
Wire SMTP if you can. Doing so removes the need for step 10.2 completely and keeps every account creation inside the product's own audit trail.
10.0 Decide your email delivery mode first
global.required.smtp.deliveryMode (rendered as EMAIL_DELIVERY_MODE) takes "smtp" (the default) or "disabled", and it changes what the registration API does. Set it deliberately before you bootstrap:
deliveryMode | smtp.server | What happens |
|---|---|---|
"smtp" (default) | set | Normal behaviour. Verification, invitation and password-reset mail is sent. |
"smtp" (default) | empty | Every email-dependent call fails with 503 — register, invite, resend-verification and password reset. This is the "you forgot to configure SMTP" state and it is deliberately loud. |
"disabled" | ignored | The deployment declares it runs without email. Nothing is sent, responses carry email_sent: false, and onboarding becomes admin-mediated. |
The 503 is an RFC 7807 application/problem+json body whose detail names both escapes — set EMAIL_HOST, or declare EMAIL_DELIVERY_MODE=disabled.
disabled does not auto-verify self-registrations — and the first admin still needs the MongoDB shortcutIt is tempting to read deliveryMode: "disabled" as "skip verification". It is not. POST /registrations writes is_verified: false in every delivery mode, by design: it is a public, unauthenticated endpoint with no backend captcha, and each registration mints a fresh tenant carrying a trial with AI credits — so auto-verifying there would turn "we have no mail server" into open, billable signup for anyone who can reach the host.
What disabled gives you instead is an admin-mediated path: an already-authenticated admin invites a user (POST /teams/self/members, or the superadmin POST /backoffice/users/invite) and the invite response itself carries a verification_link field to hand over out of band. That link is returned only to an authenticated admin, and only when the mode was explicitly declared — merely forgetting to configure SMTP never discloses one.
Redeeming a verification token is the only thing in the API that sets is_verified: true — there is no force-verify endpoint and no config flag. The MongoDB shortcut in 10.2 writes the field directly, which is why it is scoped to the first admin and to nobody else; preferring the in-product invite keeps the audit trail intact and needs no database access.
That path cannot bootstrap the first admin, because it presupposes an admin. On a fresh cluster with no mail server, step 10.2 below remains the only way in.
To declare a deployment as running without email, set it in your overlay:
global:
required:
smtp:
deliveryMode: "disabled"
disabled means smtpThe value is not a validated enum — values.schema.json does not constrain it. A typo such as "disbaled" is silently treated as "smtp", which puts you in the 503 row above. Check the rendered value if register starts returning 503 unexpectedly.
The fallback direction is deliberate: an unrecognised value lands in the loud branch (503 on registration) rather than quietly disabling email everywhere.
EMAIL_DELIVERY_MODE governs the API service and the scanning worker alike, so worker-originated mail (report-ready, scan-complete) follows the same three states as the table above.
10.1 Register the user via the API
The schema validator requires at least one uppercase letter, one lowercase letter, one digit, and one special character:
curl -s -X POST -H "Content-Type: application/json" \
-d '{
"email":"admin@yourcompany.com",
"password":"ChangeMeNow1!",
"confirm_password":"ChangeMeNow1!"
}' \
https://api.<your-domain>/registrations
# {"success":true,"message":"User successfully registered. Please check your email for verification link."}
With deliveryMode: "disabled" the same call returns 200 with an extra email_sent: false and a different message — "…This deployment runs without email delivery, so no verification email was sent — ask an administrator to activate your account." The account exists but is not verified either way.
A 503 here means SMTP is neither configured nor declared absent — go back to step 10.0.
If SMTP is wired up, the user clicks the link in the welcome email and is done. Skip to step 10.3.
10.2 Operator fallback — verify the first admin in MongoDB
Skip this step entirely if you configured SMTP. It exists for one case: a fresh cluster with no mail server, where no admin exists yet and the in-product invite flow therefore has nobody to invite from.
What it is. A direct write to the Users collection, performed by the person who already holds cluster-admin on the cluster and the MongoDB root password. It grants that operator no access they did not already have — anyone able to run it can already read every Secret in the namespace. It is not an application feature and it is not reachable through the product: the API exposes no force-verify endpoint and no configuration flag that sets is_verified, in any delivery mode. Redeeming a verification token is the only in-product path.
Scope it. Use it once, for the first administrator account, on a deployment you have just created and which holds no data. Change that account's password at first login. Every subsequent user is created from inside the product — under deliveryMode: "disabled" the invite response hands the inviting admin a verification_link to pass on out of band, which keeps the audit trail intact and needs no database access.
If your deployment is subject to a certification or an internal control regime that disallows direct database writes, configure SMTP before you bootstrap and this step does not apply to you. Note that setting deliveryMode: "disabled" does not by itself remove the need for it; see 10.0.
The collection name is Users (capital U); the verification flag is is_verified:
# With the bundled MongoDB the Deployment is `mongodb`; substitute your own
# Deployment name if you run MongoDB elsewhere.
kubectl -n plexicus exec deploy/mongodb -- mongosh \
"mongodb://root:<your-mongo-root-password>@localhost:27017/plexicus?authSource=admin" \
--quiet --eval '
db.Users.updateOne(
{ email: "admin@yourcompany.com" },
{ $set: { is_verified: true, role: "admin" } }
)
'
# { acknowledged: true, matchedCount: 1, modifiedCount: 1 }
10.3 Log in
curl -s -X POST -H "Content-Type: application/json" \
-d '{"email":"admin@yourcompany.com","password":"ChangeMeNow1!"}' \
https://api.<your-domain>/sessions
# {"access_token":"eyJhbGc…","token_type":"bearer"}
A 200 with an access_token confirms the full chain — frontend → ingress → fastapi → MongoDB authentication — is healthy. Open https://<your-domain> in a browser and sign in with the credentials you just registered.
10.4 Confirm the licence through the API
The API exposes the current licence state. It requires a bearer token, so log in first:
curl -s https://api.<your-domain>/system/license \
-H "Authorization: Bearer <your-token>"
state is lowercase and is one of valid, grace, expired or invalid — valid is what you want. The response also carries customer, plan, features, expires_at, grace_days_remaining, license_id and deployment_type. A 503 from this endpoint means the licence verifier never initialised.
Definition of done
The installation is complete when every line below is true. This is the list to sign off against. Connecting your repositories and running the first scan is post-install configuration, covered by the SCM Connection Reference.
-
kubectl -n plexicus get pods— every podRunningorCompleted; noplexalyzer-*orcodex-remediumpod present (they run as Jobs only during a scan). -
kubectl -n plexicus get ingress— both hosts have an external address. -
https://<your-domain>/returns200andcurlreportsTLS verify=0with no-k. -
kubectl -n plexicus logs deploy/fastapi | grep "License OK"showsstate=valid, andGET /system/licensewith a bearer token returns"state": "valid". -
AI_VALIDATION_MODELonfastapiandAI_REMEDIATION_MODELonworkerare real model ids, not placeholders. -
global.required.turnstile.siteKeyis a real Cloudflare key andNUXT_TURNSTILE_SECRET_KEYis set inplexicus-frontend— the login form renders and submits. -
OAUTHLIB_INSECURE_TRANSPORTis absent fromfastapi.envsor set to"0". -
global.required.smtp.deliveryModeis deliberatelysmtpwith a working relay ordisabled— and registration does not return503. - The first admin can log in, and its bootstrap password has been changed.
- You have recorded the chart version and the running image digests (Appendix B), and your backup procedure is in place.
Day-2 operations — upgrading, rollback, troubleshooting by symptom, the in-chart operator docs — are on Operations.
Appendix A — Reference
Material you need when you change something, not when you install. Nothing here is a step.
Key catalog
Nine Secrets are required for a standard installation — eight wired via existingSecret for service environment variables, plus one license Secret mounted as files:
| Secret name | Required keys |
|---|---|
plexicus-fastapi | DATABASE_PASSWORD, REDIS_PASSWORD, SECRET_KEY, PLEXALYZER_SECRET_KEY, OBJECT_STORAGE_ACCESS_KEY, OBJECT_STORAGE_SECRET_KEY, AI_REMEDIATION_API_KEY, AI_VALIDATION_API_KEY, WORKER_CONTROL_PLANE_SECRET (shared value — must be identical in the fastapi and worker Secrets), plus optional GITHUB_APP_PRIVATE_KEY, GITHUB_OAUTH_CLIENT_SECRET, GITLAB_OAUTH_CLIENT_SECRET, BITBUCKET_OAUTH_CLIENT_SECRET, EMAIL_PASSWORD, STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET, BREAK_GLASS_SECRET_KEY, SSO_RELAY_STATE_SECRET, SSO_ENCRYPTION_KEY (only the integrations you use; SSO keys required only when SAML/OIDC SSO is enabled) |
plexicus-worker | DATABASE_PASSWORD, REDIS_PASSWORD, SECRET_KEY, PLEXALYZER_SECRET_KEY, GITHUB_APP_PRIVATE_KEY, OBJECT_STORAGE_ACCESS_KEY, OBJECT_STORAGE_SECRET_KEY, PLEXALYZER_TOKEN, AI_REMEDIATION_API_KEY, AI_VALIDATION_API_KEY, PLEXICUS_AI_SCAN_API_KEY, PLEXICUS_AI_SCAN_EMBEDDING_API_KEY, WORKER_CONTROL_PLANE_SECRET (shared value — must be identical in the fastapi and worker Secrets; without it the worker refuses to start), EXPORTER_INTERNAL_TOKEN (shared value — must be identical in the worker and exporter Secrets; only when exporter.enabled: true), EMAIL_PASSWORD |
plexicus-frontend | NUXT_SECRET_KEY; optional OAuth client secrets: NUXT_GITHUB_SECRET_KEY, NUXT_GITLAB_SECRET_KEY, NUXT_BITBUCKET_CLOUD_SECRET, NUXT_GOOGLE_CLIENT_SECRET; NUXT_STRIPE_KEY if billing is enabled; NUXT_TURNSTILE_SECRET_KEY for Cloudflare Turnstile in production; NUXT_INTERNAL_SHARED_SECRET — must equal the WORKER_CONTROL_PLANE_SECRET you set on plexicus-fastapi and plexicus-worker. The Nuxt server presents it on the one FastAPI endpoint that serves a stored GitHub App's OAuth client pair. Without it, an App created via Create GitHub App automatically is stored but silently unusable for browser logins: the authorize redirect falls back to this pod's NUXT_GITHUB_CLIENT_ID, which is blank on exactly the installs that need the flow |
plexicus-analysis-scheduler | DATABASE_PASSWORD |
plexicus-codex-remedium | DATABASE_PASSWORD, REDIS_PASSWORD. Optionally CODEX_REMEDIUM_SHARED_SECRET — see Optional components below |
plexicus-exporter | DATABASE_PASSWORD, AI_ENRICHMENT_API_KEY, EXPORTER_INTERNAL_TOKEN (shared value — must be identical in the worker and exporter Secrets; the exporter refuses to start without it) |
plexicus-plexalyzer-code | PLEXALYZER_SECRET_KEY; optional: NVD_API_KEY (free API key from nvd.nist.gov — omitting it causes vulnerability enrichment to run at the unauthenticated rate limit: 10 requests/30 s instead of 50 requests/30 s; enrichment works but is slower) |
plexicus-plexalyzer-prov | PLEXALYZER_SECRET_KEY |
plexicus-license | license.jwt (required); license-key.pem (optional — include if Plexicus provided it alongside license.jwt). Both keys are mounted as files at /etc/plexicus/ by the fastapi and worker pods. |
Three chart features are off by default and need no secret, no value and no action in this step:
| Component | Chart key | Only if you want it |
|---|---|---|
| AI Gateway (per-call metering proxy) | ai-gateway.enabled: false | Setting PLEXICUS_GATEWAY_BASE_URL requires PLEXICUS_GATEWAY_IDENTITY_SECRET in the plexicus-worker Secret — the base URL alone makes every AI SAST scan fail. |
| AI Pentest (Nexus) | scanJobImages.nexus (no strix subchart since 1.2.42) | Worker-spawned Nexus scan jobs. Pin nexus image tag/digest; LLM key via worker Secret per delivery package. Strix subchart and plexicus-strix Secret are retired. |
| Remediation service-to-service auth | CODEX_REMEDIUM_SHARED_SECRET | Set the same value in the plexicus-worker and plexicus-codex-remedium Secrets, then ENFORCE_CODEX_REMEDIUM_AUTH=true to require it. Strip trailing newlines — they are illegal in an HTTP header. |
Shared keys
DATABASE_PASSWORD, REDIS_PASSWORD, OBJECT_STORAGE_ACCESS_KEY, OBJECT_STORAGE_SECRET_KEY, PLEXALYZER_SECRET_KEY, PLEXALYZER_TOKEN, and SECRET_KEY MUST be identical across every Secret that lists them. They are the same credential consumed by different services. Mismatches cause MongoDB Authentication failed, WRONGPASS from Redis, and Plexalyzer worker handshake failures at runtime.
WORKER_CONTROL_PLANE_SECRET is a shared value — it must be identical in the fastapi and worker Secrets. It is not optional: the chart sets ENVIRONMENT=production, which engages the worker's mandatory-token startup guard, so a worker pod without this key refuses to start and CrashLoops before it does any work. fastapi presents the same value as X-Shared-Secret when it calls the worker's control plane, so a mismatch is just as bad as an absence — the worker boots and then rejects every control-plane call from fastapi.
EXPORTER_INTERNAL_TOKEN follows the identical pattern and is a shared value — it must be identical in the worker and exporter Secrets. An exporter pod without it fails startup by design; a worker without it gets silent 401s on its daily enrichment refresh. This one applies only when exporter.enabled: true. Do not set it through exporter.envs — the chart's env guard rejects TOKEN-named plaintext envs, so it must arrive through the Secret.
AI_REMEDIATION_API_KEY and AI_VALIDATION_API_KEY must also be identical across the plexicus-fastapi and plexicus-worker Secrets — both services use them for their respective AI channels. By default this is the key Plexicus provides with your deployment. If you bring your own, set the matching AI_*_PROVIDER, AI_*_BASE_URL and AI_*_MODEL non-secret variables in your Helm values as Plexicus instructs for your provider.
AI channels
You do not need your own AI provider account to run Plexicus. Your delivery package includes a managed AI key in plexicus-eval.env, alongside the provider, endpoint and model ids that go with it. Use that key as the value for AI_REMEDIATION_API_KEY, AI_VALIDATION_API_KEY and AI_ENRICHMENT_API_KEY.
Bring your own AI (BYOAI) is fully supported and is the right choice when you need the traffic billed to your own account, pinned to a specific region, or routed to a provider you already have a data-processing agreement with. To switch, replace the three Secret values with your own key and set the matching non-secret AI_*_PROVIDER / AI_*_BASE_URL / AI_*_MODEL variables for that provider (see the next admonition). Nothing else in the chart changes.
Never paste an AI key into my-values.yaml — keys belong in the Secrets above, which is why the chart reads them through existingSecret.
An API key on its own is not enough. The worker resolves its AI configuration from environment variables at runtime, so every channel needs three non-secret companions in worker.envs as well as the key in the Secret. The chart does not template them for the worker.
Set them in your values overlay:
worker:
envs:
AI_VALIDATION_PROVIDER: "<provider>" # all three: copy from plexicus-eval.env, or follow the
AI_VALIDATION_BASE_URL: "<endpoint>" # instructions Plexicus gave you for your own provider
AI_VALIDATION_MODEL: "<model-id>"
AI_REMEDIATION_PROVIDER: "<provider>"
AI_REMEDIATION_BASE_URL: "<endpoint>"
AI_REMEDIATION_MODEL: "<model-id>"
If you are using the AI key Plexicus supplied, the matching provider, endpoint and model ids are in plexicus-eval.env in your delivery package — copy them across rather than guessing. If you bring your own key, they are your provider's.
Omit them and the worker falls back to a default endpoint your key does not belong to: every call 401s, usually surfacing as a repository-description or validation step failing "during None inference". Set a _PROVIDER label that does not match the endpoint the key belongs to and the key goes to the wrong provider's API shape — the platform's AI connection test 401s even though scanning itself works.
SocratiCode guided-exploration indexing is a hard dependency of every AI SAST scan — there is no opt-out, and an unindexed repo fails the scan instead of falling back. This requires two things in the plexicus-worker Secret, distinct from the AI_VALIDATION_API_KEY/AI_REMEDIATION_API_KEY channels above:
PLEXICUS_AI_SCAN_API_KEY— the chat LLM the scan engine itself calls while exploring the repo. Paired with non-secret Helm valuesworker.envs.PLEXICUS_AI_SCAN_BASE_URL/PLEXICUS_AI_SCAN_MODEL(both empty by default in the packaged chart — an install that leaves them unset fails every scan with "PLEXICUS_AI_SCAN_API_KEY not set").PLEXICUS_AI_SCAN_EMBEDDING_API_KEY— a separate key for the embedding endpoint that indexes the repo, modelBAAI/bge-m3, 1024 dimensions. Paired with the non-secret valueworker.envs.PLEXICUS_AI_SCAN_EMBEDDING_BASE_URL(also no packaged-chart default — it must be set explicitly or indexing has no endpoint to call).EMBEDDING_MODEL/EMBEDDING_DIMENSIONSalready default toBAAI/bge-m3/1024in the packaged chart; only override them if you run a different embedding deployment (dimensions MUST match what your model actually outputs, or the vector index is silently corrupted instead of erroring).
Not every provider serves an embedding model. The chat key (PLEXICUS_AI_SCAN_API_KEY) can sit on any OpenAI-compatible provider; only the embedding endpoint is constrained, and it must be one that actually serves BAAI/bge-m3 at 1024 dimensions. Where your chat provider does not, point PLEXICUS_AI_SCAN_EMBEDDING_BASE_URL at one that does — a second hosted provider, or a model server you run yourself (see Data residency below) — with its own PLEXICUS_AI_SCAN_EMBEDDING_API_KEY. Where your chat provider does serve it, one key covers both channels. The values that match a Plexicus-supplied key are in plexicus-eval.env in your delivery package.
The AI_* and PLEXICUS_AI_SCAN_* endpoints are the only places a self-hosted Plexicus sends code context outside your cluster, and they go only to the endpoints you set here. Nothing is routed through Plexicus. If you have data-residency, jurisdiction, or DPA constraints, point every one of them at an endpoint that satisfies them: a provider you already hold an agreement with, a regional deployment, or an OpenAI-compatible model server you host in-cluster. The only hard requirement is the API shape (OpenAI-compatible) and, for the embedding endpoint, BAAI/bge-m3 at 1024 dimensions. Leave them unset and AI enrichment, remediation and AI SAST do not run — the rest of the platform scans normally.
Licence failures
The fastapi and worker pods read the license from two paths, controlled by PLEXICUS_LICENSE_PATH (required) and PLEXICUS_LICENSE_PRIVATE_KEY_PATH (optional — only needed if Plexicus provided a license-key.pem). Both env vars point at the files mounted from the plexicus-license Secret above. License problems surface as one of two distinct failure modes:
- Pods stuck in
ContainerCreating, with aFailedMountevent namingplexicus-license— the Secret does not exist. Bothfastapiandworkermount it as a volume withoptional: false, so the kubelet refuses to start the container at all rather than starting it without the file; the pod never reaches a running state and there are no container logs to read.kubectl -n plexicus describe pod <pod>shows an event likeMountVolume.SetUp failed for volume "plexicus-license" : secret "plexicus-license" not found. Fix: create the Secret as shown above — the pods recover on their own once it exists, no rollout needed. CrashLoopBackOffonworkeronly, withWORKER_CONTROL_PLANE_SECRET must be set outside the dev environment— not a licence problem. The chart setsENVIRONMENT=production, which makes that key mandatory at worker startup. Add it to both theplexicus-fastapiandplexicus-workerSecrets with the same value and restart the worker. The equivalent onexporterisEXPORTER_INTERNAL_TOKEN, shared between theworkerandexporterSecrets.CrashLoopBackOffonfastapi/workerwith a clean container start — the Secret exists and mounts correctly, but the license itself is invalid or expired. Both services callSystemExitat startup when license validation fails, which Kubernetes reports as a crash loop rather than a config error. Checkkubectl -n plexicus logs deploy/fastapi(ordeploy/worker) for the validation error, then request a renewedlicense.jwtfrom engineering@plexicus.ai.
global.domain and what the schema enforces
global.domain is required and validated by values.schema.json, which also rejects the placeholders plexicus.example.com, bare example.com, and <to-fill> — so a copy-pasted overlay cannot reach a cluster. A schema violation is caught before any resource is created and the error names the offending path: there is nothing to clean up, just fix the overlay and re-run.
What the schema enforces:
| Value | Enforced | Rule |
|---|---|---|
global.domain | yes | Non-empty; rejects plexicus.example.com, example.com, <to-fill> |
global.scheme | yes | http or https only |
global.wsScheme | yes | ws or wss only |
global.required.ai.openAiDeploymentNameFree / openAiDeploymentSwe | yes | Both required; must match ^[A-Za-z0-9][A-Za-z0-9._/-]*$; placeholder values rejected |
global.required.oauth.github.appId / clientId | yes | appId must be a string; both reject shared-tenant values, so an overlay copied from elsewhere cannot point at another install's GitHub App |
global.required.smtp.deliveryMode | no | Unconstrained — a typo silently means smtp |
wsScheme must be wss, not httpsIt sits directly under scheme: "https" in the overlay, which invites copying the value down a line. https is rejected by the schema.
Beyond the ingress hostname, the chart derives from global.domain:
- the OAuth callback URLs sent to GitHub, GitLab and Bitbucket Cloud (
<scheme>://<domain>/api/callback/<provider>), and - the WebAuthn relying party used by passkeys —
WEBAUTHN_RP_IDis the bare host andWEBAUTHN_ORIGINis<scheme>://<host>. If you serve the interface from several hostnames under one parent and want a shared passkey, overrideWEBAUTHN_RP_IDwith the parent domain.
Changing global.domain after users have registered passkeys invalidates them: a credential is bound to the RP ID it was created under.
Chart 1.2.7+ defaults the in-cluster service names (mongodb, redis-master, minio:9000), the cross-service URLs (FastAPI's importer / notification / receive endpoints), the AI endpoint, and the Cloudflare Turnstile keys (test tokens for eval). All sensitive values (passwords, OAuth client secrets, API keys, real Turnstile keys for production) come from the eight existingSecret references — they are never written into my-values.yaml.
Override entries in global.required.* only when:
- you connect the chart to externally-managed MongoDB / Redis / MinIO / Temporal (a different host/port than the defaults — see Infrastructure Prerequisites),
- you use a non-default release name for the prereqs (e.g. ArgoCD prefixes them — see ArgoCD Deployment),
- you run production and need to set real Cloudflare Turnstile keys, real OAuth client_id values, or your own AI provider URL.
Cloudflare Turnstile
The login form is gated by Cloudflare Turnstile. The chart ships Cloudflare's documented "always passes" test tokens so a fresh install reaches the login form; a production deployment must replace them, or bot protection is effectively off.
global.required.turnstile.siteKey— the public widget key, in your overlay (step 7).NUXT_TURNSTILE_SECRET_KEY— the secret key, in theplexicus-frontendSecret (step 5). Do not put it invalues.yaml.- The widget's domain allowlist in the Cloudflare dashboard must include your domain. If it does not, the login fields never render (Turnstile error
110200).
Wrong siteKey shows as skeleton-loader inputs that never resolve; wrong secret shows as Turnstile validation failed, Please try again on submit.
CORS allow-list
The chart's fastapi.envs.CORS_ORIGINS defaults to <scheme>://<domain>,<scheme>://api.<domain> so the SPA at the same domain plus the api subdomain works out of the box. Override fastapi.envs.CORS_ORIGINS in your customer overlay only if you front the SPA on additional hostnames (multi-tenant, branded subdomains, embedded iframe). An empty CORS_ORIGINS makes every cross-origin request return 400 Disallowed CORS origin.
global.ingressClassName (default: traefik) and global.certManager.enabled + global.certManager.clusterIssuer (default: letsencrypt-prod) apply to the fastapi and frontend Ingress resources automatically. Per-service overrides via <service>.ingress.className or <service>.ingress.annotations are possible but rarely needed.
Only fastapi and frontend are externally reachable. The other seven services (worker, analysis-scheduler, codex-remedium, exporter, plexalyzer-code, plexalyzer-prov, and AI Pentest) are cluster-internal and do not need Ingress.
The bundled-infra overlay (values-bundled-infra.yaml) sets OAUTHLIB_INSECURE_TRANSPORT: "0", which is correct. Never set this value to "1" in any deployment that uses TLS — it disables the OAuth library's HTTPS enforcement and allows OAuth token exchanges over plain HTTP. If you copied or adapted the bundled-infra overlay for production, verify this variable is either absent or explicitly set to "0" in your fastapi.envs block.
After install, Helm prints a NEXT STEPS banner (templates/NOTES.txt) with verification commands tailored to your release. Re-display it any time with:
helm get notes plexicus -n plexicus
If your namespaces enforce the restricted Pod Security Standard, validate the chart in a non-production namespace first and tell us what you hit — the bundled infrastructure subcharts (MongoDB, Redis, MinIO, PostgreSQL) carry their own security-context defaults, and the MongoDB seed init container runs as UID 0. Deployments that use external infrastructure instead of the bundled subcharts avoid most of this. See Air-Gapped Installation for the init-container override.
Appendix B — For auditors and evaluators
This is the standard deployment procedure
This page is the complete procedure a customer follows to deploy Plexicus. Every step is manual, reproducible, and performed with standard tooling; there is no Plexicus-specific installer, wrapper script, or hidden automation, and no step is performed for the customer. The Evaluator Installation is a separate convenience for throwaway single-VM trials — it builds a disposable k3s cluster with self-signed certificates and is not a deployment path. The customer's own activities required to reach an operational state are listed below and marked where they occur in the procedure.
Deployment scenario of record
A deployment is defined by the axes below. Record them: they are what an evaluation, an audit or a support case will ask you to state, and a change to any of them is a configuration change to re-verify.
| Axis | Fixed by | Where in this guide |
|---|---|---|
| Chart version | PLEXICUS_CHART_VERSION in your delivery package, or an explicit --version | step 7, step 8 |
| Kubernetes distribution and version | your cluster | Prerequisites |
| Infrastructure topology | external MongoDB / Redis / object storage / PostgreSQL + Temporal, or the bundled subcharts | step 6 |
| Ingress controller and TLS issuer | global.ingressClassName, global.certManager.clusterIssuer (or your own certificates) | step 7 |
| AI configuration | the AI_* / PLEXICUS_AI_SCAN_* provider, endpoint and model ids you set | step 5 |
| Email delivery mode | global.required.smtp.deliveryMode — smtp or disabled | step 10.0 |
| Identity and SCM integrations | the OAuth apps / GitHub App you register | SCM Connection Reference |
| Image source | GAR directly, or a registry you mirrored to | Air-Gapped Installation |
Environment preparation — performed by the operator, not the product
Plexicus makes no change outside its own namespace and does not configure, harden or manage the environment underneath it.
| Activity | What is required | When |
|---|---|---|
| Cluster and host baseline | A CNCF-conformant cluster at v1.25+, with node OS patching, time synchronisation (NTP — token and certificate validation depend on it), and your own host hardening baseline already applied. Plexicus adds no node-level configuration. | Before step 1 |
| Node capacity | Enough allocatable CPU, memory and disk for the platform plus whichever infrastructure you run in-cluster — see Infrastructure Prerequisites. | Before step 6 |
| Inbound exposure | Only ports 80 and 443 on your ingress controller need to be reachable by users. The Kubernetes API (6443, or your provider's endpoint) is an operator interface and should not be internet-reachable. The chart creates no LoadBalancer or NodePort of its own beyond the two Ingress resources. | Before step 7 |
| Outbound egress | Allow the cluster to reach: europe-west3-docker.pkg.dev (chart and images — or your mirror instead), your ACME/CA endpoint if you use cert-manager, the AI endpoints you configure in steps 5 and 7, your SMTP relay, your SCM host (github.com, gitlab.com, or your self-hosted instance), and services.nvd.nist.gov if you set NVD_API_KEY. Nothing else is required, and the platform sends no telemetry to Plexicus. For a fully disconnected cluster see Air-Gapped Installation. | Before step 6 |
| TLS trust | Either a publicly trusted issuer via cert-manager (step 6), or your own certificates. If you terminate with an internal CA, its root must be trusted by the browsers and CI clients that talk to the platform. | Before step 6 |
| Storage | A default StorageClass supporting dynamic provisioning, with ReadWriteOnce at minimum — see Infrastructure Prerequisites. | Before step 6 |
| Secret material | You generate every password, key and shared token in step 5 yourself, on your workstation, with openssl. Plexicus supplies only the registry credential, the licence, and (optionally) the AI key. How you store and rotate them afterwards is your key-management process. | Step 5 |
| Backup | Restic/Velero or your own procedure against MongoDB, PostgreSQL and object storage — see Backup and Restore. Have it in place before the platform holds data you care about. | Before go-live |
| Monitoring | Your existing cluster monitoring covers Plexicus like any other workload. The chart exposes standard Kubernetes probes; it ships no monitoring stack of its own. | Before go-live |
Artifact integrity
The chart and every container image are served over TLS from GAR and are readable only with the credential in keys.json — the registry is private, so an anonymous pull is not possible. Two things are worth recording, so that the artefacts you deployed can be identified later:
# 1. Pin an explicit chart version and record the artifact you fetched.
helm pull oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version "$CHART_VERSION"
sha256sum plexicus-"$CHART_VERSION".tgz
helm show chart oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version "$CHART_VERSION" | grep -E '^(name|version|appVersion):'
After the install, record the image digests actually running — tags can be re-pointed, digests cannot:
kubectl -n plexicus get pods \
-o jsonpath='{range .items[*]}{range .status.containerStatuses[*]}{.imageID}{"\n"}{end}{end}' \
| sort -u
Keep the chart checksum and that digest list with your change record. They are what identifies the deployed build, and what a support case or an audit will ask you to produce. If you mirror images into your own registry (Air-Gapped Installation), copy them by digest so the mirrored artefact is provably the one you pulled.
Kubernetes distributions
| Distribution | Status |
|---|---|
| AKS, EKS, GKE | Supported. Managed control plane, nothing special to configure. |
| On-premises (kubeadm and similar) | Supported. Bring your own ingress controller and storage class. |
| k3s | Supported, and what the Evaluator Installation bundles for single-VM trials. Fine for evaluation; size and configure it yourself for anything beyond that. |
| OpenShift, RKE2, and other hardened distributions | Not in our published validation matrix yet. The chart has no known incompatibility, but these platforms add constraints (security context policies, admission controls, their own ingress model) that we have not documented end-to-end. Contact engineering@plexicus.ai with your distribution and version before you plan the rollout. |
Where next
- Operations — upgrading, rollback, troubleshooting by symptom
- SCM Connection Reference — connect your own repositories
- Infrastructure Prerequisites — run the dependency services yourself
- Backup and Restore
- ArgoCD Deployment — reconcile the same chart from Git
- Air-Gapped Installation
For questions or support, contact engineering@plexicus.ai.