Skip to main content
Star us on GitHub Star

Install the Router in Kubernetes

ziti-router

Version: 1.1.1 Type: application AppVersion: 1.1.15

Host an OpenZiti router in Kubernetes

Add the OpenZiti Charts Repo to Helm

helm repo add openziti https://docs.openziti.io/helm-charts/

Public Router

The default configuration listens for incoming edge connections and router links. Set a public address for this listener (edge.advertisedHost) or disable it (linkListeners.transport.enabled) to avoid routers continually failing to dial into it.

# get a router enrollment token from the controller's management API
ziti edge create edge-router "router1" \
--tunneler-enabled --jwt-output-file /tmp/router1.jwt

# subscribe to the openziti Helm repo
helm repo add openziti https://openziti.github.io/helm-charts/

# install the router chart with a public address
helm upgrade --install \
"ziti-router-123456789" \
openziti/ziti-router \
--set-file enrollmentJwt=/tmp/router1.jwt \
--set ctrl.endpoint=ctrl.ziti.example.com:443 \
--set edge.advertisedHost=router1.ziti.example.com \

Ingress TLS Passthrough

All router TLS listeners must terminate TLS, so it's essential that Ingress resources use TLS passthrough.

This example demonstrates creating TLS pass-through Ingress resources for use with ingress-nginx.

Ensure you have the ingress-nginx chart installed with controller.extraArgs.enable-ssl-passthrough=true. You can verify this feature is enabled by running kubectl describe pods {ingress-nginx-controller pod} and checking the args for --enable-ssl-passthrough=true.

If not enabled, then you must patch the ingress-nginx deployment to enable the SSL passthrough option.

kubectl patch deployment "ingress-nginx-controller" \
--namespace ingress-nginx \
--type json \
--patch '[{"op": "add",
"path": "/spec/template/spec/containers/0/args/-",
"value":"--enable-ssl-passthrough"
}]'
# subscribe to ingress-nginx
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx/

# install ingress-nginx
helm install \
--namespace ingress-nginx --create-namespace --generate-name \
ingress-nginx/ingress-nginx \
--set controller.extraArgs.enable-ssl-passthrough=true

Create a Helm chart values file for this router chart.

# router-values.yml
ctrl:
endpoint: ziti-controller-ctrl.ziti-controller.svc:1280
advertisedHost: router1.ziti.example.com
edge:
advertisedPort: 443
service:
type: ClusterIP
ingress:
enabled: true
ingressClassName: nginx
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/ssl-passthrough: "true"

Now upgrade your router chart release with the values file.

helm upgrade --install \
"ziti-router-1" \
openziti/ziti-router \
--set-file enrollmentJwt=/tmp/router1.jwt \
--values /tmp/router-values.yml

Private Router

Disable the link listener if the router does not have a public address set (edge.advertisedHost). Ziti identities inside the cluster can still use the private router's edge listener ClusterIP service by authorizing them with a Ziti edge router policy.

helm upgrade --install \
"ziti-router-1" \
openziti/ziti-router \
--set-file enrollmentJwt=/tmp/router1.jwt \
--set ctrl.endpoint=ctrl.ziti.example.com:443 \
--set linkListeners.transport.enabled=false

Tunnel Modes

Host tunnel mode

Default: tunnel.mode=none

Host mode enables a router's identity to reverse proxy Ziti service traffic to a target address on the regular network. Enable this mode by updating the router's identity in the controller to enable tunneling, then set tunnel.mode=host and upgrade the Helm release to start hosting Ziti services.

ziti edge update identity "router1" --tunneler-enabled

Proxy tunnel mode

tunnel.mode=proxy

Proxy mode enables the router to publish Ziti services as Kubernetes services.

Here's an example router values' snippet to merge with your other values:

