Skip to main content
Version: v3.9.0

Router and NATS: workloads, TLS, configuration, and Controller registration

This document describes how the operator creates the Router Deployment and the NATS StatefulSet, how certificates and configuration files are produced, and how the operator registers the default router and the NATS hub with the Controller API.

TLS bring-your-own CA, secret immutability, and edge trust are also covered in Securing the cluster. This page is the workload-and-config view of the same mechanisms.

CR fields that feed these workloads (services.router, ingresses.router, replicas.nats, nats.jetStream, …) are in the ControlPlane CRD reference.

Where this runs in reconcile​

While the ControlPlane condition is deploying or updating, three reconcilers run in parallel:

ReconcilerWorkloadRegisters with Controller?
reconcileRouterDeployment router, Service router, ConfigMap iofog-router, TLS SecretsNo
reconcileNatsStatefulSet nats, Services, ConfigMaps, bootstrap Secrets, TLS SecretsNo (calls Controller only to read NATS bootstrap)
reconcileIofogControllerController DeploymentYes : default router, NATS hub, and CA import

The operator process, status machine, and Controller login are in How the operator works. Registration happens only after the Controller API is reachable and the operator has logged in. Router and NATS pods can exist before registration finishes; edges use the addresses stored in the Controller after registration succeeds.

NATS reconcile exits immediately when spec.nats.enabled is false (or when NATS is treated as disabled). Omitting spec.nats means NATS is enabled.


Router​

Kubernetes objects​

KindNameNotes
Deploymentrouter1 replica. Command: /home/skrouterd/bin/router
ServicerouterType from spec.services.router.type (default LoadBalancer)
ConfigMapiofog-routerKey skrouterd.json
ServiceAccount / Role / RoleBindingrouterWatch pods, configmaps, secrets, services; leases
Secretssee Certificates

A second Deployment router-2 exists in code only when an internal HA flag is true. That flag is not wired to the CR (spec.router is unused). A standard ControlPlane creates one router.

Pod labels include application: interior-router, skupper.io/component: router, skupper.io/type: site, plus operator standard labels. Prometheus annotations scrape port 9090.

Service ports​

Service port namePortContainerRole
router-message5671amqpsMessaging (AMQPS)
router-interior55671inter-routerInter-router
router-edge45671edgeEdge listeners

HTTP health and metrics listen on container port 9090 (/healthz). That port is not on Service router.

Address required before certificates​

reconcileRouter creates the Service first, then needs an external hostname or IP for certificate SANs:

ConditionAddress used
services.router.type is LoadBalancerLoadBalancer hostname or IP on Service router (reconcile waits)
Otherwisespec.ingresses.router.address

If the Service is not a LoadBalancer and ingresses.router.address is empty, reconcile fails with missing Proxy.Router data for non LoadBalancer Router service.

Router certificates​

The operator looks up these Secrets in the ControlPlane namespace. If any is missing, it generates the missing ones with GenerateSecret (RSA 2048, 5 years, type kubernetes.io/tls). If all four already exist, it reuses them and does not regenerate.

SecretRoleSigned by
router-site-caSite CA (self-signed when the operator creates it)itself
default-router-local-caLocal CA (self-signed when the operator creates it)itself
router-site-serverSite server certrouter-site-ca
router-local-serverLocal server certdefault-router-local-ca

Server certificate subject and SANs:

SecretCommon nameDNS / IP names
router-site-serveriofog-routerrouter.<namespace>.svc.cluster.local and the external address
router-local-serveriofog-router-localsame

Each Secret has tls.crt, tls.key, and ca.crt. For a CA Secret, ca.crt is the same as tls.crt. For a server Secret, ca.crt is the signing CA.

Bring your own CA: create router-site-ca and/or default-router-local-ca (and optionally the server Secrets) before reconcile. The operator signs missing server certs with the CA that is already in the cluster. Existing Secrets are not overwritten on later reconciles. Details and rotation: Securing the cluster : Router TLS.

The Deployment mounts only the server Secrets:

VolumeMount path
Secret router-site-server/etc/skupper-router-certs/router-site-server
Secret router-local-server/etc/skupper-router-certs/router-local-server
ConfigMap iofog-router/tmp/skrouterd.json (key skrouterd.json)

Router configuration (skrouterd.json)​

