> ## Documentation Index
> Fetch the complete documentation index at: https://neuraltrust-92b43583-develop.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Secrets

> Handle credentials generated for a private TrustGate gateway.

## Wizard-issued credentials

The console issues credentials **per product instance** before you deploy. Create
each product you run — TrustGate under **Agent Gateway** and TrustGuard under
**Agent Runtime** — and collect its own set. See
[Console setup](/neuraltrust/deployment/console-setup) for the step-by-step flow.

Each private instance issues:

* A configuration-sync token, scoped to that gateway or TrustGuard
* A DataAgent enrollment token that authorizes OTLP metadata egress and DataBridge retrieval

The Docker command injects the configuration-sync token, DataAgent enrollment token, and a generated local cache key. The maintained Compose manifest also requires operator-supplied `SERVER_SECRET_KEY`, `CONFIG_SYNC_GRPC_ENDPOINT`, and `DATABRIDGE_ADDR`.

The Kubernetes wizard currently writes credentials directly into generated `values.yaml`; it does not place them in Kubernetes Secret references. Treat the entire generated file as a secret and never commit it. For production, move the credentials into pre-created Kubernetes Secrets or an approved secret manager and map them through the current maintained chart interfaces.

Manual displays only `CONTROL_PLANE_JWT` and `DATA_AGENT_JWT`. Never print credentials in logs or share them in tickets.

Regenerating install configuration from **Settings → Agent Gateway → Deployment** issues new install credentials.

## Chart-managed secrets

For an operator-managed Kubernetes deployment that uses a maintained Helm chart, these settings control runtime secret generation:

```yaml theme={null}
global:
  autoGenerateSecrets: true
  preserveExistingSecrets: false
```

For GitOps, pre-create Secrets and set `autoGenerateSecrets: false` with `preserveExistingSecrets: true`. `helm template` cannot preserve generated values via `lookup`.

## Data-plane secrets