tunnel:
mode: proxy
proxyServices:
# this will be bound on the "default" proxy Kubernetes service, see below
- zitiService: my-ziti-service.svc
containerPort: 10443
advertisedPort: 10443
# this will be bound on an additionally configured proxy Kubernetes service, see below
- zitiService: my-other-service.svc
containerPort: 10022
advertisedPort: 10022
proxyDefaultK8sService:
enabled: true
type: ClusterIP
proxyAdditionalK8sServices:
- name: myservice
type: LoadBalancer
annotations:
metallb.universe.tf/loadBalancerIPs: 192.168.1.100

Additional Listeners and Volumes

You can configure an additional edge listener by setting edge.additionalListeners. This is useful for making a WebSocket edge listener available for BrowZer clients that require a trusted server certificate.

This example configures a wss listener and requests a certificate from cert-manager. The alternative certificate must have a DNS SAN that is distinct from the public address of the default edge listener (edge.advertisedHost). This cert-manager approach has the advantage of automatically renewing the certificate and ensuring the DNS SAN of the certificate matches an additional listener's advertised host.

edge:
advertisedHost: router1.ziti.example.com
advertisedPort: 443
ingress:
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
enabled: true
ingressClassName: nginx
service:
enabled: true
type: ClusterIP
additionalListeners:
- name: router1-edge-wss
protocol: wss
containerPort: 3023 # must be unique
advertisedHost: router1-wss.ziti.example.com # must be distinct from edge.advertisedHost
advertisedPort: 443
addHostToSan: false # must be false to avoid colliding DNS SANs between listeners
service:
enabled: true
type: ClusterIP
ingress:
enabled: true
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
ingressClassName: nginx

identity:
altServerCerts:
- name: alt-server-cert-1
mode: certManager
secretName: ziti-router1-alt-server-certs1
additionalListenerName: router1-edge-wss
mountPath: /etc/ziti/alt-server-cert-1
issuerRef:
group: cert-manager.io
kind: ClusterIssuer
name: cloudflare-dns01-issuer-staging

You don't have to use cert-manager. If you have a TLS secret named ziti-router1-alt-server-certs1 from some other issuer in the same namespace as the router containing the certificate and key, you can use it by setting values like these. You must also configure the additional listener as in the prior example with an advertisedHost that matches a DNS SAN of the alternative certificate.

# this is an generic approach for mounting configmaps, secrets, csi volumes, etc.
additionalVolumes:
- name: alt-server-cert-2
volumeType: secret
mountPath: /etc/ziti/alt-server-cert-2
secretName: ziti-router1-alt-server-cert-2

# this looks up a TLS secret's mountpoint to configure the router's identity
identity:
altServerCerts:
- mode: secret
secretName: ziti-router1-alt-server-cert-2

You may also specify matching file paths for an additional volume and alternative certificate if the volume is not a TLS secret.

additionalVolumes:
- name: alt-server-cert-3
volumeType: csi
driverName: csi.bpfd.dev
attributes: volumeAttributes
mountPath: /etc/ziti/alt-server-cert-3

identity:
altServerCerts:
- mode: localFile
serverCert: /etc/ziti/alt-server-cert-3/server3.crt
serverKey: /etc/ziti/alt-server-cert-3/server3.key

Values Reference

