Skip to content

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 two client-facing hostnames:

  • dreadnode.example.com for the frontend and API
  • storage.dreadnode.example.com for the MinIO S3 API

The certificate presented by your ingress controller must include both names in its Subject Alternative Name (SAN) extension. Use both exact names: standard wildcard certificates match only one DNS label and usually do not cover both a configured domain and its nested storage subdomain.

You need:

  • DNS records for both hostnames 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.

For production, request a certificate from a public certificate authority or your organization’s internal certificate authority. Include these SANs:

DNS:dreadnode.example.com
DNS:storage.dreadnode.example.com

The SANs, rather than only the certificate’s Common Name, determine which hostnames the certificate covers.

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:

Terminal window
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"

Confirm that both SANs are present:

Terminal window
openssl x509 -in tls.crt -noout -ext subjectAltName

The TLS Secret must be in the same namespace as the Dreadnode Ingress resources.

Set NAMESPACE to the namespace used by your Helm release:

Terminal window
export NAMESPACE=dreadnode
kubectl get namespace "$NAMESPACE"

If you installed into another namespace, use that namespace instead.

Run the shell command from the directory that contains the dreadnode installer:

Terminal window
sudo ./dreadnode shell
export NAMESPACE=kotsadm
kubectl 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 dreadnode-tls from the certificate and key:

Terminal window
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:

kubernetes.io/tls
kubectl -n "$NAMESPACE" get secret dreadnode-tls \
-o jsonpath='{.type}{"\n"}'

Inspect the certificate stored in the Secret:

Terminal window
kubectl -n "$NAMESPACE" get secret dreadnode-tls \
-o jsonpath='{.data.tls\.crt}' |
base64 -d |
openssl x509 -noout -subject -issuer -ext subjectAltName

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

Install or upgrade the release with that overlay:

Terminal window
helm upgrade --install dreadnode \
oci://registry.replicated.com/dreadnode/dreadnode \
--version <version> \
--namespace "$NAMESPACE" \
--create-namespace \
-f values.yaml

If the application was already deployed, saving is not enough by itself. Select Deploy to apply the updated Ingress configuration.

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 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: traefik

This 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: traefik

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: none

First, confirm that every Dreadnode Ingress references the Secret:

Terminal window
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.

Then inspect the certificate selected by the ingress controller:

Terminal window
export DOMAIN=dreadnode.example.com
echo | openssl s_client \
-connect "${DOMAIN}:443" \
-servername "${DOMAIN}" 2>/dev/null |
openssl x509 -noout -subject -issuer -ext subjectAltName

Repeat with storage.dreadnode.example.com. The -servername argument sends Server Name Indication (SNI), which is how the ingress controller selects a certificate.

Then confirm that port 80 redirects rather than serving the application:

Terminal window
curl -sSI http://dreadnode.example.com/ | head -n 1
# HTTP/1.1 308 Permanent Redirect

Repeat 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:

Terminal window
curl https://dreadnode.example.com/api/v1/health
# {"status":"ok"}

For a self-signed certificate, pass its trust anchor explicitly:

Terminal window
curl --cacert tls.crt https://dreadnode.example.com/api/v1/health

If DNS is not available yet, preserve the hostname and SNI while directing the request to the ingress IP:

Terminal window
curl --cacert tls.crt \
--resolve dreadnode.example.com:443:<ingress-ip> \
https://dreadnode.example.com/api/v1/health

Understand 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 Host header 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:

  1. The Secret is in the Dreadnode namespace.
  2. The Secret type is kubernetes.io/tls.
  3. Every Ingress references the correct Secret name.
  4. The certificate SANs include the exact requested hostname.
  5. 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.

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.tls
  • dreadnode-frontend.ingress.tls
  • dreadnode-base.minio.apiIngress.tls
  • dreadnode-litellm.ingress.tls

Subchart-local values always override the global cascade.

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.

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.

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: none

This makes Dreadnode generate https:// URLs without requiring an in-cluster TLS Secret. Configure and verify the certificate at the external termination point. The HTTP-to-HTTPS redirect is also yours to configure there; confirm the external listener does not proxy port 80 through to the cluster.