Skip to content

Helm Installation (Kubernetes)

Rewritten 2026-08-11 by executing it end-to-end against a real cluster. Every command below was run; every output quoted is real.

The previous version of this page was auto-generated on 2026-06-25 and never verified. It could not work: its first command pointed at a hostname that does not resolve, five of its nine steps failed on invalid flags before reaching the cluster, and its secret step silently set the admin password to an empty string. See What was wrong with the previous version at the end — it is kept deliberately, so nobody reintroduces those steps.


What you get

A complete IssAssist release: the FastAPI backend, the Svelte/nginx web GUI, the IBM Storage Protect bridge, two PostgreSQL instances, two Valkey caches, and the mittwald secret-generator that mints credentials for all of them.

Verified result on a 6-node cluster: 8 Deployments Ready, 12/12 pods Running.


Before you start

You need Verified version Notes
helm v3.16.2 v3.8+ should work; --skip-crds is required (Step 5)
kubectl v1.31.2 Must match your cluster within one minor
Cluster admin once Only for the bootstrap in Steps 1–2
A GitHub PAT with read:packages — The images are private (Step 4)

The install splits into two halves, and the split matters:

  • Steps 1–2 need cluster-admin. They create cluster-scoped objects (namespace, CRDs, one ClusterRole). A human does these once per cluster.
  • Steps 3–7 need only a namespaced ServiceAccount. This is the half CI runs.

Do not run the whole thing as cluster-admin because it is easier. The chart used to require it, and that is now fixed; see Why the deployer is not cluster-admin.


Step 1 — Get the chart

There is no public Helm repository. The hostname in the previous version of this page does not exist:

$ helm repo add issassist https://issassistsource.iss-tech.io/helm/charts/issassist
Error: looks like "https://issassistsource.iss-tech.io/helm/charts/issassist" is not
a valid chart repository or cannot be reached: ... no such host

Clone the chart repository instead:

git clone https://github.com/IssTech/issassist.helm.git

Everything below assumes ./issassist.helm is that checkout. If you are working inside IssAssist-Development, it is already vendored as a submodule at IssAssist-Code/issassist.helm — run git submodule update --init first, and check it is not behind:

git -C IssAssist-Code/issassist.helm fetch origin
git -C IssAssist-Code/issassist.helm rev-parse --short HEAD origin/master   # must match

A stale submodule pin has already caused one wasted investigation: a bug "found" in the chart had been fixed upstream five commits earlier.


Step 2 — Bootstrap the cluster (cluster-admin, once)

2a. Install the secret-generator CRDs

The chart vendors the mittwald kubernetes-secret-generator subchart, which ships CRDs. CRDs are cluster-scoped, so a namespaced deployer cannot install them — which is why Step 5 passes --skip-crds. Install them once, as admin:

kubectl apply -f ./issassist.helm/charts/kubernetes-secret-generator/crds/
customresourcedefinition.apiextensions.k8s.io/basicauths.secretgenerator.mittwald.de created
customresourcedefinition.apiextensions.k8s.io/sshkeypairs.secretgenerator.mittwald.de created
customresourcedefinition.apiextensions.k8s.io/stringsecrets.secretgenerator.mittwald.de created

Skip this only if kubectl get crd | grep mittwald already returns three rows.

2b. Create the namespace, ServiceAccounts and RBAC

IssAssist-Development carries a ready-made manifest — docs/issassist-ci-role.yaml — that creates the target namespace, a read-only ServiceAccount, a deployer ServiceAccount, and their Roles.

kubectl apply -f docs/issassist-ci-role.yaml

Run it twice. The file declares a Namespace after some of the objects that live in it, and kubectl apply processes a multi-document file in order, so the first run ends with:

Error from server (NotFound): ... namespaces "issassist" not found

The second run succeeds. This is cosmetic, not a failure — but do check the second run is clean before continuing.

Do not "verify" this file with --dry-run=server first. Dry-run does not really create the namespaces, so every namespaced object in the file reports NotFound — ten errors that look alarming and mean nothing.

