TLS certificates
Configure and verify HTTPS for Dreadnode on Helm or Embedded Cluster installations.
TLS is chart configuration, applied by a redeploy. The certificate itself lives in a
Kubernetes Secret you create either way; the reference to it is a Helm value
(global.tls.secretName), an Admin Console field, or the tls_secret_name KOTS ConfigValues item.
All three paths are covered below.
Dreadnode uses three client-facing authorities:
dreadnode.example.comfor the frontend and APIstorage.dreadnode.example.comfor the MinIO S3 API*.sandbox.dreadnode.example.comfor OpenSandbox runtime HTTP and WebSocket traffic
The certificate presented by your ingress controller must cover all three in its
Subject Alternative Name (SAN) extension. Use the two exact names plus the one wildcard:
a wildcard matches only a single DNS label, so *.dreadnode.example.com covers
storage.dreadnode.example.com but neither the apex nor a runtime hostname two labels
deep.
One Secret can carry all three SANs, and the sandbox gateway falls back to
global.tls.secretName by default. Keep them separate — with
global.sandbox.tls.secretName — when the wildcard comes from a different issuer or
renewal process. Installs running E2B sandboxes need only the first two names.
Before you begin
Section titled “Before you begin”You need:
- DNS records for all three authorities pointing at your ingress IP
- A PEM-encoded certificate chain and matching private key
- Cluster access to the namespace where Dreadnode is installed
Use the full certificate chain, with the leaf certificate first followed by any
intermediate certificates, as tls.crt. Use the unencrypted private key as tls.key.
Use an existing certificate
Section titled “Use an existing certificate”For production, request a certificate from a public certificate authority or your organization’s internal certificate authority. Include these SANs:
DNS:dreadnode.example.comDNS:storage.dreadnode.example.comDNS:*.sandbox.dreadnode.example.comThe SANs, rather than only the certificate’s Common Name, determine which hostnames the certificate covers. Public certificate authorities issue wildcard SANs only after a DNS-01 challenge, so plan for a DNS provider your issuer can automate against.
Generate a self-signed certificate for evaluation
Section titled “Generate a self-signed certificate for evaluation”For a temporary test environment, generate a self-signed certificate:
openssl req -x509 -nodes -newkey rsa:2048 -sha256 -days 365 \ -keyout tls.key \ -out tls.crt \ -subj "/CN=dreadnode.example.com" \ -addext "subjectAltName=DNS:dreadnode.example.com,DNS:storage.dreadnode.example.com,DNS:*.sandbox.dreadnode.example.com"Confirm that all three SANs are present:
openssl x509 -in tls.crt -noout -ext subjectAltNameAccess the install namespace
Section titled “Access the install namespace”The TLS Secret must be in the same namespace as the Dreadnode Ingress resources.
Set NAMESPACE to the namespace used by your Helm release:
export NAMESPACE=dreadnodekubectl get namespace "$NAMESPACE"If you installed into another namespace, use that namespace instead.
Embedded Cluster
Section titled “Embedded Cluster”Run the shell command from the directory that contains the dreadnode installer:
sudo ./dreadnode shellexport NAMESPACE=kotsadmkubectl get namespace "$NAMESPACE"Embedded Cluster installs Dreadnode in kotsadm. Run kubectl directly inside the
shell — see Access Kubernetes if
kubectl tries to connect to 127.0.0.1:8080.
Create or update the TLS Secret
Section titled “Create or update the TLS Secret”Create dreadnode-tls from the certificate and key:
kubectl -n "$NAMESPACE" create secret tls dreadnode-tls \ --cert=/path/to/tls.crt \ --key=/path/to/tls.key \ --dry-run=client -o yaml | kubectl apply -f -The dry-run and apply pattern creates the Secret when it is absent and updates it when it already exists.
Confirm that Kubernetes stored it as a TLS Secret:
kubectl -n "$NAMESPACE" get secret dreadnode-tls \ -o jsonpath='{.type}{"\n"}'Inspect the certificate stored in the Secret:
kubectl -n "$NAMESPACE" get secret dreadnode-tls \ -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -issuer -ext subjectAltNameConfigure Dreadnode
Section titled “Configure Dreadnode”Set the domain, scheme, Secret name, and redirect policy in your values overlay:
global: domain: dreadnode.example.com scheme: https tls: secretName: dreadnode-tls ingress: httpsRedirect: auto sandbox: # Omit to reuse global.tls.secretName for *.sandbox.dreadnode.example.com. tls: secretName: dreadnode-sandbox-tlsInstall or upgrade the release with that overlay:
helm upgrade --install dreadnode \ oci://registry.replicated.com/dreadnode/dreadnode \ --version <version> \ --namespace "$NAMESPACE" \ --create-namespace \ -f values.yamlYou can enable HTTPS on the first deployment; you do not need to deploy over HTTP first.
- Set Config → Dreadnode Identity → URL Scheme to HTTPS.
- Set Config → Networking & TLS → TLS Certificate Secret Name to
dreadnode-tls. - Leave Config → Sandbox Runtime → Sandbox TLS Certificate Secret Name empty when
dreadnode-tlsalready covers*.sandbox.dreadnode.example.com; otherwise name the Secret that does. - Keep HTTP to HTTPS Redirect set to Automatic for Embedded Cluster’s Traefik.
- Select Save config, then Deploy.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: domain: value: dreadnode.example.com scheme: value: https tls_secret_name: value: dreadnode-tls https_redirect: value: auto # Omit to reuse tls_secret_name for the sandbox wildcard. sandbox_tls_secret_name: value: dreadnode-sandbox-tlsIf the application was already deployed, saving is not enough by itself. Select Deploy to apply the updated Ingress configuration.
Redirect HTTP to HTTPS
Section titled “Redirect HTTP to HTTPS”global.scheme controls the URLs Dreadnode generates — frontend links, OAuth redirect
URIs, and presigned storage endpoints. On its own it does not change what your ingress
controller does on port 80.
Without a redirect, port 80 keeps serving a fully functional application. Because the
frontend makes relative /api requests, a bookmark or an open tab on http:// loads a
working UI whose API traffic, including authentication cookies, crosses the network in
cleartext. Configure a redirect whenever scheme is https.
Traefik
Section titled “Traefik”Traefik is the tested ingress controller and the one Embedded Cluster installs. When
global.scheme is https and the ingress class is traefik, the chart renders a
redirect automatically:
global: scheme: https ingress: className: traefikThis creates a Traefik Middleware that answers port 80 with HTTP 308 to the
equivalent https:// URL, and attaches it to the frontend, API, and MinIO Ingresses.
The 308 status preserves the request method, host, path, and query string, so an
in-flight POST is replayed as a POST rather than downgraded to a GET.
Embedded Cluster sets the ingress class to traefik and the redirect policy to auto by default,
so selecting HTTPS in the Admin Console is sufficient.
If Traefik is your cluster’s default IngressClass and you prefer to leave className
empty, request the redirect explicitly:
global: ingress: httpsRedirect: traefikOther ingress controllers
Section titled “Other ingress controllers”Embedded Cluster uses its bundled Traefik controller. This section applies to Helm installs that use another ingress controller.
The chart cannot render a portable redirect, and applying a Traefik Middleware to a cluster
running a different controller fails the install. Supply the equivalent annotation yourself. For
ingress-nginx:
global: scheme: https ingress: className: nginx annotations: nginx.ingress.kubernetes.io/ssl-redirect: 'true'Annotations set here apply to the frontend, API, and MinIO Ingresses.
If you handle the redirect outside the cluster, record that so the install notes stop reporting it as unconfigured:
global: ingress: httpsRedirect: noneVerify HTTPS
Section titled “Verify HTTPS”First, confirm that every Dreadnode Ingress references the Secret:
kubectl -n "$NAMESPACE" get ingress \ -o custom-columns='NAME:.metadata.name,HOSTS:.spec.rules[*].host,TLS_SECRET:.spec.tls[*].secretName'Each Dreadnode Ingress should show dreadnode-tls in the TLS_SECRET column. The
dreadnode-sandbox-server-gateway Ingress shows the sandbox Secret when you set one, and
dreadnode-tls otherwise.
Then inspect the certificate selected by the ingress controller:
export DOMAIN=dreadnode.example.com
echo | openssl s_client \ -connect "${DOMAIN}:443" \ -servername "${DOMAIN}" 2>/dev/null | openssl x509 -noout -subject -issuer -ext subjectAltNameRepeat with storage.dreadnode.example.com and with any hostname under the sandbox
wildcard — the name need not belong to a live sandbox, since the certificate is selected
before routing:
echo | openssl s_client \ -connect "sandbox-check.sandbox.${DOMAIN}:443" \ -servername "sandbox-check.sandbox.${DOMAIN}" 2>/dev/null | openssl x509 -noout -subject -issuer -ext subjectAltNameThe -servername argument sends Server Name Indication (SNI), which is how the ingress
controller selects a certificate. If the sandbox hostname returns the ingress controller’s
default certificate instead of yours, the wildcard SAN is missing or the sandbox Secret
points somewhere else.
Then confirm that port 80 redirects rather than serving the application:
curl -sSI http://dreadnode.example.com/ | head -n 1# HTTP/1.1 308 Permanent RedirectRepeat for storage.dreadnode.example.com. A 200 from either hostname means the
redirect is not in place and the application is still reachable over cleartext — see
Redirect HTTP to HTTPS.
Finally, check the API:
curl https://dreadnode.example.com/api/v1/health# {"status":"ok"}For a self-signed certificate, pass its trust anchor explicitly:
curl --cacert tls.crt https://dreadnode.example.com/api/v1/healthIf DNS is not available yet, preserve the hostname and SNI while directing the request to the ingress IP:
curl --cacert tls.crt \ --resolve dreadnode.example.com:443:<ingress-ip> \ https://dreadnode.example.com/api/v1/healthUnderstand the Traefik default certificate
Section titled “Understand the Traefik default certificate”Traefik chooses a certificate during the TLS handshake, before it evaluates HTTP Ingress host and path rules. It uses the requested SNI hostname to make that choice.
If you open https://<ingress-ip> directly:
- the IP does not match a DNS-only certificate, so Traefik can present its default certificate
- the HTTP
Hostheader does not match the Dreadnode Ingress, so Traefik can return its default 404
Both results are expected for an IP-based request. Test with the configured domain, or
use curl --resolve as shown above.
Seeing TRAEFIK DEFAULT CERT when requesting the configured domain is not expected.
Check that:
- The Secret is in the Dreadnode namespace.
- The Secret type is
kubernetes.io/tls. - Every Ingress references the correct Secret name.
- The certificate SANs include the exact requested hostname.
- You deployed the configuration after entering the Secret name.
A self-signed issuer alone does not cause Traefik to use its default certificate. A missing Secret, invalid certificate, wrong namespace, or hostname mismatch can.
Per-ingress TLS
Section titled “Per-ingress TLS”global.tls.secretName covers the common case: one certificate for both hostnames. If
your API and MinIO traffic terminate on different load balancers with different
certificates, leave it empty and set per-subchart values:
dreadnode-api.ingress.tlsdreadnode-frontend.ingress.tlsdreadnode-base.minio.apiIngress.tlsdreadnode-litellm.ingress.tlsdreadnode-sandbox-server.gateway.ingress.tlsSecretName
Subchart-local values always override the global cascade. The sandbox gateway reads
global.sandbox.tls.secretName before global.tls.secretName, so most installs set the
global rather than the subchart value.
Helm operators set these paths in their values overlay. On Embedded Cluster, put the same
umbrella-chart values under Advanced Helm Values or in the advanced_helm_values ConfigValues
item. See Use the full Helm values
surface.
Authentication cookies
Section titled “Authentication cookies”Cookie transport security follows global.scheme, which the chart passes to the API as
PROTOCOL. Under scheme: https the API sets the Secure attribute on the
access_token and refresh_token cookies, on the matching logout headers, and on the
OAuth session cookie. Browsers then refuse to send any of them over a cleartext
connection.
This is independent of the deployment profile. Self-hosted installs intentionally run
with ENVIRONMENT=local, which governs unrelated concerns such as API documentation
exposure and database pooling; it does not affect cookie security.
Under scheme: http these cookies are not marked Secure, because a browser would
otherwise refuse to store them and every login would fail silently. This is why an HTTPS
install reached over http:// cannot log in — see
Login fails silently.
There is no separate cookie-security setting to configure. If TLS terminates upstream of
the cluster, keep scheme: https and set global.tls.skipCheck: true as described in
TLS terminated outside the cluster — the scheme
still describes how browsers reach you, so cookies are marked Secure automatically.
TLS terminated outside the cluster
Section titled “TLS terminated outside the cluster”If a load balancer or service mesh terminates TLS before traffic reaches the cluster, you can omit the Kubernetes TLS Secret:
global: scheme: https tls: skipCheck: true ingress: httpsRedirect: noneSet Config → Dreadnode Identity → URL Scheme to HTTPS and Config → Networking & TLS → HTTP to HTTPS Redirect to None. Under Advanced Helm Values, add:
global: tls: skipCheck: trueapiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: scheme: value: https https_redirect: value: none advanced_helm_values: value: | global: tls: skipCheck: trueThis makes Dreadnode generate https:// URLs without requiring an in-cluster TLS Secret. Configure
and verify the certificate at the external termination point, and include
*.sandbox.dreadnode.example.com in it — Dreadnode still hands browsers https:// runtime URLs
under that authority. The HTTP-to-HTTPS redirect is also yours to configure there; confirm the
external listener does not proxy port 80 through to the cluster.