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.
Prepare a combined PEM bundle
Section titled “Prepare a combined PEM bundle”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:
cp /etc/ssl/certs/ca-certificates.crt combined-ca-bundle.pemcat organization-root.pem organization-intermediate.pem >> combined-ca-bundle.pemConfirm that OpenSSL can parse every certificate:
openssl crl2pkcs7 -nocrl -certfile combined-ca-bundle.pem | openssl pkcs7 -print_certs -nooutThe bundle contains public certificate material only. Never add a private key or copy tls.key
from the Dreadnode ingress Secret.
Create the bundle and configure Dreadnode
Section titled “Create the bundle and configure Dreadnode”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:
export NAMESPACE=dreadnodekubectl -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-caEnter the Embedded Cluster shell and create or update the Secret in kotsadm:
sudo ./dreadnode shellexport NAMESPACE=kotsadmkubectl -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 -Open Config → Advanced Helm Values → Helm Values and paste:
global: caBundle: enabled: true source: secret name: dreadnode-outbound-caSelect Save config, run the preflight checks, then select Deploy.
A ConfigValues file cannot create the referenced Kubernetes Secret. Complete the initial
Embedded Cluster installation, enter sudo ./dreadnode shell, create
dreadnode-outbound-ca in kotsadm, then apply the Helm block from the Admin Console tab.
If separate automation creates the Secret before the application chart is deployed, put the
same block in advanced_helm_values:
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: advanced_helm_values: value: | global: caBundle: enabled: true source: secret name: dreadnode-outbound-caThe 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.
Know which connections use the bundle
Section titled “Know which connections use the bundle”global.caBundle is intentionally a workload cascade, not a cluster-wide trust-store change:
| Connection | Where to configure trust | Notes |
|---|---|---|
Bundled LiteLLM → model provider api_base | global.caBundle or dreadnode-litellm.caBundle | LiteLLM’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 service | global.caBundle or dreadnode-api.caBundle | The API receives the mounted bundle and standard variables for HTTP, AWS, and gRPC clients. See the exceptions below. |
| Dreadnode API → external LiteLLM | API setting above | This secures the API-to-proxy connection. Configure provider trust on the external LiteLLM deployment itself. |
| Frontend → Dreadnode API | Nothing to configure by default | Server-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 ingress | Client TLS trust | Install the ingress issuer in the workstation trust store. |
| OpenSandbox runtime → Dreadnode, LiteLLM, or another HTTPS service | global.caBundle | The 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 registry | Node or container-runtime configuration | The 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.
Limit the bundle to one workload
Section titled “Limit the bundle to one workload”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-caUse 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.
Mount an additional file in LiteLLM
Section titled “Mount an additional file in LiteLLM”For a provider that needs a separate file or trust-store format, create an operator-managed object:
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.pemSSL_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.
Verify and rotate the bundle
Section titled “Verify and rotate the bundle”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:
export NAMESPACE=dreadnode # kotsadm for Embedded Clusterexport 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:
kubectl -n "$NAMESPACE" rollout restart deployment \ -l app.kubernetes.io/name=dreadnode-apikubectl -n "$NAMESPACE" rollout restart deployment \ -l app.kubernetes.io/name=dreadnode-litellmkubectl -n "$NAMESPACE" rollout restart deployment \ -l app.kubernetes.io/name=dreadnode-frontendThe 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.