Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mutual TLS

Prerequisites

Have certificates on hands:

Have a mir deployment ready to be used:

Steps

If you have your own, skip to Step 2.

Step 1: Generate certificates

This will:

  • generate a CA private key and certificate
    • the certificate must be installed on Mir clients (CLI, Devices, Modules)
  • generate a Server private key and certificate and sign with CA
    • must be passed on Nats Message Bus
  • generate multiple Client private keys and certificates and sign with CA
    • one for Mir Server
    • one for each operator (CLI)
    • one for each devices
# Generating CA private key
openssl genrsa -out ca.key 4096 2>/dev/null

# Generating CA certificate
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
    -subj "/C=US/ST=CA/L=San Francisco/O=Mir IoT/OU=Certificate Authority/CN=Mir Root CA" \
    2>/dev/null

# Generating Server private key
openssl genrsa -out tls.key 4096 2>/dev/null

# Generating Server certificate request
openssl req -new -key tls.key -out server.csr \
    -subj "/C=US/ST=CA/L=San Francisco/O=Mir IoT/OU=NATS Server/CN=localhost" \
    2>/dev/null

# Create extensions file for SAN (Subject Alternative Names)
# ! Add your Host or IP to the list
cat > server-ext.cnf <<EOF
subjectAltName = DNS:localhost,DNS:*.localhost,DNS:local-nats,DNS:nats,IP:127.0.0.1,IP:::1
EOF

# Sign the server certificate
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key \
    -CAcreateserial -out tls.crt -extfile server-ext.cnf 2>/dev/null

# Clean up
rm -f server.csr server-ext.cnf

# Generating Mir Module client private key
openssl genrsa -out mir-module.key 4096 2>/dev/null

# Generating Mir Module client certificate request
openssl req -new -key mir-module.key -out mir-module.csr \
    -subj "/C=US/ST=CA/L=San Francisco/O=Mir IoT/OU=Services/CN=mir-module" \
    2>/dev/null

# Sign the Mir Module client certificate
openssl x509 -req -days 365 -in mir-module.csr \
    -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out mir-module.crt 2>/dev/null

# Clean up
rm -f mir-module.csr

# Generating CLI client private key
openssl genrsa -out mir-cli.key 4096 2>/dev/null

# Generating CLI client certificate request
openssl req -new -key mir-cli.key -out mir-cli.csr \
    -subj "/C=US/ST=CA/L=San Francisco/O=Mir IoT/OU=Operators/CN=mir-cli" \
    2>/dev/null

# Sign the CLI client certificate
openssl x509 -req -days 365 -in mir-cli.csr \
    -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out mir-cli.crt 2>/dev/null

# Clean up
rm -f mir-cli.csr

# Generating Device client private key
openssl genrsa -out mir-device.key 4096 2>/dev/null

# Generating Device client certificate request
openssl req -new -key mir-device.key -out mir-device.csr \
    -subj "/C=US/ST=CA/L=San Francisco/O=Mir IoT/OU=Devices/CN=mir-device-001" \
    2>/dev/null

# Sign the Device client certificate
openssl x509 -req -days 365 -in mir-device.csr \
    -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out mir-device.crt 2>/dev/null

Step 2: Configure Nats Server

Docker

Copy ca.crt, tls.crt and tls.key under ./mir-compose/natsio/certs.

In the ./mir-compose/natsio/config.conf, update with the following:

# TLS/Security
tls: {
  cert_file: "/etc/nats/certs/tls.crt"
  key_file: "/etc/nats/certs/tls.key"
  # Required for mTLS
  ca_file: "/etc/nats/certs/ca.crt"
  verify: true
  timeout: 2
}
# WebSocket Configuration with TLS
websocket: {
  listen: 0.0.0.0:9222
  tls: {
    cert_file: "/etc/nats/certs/tls.crt"
    key_file:  "/etc/nats/certs/tls.key"
  }
  no_tls: false
  compress: true
}

Update Compose to pass the certificates:

services:
  nats:
    ...
    volumes:
      ...
      - ./certs:/etc/nats/certs
    ...

Start server docker compose up.

Kubernetes

Create two Kubernetes Secrets, CA and TLS:

# Directly to the cluster
kubectl create secret tls nats-tls-secret --cert=tls.crt --key=tls.key
kubectl create secret generic nats-ca-secret --from-file=ca.crt=ca.crt
# As file
kubectl create secret tls nats-tls-secret --cert=tls.crt --key=tls.key -o yaml --dry-run=client > nats-tls.secret.yaml
kubectl create secret generic nats-ca-secret --from-file=ca.crt=ca.crt -o yaml --dry-run=client > nats-ca.secret.yaml

Update values file:

## NATS
nats:
  config:
    nats:
      tls:
        enabled: true
        secretName: nats-tls-secret
        dir: /etc/nats-certs
        cert: tls.crt
        key: tls.key
        merge:
          verify: true # True for Mutual TLS, false for ServerOnly TLS
          timeout: 2
  # Reference CA for mTLS
  tlsCA:
    enabled: true
    secretName: nats-ca-secret
    dir: /etc/nats-ca-certs
    key: ca.crt

Step 3: Configure Cockpit HTTPS (Optional)

