SettleMint
DeploymentInstall

Configure values and secrets

Set the dalp chart's datastore, TLS, and secret-manager values, and choose how secrets reach each pod.

This page sets the dalp chart's values.yaml: which datastore, TLS, and secret values you configure before a deployment goes live, and how each secret reaches its pod once it does. Read it after Install the DALP Helm chart has you running helm install, and before you point that release at a production datastore or secret manager.

Two decisions here fail closed and one does not, so treat them differently. The master password that encrypts application data at rest ships empty; every pod refuses to start until you set one. BETTER_AUTH_SECRET ships with a known placeholder. A production pod rejects that exact value at startup, so it cannot sign sessions with the published secret. See Encryption at rest for both checks. The secret-manager provider that holds application secrets once they leave the chart ships as local, a working default. Leaving it there starts a production deployment on the wrong footing rather than blocking it; that is the actual failure mode, not an unworkable state. Review datastore TLS, credentials, connection capacity, and any Conjur settings against the target environment before installation.

dapi (the Platform API) and dapp (the Console) are the two application subcharts this page configures most. The migrator is the dapp subchart's schema-migration Helm hook, a Job that runs automatically on every install and upgrade; it appears throughout this page because two of its steps, direct database access and encryption rotation, shape the values below.

Values structure

values.yaml groups into a shared global block (observability, image-pull credentials, datastores, the embedded application config, Conjur, HTTP routing). One block exists per service: dapi (the Platform API), dapp (the Console), blockscout (an optional block explorer), investorPortal (the optional Investor portal), contractsDeploy, docs, backup, overprovisioning, and postgres-exporter. The PgBouncer connection pooler is a separate chart dependency gated by global.pgbouncer.enabled, not a values block of its own. dapi and dapp each carry autoscaling and image settings for you to tune, plus resource requests and limits and exposure settings through ingress or a routing API (Gateway httpRoute on Kubernetes, openShiftRoute on OpenShift).

Managed services or the bundled support chart

Every datastore value in global.datastores accepts either an externally managed endpoint or the bundled support chart's in-cluster service; the chart does not require one or the other, but the bundled defaults are development-shaped (see Topologies). Point PostgreSQL and Redis at managed services for a production deployment, along with object storage. Reserve the bundled chart for development and test environments instead. A managed service can provide replication, patching, and backups, depending on the plan and configuration you choose. Verify those capabilities and test recovery. That removes exactly the operational surface support's single-Deployment PostgreSQL and single-replica Redis leave you to run yourself.

Datastores

global.datastores sets connections per service for PostgreSQL and Redis, plus object storage, either inline or through existingSecret. Default passwords in the chart (for example "dalp" on the bundled PostgreSQL) are placeholders; change every one before you deploy to production.

PostgreSQL TLS reads sslMode, but which leg it secures depends on whether pooling is on. global.pgbouncer.enabled defaults to true, and a pooled deployment strips sslMode out of the application's own connection URL entirely. The pooler's client listener is plaintext in-cluster by design: an application still trying to negotiate TLS against it gets a hard connect failure, not a silent downgrade to plaintext. sslMode instead sets the pooler's own upstream leg to Postgres, and the chart ships it as disable: the upstream leg is plaintext by default. The pooler accepts four values there: disable, require, verify-ca, and verify-full; any other value fails the render.

This page's own Managed services advice points you toward a managed PostgreSQL on a public certificate authority. For that shape, set sslMode to require. The chart maps it to verify-full over the image's system CA bundle, verifying both certificate and hostname with no CA to mount. For a server behind a private certificate authority, set sslMode to an empty string instead. Supply the CA through the pooler subchart's own values, a different root than the umbrella gate above:

global:
  pgbouncer:
    enabled: true # umbrella gate: whether pooling exists at all
pgbouncer:
  upstreamTls:
    ca: | # the private CA's certificate, PEM-encoded
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----
    # existingSecret: my-ca-secret   # or point at a Secret instead

An empty sslMode with that CA configured resolves to a verified verify-full connection. Leaving the shipped disable in place while mounting a CA does nothing, since disable passes through literally and the CA sits unused. The application's own typed DATABASE_TLS value only takes effect when pooling is off, on the direct connections described below.

