Alpha / experimental — unofficial, community-maintained. This is not an official Camunda product, has not been through Camunda's product or security review process, and carries no support guarantee. It is built and validated by one engineer. Read the Disclaimer before pointing it at anything you care about.
A pre-flight, live, upgrade, and bootstrap checker for Camunda 8 Self-Managed Helm installs. New here? See Getting started for the whole toolkit walked through in the order you'd actually use it. Three core commands, plus several more that turn a finding into a fix instead of just a report:
initbuilds a starter values.yaml from a handful of questions, and — this is the actual point of it — refuses to hand you the result unless it already passes everycheckrule this tool can close via values.yaml alone. Most values.yaml generators stop at "does this parse"; this one stops at "does this pass the audit."checkstatically scans a chart + values overlay (or an already-installed Helm release, optionally cross-checked against the live cluster) for recurring Kubernetes misconfigurations that are individually easy to make, hard to notice, and expensive to hit in production: burstable memory QoS on a stateful broker, a disabled PodDisruptionBudget on a quorum-based cluster, a secret reference that silently no-ops, default credentials left in place, disabled log/index retention, and more.upgradeanswers a different question: what will break if I move this release to a newer chart line? It reads the values you actually supplied, reports every values.yaml key the target line removed, renamed or deprecated, rewrites what it can rewrite, and prints a runbook for the imperative steps no values file can express — most notably the Keycloak/PostgreSQL data migration that 8.10 requires. It never writes to your cluster.
See Checks for the full list and the failure mode each one guards against.
Passing every check is not proof a deployment is production-ready or that an upgrade will succeed — it only means this specific, known set of footguns isn't present.
Download a binary from the Releases page for your platform, or build from source:
go install github.com/hamza-m-masood/camunda-helm-toolkit/cmd/camunda-helm-toolkit@latestRequires helm on PATH for chart-based checks and for upgrade --release, and
kubectl or oc on PATH for --live checks. The migration data is compiled into the
binary, so upgrade needs no chart checkout.
Build a starter values.yaml, answering questions on the command line:
camunda-helm-toolkit init --chart ./camunda-platformOr fully non-interactively, for CI or scripting — every question has a flag, and
--non-interactive fails on any missing required one instead of prompting:
camunda-helm-toolkit init --chart ./camunda-platform --non-interactive \
--release-name camunda --secondary-storage elasticsearch \
--auth-method basic --admin-username admin --admin-password '<a real password>' \
--enable connectors \
--throughput 800 --avg-payload-kb 3 \
--write-values my-values.yamlBefore printing or writing anything, init runs the exact same rules check would run
against the result — both the values-based rules and, since --chart is required, the
manifest-based ones too, against a real helm template render — and refuses to produce
output at all if anything comes back that it can't explain. Four findings are
structurally impossible for it to close and are the only ones ever expected: digest
pinning (CCD008 — the right digest is specific to the exact release you install), a
chart-template limitation with no values.yaml field at all (CCD015), one pre-existing
8.9 chart-default defect unrelated to anything init controls (CCD003 on
identityKeycloak.*), and the admin credential this tool sets landing verbatim in a
ConfigMap rather than a Secret regardless of its value (CCD010 on
<release>-zeebe-configuration and, if Connectors is enabled,
<release>-connectors-configuration too — the chart's own template, not a values.yaml
choice). Anything else that fires is a bug in init, not something it will hand you —
see How this was built for how that guarantee is tested.
--enable is repeatable (identity, connectors, optimize, webModeler); enabling
webModeler also requires --web-modeler-from-email (the chart's own hard render
requirement, not this tool's). Sizing (--throughput/--avg-payload-kb) calls the same
heuristic size uses — see that command's own section for what the numbers mean.
Pre-install — check a chart plus your values overlay(s), the same way you'd pass
them to helm install/helm upgrade:
camunda-helm-toolkit check --chart ./camunda-platform -f my-values.yamlPost-install — check an installed release using Helm's own recorded effective values:
camunda-helm-toolkit check --release my-camunda -n my-namespacePost-install + live drift check — additionally query the cluster directly for things values.yaml can't show (was the PodDisruptionBudget actually created? does a referenced Secret still exist with the expected key?):
camunda-helm-toolkit check --release my-camunda -n my-namespace --livePlan an upgrade — point it at an installed release and say where you want to go. It detects which chart line you are on from the release itself:
camunda-helm-toolkit upgrade --release my-camunda -n my-namespace --to 8.10Save the rewritten values alongside the report, and optionally drop the keys the target line removed:
camunda-helm-toolkit upgrade --release my-camunda -n my-namespace \
--to 8.10 --write-values migrated-values.yaml --strip-removedRenames are always applied (the replacement key is known). Deleting a setting is a
judgement call about intent, so --strip-removed is opt-in.
Plan without a cluster — from a values file you already have. A values file does not
record its own chart version, so --from is required:
camunda-helm-toolkit upgrade -f my-values.yaml --from 8.9 --to 8.10Add --json for machine-readable output, --fail-on critical|high|medium|low to
control which severities cause a nonzero exit code (default high) — useful for gating
a CI pipeline without failing the build on advisory-level findings.
| ID | Severity* | What it catches |
|---|---|---|
| CCD001 | High | orchestration.podDisruptionBudget.enabled left at its default false — nothing stops a node drain from evicting a quorum of brokers at once. |
| CCD002 | Critical (orchestration) / Medium (elsewhere) | resources.requests.memory set below resources.limits.memory anywhere in the chart — Burstable QoS, evicted ahead of Guaranteed pods under memory pressure, and an unwarned OOMKill on any spike past the limit. |
| CCD003 | High | An existingSecret reference configured without its matching existingSecretKey — the chart silently drops the reference rather than failing, and the component falls back to an empty-string credential. |
| CCD004 | Medium | The chart's shipped example users (demo/demo, connectors/connector) still present in the initial user list. |
| CCD005 | Medium | Index/log retention (ILM) disabled — exported/archived indices grow unbounded toward the secondary storage disk. |
| CCD006 | Low | orchestration.readinessProbe.timeoutSeconds at 1 — inside normal JVM GC-pause range; a cluster-wide readiness flap can empty the gRPC Service's endpoint list. |
| CCD007 | High | replicationFactor greater than clusterSize — every broker crash-loops at startup on a config-validation error, while Helm reports success. |
| CCD008 | Low | Any image pinned by tag with no digest — the tag can silently move to a different image over time. |
| CCD009 | High | A rendered manifest referencing a bitnamilegacy/* image — Bitnami's archived, no-longer-patched registry. |
| CCD010 | High | A rendered ConfigMap embedding what looks like a literal (non-templated) password or client secret. |
| CCD011 | High / Medium | --live only: no PodDisruptionBudget object actually exists for the orchestration component, or an existing one currently allows zero disruptions. |
| CCD012 | High | --live only: a referenced Secret or key declared in values.yaml doesn't actually exist in the cluster (renamed, deleted, or never created). |
| CCD013 | Medium | prometheusServiceMonitor.enabled left at its default false — zero scrape targets, so nothing else here is observable either. |
| CCD014 | Medium | A bundled identityPostgresql/webModelerPostgresql subchart is enabled with its own backup.enabled left off (a logical-dump-only mechanism, not PITR, but better than nothing). |
| CCD015 | Low | Informational, not a misconfiguration you made: the chart's own rendered StatefulSet has no terminationGracePeriodSeconds and/or writes JVM heap dumps onto the same volume as the Raft/RocksDB data. Framed around the orchestration.env/javaOpts workaround, since there's no values.yaml field for the grace period itself. |
| CCD016 | Low | Default broker anti-affinity still matches only app.kubernetes.io/component, with no app.kubernetes.io/instance — a second release in the same namespace can't co-schedule, and a drain has nowhere else to place an evicted broker on a minimal node pool. |
* Severity reflects likely blast radius and how easy the misconfiguration is to make — not a guarantee about your specific environment.
| ID | Severity | What it catches |
|---|---|---|
| CCD101 | High | A values key the target line removed. The chart calls fail(), so helm upgrade renders nothing until the key is gone. |
| CCD102 | High | A values key the target line renamed. Also a hard render failure, but mechanically fixable — the rewrite is applied for you, including subtree renames like camundaHub.webModeler.* → camundaHub.*. |
| CCD103 | Low | A deprecated key. The upgrade still succeeds; the chart keeps warning, and names the release it is scheduled for removal in. |
| CCD104 | Critical | Bundled Bitnami subcharts (Keycloak / PostgreSQL / Elasticsearch) are enabled and the target line removes them. Your Keycloak realm and the Identity and Web Modeler databases live inside those workloads — upgrading before moving that data deletes what holds it. |
| CCD105 | High | The requested jump spans more than one minor. Camunda does not support skipping minors, so this is N separate upgrades, each of which must land healthy before the next. |
| CCD106 | Medium | A component's env / extraConfiguration / command / extraVolumeMounts is set. The chart cannot validate these, so they are invisible to every other check — and an override that works around a limitation the target version fixes internally can conflict after upgrading. |
| CCD107 | Medium | The target chart requires a newer Helm CLI major than the constraint allows (chart 15.x requires Helm v4). Fails at render time, before anything is applied. |
| CCD108 | Low | The line you are leaving is end-of-life. |
| CCD109 | Medium | No migration data is embedded for one of the hops, so findings are incomplete for it. See Upgrade coverage. |
The key data is generated from the chart's own deprecation helpers
(camundaPlatform.keyRemoved, keyRenamed, keyDeprecated in
templates/**/constraints.tpl) rather than maintained by hand, so this tool cannot
disagree with the chart about what changed, and every finding cites the chart file and
line it came from. Regenerate with camunda-helm-toolkit generate --chart-repo <path>.
Those helpers were introduced in the 8.8 chart. Hops into 8.8, 8.9 and 8.10 are covered; hops into 8.7 and earlier are not — those charts express deprecations as ad-hoc warning strings with no machine-readable structure. Planning an upgrade that passes through an uncovered hop raises CCD109 rather than implying the hop is clean.
Where the chart's condition for a key is too complex to model exactly (a compound
and/or), the finding is reported on key presence alone and the report says how many
findings are approximate. It errs toward showing you a key you may not need to change,
rather than hiding one you do.
It does not upgrade anything. It reads helm get values and helm get metadata, and
everything it suggests is printed for you to run, labelled safe, destructive, or
downtime. A clean render is also not a safe upgrade: CCD104 in particular describes a
data migration that has to happen between two Helm operations, and no amount of
values rewriting substitutes for it.
A .chartdoctor-ignore.yaml in the current directory (or passed via --ignore-file) is
auto-loaded by check:
suppress:
- ruleId: CCD002
path: identity.resources # optional; exact match or dot-prefix match
reason: "identity is intentionally run Burstable in this cluster — accepted 2026-08."reason is required — a suppression file stays self-documenting. Suppressed findings are
never silently dropped: the summary line always shows a count, and --show-suppressed
lists them in full.
check --rules-from <path-or-url> (repeatable, local path or http(s)://) loads
operator-defined rules from a small declarative format, evaluated against the exact
same effective values the 16 built-in rules see:
rules:
- id: CUSTOM001 # required; must not start with "CCD" — that prefix
# is reserved for rules this project has verified
# against the real chart, which yours has not
severity: high # critical|high|medium|low
title: "orchestration.podDisruptionBudget is disabled (org policy)"
path: orchestration.podDisruptionBudget.enabled # same dot/"[N]" syntax as everywhere else
condition: equals # exists | absent | equals | notEquals | matches (regex)
value: "false" # required for equals/notEquals/matches
detail: "..."
remediation: "..."Findings from a custom rule flow through exactly the same pipeline as a built-in one —
text, JSON, SARIF, suppression file, --fail-on — nothing about them is second-class.
equals/notEquals/matches all require the path to actually resolve; "the path was
never set" is its own condition (absent), not an implicit case of notEquals also
happening to match, so "flag if absent OR wrong value" is two rules, not one surprising
one.
This is the lightweight tier: no policy language to learn, but limited to "does a value exist / equal / match a pattern". For anything that needs real logic — see below.
check --policy-dir <dir> (requires --chart) evaluates a directory of
Conftest/Rego policies against the chart's rendered
manifests — the same helm template output the built-in manifest-based rules already
consume. This is deliberately a separate, more expressive tier from custom rules above,
not a second way to do the same thing: custom rules cover "does a value look right";
this tier is for cross-field logic and anything else Rego can express that a
path/condition/value check can't reach.
package main
deny contains msg if {
input.kind == "StatefulSet"
# ... your own condition ...
msg := "why this failed"
}
warn contains msg if {
# ...
}deny findings map to High, warn to Medium, through the same pipeline as
everything else. Requires conftest on PATH — built and tested against Conftest's
current OPA runtime, which needs the if/contains keywords above (older Rego syntax
without them will fail to load). If conftest isn't installed, or the chart doesn't
render, --policy-dir fails loudly rather than silently skipping your policies — a
--policy-dir that quietly evaluates nothing is exactly the kind of "looks configured,
isn't" gap this whole tool exists to catch elsewhere, not repeat here.
--json for machine-readable output, or --format sarif for GitHub code-scanning / PR
annotations instead of plain CI log text. --fail-on critical|high|medium|low controls
which severities cause a nonzero exit code (default high).
- uses: hamza-m-masood/camunda-helm-toolkit@v0.5.0-alpha
with:
chart: ./camunda-platform
values: |
values.yaml
values-prod.yaml
fail-on: highBuilds the tool from source at the pinned ref (not a downloaded release asset, so it
always matches exactly what that ref's source produces) and runs check with inputs
mirroring the CLI flags (release, namespace, live, format, ignore-file).
Outputs findings-file (path to the raw output) and exit-code.
Beyond check, upgrade, and generate (see above), a few more:
plan-secrets --release <name> -n <namespace> — finds chart-managed Secrets not yet
pinned via existingSecret and prints the exact fix: a command to copy the Secret's
current value to a new, independently-named object the chart will never own or prune
(pointing existingSecret at the same name it already uses does not work — the
chart stops rendering that Secret once every field is pinned, and Helm then deletes it),
plus the values overlay to apply.
bundle --release <name> -n <namespace> -o <path.tar.gz> (--dry-run to preview) —
a support-ticket-ready archive: live findings, a redacted helm get values -a
(every password/secret/token/credential/*Key value replaced with <redacted>),
describe/events/logs for the orchestration component, and version info — all listed in
a manifest.json so you can see exactly what you're about to send.
scaffold-monitoring --release <name> -n <namespace> --chart <path> — generates
ServiceMonitor manifests using port names taken from the chart's own rendered Service
objects (never hardcoded), plus a baseline PrometheusRule. Output only; review and
kubectl apply yourself.
size --throughput <cmds/sec> --avg-payload-kb <n> [--retention-days <n>] — a
heuristic (not a certified benchmark) starting point for clusterSize/partitionCount/
pvcSize/resources, with the arithmetic behind every number printed alongside it.
scaffold-watcher --release <name> -n <namespace> --schedule "<cron>" — generates a
CronJob + least-privilege RBAC that reruns check --live on a schedule and reports only
new findings (via a state ConfigMap), optionally to --webhook-url. Needs the image
built from this repo's Dockerfile (published to ghcr.io/hamza-m-masood/camunda-helm-toolkit
on tag push).
diff --release-a <n1> --namespace-a <ns1> --release-b <n2> --namespace-b <ns2> —
a structural diff between two installed releases' effective values (staging vs prod,
before vs after a manual change someone forgot to write down). Diffs the parsed trees,
not the raw YAML text, so two releases that differ only in key order or formatting
report zero differences — a text diff of the same two helm get values dumps would not.
Arrays are compared as opaque values (Helm replaces them wholesale, never merges
element-by-element, so that's the meaningful diff unit). Both releases must be in the
cluster this tool is already pointed at; --format json for machine consumption. Exit
code follows diff(1): 0 identical, 1 differences found, 2 on error.
0 clean (at or below the --fail-on threshold) · 1 worst finding is medium/low ·
2 worst finding is high · 3 worst finding is critical. Every finding-producing
command uses this same severity scale — diff is the one exception, since it has no
severity concept: it follows the plain diff(1) convention instead (0 identical, 1
differences found, 2 on error).
Every check rule traces back to a documented Kubernetes failure mode, verified against
the actual Camunda Platform Helm chart templates and by rendering/deploying the chart —
not inferred from documentation. The upgrade command's key data is generated from those
same chart templates, and its runbook steps are transcribed from the chart repo's own CI
upgrade hooks — so each one is a step the chart's test matrix actually runs, and carries
a source: pointing at it. Verified end to end against a real 8.9 install on OpenShift:
the unmigrated values fail the 8.10 render on exactly the keys reported, and the migrated
values render clean. None of it encodes anything specific to any customer,
deployment, or incident; the checks are general Kubernetes/Helm patterns (Burstable QoS,
PodDisruptionBudget coverage, secret-reference validation) applied to this chart's
specific value paths.
This tool is provided as-is, with no warranty of any kind, and no commitment to keep it updated as the underlying Helm chart evolves. It is not built, reviewed, or endorsed by Camunda. A clean run does not mean your deployment is secure, highly available, or ready for production traffic — it means the specific checks above didn't fire. Always validate changes in a non-production environment first, and treat this as one input among several (chart documentation, your own architecture review, load testing) rather than a substitute for any of them.
Apache 2.0 — see LICENSE.