| Kubernetes Secret                                   | Important keys                                                                  | When                                                                                                                           |
| --------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| TrustGate (`agentgateway-secrets`)                  | `SERVER_SECRET_KEY`, `STS_SIGNING_KEY`                                          | Always                                                                                                                         |
| `trustguard-secrets`                                | `ADMIN_JWT_SECRET`, `TRUSTGUARD_TOKEN_SIGNING_SECRET`, `REDIS_EVENTS_SECRET`    | Always                                                                                                                         |
| `trustguard-client-credentials`                     | `CLIENT_ID`, `CLIENT_SECRET`                                                    | Always                                                                                                                         |
| `postgresql-secrets`                                | Connection and auth-mode facts (`POSTGRES_*`), plus `SENSIBLE_PG_DSN` in Hybrid | Always (your Postgres)                                                                                                         |
| `redis-secrets`                                     | `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, …                                 | Always (your Redis)                                                                                                            |
| `firewall-secrets`                                  | `JWT_SECRET`                                                                    | TrustGuard enabled (Firewall deploys with it)                                                                                  |
| `platform-secrets`                                  | Credentials shared by two or more services                                      | Default (see below)                                                                                                            |
| `dataagent-secrets`, `dataagent-trustguard-secrets` | `ENROLMENT_TOKEN` (+ DB keys if needed)                                         | Hybrid, one per enabled product. Renders empty when you point at your own Secret with `existingSecret` — the recommended path. |

### `postgresql-secrets` stores one name per fact

The Secret holds these nine keys:

```
POSTGRES_HOST         POSTGRES_PORT       POSTGRES_USER
POSTGRES_PASSWORD     POSTGRES_DB         POSTGRES_SSLMODE
POSTGRES_LOGIN        POSTGRES_AUTH_MODE  POSTGRES_CONNECTION_TYPE
```

Plus one connection string, which differs by mode. Hybrid adds
`SENSIBLE_PG_DSN`, the lib/pq DSN that the TrustGate and TrustGuard telemetry
exporters and DataAgent read. [External](/neuraltrust/deployment/external) adds
`POSTGRES_PRISMA_URL` for the console instead, and since chart 2.8.0 no longer
stores `SENSIBLE_PG_DSN` at all — nothing in External ever read it.

Services that expect different variable names still get them: TrustGate,
TrustGuard and AlertEngine read `DB_HOST`, `DB_NAME`, `DB_SSL_MODE` and so on, and
the chart maps the canonical keys to those names on each Deployment. **Do not add
`DB_*` or `DATABASE_URL` to this Secret** — earlier chart versions stored those
duplicates and no longer do. If you pre-create the Secret for GitOps, populate the
keys above.

#### Bringing your own Postgres Secret

<Warning>
  The two ways of supplying Postgres credentials need **opposite key names**. This
  is the most common managed-Postgres install failure.
</Warning>

| You do                                                           | Secret name          | Keys it must hold            |
| ---------------------------------------------------------------- | -------------------- | ---------------------------- |
| Pre-create the chart's own Secret (GitOps)                       | `postgresql-secrets` | `POSTGRES_*` as listed above |
| Point `global.postgresql.existingSecret.name` at your own Secret | yours                | `DB_*` as listed below       |

The reason is that setting `global.postgresql.existingSecret.name` stops the chart
rendering `postgresql-secrets` at all. With no Secret of its own to map from, every
consumer takes yours wholesale with `envFrom` and **nothing gets renamed** — so your
keys have to be the names the containers actually read. Supply `POSTGRES_*` here and
the pods start with no database configuration and fail on their first query.

## Connect to a managed PostgreSQL

This section covers the **Hybrid** shared role (and External's control-plane
`neuraltrust` role when you replace the whole `postgresql-secrets` Secret). For
External runtime services — AgentGateway, TrustGuard, AlertEngine, DataCore —
prefer the [per-service hooks](#external-per-service-datastore-credentials)
below instead of replacing `postgresql-secrets` wholesale.

Two ways, depending on whether you are willing to put the password in a values
file. Either way, create the role and database yourself first on a **managed**
instance — the chart never runs `CREATE USER` against a store it does not own.
(When the chart runs Postgres itself in External mode, chart 2.7.0+ creates the
per-service roles for you — see
[External datastores](/neuraltrust/deployment/external#datastores).)

<Tabs>
  <Tab title="Chart builds the Secret">
    The simpler path. Give the chart the connection facts and let it assemble
    `postgresql-secrets` in the canonical `POSTGRES_*` shape. Every consumer then
    resolves without further wiring.

    ```yaml theme={null}
    global:
      postgresql:
        deploy: false
        host: "pg.managed.example.com"
        port: 5432
        user: "neuraltrust"
        database: "neuraltrust"
        password: "<password>"
        sslMode: "require"
    ```

    The password lands in the values file, so treat that file as a secret or supply
    it with `--set` from your secret store at install time. In External, chart 2.8.0+
    lets you drop it entirely and name a Secret instead — see
    [`global.postgresql.passwordSecret`](#external-per-service-datastore-credentials).
  </Tab>

  <Tab title="You bring the Secret">
    Keeps the password out of values entirely. Your Secret is injected verbatim with
    `envFrom`, so the keys must be the variable names the containers read:

    ```bash theme={null}
    kubectl create secret generic managed-postgres -n neuraltrust \
      --from-literal=DB_HOST='pg.managed.example.com' \
      --from-literal=DB_PORT='5432' \
      --from-literal=DB_USER='neuraltrust' \
      --from-literal=DB_PASSWORD='<password>' \
      --from-literal=DB_NAME='neuraltrust' \
      --from-literal=DB_SSL_MODE='require' \
      --from-literal=POSTGRES_LOGIN='default' \
      --from-literal=SENSIBLE_PG_DSN='postgresql://neuraltrust:<password>@pg.managed.example.com:5432/neuraltrust?sslmode=require'
    ```

    Two of those are easy to miss. **`DB_SSL_MODE`** is not optional in practice: omit
    it and the gateways fall back to their compiled-in `disable`, quietly turning a
    TLS-configured install into a plaintext one. **`SENSIBLE_PG_DSN`** is what
    DataAgent reads as its `DATABASE_URL` in Hybrid; External has no reader for it.
    `POSTGRES_LOGIN` is the authentication switch — `default` for password auth,
    `aws` for IAM.

    ```yaml theme={null}
    global:
      postgresql:
        deploy: false
        host: "pg.managed.example.com"
        port: 5432
        user: "neuraltrust"
        database: "neuraltrust"
        sslMode: "require"
        existingSecret:
          name: "managed-postgres"

    # data-plane-api does not follow global.postgresql.existingSecret — see below
    data-plane-api:
      dataPlane:
        components:
          api:
            database:
              postgresql:
                existingSecret:
                  name: "managed-postgres"
                  keys:
                    host: "DB_HOST"
                    port: "DB_PORT"
                    user: "DB_USER"
                    password: "DB_PASSWORD"
                    database: "DB_NAME"
    ```
  </Tab>
</Tabs>

<Warning>
  **`data-plane-api` needs the Secret named a second time.** It resolves its own
  Postgres reference, defaulting to `postgresql-secrets` — the Secret that naming
  an `existingSecret` prevents the chart from rendering. Without the
  `data-plane-api` block above, its pods reference a Secret that does not exist
  and sit in `CreateContainerConfigError`, while every other workload runs
  normally. It also looks up different key names, which is why the block maps
  yours onto them.

  This applies whenever `global.products.dataPlane` is `true`. Omit the block
  entirely when you let the chart build the Secret — the defaults resolve.
</Warning>

For AWS IAM authentication set `POSTGRES_LOGIN: aws` instead, and see
[IAM authentication](/neuraltrust/deployment/configuration).

## External per-service datastore credentials

In External mode each runtime service has its own database and its own password.
Writing those passwords into a values file works, but they then sit in Helm
release history. Since chart 2.6.0 you can name a Secret you created instead:
the chart leaves that key out of the Secret it renders and injects the variable
with a `secretKeyRef`.

| Values path                                                    | Variable            | Default key         |
| -------------------------------------------------------------- | ------------------- | ------------------- |
| `agentgateway.database.existingSecret`                         | `DB_PASSWORD`       | `DB_PASSWORD`       |
| `agentgateway.redis.existingSecret`                            | `REDIS_PASSWORD`    | `REDIS_PASSWORD`    |
| `trustguard.database.existingSecret`                           | `DB_PASSWORD`       | `DB_PASSWORD`       |
| `trustguard.redis.existingSecret`                              | `REDIS_PASSWORD`    | `REDIS_PASSWORD`    |
| `alertengine.database.existingSecret`                          | `DB_PASSWORD`       | `DB_PASSWORD`       |
| `datacore.database.existingSecret`                             | `POSTGRES_PASSWORD` | `POSTGRES_PASSWORD` |
| `data-plane-api.dataPlane.components.api.redis.existingSecret` | `REDIS_URL`         | `REDIS_URL`         |
| `global.postgresql.passwordSecret`                             | `POSTGRES_PASSWORD` | `POSTGRES_PASSWORD` |

The last row arrived in chart 2.8.0 and covers the control-plane `neuraltrust`
role, the credential the console and the API share. It behaves like the others
but applies globally: the chart omits `POSTGRES_PASSWORD` and
`POSTGRES_PRISMA_URL` from `postgresql-secrets`, writes every other key, and
points the console (both containers), the API and `data-plane-api` at your
Secret. The console then assembles its own connection URL from the `POSTGRES_*`
parts, so its image must be recent enough to carry
`scripts/postgres-password-url.mjs` — the migration step needs it. It is
rejected in Hybrid, which still composes a DSN, and while the chart runs its own
Postgres.

`key` is configurable, so one Secret can hold every Postgres role under a
different key, and one Redis Secret can hold both `REDIS_PASSWORD` (gateways)
and the assembled `REDIS_URL` that `data-plane-api` reads.

```bash theme={null}
kubectl create secret generic postgres-roles -n neuraltrust \
  --from-literal=CONTROL_PLANE='<pw-control-plane>' \
  --from-literal=AGENTGATEWAY='<pw-agentgateway>' \
  --from-literal=TRUSTGUARD='<pw-trustguard>' \
  --from-literal=ALERTENGINE='<pw-alertengine>' \
  --from-literal=DATACORE='<pw-datacore>'