Confirm the deployer can do its job and nothing more:

SA=system:serviceaccount:issassist-staging:issassist-ci-deployer
kubectl auth can-i create deployments -n issassist-staging --as=$SA   # yes
kubectl auth can-i create clusterissuers.cert-manager.io  --as=$SA    # yes
kubectl auth can-i list secrets --all-namespaces          --as=$SA    # no
kubectl auth can-i create clusterroles                    --as=$SA    # no
kubectl auth can-i escalate clusterroles                  --as=$SA    # no

If any of the last three answers yes, stop — something has re-granted cluster-admin, and the token you are about to mint would read every secret on the cluster.

2c. Mint the deployer token and collect the CA bundle

kubectl -n issassist-staging create token issassist-ci-deployer --duration=24h

Automation needs three values. Name them explicitly — the previous version of this page said "set the token" without saying which one, from where, or into what:

Variable Where it comes from
KUBE_DEPLOY_TOKEN the create token command above
KUBE_API_URL kubectl config view -o jsonpath='{.clusters[?(@.name=="<cluster>")].cluster.server}'
KUBE_CA_BUNDLE the same query for .cluster.certificate-authority-data, base64-decoded

KUBE_CA_BUNDLE is not optional. Since 2026-08-08 the End-User Test deployer verifies TLS against the target cluster's CA and fails closed without it.

Use the target cluster's CA. Do not substitute the in-pod /var/run/secrets/kubernetes.io/serviceaccount/ca.crt — that is the CA of the cluster the pod runs in, which is not necessarily the cluster you are installing to, and the mismatch surfaces as an unhelpful x509 error.

Verify the token before using it:

kubectl --server="$KUBE_API_URL" --certificate-authority=ca.crt \
        --token="$KUBE_DEPLOY_TOKEN" auth whoami
# system:serviceaccount:issassist-staging:issassist-ci-deployer

Step 3 — Do not pre-create secrets

The chart creates its own secrets and lets the mittwald generator fill them — see templates/accounts/. admin-auth carries secret-generator.v1.mittwald.de/autogenerate: "password" and a length of 32.

Creating admin-auth yourself will either break the install or, worse, give you an admin account with a blank password. The previous version of this page told you to do exactly that. There is no api-keys secret either — that name appears nowhere in the chart.

The one secret you do supply is the image pull credential, and you supply it through values, not kubectl — see Step 4.


Step 4 — Write my-values.yaml

Nesting is the thing to get right. Most of these keys live under global:. Helm silently ignores unknown top-level keys, so putting them at the top level produces chart defaults with no warning at all — the failure mode is a successful-looking install with the wrong settings.

# Cluster-scoped objects are gated off: the deployer is not cluster-admin.
clusterScoped:
  create: false

# Keeps the secret-generator's own RBAC namespaced instead of cluster-wide.
# The subchart handles this correctly — it moves secret access into a
# namespaced Role, so password generation still works.
kubernetes-secret-generator:
  rbac:
    clusterRole: false

global:
  publicDomainName: "issassist-staging.local"
  publicPort: 443
  publicApiPort: 443
  publicScheme: https
  basePath: ""

  adminAccount:                    # global.adminAccount — NOT top-level
    username: admin
    email: you@example.com
    timezone: "Europe/Stockholm"
    lockPassword: false

  ingress:                         # global.ingress — NOT top-level
    ingressClass: nginx
    tls:
      issuerType: "disabled"       # global.ingress.tls — there is no top-level `tls:`

imageCredentials:
  registry: ghcr.io
  # Pass username/password on the command line (Step 5) rather than writing a
  # PAT into a file that is easy to commit by accident.

Keys that belong under global: and are commonly put at the top level by mistake: adminAccount, mail, certDuration, caCertDuration, priorityClass, ingress.

