> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qodo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage Qodo secrets with External Secrets Operator

> Connect your secret manager to Qodo's Kubernetes Secrets, then verify synchronization, ownership, rotation, and recovery.

* **Deployment model**: On-premises
* **Configuration reference**: Qodo Helm chart `7.21.0`, bundled module `0.20.0-qodo.1`, and the `external-secrets.io/v1` API.
* **Live validation**: ESO `0.16.2` on GKE, including Google Cloud Secret Manager synchronization and source-version refresh.

Use your existing External Secrets Operator (ESO) to copy credentials from
your secret manager into the Kubernetes Secrets Qodo already consumes. Qodo
continues to use Secret references; application Pods do not need permission
to read the cloud secret manager for this integration.

Your cluster team owns ESO, cloud identity, permissions, and the source secrets.
The Qodo chart does not install ESO or configure your cloud account. The examples
use separate, customer-managed `ExternalSecret` resources so the target names
match Qodo's fixed Secret references.

## Choose the integration path

| Your existing integration | How it connects to Qodo |
| - | - |
| External Secrets Operator | Create a `SecretStore` or reuse your approved `ClusterSecretStore`, then reconcile the target Kubernetes Secrets before deploying Qodo |
| AWS Secrets and Configuration Provider with Secrets Store CSI Driver | Qodo's Secret references still require Kubernetes Secrets. CSI synchronization needs a Pod mounting the secret, and the synced Secret can disappear when all mounting Pods are removed. Plan bootstrap and lifecycle behavior before choosing this path |
| Another secrets operator | Confirm its exact product and version, whether it produces Kubernetes Secrets, and who owns their names, keys, retention, and rotation |