How application pods reach Postgres

A central transaction-mode connection pooler, PgBouncer, sits between every application pod and Postgres. Three dedicated pooler aliases split the traffic: one for dapi and dapp application queries, one for Effect Cluster storage traffic (the platform's durable-execution engine for background jobs), and one for the health-probe connection, each with its own fixed pool. A pod holds pooler client connections against that fixed pool, a cheap resource with a 1,000-per-process ceiling, not a steady direct Postgres backend of its own; raising dapi's replica count changes that client load and leaves Postgres's own max_connections untouched.

The chart pins the pool, reserve, and hard-cap values for each alias rather than leaving them for you to size. A comment in the pooler subchart's values.yaml prices the aggregate at the pinned replicaCount of 2 times processCount of 1, 2 peers total: 132 connections at peak, against 197 usable slots (its assumed max_connections of 200 minus 3 reserved superuser slots). That arithmetic lives in a comment, not a gate that runs against your own server: nothing in the chart recomputes the 132-against-197 sum for your actual max_connections. Before you raise a pool, a reserve, or the pooler's own replica or process count, compare that assumption against your server's actual max_connections and superuser_reserved_connections by hand, or watch PostgresAggregateBackendsHigh in the observability chart, described below, which does check the real server. This does not show up in a dry run: PostgreSQL accepts the deployment either way, and only starts failing individual connections once real traffic pushes past the ceiling. Watch your server's actual connection count, through postgres-exporter or pg_stat_activity, both before and after the change.

The migrator is the platform's own sanctioned direct, non-pooled consumer, because its schema-migration locks are session-scoped and cannot survive a transaction-mode pooler; integration-test fixtures connect the same way in test environments but are not part of the priced sum below. postgres-exporter holds one direct session by default too, to report the server's aggregate backend count. Blockscout, an optional block explorer that ships disabled (blockscout.enabled: false), holds the largest direct share once you turn it on: 20 sessions. Those three, blockscout's 20, the exporter's 1, and the migrator's 1, sum to the 22 direct connections priced into the 132-connection peak above. Enabling Blockscout adds its configured pool to the database connection budget.

A saturating pooler queues clients rather than rejecting them immediately, so the signal to watch is a growing client queue, not a connect-time error. The observability chart's alert rules catch that. PgBouncerClientsWaiting fires the moment any client is waiting, and PgBouncerClientMaxWait fires when the longest queued client crosses one second; both evaluate every 10 seconds and sustain for 5 seconds. That puts the page well ahead of PgBouncer's own 20-second queryWaitTimeout rejection, the application role's 30-second statement timeout, and the 60-second HTTP deadline that follow it. PgBouncerDeploymentAbsent and PgBouncerDown page separately when the pooler itself goes missing or becomes fully unavailable, since every application DATABASE_URL depends on it existing. PgBouncerReplicaUnavailable catches the state between those two: redundancy drops below target, but at least one replica still serves, halving pool capacity without tripping either alert. Watch the exporter's own health too. PgBouncerMetricsAbsent and PgBouncerMetricsUnreadable both mean the saturation alerts above have gone silent rather than passing, since they read the same telemetry the exporter reports; treat silence from them as unproven, not as healthy, until the exporter is confirmed up. These alerts ship in the observability chart (see Install the observability stack), not in dalp itself, so install it too if you want them to fire.

Disabling pooling entirely (global.pgbouncer.enabled: false) falls application traffic back to direct Postgres connections, sized by each role's own pool-size setting. The pooler-specific alerts above stop applying. PostgresAggregateBackendsHigh still watches the server directly regardless of whether pooling is on, and pages at 80% of max_connections; it is the one alert here that answers the unpooled case. Sizing the change up front against max_connections by hand remains worthwhile.

Secret injection: Conjur

Two secret systems, not one, each with its own value and its own consumer.Two columns. On the left, Conjur and summon deliver deployment secrets. A Conjur authenticator init container fetches an access token, then summon injects the resulting secrets as environment variables at process start, gated on that init container plus a conjur-certificate ConfigMap present in the namespace before the pod starts. Miss either one and the pod crash-loops at startup, naming the environment variable summon never injected, a faster signal than the underlying Conjur connectivity failure. On the right, the application's own secret-manager setting is a separate decision: pick one of local, aws, gcp, azure, or vault, and that choice decides where application-level secrets live at rest, independent of Conjur entirely. The highest-stakes value in this second system is the encryption master password, boxed below the provider choice. Rotate it in two steps: set the outgoing password as ENCRYPTION_PREVIOUS_MASTER_PASSWORD for the migration Job only, keep the new one as ENCRYPTION_MASTER_PASSWORD, then deploy. The migration Job decrypts with either key and re-seals every row under the new one before runtime pods start with only the new key. The migrator's rotation boot step runs automatically on that same deploy; only remove the old password once that job succeeds, not just once its summary lines look clean. Removing it too early fails the next deploy loudly rather than silently losing data; only the master password itself being lost makes a row permanently unreadable. A closing bar beneath both columns states the risk of getting the secret-manager provider wrong: mismatch it against your existing practice and you run two separate secret stores with two separate audit trails.Two secret systems, not one, each with its own value and its own consumer.The same figure as the desktop variant, drawn portrait for phone widths. Conjur and summon deliver deployment secrets as environment variables at pod start, gated on an init container and a certificate ConfigMap. A missing one crash-loops the pod, naming the environment variable summon never injected, a faster signal than the underlying connectivity failure. The application's own secret-manager provider is a separate decision, one of local, aws, gcp, azure, or vault, that places application-level secrets, above all the encryption master password, at rest. Rotate the master password in two steps, old key then new, and remove the old one only once the rotation job succeeds, not just once its summary lines look clean. Mismatching the secret-manager provider against your existing practice runs two separate secret stores with two separate audit trails.

Conjur and summon deliver deployment secrets as environment variables at pod start, gated on an init container and a certificate ConfigMap. The application's own secret-manager provider is a separate decision that places application-level secrets, above all the encryption master password, at rest, rotated in two steps so no encrypted row ever goes unreadable mid-rotation.

By convention, application secrets in these charts are wired through CyberArk Conjur and summon, not a native Kubernetes Secret or secretKeyRef. summon is Conjur's CLI: it reads a secrets.yml mapping file, a plain list of environment variable names to Conjur vault paths, resolves each path against Conjur, and execs the service process with the resolved values already in its environment. Each pod runs a Conjur authenticator init container that fetches an access token, and summon injects the resulting secrets as environment variables at process start. This requires summon and summon-conjur baked into the service image, the authenticator init container, and a conjur-certificate ConfigMap present in your release namespace before you deploy.

Conjur scope can be set globally or per service, through a summonConfigMap that mounts secrets.yml as a ConfigMap volume. Pick a summonConfigMap when you expect to change which secrets a service reads: the mapping lives outside the image, so an edit is a config change and a pod restart, no rebuild. Leave it unset only when the mapping is fixed for the service's lifetime, since without a summonConfigMap, secrets.yml is instead baked into the image at /etc/summon/secrets.yml during the Docker build, and changing it means rebuilding and redeploying the image.

What goes wrong: a pod that starts before its Conjur authenticator init container can reach the Conjur server, or before the conjur-certificate ConfigMap exists in the namespace, fails at process start rather than starting with empty secrets. The pod's crash loop names the missing environment variable summon never injected, which is a faster signal than the underlying Conjur connectivity failure; check the init container's own logs first, since the main container's log only shows the symptom.

Application secret-manager providers

Separately from Conjur, the application's own secret-manager config selects one provider: local, aws, gcp, azure, or vault. Selecting a provider makes its configuration block required; for example, selecting vault requires a token, and selecting aws accepts an access key ID, region, and secret access key.

Choose local only for development, since it keeps secrets inside the application's own storage rather than a dedicated secret manager. For production, choose whichever of aws, gcp, azure, or vault matches infrastructure you already operate, with rotation and audit practices already in place for it. This setting decides where application-level secrets, as opposed to the Conjur-injected deployment secrets above, live at rest. Mismatching it against your existing secret-management practice means running two separate secret stores with two separate audit trails.

Encryption at rest

The application requires a master password for encryption at rest. Config decode enforces a 16-character floor, but the encryption service applies a stricter check on top of it at pod startup: a minimum of 128 bits of estimated entropy. A compliant 16-character password can still fail there if it is guessable, so pick a randomly generated password well past the floor rather than stopping at 16 characters. A repository-committed development password exists and is rejected outside NODE_ENV=development; never reuse it.

BETTER_AUTH_SECRET ships with the known placeholder "strong-secret-of-at-least-32-characters", but a production pod does not sign a single session on it: the config guard checks the resolved value against that exact literal and fails the pod with "BETTER_AUTH_SECRET must be set to a unique value in production" rather than booting on it. Set your own, at least 32 characters, before production, and never re-mint ENCRYPTION_MASTER_PASSWORD or BETTER_AUTH_SECRET for an existing environment afterward. The master password protects encrypted application data; the auth secret protects authentication material. Changing either without its supported rotation procedure can break access. A new key alone cannot decrypt data encrypted under a lost key.

Rotate the master password in two steps, not one. Set the outgoing password as ENCRYPTION_PREVIOUS_MASTER_PASSWORD for the migration Job only, through dapp.migrator.env or, with Conjur, dapp.migrator.conjur.secrets, which the chart merges over the runtime dapp.conjur.secrets map so the Job keeps the current key. Keep the new password as ENCRYPTION_MASTER_PASSWORD, then deploy: the migration Job can now decrypt with either key, so it reads every row sealed under the outgoing one. Runtime pods never receive the previous password. The migrator runs as a Helm hook on every install and upgrade. One of its required boot steps, rotate-encryption, scans every encrypted row and rewrites it under the new key whenever ENCRYPTION_PREVIOUS_MASTER_PASSWORD is set; no separate command is needed, the same deploy's migration Job re-seals the data. It logs an Encrypted secret rotation summary line, and a matching KYC encryption rotation summary line, each with scanned, rotated, skipped, and failed counts, and fails the job if any row fails. Use the migrator's successful exit as the completion signal. The Job also verifies KYC documents after rotation and can fail even when both rotation summaries show zero failures. Wait for the Job to succeed, not just for those two lines to look clean, before you remove ENCRYPTION_PREVIOUS_MASTER_PASSWORD. Removing it too early does not silently destroy data: the next deploy's rotation step checks for leftover pre-rotation rows and fails loudly, naming exactly how many secret rows and KYC objects it found, rather than skipping past them. Only the master password itself being lost makes an encrypted row permanently unreadable.

1Password overlays

Each *.1p.yaml overlay is a 1Password template rendered at deploy time with op inject. Sign in to 1Password before you run a deploy that uses one; an unauthenticated CLI fails your deploy before Helm runs. op inject writes the resolved secrets to a plaintext values.yaml file on disk before Helm ever reads it; version control excludes that file, but it still sits in your working tree afterward. Treat the machine that ran the deploy as holding secrets in plaintext, and remove the rendered file when you are done rather than leaving it beside the template.

Frequently asked questions

Can I supply a datastore password through a plain Kubernetes Secret instead of Conjur?

Yes, for datastore credentials specifically. Each postgresql and redis block under global.datastores accepts its own existingSecret, which the chart resolves as an ordinary Kubernetes Secret lookup, independent of Conjur entirely. That covers connection credentials for PostgreSQL and Redis. Conjur and summon are the separate system that injects the application's own config secrets, such as the encryption master password and secret-manager credentials, as environment variables at process start.

If I set the application's DATABASE_TLS value while pooling is enabled, does it do anything?

No, it is silently ignored rather than erroring. When global.pgbouncer.enabled is true, the application always connects to the pooler's plaintext in-cluster listener, and the client's own TLS configuration is force-disabled at that connection regardless of what you set. DATABASE_TLS only takes effect on the direct, non-pooled connections, such as the migrator, or on every connection if you disable pooling entirely.

Does postgres-exporter route through PgBouncer like the application does?

No, it connects directly to Postgres. Turn it off and you lose the aggregate backend-count view, PostgresAggregateBackendsHigh included, while your pods' own pool-pressure gauges keep working independently.

On this page