Skip to content

Outbound TLS trust

Mount a combined CA bundle for outbound HTTPS from the Dreadnode API, bundled LiteLLM, frontend, and sandbox runtimes.

If Test connection fails with CERTIFICATE_VERIFY_FAILED for an internal model endpoint, configure an outbound CA bundle. Dreadnode mounts the bundle into the platform API, the bundled LiteLLM, and the frontend, then sets the trust variables each runtime reads: the standard HTTP, AWS, and gRPC variables for the two Python workloads, and NODE_EXTRA_CA_CERTS for the Node frontend.

Sandbox runtimes receive the same bundle by a different route. A sandbox cannot mount a cluster secret, so the API passes the bundle’s contents to each one, which merges it with the image’s own system roots and exports the full trust variable set — including NODE_EXTRA_CA_CERTS for the JavaScript tooling and UV_NATIVE_TLS for uv, which otherwise ignores all of them.

Outbound trust is chart configuration. Helm operators set it in a values overlay; Embedded Cluster operators use Config → Advanced Helm Values. Applying the configuration rolls the affected pods.

This is separate from the certificate Dreadnode presents at its own ingress. Use TLS certificates to configure that certificate, and Trust an internal certificate to make workstations trust it.

The bundle must contain your normal public roots as well as the private root and any intermediates needed for internal services. SSL_CERT_FILE replaces a client’s default CA file; a bundle that contains only the private root can make public providers stop working. The frontend’s NODE_EXTRA_CA_CERTS appends instead of replacing, so the same combined bundle is safe there too — build one bundle and use it everywhere.

On a Debian or Ubuntu workstation, for example:

Terminal window
cp /etc/ssl/certs/ca-certificates.crt combined-ca-bundle.pem
cat organization-root.pem organization-intermediate.pem >> combined-ca-bundle.pem

Confirm that OpenSSL can parse every certificate:

Terminal window
openssl crl2pkcs7 -nocrl -certfile combined-ca-bundle.pem |
openssl pkcs7 -print_certs -noout

The bundle contains public certificate material only. Never add a private key or copy tls.key from the Dreadnode ingress Secret.

The chart references an existing Kubernetes Secret or ConfigMap. A Secret is the default below; use source: configMap when your organization manages public CA material that way.

Every command on this page that creates a bundle object applies it server-side. A client-side kubectl apply copies the whole object into a last-applied-configuration annotation, and Kubernetes caps that annotation at 262,144 bytes. A bundle of public roots exceeds the cap on its own, before you append a private root.

Set NAMESPACE to the Helm release namespace, then create or update the Secret:

Terminal window
export NAMESPACE=dreadnode
kubectl -n "$NAMESPACE" create secret generic dreadnode-outbound-ca \
--from-file=ca.crt=/absolute/path/to/combined-ca-bundle.pem \
--dry-run=client -o yaml | kubectl apply --server-side -f -

Add the Secret reference to your values overlay and upgrade the release:

global:
caBundle:
enabled: true
source: secret
name: dreadnode-outbound-ca

The defaults read the ca.crt key and mount it at /etc/ssl/certs/dreadnode-ca-bundle.pem. Set key or mountPath in the same block when your object uses a different layout.

global.caBundle is intentionally a workload cascade, not a cluster-wide trust-store change:

ConnectionWhere to configure trustNotes
Bundled LiteLLM → model provider api_baseglobal.caBundle or dreadnode-litellm.caBundleLiteLLM’s standard HTTP, AWS, and gRPC paths receive the bundle variables. Provider-specific SDKs can require their own TLS option; verify with Test connection.
Dreadnode API → external HTTPS serviceglobal.caBundle or dreadnode-api.caBundleThe API receives the mounted bundle and standard variables for HTTP, AWS, and gRPC clients. See the exceptions below.
Dreadnode API → external LiteLLMAPI setting aboveThis secures the API-to-proxy connection. Configure provider trust on the external LiteLLM deployment itself.
Frontend → Dreadnode APINothing to configure by defaultServer-side rendering reaches the API over the in-cluster INTERNAL_API_URL, which is plain HTTP and needs no bundle. Set dreadnode-frontend.caBundle only if you point internalApiUrl at an HTTPS host.
Workstation → Dreadnode ingressClient TLS trustInstall the ingress issuer in the workstation trust store.
OpenSandbox runtime → Dreadnode, LiteLLM, or another HTTPS serviceglobal.caBundleThe API passes the bundle to each runtime, which merges it with the image’s system roots and exports it to Python, curl, git, Node, Bun, the AWS SDKs, gRPC and uv.
Kubernetes node → private image registryNode or container-runtime configurationThe kubelet pulls images before a pod starts, so a pod volume cannot change registry trust.