The operator builds JSON from controllers/controlplanes/router/config.go and stores it in ConfigMap iofog-router, key skrouterd.json. The container reads it via:

EnvValue
QDROUTERD_CONF/tmp/skrouterd.json
QDROUTERD_CONF_TYPEjson
SSL_PROFILE_PATH/etc/skupper-router-certs
SKUPPER_SITE_IDdefault-router
SKUPPER_PLATFORMkubernetes
QDROUTERD_AUTO_MESH_DISCOVERYQUERY
APPLICATION_NAMErouter
POD_NAMESPACE / POD_IPdownward API

Placeholders in the template are filled with fixed ports and the ControlPlane namespace (site namespace field). They are not taken from ingresses.router.* ports. Those ingress ports are used only when registering the router with the Controller (next section).

Config entityNameWhat it does
routerid default-router, mode interiorInterior router identity. Metadata includes "iofog-config": "1.0.0"
sitedefault-routerPlatform kubernetes, namespace = ControlPlane namespace
sslProfilesystem-defaultSystem CA bundle /etc/pki/tls/certs/ca-bundle.crt
sslProfilerouter-site-servertls.crt / tls.key / ca.crt under /etc/skupper-router-certs/router-site-server
sslProfilerouter-local-serversame layout under router-local-server
listeneriofog-router-edgerole edge, port 45671, sslProfile router-site-server, SASL EXTERNAL, authenticatePeer: true
listeneramqphost localhost, port 5672, no TLS (in-pod only; not on the Service)
listeneramqpsport 5671, sslProfile router-local-server, SASL EXTERNAL
listener@9090HTTP health and metrics on 9090
listeneriofog-router-inter-routerrole inter-router, port 55671, sslProfile router-site-server
addressprefix mcmulticast distribution
logROUTER_COREerror+

ConfigMap updates: if iofog-router already exists, the operator merges. Entries of type router, site, address, and log are replaced from the new template. sslProfile and listener entries are replaced only when their names match the table above. Other existing entries are kept. If metadata iofog-config versions match, the existing document is left unchanged.

Registering the default router​

This runs in controller reconcile, not in router reconcile, after login.

The operator chooses a proxy and calls the Controller client PutDefaultRouter (default-router API). Payload:

API fieldSource
hostLoadBalancer address of Service router, or spec.ingresses.router.address
messaging portLoadBalancer path: 5671. Ingress path: ingresses.router.messagePort (0 means the API receives 0; set the port explicitly when using ingress)
inter-router portLoadBalancer: 55671. Ingress: ingresses.router.interiorPort
edge portLoadBalancer: 45671. Ingress: ingresses.router.edgePort

Sample CR comments use 5671 / 55671 / 45671 for the ingress block so the registered ports match the listeners in skrouterd.json.

Edges and tools then learn the router endpoint from the Controller, not by reading the Kubernetes Service directly.


NATS​

Kubernetes objects​

KindNameNotes
StatefulSetnatsserviceName: nats-headless. Replicas = spec.replicas.nats, minimum 2
Servicenats-headlessClusterIP: None. All NATS ports. Pod DNS nats-0.nats-headless, nats-1.nats-headless, …
ServicenatsType from spec.services.nats (reconcile default LoadBalancer if type is empty). Ports: cluster, leaf, mqtt
Servicenats-serverType from spec.services.natsServer. Ports: client and monitor only
ConfigMapiofog-nats-configKey server.conf
ConfigMapiofog-nats-jwt-bundleAccount JWT files
PVC templatejs-dataJetStream file store. Default size 10Gi
ServiceAccount / Role / RoleBindingnatsget/list/watch configmaps and secrets

Run-as user/group/fsGroup is 10000.

Ports​

NamePortWhere it is exposed
client4222Headless and nats-server. Not on Service nats
cluster6222Headless and Service nats
leaf7422Headless and Service nats
mqtt8883Headless and Service nats
monitor8222Headless and nats-server. HTTP /healthz?js-enabled-only=true

These ports are compiled into the NATS container env and into server.conf. spec.ingresses.nats.*Port does not change the in-cluster listeners. Those fields are used when registering the hub (and the leaf advertise host:port uses the ingress leaf port when it is greater than 0).

Bootstrap from the Controller (before the StatefulSet)​

NATS reconcile talks to the Controller inside the cluster:

http(s)://controller.<namespace>.svc.cluster.local:51121

Scheme is https only when spec.controller.https is true. It logs in, then calls GET /api/v3/nats/bootstrap. The Controller creates operator JWT, system account, and hub system-user credentials. The operator only stores the response:

SecretContents
nats-operator-seedkey seed : operator seed from the API
nats-creds-sys-admin-hubkey admin-hub.creds : base64-decoded creds file

The operator JWT and system-account public key are written into server.conf and the JWT ConfigMap (below). They are not a separate “JWT secret”.

JetStream encryption key Secret nats-jetstream-key-<controlplane-name>, key jsk: 32 random bytes, base64-encoded. Created once; reused if jsk is already present. Mounted at /etc/nats/jetstream and also injected as env JETSTREAM_KEY. JETSTREAM_PREV_KEY is empty on first install.

If the Controller is not ready yet, NATS reconcile requeues. That is expected while the three reconcilers run together.

NATS certificates​

Checked independently. A missing Secret is generated; an existing Secret is left as-is.

SecretRoleSigned by
nats-site-caSite CAself-signed when generated
default-nats-local-caLocal CAself-signed when generated
nats-site-serverClient, cluster, and leaf TLSnats-site-ca
nats-mqtt-serverMQTT TLSdefault-nats-local-ca

Common names: iofog-nats (site server), iofog-nats-mqtt (MQTT).

SANs (same list for both server certs) include:

  • nats-0.nats-headless, nats-1.nats-headless, … for each replica
  • *.nats-headless.<namespace>.svc.cluster.local
  • nats.<namespace>.svc.cluster.local
  • nats-server.<namespace>.svc.cluster.local
  • the external address when known (LoadBalancer host of Service nats, or ingresses.nats.address)

Server Secrets are annotated datasance.com/nats-replicas. If spec.replicas.nats changes, the operator deletes nats-site-server and nats-mqtt-server and recreates them so SANs match the new replica count. CA Secrets are not deleted.

External address for SANs:

ConditionAddress
services.nats.type is LoadBalancerLB address of Service nats
ingresses.nats.address is setthat hostname
neitherSANs are in-cluster names only; hub registration is skipped later if address is still empty

Bring-your-own CA uses the same secret names. See Securing the cluster : NATS TLS.

Mounts:

VolumeMount
Secret nats-site-server/etc/nats/certs/nats-site-server
Secret nats-mqtt-server/etc/nats/certs/nats-mqtt-server
ConfigMap iofog-nats-config/etc/nats/config
ConfigMap iofog-nats-jwt-bundle/tmp/nats/jwt
Secret nats-creds-sys-admin-hub/etc/nats/creds/admin-hub.creds
PVC js-data/home/runner/data

NATS configuration (server.conf)​

ConfigMap iofog-nats-config, key server.conf, is rewritten on every NATS reconcile so replica routes stay current. Container env NATS_CONF=/etc/nats/config/server.conf.

$SELFNAME is left in the file. Env SELFNAME is the pod name (metadata.name), and the NATS image substitutes it so each replica has a distinct server_name.

server.conf settingValue the operator writes
port4222
http_port8222
operatorOperator JWT from bootstrap
system_accountSystem account public key from bootstrap
jetstream.store_dir/home/runner/data
jetstream.domainControlPlane namespace
jetstream.max_memory_storespec.nats.jetStream.memoryStoreSize, default 1G (NATS units; 1Gi in the CR becomes 1G)
jetstream.max_file_storestorage size, default 10G. PVC uses Kubernetes units (default 10Gi)
jetstream.cipher / keychachapoly and the jsk secret value
cluster.nameControlPlane metadata.name
cluster.port6222
cluster.no_advertisetrue
cluster.routesnats://nats-<i>.nats-headless:6222 for each replica, plus any non-ordinal routes already in the ConfigMap (for example routes the Controller added for agents). Operator ordinals are replaced, not duplicated
cluster.tlsfiles under /etc/nats/certs/nats-site-server (ca.crt, tls.crt, tls.key), verify: true, handshake_first: true
leafnodes.port7422
leafnodes.advertise<external-address>:<leafPort> when an external address exists. leafPort is ingresses.nats.leafPort if > 0, otherwise 7422. Omitted when there is no external address
leafnodes.tlssame site server cert as cluster
mqtt.port8883
mqtt.tls/etc/nats/certs/nats-mqtt-server (local CA)
resolvertype: full, dir: /home/runner/nats/jwt, interval 2m