kubectl create secret generic redis-auth -n neuraltrust \
  --from-literal=REDIS_PASSWORD='<redis-password>' \
  --from-literal=REDIS_URL='rediss://<redis-user>:<redis-password>@<redis-host>:6379/0'
```

```yaml theme={null}
global:
  postgresql:
    deploy: false
    host: "postgres.example.com"
    sslMode: "require"
    passwordSecret:
      name: "postgres-roles"
      key: "CONTROL_PLANE"
  redis:
    deploy: false
    host: "redis.example.com"
    username: "<redis-user>"
    tls: "true"

agentgateway:
  database:
    existingSecret:
      name: "postgres-roles"
      key: "AGENTGATEWAY"
  redis:
    existingSecret:
      name: "redis-auth"

trustguard:
  database:
    existingSecret:
      name: "postgres-roles"
      key: "TRUSTGUARD"
  redis:
    existingSecret:
      name: "redis-auth"

alertengine:
  database:
    existingSecret:
      name: "postgres-roles"
      key: "ALERTENGINE"

datacore:
  database:
    existingSecret:
      name: "postgres-roles"
      key: "DATACORE"

data-plane-api:
  dataPlane:
    components:
      api:
        redis:
          existingSecret:
            name: "redis-auth"
            key: "REDIS_URL"
