Skip to main content

Overview

The CrewAI Platform chart can source every runtime secret from an external secret store via the External Secrets Operator. When enabled fully, your backend (AWS Secrets Manager, Vault, etc.) becomes the canonical source of truth — the chart neither generates random values nor reads any secret material from values.yaml. This page covers:
This page applies to direct Helm and ArgoCD deployments. KOTS / Replicated deployments auto-populate the secrets stored in crewai-secrets from license fields and do not need ESO. If you’re on KOTS and want to layer ESO on top, the same flags apply but you’ll need to coordinate with Replicated’s secret hydration.

How the chart consumes secrets

The chart supports three modes, controlled by two values: externalSecret.enabled and externalSecret.manageAutoGenerated. Mode A is the chart’s default. Mode B existed before the manageAutoGenerated flag was introduced and is not a viable production state — it’s the historical default that ships an incomplete Secret. Mode C is what you want for any deployment that treats the external store as the source of truth. For the rest of this page, “ESO” means Mode C unless explicitly noted.

Fresh install with ESO

If you are deploying CrewAI Platform for the first time and want ESO from day one, follow these steps in order.

Prerequisites

  • External Secrets Operator installed and healthy in the cluster (typically in the external-secrets namespace).
  • A SecretStore resource that can authenticate against your backend. The chart can render one via secretStore.enabled: true (see Configuration Reference), or you can bring your own.
  • An empty path reserved in your backend for CrewAI secrets (e.g. AWS SM secret crewai/platform/prod). For OAuth, optionally a second path (e.g. crewai/platform/prod/oauth).

1. Generate the auto-gen secret values

The chart generates these values for you on a non-ESO install via lookup + randAlphaNum. Even for a fresh ESO install, the easiest way to get reasonable defaults is to render them once locally:
If you prefer to generate independently of the chart, use these commands. The | tr -dc 'A-Za-z0-9' | head -c N filter mirrors the chart’s randAlphaNum output exactly — pure alphanumeric, no +/= from base64 or special characters that would otherwise drift from the values a non-ESO chart install produces.

2. Upload to your backend

Store the values as a JSON document at externalSecret.secretPath. Property names must match the lowercased Secret key (SECRET_KEY_BASEsecret_key_base) unless you override via autoGeneratedPropertyMap.
See Backend key reference below for the complete list.
OIDC_PRIVATE_KEY is a multi-line PEM. Use jq --rawfile (as shown) to preserve newlines through JSON round-tripping. Hand-edited JSON or --secret-string "$(cat file.pem)" easily mangles the line breaks.

3. Configure values

Do not populate the secrets: block. With externalSecret.enabled=true the chart skips crewai-secrets.yaml entirely; values there are silently ignored.

4. Install

The chart’s pre-upgrade flow (which also runs on first install) sequences ESO sync before pod startup. You’ll see:
If any backend property is missing, the wait Job times out with an explicit message naming the missing key. Add it to your backend, re-run helm install.

Migration paths

If you have an existing CrewAI deployment and are moving onto ESO, follow the path that matches your current state.

Path 1 — From inline secrets (Mode A) to full ESO (Mode C)

You’re currently running with externalSecret.enabled: false. The chart generates the auto-gen secrets and stores them in the in-cluster Secret <release>-secrets. You want to move to ESO. This is the safest migration path. The existing Secret is annotated with helm.sh/resource-policy: keep — it survives the cutover, and ESO adopts it on first reconcile. Step 1 — Extract the current values. They’re what your pods are currently using; reusing them avoids rotating session-encryption keys (which would invalidate all existing user sessions) or Active Record encryption keys (which would render encrypted database columns unreadable).
If oauth.enabled: true, also extract <release>-oauth-secrets. Step 2 — Upload to your backend. Map each Secret key to its lowercased property name (or whatever your autoGeneratedPropertyMap overrides specify). For OIDC_PRIVATE_KEY, use jq --rawfile to preserve newlines.
Step 3 — Update values and upgrade.
Delete the secrets: block from your values file — it has no effect with ESO enabled, and keeping it gives future readers a false sense that those values are wired in.
The inline Secret persists (via keep). The new ExternalSecret is created during the pre-upgrade hook, ESO adopts the existing Secret (sets its OwnerReference), the wait Job verifies completeness, and migrations run. Pods do not need to restart unless any backend value differs from the current in-cluster value.