Two things worth knowing before you install:

  • ingressClass: nginx only does something if that IngressClass exists. Check with kubectl get ingressclass. If the cluster has none — a Cilium/Gateway-API cluster typically has none — the chart's two Ingresses are created but no controller claims them, so they route nothing. That is fine for a private staging install; it is not a way to disable Ingress.
  • The chart has no switch to disable Ingress. global.ingress accepts only ingressClass and tls. An enabled: false there is silently ignored.

api.image defaults to ghcr.io/isstech/issassist-api:1.3. Pin it deliberately rather than inheriting whatever the chart happens to default to.


Step 5 — Install

helm upgrade --install issassist ./issassist.helm \
  --namespace issassist-staging \
  -f my-values.yaml \
  --set imageCredentials.username="$GHCR_USERNAME" \
  --set imageCredentials.password="$GHCR_PAT" \
  --skip-crds \
  --kube-apiserver "$KUBE_API_URL" \
  --kube-ca-file ca.crt \
  --kube-token "$KUBE_DEPLOY_TOKEN" \
  --timeout 12m --wait

Each flag earns its place:

  • --skip-crds — CRDs went in at Step 2a; a namespaced deployer cannot create them and the install fails without this.
  • --wait — otherwise Helm reports success the moment the objects are accepted, long before any pod is Ready.
  • imageCredentials via --set — the images are private. Omit these and every pod sits in ImagePullBackOff: the install appears to succeed and then quietly does not run. The previous version of this page never mentioned a pull secret.

helm upgrade --install rather than helm install so a re-run is safe.

You will see a wall of would violate PodSecurity "restricted:latest" warnings. They are warnings, not errors — the namespace enforces no Pod Security Standard. Worth fixing in the chart; not a failed install.

If it fails with "attempting to grant RBAC permissions not currently held"

roles.rbac.authorization.k8s.io "mittwald:secret-generator" is forbidden:
user "system:serviceaccount:issassist-staging:issassist-ci-deployer" is
attempting to grant RBAC permissions not currently held