The AWS CSI provider and ESO are different integrations. This page's manifests
apply to ESO. For more information, see [Sync as Kubernetes Secret](https://secrets-store-csi-driver.sigs.k8s.io/topics/sync-as-kubernetes-secret).

## Before you begin

Have your platform team install an ESO release compatible with your
Kubernetes version and the API used below. A standard Helm installation
includes the core controller, admission webhook, certificate controller, and
CRDs. If you supply webhook certificates through another approved mechanism,
verify that certificate management and webhook TLS are ready. The Qodo chart
does not install ESO or its CRDs. For more information, see [ESO getting started](https://external-secrets.io/latest/introduction/getting-started/).

Prepare:

* A ready ESO core controller and webhook, with the `ExternalSecret` and `SecretStore` CRDs serving the API used below. A shared store also requires the `ClusterSecretStore` CRD.
* The target namespace, Kubernetes context, pinned Qodo chart, and existing customer values.
* Your provider, source secret identifiers, and an approved cloud identity.
* Access from ESO to the provider and identity endpoints required by your authentication method.
* ESO permission to reconcile Kubernetes Secrets in the target namespace, plus permission to request service account tokens if the selected authentication method uses a referenced service account.

Ensure ESO watches your Qodo namespace. Its identity needs permission to read
the store and ExternalSecret resources and create/update their target Secrets.
Your deployment identity needs permission to create the namespaced
ExternalSecrets and any new SecretStore. Installing cluster-wide CRDs or a
ClusterSecretStore requires the appropriate cluster administration permissions.

These examples use namespace `qodo`. Replace it consistently if your
installation uses another namespace. Create that namespace through your
normal cluster management process before continuing.

```bash theme={null}
KUBE_CONTEXT="REPLACE_WITH_YOUR_CONTEXT"
NAMESPACE=qodo
ESO_NAMESPACE=external-secrets

kubectl --context "$KUBE_CONTEXT" wait --for=condition=Established \
  crd/externalsecrets.external-secrets.io \
  crd/secretstores.external-secrets.io --timeout=120s
kubectl --context "$KUBE_CONTEXT" -n "$ESO_NAMESPACE" \
  rollout status deployment/external-secrets --timeout=120s
kubectl --context "$KUBE_CONTEXT" -n "$ESO_NAMESPACE" \
  rollout status deployment/external-secrets-webhook --timeout=120s
kubectl --context "$KUBE_CONTEXT" explain externalsecret \
  --api-version=external-secrets.io/v1
kubectl --context "$KUBE_CONTEXT" auth can-i create externalsecrets.external-secrets.io \
  -n "$NAMESPACE"
```

The commands use the standard Helm Deployment names and namespace
`external-secrets`. Replace them with your existing installation's names.
If using the chart's certificate controller, check it too:

```bash theme={null}
kubectl --context "$KUBE_CONTEXT" -n "$ESO_NAMESPACE" \
  rollout status deployment/external-secrets-cert-controller --timeout=120s
```

If creating a new namespaced store, also check your permission to create it:

```bash theme={null}
kubectl --context "$KUBE_CONTEXT" auth can-i create secretstores.external-secrets.io \
  -n "$NAMESPACE"
```

If reusing a shared store, check that its CRD is established:

```bash theme={null}
kubectl --context "$KUBE_CONTEXT" wait --for=condition=Established \
  crd/clustersecretstores.external-secrets.io --timeout=120s
```

Run the checks relevant to your chosen store type. Resolve failed controller,
webhook, certificate, CRD, or permission checks before creating provider
resources. After configuring your provider, require the store and each
ExternalSecret to report `Ready` before Qodo starts.

Match the manifests to your installed CRDs. The module chart's optional
`externalSecrets` template defaults to the older API unless
`externalSecrets.v1StableApi` is enabled, and prefixes its target names with
the component fullname. That template is not a replacement for checking the
fixed target names below.

## 1. Connect your secret manager

Choose your provider's setup page. Each creates a namespaced `SecretStore`
named `qodo-secrets` and grants its identity access to your source secrets.
Use one provider configuration for that store name in a namespace.

> **[AWS Secrets Manager](https://docs.qodo.ai/on-prem/external-secrets/aws.md)**
>
> Configure IRSA or EKS Pod Identity.

> **[Google Cloud Secret Manager](https://docs.qodo.ai/on-prem/external-secrets/google-cloud.md)**
>
> Configure GKE Workload Identity.

> **[Azure Key Vault](https://docs.qodo.ai/on-prem/external-secrets/azure.md)**
>
> Configure Microsoft Entra Workload ID.

You can instead reuse an approved `ClusterSecretStore`. In that case, set
`secretStoreRef.kind: ClusterSecretStore` and its approved name in every
`ExternalSecret` below. A namespaced `SecretStore` must share the
`ExternalSecret`'s namespace. Source access is determined by the store's
identity, so restrict both cloud permissions and who can reference the store.

## 2. Map the customer-managed credentials

Start with the Secrets your deployment already expects you to create:

| Kubernetes Secret | Required key and use |
| - | - |
| `qodo-db-admin` | `POSTGRES_PASSWORD`, for the external PostgreSQL credential |
| `qodo-pull` | `.dockerconfigjson`, with type `kubernetes.io/dockerconfigjson`, for direct-registry image pulls |
| `qodo-entra-idp` | `IDP_CLIENT_SECRET`, when the configured identity provider uses this Secret |
| `qodo-openai` | `OPENAI_API_KEY`, when required by the bundled gateway's selected provider configuration |
| `qodo-bedrock` | `BEARER_TOKEN_BEDROCK`, only when your gateway values reference this Bedrock credential |
| `qodo-embeddings` | `EMBEDDINGS_API_KEY`, only when your gateway values reference this separate embeddings credential |

Use the exact names and keys from your release and provider profile. Creating
a Bedrock Secret alone does not configure model routing. Replicated-delivered
image credentials also have a different ownership path; do not replace them
with a new `qodo-pull` Secret without following your delivery instructions.

For an external PostgreSQL deployment, use a source Secret named
`qodo-database` whose JSON value contains property `POSTGRES_PASSWORD`. Follow
your provider page to create it using the existing database password. Keep
credential values in your approved secret
management process, not in Helm values, Git, or terminal command arguments.

Save this as `qodo-database-external-secret.yaml`:

```yaml qodo-database-external-secret.yaml theme={null}
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: qodo-db-admin
  namespace: qodo
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: SecretStore
    name: qodo-secrets
  target:
    name: qodo-db-admin
    creationPolicy: Orphan
    deletionPolicy: Retain
  data:
    - secretKey: POSTGRES_PASSWORD
      remoteRef:
        key: qodo-database
        property: POSTGRES_PASSWORD
```

`Orphan` keeps the target Secret independent of the `ExternalSecret`'s
garbage-collection lifecycle. `Retain` keeps it if the provider secret is
removed. These settings do not prevent ESO from overwriting data during
reconciliation, and they do not protect against explicit deletion or GitOps
pruning. Before adopting an existing Secret, preserve its current bytes and
make sure no other controller writes it. For more information, see [ESO lifecycle and ownership](https://external-secrets.io/latest/guides/ownership-deletion-policy/).

For a direct Helm installation, merge the following into your existing
customer values. Retain your database host, username, and TLS settings. The
database Secret above belongs to your external database setup; direct Helm
continues generating and reusing Qodo's internal credentials:

```yaml external-secrets-values.yaml theme={null}
global:
  externalDatabase:
    existingSecret: qodo-db-admin
  generatedSecrets:
    render: true
postgres:
  enabled: false
```

Keep the fixed name `qodo-db-admin` because some component references do not
follow a renamed `global.externalDatabase.existingSecret`. The full external
credential mode for Argo CD is described separately below.

> **Example: Bedrock and separate embeddings credentials**
>
> Use these resources only if your gateway values already reference the two
> target Secrets. The source values are JSON objects with the matching
> properties. Save this as `qodo-provider-external-secrets.yaml` and replace
> the source paths if your naming differs.
>
> ```yaml qodo-provider-external-secrets.yaml theme={null}
> apiVersion: external-secrets.io/v1
> kind: ExternalSecret
> metadata:
>   name: qodo-bedrock
>   namespace: qodo
> spec:
>   refreshInterval: 1h
>   secretStoreRef:
>     kind: SecretStore
>     name: qodo-secrets
>   target:
>     name: qodo-bedrock
>     creationPolicy: Orphan
>     deletionPolicy: Retain
>   data:
>     - secretKey: BEARER_TOKEN_BEDROCK
>       remoteRef:
>         key: qodo-bedrock
>         property: BEARER_TOKEN_BEDROCK
> ---
> apiVersion: external-secrets.io/v1
> kind: ExternalSecret
> metadata:
>   name: qodo-embeddings
>   namespace: qodo
> spec:
>   refreshInterval: 1h
>   secretStoreRef:
>     kind: SecretStore
>     name: qodo-secrets
>   target:
>     name: qodo-embeddings
>     creationPolicy: Orphan
>     deletionPolicy: Retain
>   data:
>     - secretKey: EMBEDDINGS_API_KEY
>       remoteRef:
>         key: qodo-embeddings
>         property: EMBEDDINGS_API_KEY
> ```

> **Example: direct-registry image-pull credentials**
>
> Save this as `qodo-pull-external-secret.yaml`. In your secret manager,
> `qodo-image-pull` contains a JSON property named `dockerconfigjson`. Its
> value must be the complete Docker configuration JSON as a string, not a
> base64-encoded Kubernetes `data` value. ESO performs the Kubernetes
> encoding.
>
> ```yaml qodo-pull-external-secret.yaml theme={null}
> apiVersion: external-secrets.io/v1
> kind: ExternalSecret
> metadata:
>   name: qodo-pull
>   namespace: qodo
> spec:
>   refreshInterval: 1h
>   secretStoreRef:
>     kind: SecretStore
>     name: qodo-secrets
>   target:
>     name: qodo-pull
>     creationPolicy: Orphan
>     deletionPolicy: Retain
>     template:
>       type: kubernetes.io/dockerconfigjson
>   data:
>     - secretKey: .dockerconfigjson
>       remoteRef:
>         key: qodo-image-pull
>         property: dockerconfigjson
> ```

## 3. Reconcile Secrets before deploying Qodo

Apply the database example after updating the store and source path:

```bash theme={null}
kubectl --context "$KUBE_CONTEXT" apply \
  --dry-run=server -f qodo-database-external-secret.yaml
kubectl --context "$KUBE_CONTEXT" apply -f qodo-database-external-secret.yaml
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" \
  wait externalsecret/qodo-db-admin --for=condition=Ready --timeout=120s
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" \
  get secret qodo-db-admin \
  -o go-template='{{range $key, $value := .data}}{{$key}}{{"\n"}}{{end}}'
```

The last command prints key names only. Confirm `POSTGRES_PASSWORD` is
present. Apply and wait for each other Secret required by your provider
profile in the same way. For the pull Secret, also check its type:

```bash theme={null}
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" \
  get secret qodo-pull -o jsonpath='{.type}{"\n"}'
```

Proceed with your existing Qodo installation procedure only after the
required targets exist with the expected keys and types. A server-side dry
run validates the resource shape, not cloud permissions or successful secret
synchronization. For more information, see [Deploy Qodo using Helm](https://docs.qodo.ai/on-prem/qodo-on-prem-helm-chart-overview.md).

## Manage Qodo's internal credentials separately

The previous examples leave internal credentials under direct Helm's
existing generation and reuse behavior. Moving these credentials to ESO is
an ownership migration, not an additional provider Secret:

| Internal Secret | Required keys |
| - | - |
| `qodo-zitadel-secrets` | `ZITADEL_MASTERKEY`, `ZITADEL_DB_PASSWORD` |
| `qodo-platform-jwt` | `DYNACONF_AUTH__OIDC__JWT_ENCRIPTION_KEY` |
| `qodo-qodo-platform-api-key` | `DYNACONF_AUTH__QODO_PLATFORM__API_KEY`, `QODO_PLATFORM__API_KEY`, `DYNACONF_AUTH__SERVICE_API_KEYS` |
| `qodo-litegit-master-secrets` | `LITEGIT_MASTER_KEY`, `LITEGIT_MASTER_API_KEY` |
| `qodo-rabbitmq-password` | `RABBITMQ_PASSWORD` |
| `qodo-llm-gateway-key` | `LLM_GATEWAY_API_KEY` |
| `qodo-pr-agent-secrets-toml` | `.secrets.toml`, containing the matching `[qodo_platform].api_key` |

The spelling `JWT_ENCRIPTION_KEY` is part of the existing contract. Preserve
it. The Platform bearer is derived from its raw key, its JSON allowlist must
contain that bearer, and the TOML credential must match it. The database
Secret from the previous section is also required. Preserve the
runtime-written `qodo-zitadel-generated` Secret on existing installations.

For Argo CD, the packaged `values-argocd.yaml` selects
`global.generatedSecrets.render: false` and must be the last values layer.
Pre-provision the full inventory before the first sync. For an existing
installation, back up and import the existing bytes, transfer ownership
without deleting live Secrets, and retain credentials against pruning.
Do not add the Argo CD values layer to a direct Helm installation just to
externalize a provider credential.

Follow the release's full Argo CD setup or handoff procedure, including its
Application tracking, retention, ordered migration, and recovery steps. For
more information, see [Deploy Qodo using Helm](https://docs.qodo.ai/on-prem/qodo-on-prem-helm-chart-overview.md).

From the extracted release, run its credential preflight:

```bash theme={null}
python3 qodo-onprem/verify-external-secrets.py \
  --context "$KUBE_CONTEXT" --namespace "$NAMESPACE"
```

For an existing installation, add `--require-zitadel-generated`. The preflight
checks required keys and related credential consistency without printing
their values. It does not establish that the database, broker, or model
provider accepts the credentials. Use the release's application checks too.

## Validation scope

Live development-cluster tests verified cloud Secret synchronization, JSON
property extraction, source-version refresh, missing-property failure and
recovery, and target retention. Isolated marker consumers also verified that
environment variables and `subPath` mounts keep the old value until rollout.
The tests left the shared Qodo release and application credentials unchanged.

AWS and Azure authentication examples have configuration/schema validation;
their cloud identity flows still require a live check in your environment.
A successful marker test does not verify database password rotation, sign-in,
or a complete Qodo code review. Include those in application acceptance.

## Verify rotation and recovery

After the first sync, verify Pod startup, database access, sign-in, and an
actual code review using the configured model provider.

ESO updating a Kubernetes Secret does not refresh environment variables or
`subPath` mounts in running Pods. For more information, see [Define container environment variables using Secret data](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data)
and [Using Secrets as files from a Pod](https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-files-from-a-pod).

Qodo's revision rollout mechanism does not
automatically restart consumers when an external Secret changes. Your
rotation process must coordinate the upstream credential change, ESO
synchronization, and the rollout of every affected consumer.

For example, after rotating a provider credential consumed by `qodo-litellm`,
first confirm ESO reports a refresh after the source change and that the
target Secret was updated. Compare the refresh timestamp and the target's
resource version with your pre-rotation observations, without printing its
data:

```bash theme={null}
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" \
  get externalsecret qodo-bedrock -o jsonpath='{.status.refreshTime}{"\n"}'
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" \
  get secret qodo-bedrock -o jsonpath='{.metadata.resourceVersion}{"\n"}'
```

A previously true `Ready` condition alone can describe the old value. After
confirming synchronization, restart the consumer:

```bash theme={null}
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" \
  wait externalsecret/qodo-bedrock --for=condition=Ready --timeout=120s
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" \
  rollout restart deployment/qodo-litellm
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" \
  rollout status deployment/qodo-litellm --timeout=300s
```

Then run a real review and verify the provider accepts the new credential.

Do not regenerate Zitadel's master key or LiteGit's vault key as an incidental
rotation: they protect persisted data. A database or RabbitMQ password update
also needs the server-side credential change. Coordinate these operations
with Qodo and your database or broker administrator.

If synchronization or rollout fails, inspect conditions and events, keep
live Secrets intact, and use your approved source-version and application
recovery procedure. Do not delete and recreate credential Secrets to retry.

## Troubleshoot synchronization

| Symptom | Check and recovery |
| - | - |
| No match for `ExternalSecret` or `SecretStore` | Check the installed ESO CRDs and served API version |
| `SecretStore` is not ready | Check the selected cloud identity, trust relationship, permissions, encryption-key policy, and network access |
| `ExternalSecret` reports a missing value | Check the provider Secret identifier and case-sensitive JSON property. Confirm the source contains the expected credential |
| A target exists with the wrong name | Set `spec.target.name` to Qodo's exact reference; inspect component prefixes if using the module template |
| ESO and Helm repeatedly change the same Secret | Stop the conflicting writes and follow the ownership handoff before continuing |
| Pods report `CreateContainerConfigError` | Check the referenced namespace, Secret name, and required key |
| Pods report `ImagePullBackOff` | Check the pull Secret's type and Docker configuration, registry access, and your delivery path |
| ESO is ready but the application still rejects the credential | Confirm the upstream server accepts the value and roll out every consumer that retains it in memory or a `subPath` mount |

Use `kubectl describe secretstore` and `kubectl describe externalsecret` for
status and events. Avoid commands that print or decode Secret values into a
terminal transcript.

## Next steps

* [Deploy Qodo using Helm](https://docs.qodo.ai/on-prem/qodo-on-prem-helm-chart-overview.md): Complete the release-specific installation or ownership handoff.
* [Autoscale Qodo with KEDA](https://docs.qodo.ai/on-prem/keda-autoscaling.md): Connect workload scaling while retaining your credential ownership model.
* [Monitor Qodo with Prometheus and Grafana](https://docs.qodo.ai/on-prem/monitoring.md): Observe application behavior after deployment and rotation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.