KeyTypeDefaultDescription
additionalVolumeslist[]additional volumes to mount to ziti-router container
affinityobject{}deployment template spec affinity
configFilestring"ziti-router.yaml"filename of router config YAML
configMountDirstring"/etc/ziti/config"writeable mountpoint where read-only config file is projected to allow router to write ./endpoints statefile in same dir
csrobject{"country":"","locality":"","organization":"","organizationalUnit":"","province":"","sans":{"dns":[],"email":[],"ip":[],"noDefaults":false,"uri":[]}}Certificate signing request distinguished name and subject alternative names
csr.countrystring""country
csr.localitystring""city
csr.organizationstring""organization
csr.organizationalUnitstring""organizational unit
csr.provincestring""state
csr.sans.dnslist[]additional DNS SANs
csr.sans.emaillist[]additional email SANs
csr.sans.iplist[]additional IP SANs
csr.sans.noDefaultsboolfalseif true, disable computing default SANs from the advertisedHost, etc.
csr.sans.urilist[]additional URI SANs
ctrl.endpointstring""required control plane endpoint, e.g., ctrl.ziti.example.com:443
dnsConfigobject{}it allows to override dns options when dnsPolicy is set to None.
dnsPolicystring"ClusterFirstWithHostNet"
edge.additionalListenerslist[]additional edge listeners have the same shape as the default edge listener, except they're enabled if defined, and you must specify a unique name for each additional edge listener. The name distinguishes their respective cluster services. This is useful for BrowZer clients that require a trusted certificate for the edge WebSocket and advertisedHost must resolve to a public IP that presents a trusted certificate, e.g., an Ingress with TLS termination.
edge.advertisedHoststring""Domain name that edge clients will use to reach this router's edge listener. Default is cluster-internal service DNS name:port.
edge.advertisedPortint443cluster service, node port, load balancer, and ingress port
edge.containerPortint3022cluster service target port on the container
edge.enabledbooltrueenable the edge listener in the router config; usually true because tunnel bindings require the edge which must have at least on listener
edge.ingress.annotationsobject{}ingress annotations, e.g., to configure ingress-nginx for passthrough TLS
edge.ingress.enabledboolfalsecreate an ingress for the cluster service; typically paired with a ClusterIP service type when enabled
edge.ingress.ingressClassNamestring""ingress class name
edge.ingress.labelsobject{}ingress labels
edge.optionsobject{}additional common xgress options
edge.protocolstring"tls"edge listener protocol: tls, wss; usually tls because additionalListeners can be used to provide a wss listener
edge.service.annotationsobject{}service annotations
edge.service.enabledbooltruecreate a cluster service for the edge listener; usually true, but you can disable this to effectively un-publish the edge listener
edge.service.labelsobject{}service labels
edge.service.typestring"ClusterIP"expose the service as a ClusterIP, NodePort, or LoadBalancer; default is ClusterIP, but you could use NodePort or LoadBalancer instead of an ingress controller
enrollJwtFilestring"enrollment.jwt"
enrollmentJwtstringnilenrollment one time token from the controller's management API
envobject{}assign key=value in pod environment
execMountDirstring"/usr/local/bin"read-only mountpoint for executables (must be in image's executable search PATH)
fabric.metrics.enabledboolfalseconfigure fabric metrics in the router config
forwarder.latencyProbeIntervalint10
forwarder.linkDialQueueLengthint1000
forwarder.linkDialWorkerCountint32
forwarder.rateLimitedQueueLengthint5000
forwarder.rateLimitedWorkerCountint64
forwarder.xgressDialQueueLengthint1000
forwarder.xgressDialWorkerCountint128
hostNetworkboolfalseHost networking requested for a pod if set, i.e. tproxy ports enabled in the host namespace. i.e. egress gateway
identity.altServerCertslist[]
identityMountDirstring"/etc/ziti/identity"read-only mountpoint for router identity secret specified in deployment for use by router run container
image.additionalArgslist[]additional arguments can be passed directly to the container to modify ziti runtime arguments
image.argslist["run","{{ .Values.configMountDir }}/{{ .Values.configFile }}"]deployment container command args and opts
image.commandlist["/entrypoint.bash"]deployment container command
image.pullPolicystring"Always"deployment image pull policy
image.repositorystring"docker.io/openziti/ziti-router"container image tag for deployment
image.tagstringnilcontainer image tag (default is Chart's appVersion)
linkListeners.transport.advertisedHoststring"{{ .Values.edge.advertisedHost }}"DNS name that other routers will use to form mesh transport links to this listener
linkListeners.transport.advertisedPortstring"{{ .Values.edge.advertisedPort }}"cluster service, node port, load balancer, and ingress port for this listener; default is edge.advertisedPort
linkListeners.transport.containerPortstring"{{ .Values.edge.containerPort }}"cluster service target port on the container; default is to share a listener with the edge, providing ziti-link ALPN
linkListeners.transport.enabledbooltrueenable the transport listener in the router config; set false for a private router that only connects to other routers and does not accept incoming links
linkListeners.transport.ingress.annotationsobject{}ingress annotations, e.g., to configure ingress-nginx
linkListeners.transport.ingress.enabledboolfalsecreate an ingress for the cluster service
linkListeners.transport.ingress.ingressClassNamestring""ingress class name
linkListeners.transport.ingress.labelsobject{}ingress labels
linkListeners.transport.optionsobject{}link listener options
linkListeners.transport.service.annotationsobject{}service annotations
linkListeners.transport.service.enabledbooltruecreate a cluster service for the router transport link listener; unnecessary if advertisedHost is shared with edge listener (the default)
linkListeners.transport.service.labelsobject{}service labels
linkListeners.transport.service.typestring"ClusterIP"expose the service as a ClusterIP, NodePort, or LoadBalancer
nodeSelectorobject{}deployment template spec node selector
persistence.accessModestring"ReadWriteOnce"PVC access mode: ReadWriteOnce (concurrent mounts not allowed), ReadWriteMany (concurrent allowed)
persistence.annotationsobject{}annotations for the PVC
persistence.enabledbooltruerequired: place a storage claim for the ctrl endpoints state file
persistence.existingClaimstring""A manually managed Persistent Volume and Claim Requires persistence.enabled: true If defined, PVC must be created manually before volume will be bound
persistence.sizestring"50Mi"50Mi is plenty for this state file
persistence.storageClassstring""Storage class of PV to bind. By default it looks for the default storage class. If the PV uses a different storage class, specify that here.
persistence.volumeNamestringnilPVC volume name
podAnnotationsobject{}annotations to apply to all pods deployed by this chart
podSecurityContextobject{"fsGroup":2171}deployment template spec security context
podSecurityContext.fsGroupint2171this is the GID of "ziggy" run-as user in the container that has access to any files created by the router process in the emptyDir volume used to persist the list of ctrl endpoints
proxyobject{}Explicit proxy setting in the router configuration. Router can be deployed in a site where all egress traffic is forwarded through an explicit proxy. The enrollment will also be forwarded through the proxy.
resourcesobject{}deployment container resources
securityContextstringnildeployment container security context
tolerationslist[]deployment template spec tolerations
tunnel.diverterPathstringnilthe tproxy mode can be switched from iptables based interception to bpf interception by passing the user space bpf program path. bpf kernel space program is expected to be loaded prior or during router deployment, e.g. bpfman agent, hostpath, etc
tunnel.dnsSvcIpRangestringnilCIDR range for the internal service fqdn to dynamic intercept IP address resolution (default: 100.64.0.0/10)
tunnel.lanIfstring"lo"interface device name for setting up INPUT firewall rules if fw enabled. It must be set but not needed in containers. Thus, it is set to lo by default
tunnel.modestring"none"run mode for the router's built-in tunnel component: host, tproxy, proxy, or none
tunnel.proxyAdditionalK8sServiceslist[]if tunnel mode is "proxy", create a separate cluster service for each Ziti service listed in "proxyServices" which k8sService == name
tunnel.proxyDefaultK8sServiceobject{"enabled":true,"type":"ClusterIP"}if tunnel mode is "proxy", create the a cluster service named {{ release }}-proxy-default listening on each "advertisedPort" defined in "proxyServices"
tunnel.proxyServiceslist[]list of Ziti services for which K8s services are to be created by this deployment, default is one cluster service port per Ziti service
tunnel.resolverstringnilZiti nameserver listener where OS must be configured to send DNS queries (default: udp://127.0.0.1:53)
websocket.enableCompressionbooltrueenable compression on websocket
websocket.enabledboolfalseenable the websocket transport. Also requires an appropriate edge.additionalListeners entry.
websocket.handshakeTimeoutint10websocket handshake timeout
websocket.idleTimeoutint5websocket idle timeout
websocket.pingIntervalint54websocket ping timeout
websocket.pongTimeoutint60websocket pong timeout
websocket.readBufferSizeint4096websocket read buffer size
websocket.readTimeoutint5websocket read timeout
websocket.writeBufferSizeint4096websocket write buffer size
websocket.writeTimeoutint10websocket write timeout