Kubernetes refuses to let any caller grant permissions it does not itself hold. The subchart creates a Role, so the deployer must hold every verb in that Role — even though it never uses them. docs/issassist-ci-role.yaml already carries them (secrets: watch, monitoring.coreos.com/servicemonitors, secretgenerator.mittwald.de/*). If you hit this, your copy predates 2026-08-11; re-apply it.

Do not fix this by granting cluster-admin.


Step 6 — Verify

kubectl get pods -n issassist-staging
kubectl get deploy -n issassist-staging

(There is no --wait flag on kubectl get. The previous version of this page used one; it errors out.)

A healthy install — 8 Deployments, all Ready:

api  1/1   api-postgres  1/1   api-valkey  1/1   secret-generator  1/1
tsm-agent  3/3   tsm-agent-postgres  1/1   tsm-agent-valkey  1/1   webgui  3/3

A few restarts on the postgres/valkey/secret-generator pods during the first 90 seconds are normal — they settle once the generator has minted credentials. No pod should end in ImagePullBackOff, CrashLoopBackOff or Error.

Confirm the admin password really was generated — this is the check that catches the old Step 3 mistake:

kubectl get secret admin-auth -n issassist-staging \
  -o jsonpath='{.data.password}' | base64 -d | wc -c    # 32

32 means the generator worked. 0 means something pre-created the secret.

Then confirm the services answer:

kubectl run curltest -n issassist-staging --rm -i --restart=Never \
  --image=curlimages/curl -- sh -c '
    curl -s -o /dev/null -w "api    %{http_code}\n" http://api:8000/docs
    curl -s -o /dev/null -w "webgui %{http_code}\n" http://webgui:8000/'
# api    200
# webgui 200

Step 7 — Access the web GUI

kubectl port-forward -n issassist-staging service/webgui 8080:8000

Then open http://localhost:8080.

Three corrections to the previous version, all of which made its command fail:

  • The service is webgui, not issassist-webgui.
  • It listens on 8000, not 80.
  • There is no --wait flag on kubectl port-forward.

Binding a local port below 1024 also needs root; use 8080.

Log in as admin with the generated password:

kubectl get secret admin-auth -n issassist-staging -o jsonpath='{.data.password}' | base64 -d

Step 8 — Upgrade

Re-run the exact Step 5 command. It is already upgrade --install. Keep --skip-crds; CRDs are managed out-of-band at Step 2a.

Step 9 — Uninstall

helm uninstall issassist -n issassist-staging --wait

This leaves behind, by design: the PersistentVolumeClaims, the CRDs from Step 2a, and ClusterIssuer root-issuer. Delete the PVCs explicitly if you want the data gone:

kubectl delete pvc --all -n issassist-staging

Known issues in the chart

Found while running this install. These are chart defects, not installation mistakes — you cannot avoid them by installing differently.

The datastore services are hardcoded NodePort. api-postgres (30101), api-valkey (30102), tsm-agent-postgres (30103) and tsm-agent-valkey (30104) set type: NodePort and a fixed port directly in their templates, with no values override. Consequences:

  • PostgreSQL and Valkey are reachable on every node IP, from anywhere that can route to the node network — verified by connecting to port 30101 from a pod in an unrelated namespace. Namespace isolation does not contain this. It is bounded only by how reachable your node network is; on the verified cluster all node addresses are private.
  • Two releases of this chart cannot coexist on one cluster — the fixed nodePorts collide.

No pod sets a securityContext, so every workload fails a restricted Pod Security Standard. Harmless on a namespace that enforces nothing; blocking on a hardened one.


Why the deployer is not cluster-admin

Until 2026-08-11 issassist-ci-role.yaml bound the deployer to cluster-admin, annotated as acceptable "on a dedicated staging cluster with no production workloads". That stopped being true when staging moved onto a shared cluster.

The grant was not arbitrary. Two of the chart's own ClusterRoles granted cluster-wide access — one over secrets in every namespace, one with * on pods, pods/exec, deployments and persistentvolumes — and RBAC escalation-prevention means the installer must already hold whatever it grants. issassist.helm#10 put both behind clusterScoped.create, which is why Step 4 sets it to false.

What is left is one cluster-scoped object the chart cannot do without: the self-signed ClusterIssuer root-issuer, referenced by four Certificates. The deployer holds exactly one ClusterRole with one rule for it.

If a future chart change needs a new cluster-scoped resource, gate it behind clusterScoped.create or add a named rule. Do not widen the grant to make an install succeed.


What was wrong with the previous version

Kept so these steps are not reintroduced. Every item was reproduced against a real cluster on 2026-08-11.

Step Problem Result
1 helm repo add ... --update-cache Error: unknown flag: --update-cache
1 Repo host issassistsource.iss-tech.io Does not resolve — no such host
1 helm upgrade ./issassist.helm -n ... No release name — requires 2 arguments
2 kubectl create namespace --wait=true error: unknown flag: --wait
3 admin-auth from $(kubectl get secrets \| grep admin-auth) Reads the secret it is creating; resolves to empty. Admin password set to ""
3 api-keys secret Referenced nowhere in the chart
4 adminAccount, mail, certDuration, caCertDuration, priorityClass, ingress at top level All belong under global:; silently ignored
4 tls: at top level Not a chart key at all — it is global.ingress.tls
4 No imageCredentials Private images — every pod ImagePullBackOff
5 No --skip-crds Namespaced deployer cannot create CRDs
6 kubectl get pods --wait=true error: unknown flag: --wait
7 service/issassist-webgui 80:80 --wait=true Wrong name, wrong port, invalid flag
— No RBAC / ServiceAccount step No path to an automatable install
— No KUBE_CA_BUNDLE Deployer fails closed on TLS verification
— No CRD prerequisite Install fails at the secret-generator subchart

Five of the nine steps failed on a syntax error before reaching the cluster. The page carried a "remove [TODO: verify] markers as you verify each detail" banner for roughly seven weeks; the markers were removed, the verification never happened.