Katta: transform your S3 storage into a secure, team-friendly workspace with client-side encryption.
Helm chart for Katta Server, a downstream fork of Cryptomator Hub. This chart deploys:
- Katta Server (the "Hub" backend, required)
- Keycloak (optional, enabled by default) — uses the Katta-customized image with the Katta theme; the realm is configured for Keycloak's Standard Token Exchange (V2)
- PostgreSQL (optional, enabled by default)
- MinIO (optional, disabled by default — see Bundled MinIO)
Image repositories are fixed in templates; the core-component tags are overridable per workload:
- Hub:
ghcr.io/shift7-ch/katta-server:<hub.image.tag>(defaults to chartappVersion, which islatestuntil Katta Server is released;hub.imagePullPolicydefaults toAlwaysaccordingly) - Keycloak:
ghcr.io/shift7-ch/keycloak:<keycloak.image.tag>(default26.6.2) - PostgreSQL:
postgres:<postgres.image.tag>(default17-alpine) - MinIO (StatefulSet + seed-Job setup container, shares one tag):
quay.io/minio/minio:<minio.image.tag>(defaultRELEASE.2025-09-07T16-13-09Z) - Storage-profile seed Job:
alpine/curl:8.17.0(hardcoded) - Init waits (DB/OIDC readiness):
busybox:1.36,alpine/curl:8.17.0(hardcoded)
TLS termination is currently expected to be done by the ingress controller. Supported ingress controller templates:
ingress.controller=nginxingress.controller=traefik
The fastest way to spin up a complete Katta stack — Hub + Keycloak + Postgres + MinIO + a pre-seeded storage profile — is via values-demo.yaml against any local single-user cluster with the nginx-ingress addon (tested on minikube + Podman).
Prerequisites:
- Single-user local cluster (kind, minikube, k3d, Docker Desktop). The installer needs cluster-admin: the chart provisions a
Role+RoleBindinginkube-systemso a post-install hook Job can patch the CoreDNS ConfigMap. Don't use these values on a shared or managed cluster. - nginx-ingress addon enabled (
minikube addons enable ingressetc.). - Hostname split: Hub UI lives on
hub.localhost(a browser "secure context" so WebCrypto works without HTTPS); Keycloak, MinIO console, and S3 live under the katta-controlled wildcard A-record*.local.katta.cloud → 127.0.0.1(resolvable from inside the cluster via the CoreDNS patch below, which.localhostis not — and the desktop client doesn't accept*.localhostURLs anyway). Both halves resolve to127.0.0.1browser-side without any/etc/hostsedits.
# one-off (skip if your cluster already has nginx-ingress)
minikube addons enable ingress
# wait for the ingress controller to be ready
kubectl wait -n ingress-nginx --for=condition=ready pod \
-l app.kubernetes.io/component=controller --timeout=180s
# deploy
helm install katta . \
--namespace katta \
--create-namespace \
-f values-demo.yamlImportant
The ingress controller must be running before helm install. The chart looks up the controller's pod IPs at render time to create the port-translation proxy Service (<release>-ingress-proxy) that in-cluster DNS for *.local.katta.cloud points at. If no controller pods are found, the proxy is silently skipped and Hub fails to reach MinIO (see Troubleshooting). If you enabled the ingress addon after installing, re-run helm upgrade katta . --namespace katta -f values-demo.yaml.
Verify that the ingress controller has picked up the chart's Ingress resources (the ADDRESS column should be populated after a minute or so):
kubectl get ingress -n kattaExpose the ingress controller on localhost:9090:
kubectl port-forward -n ingress-nginx svc/ingress-nginx-controller 9090:80Once both commands are running:
| URL | Credentials |
|---|---|
| Hub UI: http://hub.localhost:9090 | admin / admin |
| Keycloak admin: http://kc.local.katta.cloud:9090 | admin / admin |
| MinIO console: http://minio.local.katta.cloud:9090 | minioadmin / minioadmin |
| MinIO S3 API: http://s3.local.katta.cloud:9090 | (used by the seeded storage profile) |
The demo values configure a test license with 5 seats, as Hub otherwise stays in setup mode until a license is provided.
A post-install Helm hook Job (<release>-storageprofile-seed) registers an S3STATIC storage profile named "Bundled MinIO" pointing at http://s3.local.katta.cloud:9090, so vault creation works end-to-end immediately after install. Re-runs are idempotent (the seed Job skips profiles whose name already exists).
How in-cluster DNS works in demo mode: the same hostnames the browser uses need to resolve inside the cluster too (MinIO has to fetch Keycloak's OIDC discovery URL, and the issuer it sees must match the browser-facing one). The chart's demo profile sets coredns.patch.enabled=true, which runs a post-install hook Job that adds a release-scoped # BEGIN katta:<release> / # END katta:<release> stanza to kube-system/coredns's Corefile, rewriting *.local.katta.cloud queries to the chart's port-translation proxy Service. A matching pre-delete Job removes the stanza on helm uninstall. CoreDNS's reload plugin picks up the change within ~30 s; the apply Job sleeps 45 s as a settling buffer before the storage-profile seed Job runs.
When the Hub API returns an error, check the Hub server logs. API error responses include an error id that matches the stack trace in the log:
kubectl logs -n katta deploy/katta-hub -c hub --since=1hTo show only errors and their root causes (add -f to follow while reproducing):
kubectl logs -n katta deploy/katta-hub -c hub --since=1h | grep -E "ERROR|Caused by"Logs of a crashed or restarted container are available with --previous. The init containers (wait-for-postgres, wait-for-oidc) log separately; select them with -c <name> if the pod is stuck in Init.
Log files: Hub writes its log only to stdout; there is no log file inside the container. The container runtime stores that output as files on the Kubernetes node, rotated by the kubelet and deleted together with the pod:
/var/log/pods/<namespace>_<pod>_<uid>/<container>/0.log, e.g./var/log/pods/katta_katta-hub-<hash>_<uid>/hub/0.log/var/log/containers/<pod>_<namespace>_<container>-<id>.log(symlinks to the above)
On minikube these live inside the node VM/container; open a shell with minikube ssh (use sudo, the files are root-only):
minikube ssh -- "sudo sh -c 'tail -n 200 /var/log/pods/katta_katta-hub-*/hub/0.log'"For logs that survive pod deletion, configure an OpenTelemetry/OTLP endpoint for Hub (hub.metrics.enabled / hub.metrics.endpoint in values.yaml) or run a cluster log collector.
java.net.UnknownHostException: s3.local.katta.cloud (e.g. a 500 on PUT /api/storage/...) means in-cluster DNS for the public hostnames isn't working. Check that the proxy Service exists and that the name resolves from inside the cluster:
kubectl get svc -n katta katta-ingress-proxykubectl run dnstest -n katta --rm -i --restart=Never --image=busybox:1.36 -- nslookup s3.local.katta.cloudIf the Service is missing, the ingress controller wasn't running at install time. Make sure it is ready, then re-render the chart:
helm upgrade katta . --namespace katta -f values-demo.yamlAssumes a real domain with public DNS and a pre-existing Traefik ingress controller in the cluster — this chart only registers Ingress and Middleware resources against it; it does not install Traefik. Confirm the IngressClass you want to use (kubectl get ingressclass) and substitute its name below if it isn't traefik.
helm install katta . \
--namespace katta \
--create-namespace \
--wait --timeout 5m \
--set urls.hub.public=https://hub.example.com \
--set urls.kc.public=https://kc.example.com \
--set ingress.controller=traefik \
--set ingress.className=traefik \
--set hub.admin.password=changemeReal public DNS handles in-cluster resolution naturally (the chart's port-translation proxy stays disabled when URLs use the default ports 80/443, and coredns.patch.enabled defaults to off), so neither the CoreDNS patch nor the port-translation proxy is created for production deployments.
Passwords are optional by default. If unset, the chart generates random values and
prints commands in helm notes to retrieve them from Kubernetes Secrets.
ingress.tls.* is opt-in. The chart emits Ingress resources without a spec.tls: block by default, which is the right choice if Traefik is already configured with a default certificate or wildcard. Three common variants:
| Scenario | Add to helm install |
|---|---|
| Traefik default cert / wildcard at the controller | — (no extra flags) |
| cert-manager provisions per-host certs | --set ingress.tls.enabled=true --set ingress.tls.secretName=katta-tls plus a matching Certificate referencing your ClusterIssuer |
| Bring-your-own Secret (pre-created in the release namespace) | --set ingress.tls.enabled=true --set ingress.tls.secretName=<your-secret> |
When ingress.tls.enabled=true, each Ingress gets a tls: block binding the host to the named Secret; that's what per-host certificate selection and cert-manager pickup hook into.
The Keycloak realm import is rendered from a dedicated template using:
keycloak.realmBootstrap.realmIdhub.secrets.systemClientSecret(optional; auto-generated when chart-managed Hub secret is used)hub.secrets.cryptomatorvaultsClientSecret(optional; auto-generated when chart-managed Hub secret is used; required for the Katta token-exchange flow)hub.admin.*(realm-level Hub admin user; separate fromkeycloak.admin.*bootstrap user)
Set minio.enabled=true to deploy a single-replica MinIO StatefulSet with a PVC alongside the Hub.
When minio.enabled=true and either storageProfileSeed.static.enabled=true or storageProfileSeed.sts.enabled=true, a post-install,post-upgrade Helm hook Job:
- Waits for the Hub
/q/health/readyendpoint to return 200. - Obtains an admin access token via Keycloak
client_credentials(using thecryptomatorhub-systemservice account). - POSTs a
S3STATICstorage profile pointing at the bundled MinIO service to the polymorphic/api/storageprofile/endpoint (dispatching on theprotocoldiscriminator). - Skips seeding any profile whose name already exists on the Hub, so re-runs are idempotent. Profile UUIDs are assigned by the server on creation.
MinIO is exposed via ingress only for the hostnames you explicitly configure:
- Set
urls.s3.publicto expose the S3 API. This must be a dedicated host served at the root (e.g.https://s3.example.com), not a subpath. S3 path-style addressing ignores any base path — clients address buckets at the host root (<host>/<bucket>/<key>) — so a subpath in the seeded profile's endpoint URL would be silently dropped and the client's root requests would 404 at the ingress (surfacing as a misleading CORS error). The chart fails fast ifurls.s3.publiccontains a path. This address is baked into the seeded storage profile, so it must resolve for both the Hub pod and external clients. - Set
urls.minio.publicto expose the web console. Unlike the S3 API, the console may be served under a subpath (e.g.https://minio.example.com/minio); the chart applies the same strip-prefix routing as Hub/Keycloak and setsMINIO_BROWSER_REDIRECT_URLso the console emits correctly-prefixed asset/redirect URLs.
Leave either blank and that ingress isn't created — the corresponding service is then reachable only in-cluster (or via kubectl port-forward svc/<release>-service-minio 9001:9001 for the console). The demo serves the S3 API at http://s3.local.katta.cloud:9090 (its own root host) and the console at http://minio.local.katta.cloud:9090.
mc alias set helm http://s3.local.katta.cloud:9090 minioadmin minioadmin
mc idp openid ls helm a05d2a4c
╭──────────────────────────────────────────────────────────────────────────╮
│ On? Name RoleARN │
│ 🔴 (default) │
│ 🟢 cryptomator arn:minio:iam:::role/IqZpDC5ahW_DCAvZPZA4ACjEnDE │
│ 🟢 cryptomatorhub arn:minio:iam:::role/HGKdlY4eFFsXVvJmwlMYMhmbnDE │
│ 🟢 cryptomatorvaults arn:minio:iam:::role/Hdms6XDZ6oOpuWYI3gu4gmgHN94 │
╰──────────────────────────────────────────────────────────────────────────╯
mc admin policy list helm
# ...
mc admin policy info helm katta-accessbucketpolicy a05d2a4c
# {
# "PolicyName": "katta-accessbucketpolicy",
# ...
# }
```
## Telemetry (OpenTelemetry)
Hub exports metrics, traces and logs via OpenTelemetry / OTLP. Telemetry is **off by default**; enable it via:
- `hub.metrics.enabled` (default `false`)
- `hub.metrics.endpoint` — OTLP endpoint, default `https://otel-collector:443`
- `hub.metrics.protocol` — OTLP wire protocol: `http/protobuf` (default) or `grpc`
- `hub.metrics.resourceAttributes` — extra OTel resource attributes merged into the chart defaults (`service.name`, `service.version`). Setting a key with the same name overrides the default.
- `hub.metrics.otlp.username` / `hub.metrics.otlp.password` — Credentials used to add `QUARKUS_OTEL_EXPORTER_OTLP_HEADERS` header `Authorization: Basic <base64(user:pass)>`.
When disabled, the chart sets `QUARKUS_OTEL_SDK_DISABLED=true` so the SDK does not start. When enabled, the chart sets `QUARKUS_OTEL_EXPORTER_OTLP_ENDPOINT` and Hub pushes to your collector — there is no `/q/metrics` scrape endpoint. To bridge to Prometheus, run an OpenTelemetry Collector with a `prometheus` or `prometheusremotewrite` exporter.
## Hub with External PostgreSQL and Keycloak
```bash
helm install katta . \
--namespace katta \
--create-namespace \
--wait --timeout 5m \
--set keycloak.enabled=false \
--set postgres.enabled=false \
--set hub.database.jdbcUrl='jdbc:postgresql://db.example:5432/hub' \
--set hub.database.username='hub' \
--set urls.kc.public='https://sso.example/kc' \
--set urls.kc.clusterInternal='http://keycloak.svc.cluster.local:8080/kc' \
--set urls.kc.authServerUrl='http://keycloak.svc.cluster.local:8080/kc/realms/cryptomator' \
--set urls.kc.tokenIssuer='https://sso.example/kc/realms/cryptomator'
```
### Importing `realm.json`
Even with `keycloak.enabled=false`, the chart still renders `realm.json` in Secret `<release>-secrets-kc` so you can manually export/import it for your existing Keycloak.
Assuming namespace `katta` and name `hub`:
```bash
kubectl get secret -n katta hub-secrets-kc -o jsonpath='{.data.realm\.json}' | base64 -d | ...
```
When pointing Katta Server at an external Keycloak, you must ensure the realm contains:
- A `cryptomatorhub` public OIDC client
- A `cryptomator` public OIDC client (for desktop/mobile)
- A `cryptomatorvaults` confidential client with `standard.token.exchange.enabled=true` (required by the Katta token-exchange flow)
- A `cryptomatorhub-system` service-account client with `realm-admin` and `view-system` roles
## Install from Registry
The chart is published as an OCI artifact at `oci://ghcr.io/shift7-ch/katta-helm/katta-server`. The `values-*.yaml` files are not part of the published chart; pass them from this repository instead:
```bash
helm install katta oci://ghcr.io/shift7-ch/katta-helm/katta-server \
--namespace katta \
--create-namespace \
-f https://raw.githubusercontent.com/shift7-ch/katta-helm/main/values-demo.yaml
```
Without `--version`, Helm installs the latest version. Use a range such as `--version '^1'` to stay on a major version,
or an exact version such as `--version 1.0.0` for reproducible deployments.
## Release
The chart is versioned independently of Katta Server using [Semantic Versioning](https://semver.org/). The
[publish workflow](.github/workflows/publish.yml) publishes every push to `main` as a release. The major and minor
version are set in [`Chart.yaml`](Chart.yaml); the patch version is the number of commits since the `version` line in
`Chart.yaml` last changed. Setting `version: 1.1.0` publishes `1.1.0`, and the following commits `1.1.1`, `1.1.2` and so on.
- Increment the minor version for new features, such as new values.
- Increment the major version for breaking changes, such as renamed or removed values.
## Verify Published Chart (Signature + Provenance)
This chart contains an OCI chart signature, which can be verified as follows (assuming chart version `1.0.0`):
```bash
cosign verify \
--certificate-identity-regexp 'https://github.com/shift7-ch/katta-helm/.github/workflows/publish.yml@refs/heads/main' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/shift7-ch/katta-helm/katta-server:1.0.0
```
You can additionally inspect provenance attestations:
```bash
cosign verify-attestation \
--type https://slsa.dev/provenance/v1 \
--certificate-identity-regexp 'https://github.com/shift7-ch/katta-helm/.github/workflows/publish.yml@refs/heads/main' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/shift7-ch/katta-helm/katta-server:1.0.0
```
Charts published before the move to this repository at `ghcr.io/shift7-ch/charts/katta-server` are signed by the
`https://github.com/shift7-ch/katta-server/.github/workflows/helm-chart.yml` workflow identity instead.