```

With every hook in place the values file holds no credential at all. An inline
`password` next to a hook is rejected at render. The hooks are ignored under
`iamAuth: true` and in Hybrid (which already has
`global.postgresql.existingSecret` / `global.redis.existingSecret` for the shared
role). Do **not** use `global.postgresql.existingSecret` for this External layout
unless you intend to hand-write every key in `postgresql-secrets`, connection
strings included — `passwordSecret` above is the narrow version of it, and the
one you want.

Full example:
[`values-managed-datastores.yaml.example`](https://github.com/NeuralTrust/neuraltrust-platform/blob/main/values-managed-datastores.yaml.example).

### `platform-secrets` keeps both halves of a shared credential in step

Some credentials must be **identical on two sides** — a signing key on one service
and its validator on another. Those live in one `platform-secrets` Secret that the
chart resolves once and every consumer references, so the halves cannot drift.

On upgrade it adopts whatever your existing per-service Secrets already hold, so
**nothing rotates**. It is created when `global.autoGenerateSecrets` is enabled
(the default); with `autoGenerateSecrets: false` each service falls back to its own
Secret, which you then keep in step yourself.

#### Four extra keys under a Central control plane

`global.deploymentMode: saas` adds four credentials to `platform-secrets`,
generated for you on the same terms as the rest:

| Key                             | Used by                                                              |
| ------------------------------- | -------------------------------------------------------------------- |
| `ENROLMENT_INTROSPECTION_TOKEN` | DataCore — compares what DataBridge presents                         |
| `DATACORE_SERVICE_TOKEN`        | DataBridge — alias of the above, must hold the identical value       |
| `ENROLMENT_SIGNING_SECRET`      | DataCore — signs enrolment tokens                                    |
| `TELEMETRY_JWT_PRIVATE_KEY_PEM` | DataCore — RS256 key for the OTLP tokens the ingest gateway verifies |

<Warning>
  If you pre-provision secrets — `global.autoGenerateSecrets: false`,
  `global.preserveExistingSecrets: true`, or
  `global.platformSecret.existingSecret` — all four must be present, and the two
  token keys must hold **one identical value**. When they drift, every data-plane
  connection returns 401 with nothing visibly wrong on either side. Running
  `./create-secrets.sh` with `DEPLOYMENT_MODE=saas` writes all four correctly,
  including the alias.
</Warning>

`TELEMETRY_JWT_PRIVATE_KEY_PEM` is a signing key rather than a shared secret —
the ingest gateway reads the public half from DataCore's JWKS endpoint, so only
the private PEM is stored. Regenerating it invalidates every token already
issued, so if you intend to own it, put it in place before the first install.

## Credential contracts

* The configuration-sync token authenticates the data plane's outbound configuration pull from the SaaS control plane. Each product gets its own: one for TrustGate, one for TrustGuard.
* Alongside it, `CONFIG_SYNC_LKG_KEY` encrypts the last-known-good configuration snapshot on disk. Unlike the token it is not a shared credential — the control plane never sees it — so as of chart 2.6.0 **the chart generates it and you should not create one**. It becomes yours to supply only under `global.autoGenerateSecrets: false` or `global.preserveExistingSecrets: true`, where the chart generates nothing at all. In that case supply it alongside the token as base64 that decodes to exactly 32 bytes — the data planes will not start without it.
* The DataAgent enrollment token authorizes DataBridge retrieval **and** metadata export. In Hybrid there is no separate OTLP token to manage: TrustGate and TrustGuard send plain OTLP to a local collector co-located with DataAgent, which exchanges the enrollment JWT for a short-lived access token on their behalf.
* The LLM and MCP URLs are not secrets. Configure them separately in **Settings → Agent Gateway → General**.

## Credentials across multiple clusters

When you run active/passive clusters, some credentials are per cluster and some
must be identical in both. Getting this wrong is invisible until a promotion.

| Credential                                                                          | Across clusters                                                                                                           |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Config-sync token (per product)                                                     | **Same** — both clusters serve the same gateway scope                                                                     |
| DataAgent enrollment token                                                          | **Same** token, but enabled only in the active cluster                                                                    |
| `CONFIG_SYNC_LKG_KEY`                                                               | **Per cluster** — generated locally, encrypts a local cache, nothing to distribute                                        |
| PostgreSQL credentials                                                              | **Same** — both clusters point at one writable primary                                                                    |
| Redis credentials                                                                   | **Per cluster** in two regions, where each has its own Redis; the same when twin clusters in one region share an instance |
| `ENROLMENT_SIGNING_SECRET`, `TELEMETRY_JWT_PRIVATE_KEY_PEM` (central control plane) | **Same** — tokens minted by one cluster must verify in the other                                                          |

Only the active cluster should run the enrolled DataAgent; two would register
duplicate streams. Keep the enrollment token in place in the passive cluster and
enable DataAgent as part of the promotion.

To let a cluster restart while the control plane is unavailable, the encrypted
last-known-good file has to survive the restart. The chart mounts it on an
`emptyDir`, so it does not: a pod that restarts during a control-plane outage has
no cache to fall back on and will not become Ready. Plan failover on that basis,
or move the mount to persistent storage yourself.

See [Hybrid → High availability](/neuraltrust/deployment/hybrid#high-availability),
[External](/neuraltrust/deployment/external#high-availability), or
[Central](/neuraltrust/deployment/central#high-availability).

## Chart references

Reference pre-created Secrets by name so no token is ever written into a values
file. Both settings are per product — TrustGate and TrustGuard each need their
own.

**Configuration sync** — Secrets holding `CONFIG_SYNC_TOKEN`:

```yaml theme={null}
agentgateway:
  configSync:
    existingSecret:
      name: agentgateway-config-sync