Path 2 — From partial ESO (Mode B) to full ESO (Mode C)

You’re currently running with externalSecret.enabled: true and manageAutoGenerated unset (or false). Your <release>-secrets materialized Secret is partial — it has the keys the old ExternalSecret requested (DB, GitHub, SSL, AWS/Azure) but is missing the auto-gen keys. Pods are likely already failing on missing env vars, or surviving only because the Secret was created by an even older non-ESO install and the old keys have been frozen in place via helm.sh/resource-policy: keep. This path requires a one-time orphan-delete because Helm refuses to convert a regular resource into a hook resource in place. The new chart annotates the ExternalSecret as a pre-install,pre-upgrade hook; on first sync, Helm sees a same-name non-hook resource already exists and aborts with:
This step is only required on the first upgrade from a chart version that didn’t use hooks for the ExternalSecret. Subsequent upgrades are fully idempotent — Helm handles the hook lifecycle automatically via before-hook-creation,hook-failed.
Step 1 — Confirm AWS SM has every key the new ExternalSecret will request. This is where most migrations stall. Take inventory of what’s in your backend vs. what the chart’s full mode needs:
For any MISS, extract the value from the live cluster Secret (still alive thanks to keep):
Upload it to the backend. Step 2 — Orphan-delete the existing ExternalSecret(s). This removes the CR but preserves the materialized Secret (which is what your running pods depend on). --cascade=orphan strips the OwnerReference from the Secret instead of garbage-collecting it.
Running pods continue to function — they cached their env vars at startup and don’t re-read the Secret. The Secret resource itself stays in the cluster with no owner; ESO will adopt it on first reconcile after the upgrade. Step 3 — Update values:
Step 4 — Upgrade.
Helm now applies the hook-annotated ExternalSecret cleanly during the pre-upgrade phase, the wait Job confirms the Secret has every key, and migrations run.

Path 3 — From values-file pre-set secrets to ESO

You may have been on a hybrid where you pre-set every secret in your secrets: block in values.yaml (the Pre-Set All Secrets pattern). Migrating to ESO is the same as Path 1 — extract the values from the cluster Secret (or directly from your values file), upload to the backend, then flip the flags. Delete the secrets: block once values are confirmed in the backend.

Backend key reference

Every property the chart will request from your secret store, with defaults and conditions.

Main secret (at externalSecret.secretPath)

Pulled when manageAutoGenerated: true. Property names below are the defaults; override per-key via autoGeneratedPropertyMap.

Synthesized / auto-linked (do NOT put in the backend)

These appear in the materialized Secret but you do not populate them yourself:
  • WHARF_POSTGRES_URL — when wharf.enabled: true. The chart synthesizes this at materialization time as postgres://<DB_USER>:<DB_PASSWORD>@<host>:<port>/<wharf_db_name>. DB_PASSWORD is pulled from your backend; everything else comes from chart values. Rotating DB_PASSWORD in your backend propagates here automatically on the next ESO refresh — no second key to keep in sync.
  • CREWAI_OAUTH_API_KEY — when oauth.enabled: true. Sourced from the same backend property as OAUTH_INTERNAL_API_KEY (default oauth_internal_api_key at oauthSecretPath). The chart guarantees the two values are byte-identical, eliminating a silent OAuth-to-Rails 401 failure mode.
The chart’s values.schema.json rejects attempts to override autoGeneratedPropertyMap.CREWAI_OAUTH_API_KEY or autoGeneratedPropertyMap.WHARF_POSTGRES_URL to enforce this invariant.

OAuth secret (at externalSecret.oauthSecretPath)