JWT bundle ConfigMap iofog-nats-jwt-bundle: one key <systemAccountPublicKey>.jwt whose value is the system account JWT from bootstrap. Created if missing; an existing ConfigMap is not replaced on the create path (AlreadyExists is ignored).

Relevant container env (fixed by the operator, not CR fields):

EnvValue
NATS_SERVER_MODEserver
NATS_JWT_DIR/home/runner/nats/jwt
NATS_JWT_MOUNT_DIR/tmp/nats/jwt
NATS_TLS_DIR/etc/nats/certs
NATS_CERT_NAMEnats-site-server
NATS_MQTT_CERT_NAMEnats-mqtt-server
NATS_SYS_USER_CRED_PATH/etc/nats/creds/admin-hub.creds
JETSTREAM_KEYfrom Secret key jsk

Scale-down: if the live StatefulSet has more replicas than desired, the operator sets pod annotation kubectl.kubernetes.io/restartedAt so NATS restarts and drops removed cluster routes. Scale-up does not add that annotation.

Registering the NATS hub​

This runs in controller reconcile when NATS is enabled, after the default router is registered.

The operator calls UpsertNatsHub. Host:

ConditionHost
services.nats.type is LoadBalancerLB address of Service nats (waits until assigned)
otherwisespec.ingresses.nats.address

If the host is still empty, the hub is not registered (in-cluster-only NATS with no ingress address).

Ports sent to the API (0 in the CR is replaced by the default):

FieldDefault
server4222
cluster6222
leaf7422
mqtt8883
http (monitor)8222

When using ingress, set ingresses.nats to the hostname and ports that external clients use. Those values are what the Controller stores for agents. They do not retarget the Kubernetes Service ports.

Importing CAs into the Controller​

After hub registration, controller reconcile imports CA Secrets so the Controller catalog can hand trust material to agents. Each call is CreateCA with type: k8s-secret and secretName equal to the Secret name, only if GetCA returns not found.

Secret importedWhen
router-site-caalways
default-router-local-caalways
nats-site-caNATS enabled
default-nats-local-caNATS enabled

The Controller reads the Secret in the ControlPlane namespace. This is the link between the certificates mounted on Router/NATS and the CA list agents receive. See Securing the cluster : Importing CAs.


How the pieces line up​

Router pod
skrouterd.json -> listeners 5671 / 45671 / 55671 + sslProfiles
Secrets -> router-site-server, router-local-server
Service router -> same three ports to the outside
|
+--> PutDefaultRouter(host, ports) on Controller
+--> CreateCA(router-site-ca, default-router-local-ca)

NATS pod nats-0 / nats-1
server.conf -> ports, JWT, JetStream, cluster routes, TLS paths
Secrets -> nats-site-server, nats-mqtt-server, creds, jetstream key
Service nats -> 6222 / 7422 / 8883 (and LB or ingress host for advertise + hub)
Service nats-server -> 4222 / 8222
|
+--> GET /nats/bootstrap (operator stores JWT and creds)
+--> UpsertNatsHub(host, ports) on Controller
+--> CreateCA(nats-site-ca, default-nats-local-ca)

Code map​

TopicLocation
Router reconcile ordercontrollers/controlplanes/reconcile.go (reconcileRouter)
Router JSON templatecontrollers/controlplanes/router/config.go
Router ConfigMap mergecontrollers/controlplanes/k8s.go (createConfigMap, mergeConfigs)
Router Deployment volumes and envcontrollers/controlplanes/microservices.go (newRouterMicroservice)
PutDefaultRoutercontrollers/controlplanes/k8s.go (createDefaultRouter)
NATS reconcilecontrollers/controlplanes/reconcile.go (reconcileNats)
server.conf templatecontrollers/controlplanes/nats/config.go
TLS Secret generationcontrollers/controlplanes/nats/certs.go
Bootstrap persistencecontrollers/controlplanes/nats/bootstrap.go
Hub upsertcontrollers/controlplanes/k8s.go (createDefaultNatsHub)
CA importcontrollers/controlplanes/reconcile.go (ImportCertificates)
Group 3See anything wrong with the document? Help us improve it!