trustguard:
  configSync:
    existingSecret:
      name: trustguard-config-sync
```

Set `existingSecret` only. Config sync is already on by default in Hybrid, so
restating `enabled: true` is redundant.

**DataAgent enrollment** — the wizard-issued token, one Secret per product:

```yaml theme={null}
agentgateway:
  dataagent:
    enrolment:
      existingSecret:
        name: dataagent-enrolment-trustgate
        key: ENROLMENT_TOKEN   # default; omit unless your key differs

trustguard:
  dataagent:
    enrolment:
      existingSecret:
        name: dataagent-enrolment-trustguard
```

Do not set a tenant ID in values. The enrollment JWT already carries `tenant_id`
and `instance_id`, and the chart reads them from the token.

Keep configuration-sync and DataAgent enrollment tokens out of values files and
source control.

## Registry

Create `gcr-secret` (or set `global.imagePullSecrets`) yourself — the chart does not create registry credentials.

## Full key reference

This page covers the Secrets you create. The exhaustive per-component key
contract — every Secret the chart renders, every key inside it, and which
container reads it — is maintained alongside the chart in
[`SECRETS.md`](https://github.com/NeuralTrust/neuraltrust-platform/blob/main/SECRETS.md).
It is the reference to use when integrating Vault, Sealed Secrets or External
Secrets Operator, and it ships inside the chart artifact too, so `helm pull <chart-ref> --version <VERSION> --untar` gives you the copy matching your
version.