By default Cockpit serves on plain HTTP. If you want the web UI over HTTPS (so the browser uses wss:// for its NATS WebSocket connection), follow the steps for your deployment.

Docker

Configure in ./mir-compose/mir/local-config.yaml:

mir:
  http:
    port: 3015
    tlsCert: "/home/mir/certs/tls.crt"
    tlsKey: "/home/mir/certs/tls.key"

Mount the certs into the Mir container in ./mir-compose/mir/compose.yaml:

services:
  mir:
    volumes:
      - ./local-config.yaml:/home/mir/.config/mir/mir.yaml
      - ./local-contexts.yaml:/home/mir/.config/mir/cli.yaml
      - ./certs:/home/mir/certs

Update ./mir-compose/mir/local-contexts.yaml so Cockpit connects over secure WebSocket:

contexts:
  - name: local
    target: nats+tls://localhost:4222
    webTarget: wss://localhost:9222 # Note 'wss' for secure connection
    grafana: localhost:3000

Restart: docker compose down && docker compose up. Cockpit is now available at https://localhost:3015.

Kubernetes

Option A and B are mutually exclusive. With ingress TLS (Option A), NATS WebSocket TLS is terminated at the ingress. Without ingress (Option B), NATS WebSocket must serve TLS directly. Mutual TLS is not supported for browser clients.

Option A - Via Ingress (recommended)

The ingress controller terminates TLS. Mir and NATS run plain HTTP/WS internally.

kubectl create secret tls mir-http-tls-secret --cert=tls.crt --key=tls.key
ingress:
  enabled: true
  className: "traefik"  # or "nginx", etc.
  hosts:
    - host: mir.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: mir-http-tls-secret
      hosts:
        - mir.example.com

nats:
  config:
    websocket:
      ingress:
        enabled: true
        hosts:
          - mir.example.com
        path: /nats-ws
        pathType: Prefix
        className: "traefik"
        tlsSecretName: mir-http-tls-secret

config:
  contexts:
    - name: "production"
      webTarget: "wss://mir.example.com/nats-ws"

Option B - Via App (no ingress)

Mir and NATS serve TLS directly. Use when there is no ingress controller.

kubectl create secret tls mir-http-tls-secret --cert=tls.crt --key=tls.key
kubectl create secret tls nats-ws-tls-secret --cert=tls.crt --key=tls.key
mirHttpTlsSecretRef: mir-http-tls-secret

nats:
  config:
    websocket:
      tls:
        enabled: true
        secretName: nats-ws-tls-secret
        dir: /etc/nats-certs/websocket
        cert: tls.crt
        key: tls.key

config:
  contexts:
    - name: "production"
      webTarget: "wss://<host>:31922"  # NATS WebSocket NodePort

Apply with Helm:

helm upgrade <release-name> <chart-path> -f values.yaml

Step 4: Install Certificates on Module

Docker

Edit ./mir-compose/mir/local-config.yaml with the RootCA, certificate and private key paths under nats.*. Mount the files in ./mir-compose/mir/compose.yaml.

nats:
  url: "nats+tls://local_mir_support-nats-1:4222"
  rootCA: "/home/mir/certs/ca.crt"
  tlsCert: "/home/mir/certs/mir-module.crt"
  tlsKey: "/home/mir/certs/mir-module.key"
# Restart server
docker compose down
docker compose up

Kubernetes

Create two Kubernetes Secret, CA and TLS:

# Directly to the cluster
kubectl create secret tls mir-tls-secret --cert=tls.crt --key=tls.key
kubectl create secret generic mir-ca-secret --from-file=ca.crt=ca.crt
# As file
kubectl create secret tls mir-tls-secret --cert=tls.crt --key=tls.key -o yaml --dry-run=client > mir-tls.secret.yaml
kubectl create secret generic mir-ca-secret --from-file=ca.crt=ca.crt -o yaml --dry-run=client > mir-ca.secret.yaml

Update values file:

## MIR
caSecretRef: mir-ca-secret
tlsSecretRef: mir-tls-secret

Step 5: Install the Certificates on the Clients

CLI

Edit CLI configuration file to add the RootCA mir tools config edit:

- name: local
  target: nats+tls://localhost:4222
  webTarget: wss://localhost:9222
  grafana: localhost:3000
  sec:
    rootCA: <path>/ca.crt
    tlsKey: <path>/mir-cli.key
    tlsCert: <path>/mir-cli.crt

Run mir dev ls to validate.

Device

There are a few options to load the RootCA and Certificate files with the DeviceSDK.

# Using Builder with fix path
device := mir.Builder().
    RootCA("/<path>/ca.crt").
    ClientCertificateFile("/<path>/mir-device.crt", "/<path>/mir-device.key").
    DeviceId("dev1").
    Build()
# Using Builder with default lookup
#   ./ca.crt
#   ~/.config/mir/ca.crt
#   /etc/mir/ca.crt
#
#   ./tls.crt & ./tls.key
#   ~/.config/mir/tls.crt & ~/.config/mir/tls.key
#   /etc/mir/tls.crt & /etc/mir/tls.key
device := mir.NewDevice().
    Target("nats://nats.example.com:4222").
    DefaultRootCAFile().
    DefaultClientCertificateFile().
    Build()

It is also possible to load the RootCA from the config file:

# Using Builder with config file
device := mir.Builder().
    DefaultConfigFile().
    Build()
mir:
  rootCA: "<path>/ca.crt"
  tlsKey: "<path>/mir-device.key"
  tlsCert: "<path>/mir-device.crt"
  device:
    id: "dev1"

Run the device and no TLS errors should be displayed. Now run mir dev ls and you should see:

➜ mir dev ls
NAMESPACE/NAME                                DEVICE_ID        STATUS     LAST_HEARTHBEAT      LAST_SCHEMA_FETCH    LABELS
default/dev1                                  dev1             online     2025-09-18 16:16:27  2025-09-18 16:15:18

Completed

Congratulation, you now have Mutual TLS configured.