The global bundle does not mean every API transport validates with that CA. caBundle cannot add private trust to Worlds HTTP backends or arbitrary webhook destinations today because those paths disable environment-derived HTTP trust. Use a destination certificate chained to a root already trusted by the container, or open a support request for a transport-specific option.

PostgreSQL’s DATABASE_USE_SSL mode encrypts the connection but does not validate its certificate, so do not treat caBundle as server-authentication support for PostgreSQL. The ClickHouse client is not passed an explicit CA file; validate that path in your environment before relying on the standard variables alone.

The sandbox server, sandbox gateway, and controller normally use Kubernetes’ automatically mounted service-account CA to reach the Kubernetes API. They do not need the outbound bundle for that in-cluster path.

The remaining workloads receive no bundle because they open no outbound TLS connection to an endpoint you control. The documentation site serves static files. The bundled PostgreSQL, ClickHouse, and MinIO workloads accept connections rather than making them, and the MinIO bucket bootstrap job reaches MinIO over plain HTTP inside the cluster. Enabling global.caBundle does not mount anything into them.

Leave global.caBundle.enabled false and configure a subchart when only one workload needs the private issuer. For a model provider reachable only from bundled LiteLLM:

dreadnode-litellm:
caBundle:
enabled: true
source: secret
name: dreadnode-outbound-ca

Use dreadnode-api.caBundle instead for an API-only destination, or dreadnode-frontend.caBundle for a frontend-only one. Non-empty local fields override the global name, source, key, mount path, and environment list.

Prefer these per-workload blocks over global.caBundle.env when a single workload needs a different variable. The environment list replaces the defaults wherever it applies, so a global list spelled for one runtime removes the other runtime’s variables — setting NODE_EXTRA_CA_CERTS globally would strip SSL_CERT_FILE and its companions from the API and LiteLLM. Left empty, each workload renders the variables its own runtime reads.

For a provider that needs a separate file or trust-store format, create an operator-managed object:

Terminal window
kubectl -n "$NAMESPACE" create secret generic provider-trust \
--from-file=provider-ca.pem=/absolute/path/to/combined-provider-ca-bundle.pem \
--dry-run=client -o yaml | kubectl apply --server-side -f -

Then mount it with the LiteLLM escape hatch:

dreadnode-litellm:
extraVolumes:
- name: provider-trust
secret:
secretName: provider-trust
extraVolumeMounts:
- name: provider-trust
mountPath: /etc/dreadnode/provider-ca.pem
subPath: provider-ca.pem
readOnly: true
extraEnv:
- name: SSL_CERT_FILE
value: /etc/dreadnode/provider-ca.pem

SSL_CERT_FILE affects every standard LiteLLM HTTP provider, not only one deployment. The mounted file therefore must remain a combined public-and-private bundle. When a provider SDK documents a more specific TLS environment variable, set that variable through extraEnv instead.

Set the namespace and choose an enabled consumer: litellm for a model-provider connection, api for an API-only destination, or frontend. Embedded Cluster uses kotsadm.

Confirm that the chosen workload received the file. Read the variable that workload’s runtime uses — SSL_CERT_FILE for api and litellm, NODE_EXTRA_CA_CERTS for frontend:

Terminal window
export NAMESPACE=dreadnode # kotsadm for Embedded Cluster
export COMPONENT=litellm # or api, or frontend
kubectl -n "$NAMESPACE" get pods \
-l app.kubernetes.io/component="$COMPONENT"
POD=$(kubectl -n "$NAMESPACE" get pod \
-l app.kubernetes.io/component="$COMPONENT" \
-o jsonpath='{.items[0].metadata.name}')
kubectl -n "$NAMESPACE" exec "$POD" -- \
sh -c 'printf "%s\n" "$SSL_CERT_FILE"; test -r "$SSL_CERT_FILE"'

For frontend, substitute NODE_EXTRA_CA_CERTS for SSL_CERT_FILE in that last command — the Node runtime does not read SSL_CERT_FILE, so checking it there reports an empty path even when the bundle is mounted correctly.

For LiteLLM, open Admin → Model Deployments, select the deployment that uses the private endpoint, and run Test connection. This exercises the actual provider path rather than only checking that the file exists. For the API, exercise the affected external service or integration. For the frontend, load a page that renders server-side and confirm it does not fail with a certificate error in the pod logs.

To rotate the CA, rerun the Secret creation command with the updated combined bundle, then restart the consumers:

Terminal window
kubectl -n "$NAMESPACE" rollout restart deployment \
-l app.kubernetes.io/name=dreadnode-api
kubectl -n "$NAMESPACE" rollout restart deployment \
-l app.kubernetes.io/name=dreadnode-litellm
kubectl -n "$NAMESPACE" rollout restart deployment \
-l app.kubernetes.io/name=dreadnode-frontend

The bundle is mounted with subPath, so an updated Secret does not replace the file in a running pod. A restart is required, and it also clears TLS contexts cached by the application.