Rendered as a second ExternalSecret targeting <release>-oauth-secrets when both oauth.enabled and manageAutoGenerated are true. Falls back to secretPath if oauthSecretPath is empty. Provider client IDs/secrets (GOOGLE_*, MICROSOFT_*, HUBSPOT_*, NOTION_*, SALESFORCE_*) are not requested by default. Add them to oauthPropertyMap if you use those OAuth providers for end-user integrations:
UV_DEFAULT_INDEX (the private PyPI registry URL) deserves a special callout. In KOTS deployments it auto-derives from the Replicated license; in direct-Helm/ArgoCD deployments it must be set explicitly. With ESO enabled, only the autoGeneratedPropertyMap route works — values placed under secrets.UV_DEFAULT_INDEX are ignored because the inline Secret template does not render. Put the URL in your backend at externalSecret.secretPath (property uv_default_index) and list it in the map.

The pre-upgrade wait Job

When manageAutoGenerated: true and waitForSync: true (the default), the chart renders a pre-upgrade Job (<release>-wait-for-eso-<revision>) that blocks Helm until ESO has populated the materialized Secret with every required key. This sequences ESO synchronization ahead of every pre-upgrade hook that depends on the auto-generated keys — Rails db:migrate, wharf migrate, oauth migrate. Without it, those migrations would race against the ExternalSecret being created during the same phase, hitting couldn't find key X in Secret. Helm hook weight ordering when manageAutoGenerated: true:
The wait Job uses BusyBox + raw HTTPS calls to the in-cluster Kubernetes API. Default timeout: 3 minutes. Successful syncs converge in under 30 seconds; the tight timeout surfaces broken syncs quickly without making operators wait.

Troubleshooting

Wait Job times out

Followed by a dump of the live ExternalSecret YAML. The <KEY_LIST> is exactly the set of property names missing from your backend. Add them and re-run helm upgrade. If the dump shows ESO errors (e.g. secretsmanager:GetSecretValue Forbidden), the issue is in your SecretStore configuration — IAM role trust policy, IRSA annotation on the ServiceAccount, or the role’s permission boundary. Fix at the platform level; ESO reconciles automatically.

Wait Job hard-fails on TLS / connectivity

Your busybox.image.tag was pulled as a minimal variant that ships without OpenSSL. Pin to a known-good version:

Wait Job itself is broken (unrecoverable bug)

If the wait Job is producing unexpected failures and you need to unblock an upgrade immediately, two escape valves:
Set in your values file:
The wait Job stops rendering. Migrations still run as pre-upgrade hooks; they may CrashLoop briefly until ESO catches up. After the upgrade settles, kubectl rollout restart deploy -n <ns> to refresh pod env caches.
Disabling the wait Job reverts to the pre-hook behavior where there is no ordering guarantee between ESO synchronization and pre-upgrade migrations. Use only as a temporary unblock while the wait Job is being patched.

”ExternalSecret already exists” on first upgrade to a chart with hook-based ESO

You’re upgrading from a chart version where the ExternalSecret was a regular manifest to one where it’s a hook. This is the one-time migration documented in Path 2. Orphan-delete the existing ExternalSecret(s), then re-run the upgrade. Running pods are unaffected because the materialized Secret is preserved by --cascade=orphan.

Materialized Secret has only WHARF_POSTGRES_URL, everything else is missing

This was a real bug fixed in chart version that introduced mergePolicy: Merge on the ExternalSecret template. If you’re seeing it on a current version, your custom ExternalSecret manifest may have been hand-modified — check that spec.target.template.mergePolicy is Merge, not Replace (the default).

OIDC_PRIVATE_KEY mangled in the Secret

The most common cause is uploading the PEM to your backend with \n literals instead of real newlines. Round-trip verify:
If openssl rejects the key, re-upload using jq --rawfile (which preserves newlines through JSON encoding) instead of any approach that involves shell string interpolation.

Secret has the key but it’s empty (0 bytes)

ESO fetched an empty string from your backend. Either the property exists with an empty value, or your backend treats absent properties as empty strings. Pods consuming such a value will fail when the value is actually needed. Populate the property — even if your deployment doesn’t use the feature, an empty string is safer than failing at runtime.