Managing TLS Certs for HTTPS APIs via OpenZiti

I'm currently using OpenZiti to secure communication between my backend and an IoT device running a private API over HTTPS. The API uses a self-signed TLS certificate, and I want to access it via Ziti

To avoid hardcoding the public certificate or distributing it separately, I'm wondering:

Is there a way to store and use a public TLS certificate (e.g., self-signed cert) inside the OpenZiti controller or console, so that Ziti can validate or trust it when routing HTTPS traffic?

My goal is to:

Access the API only via https:// (not plain HTTP)

Avoid embedding the cert in my backend application code

Let Ziti handle the trust/validation, if possible

Keep the service internal and secure via Ziti's fabric

Is there support for this in the controller, or do I need to trust the cert at the OS/tunneler level manually?

In order to create a TLS connection, the server (the side receiving the connection) will require a keypair. There's no way around that. If you want the initiating client to create a TLS connection, the receiving side (server) needs that keypair.

This isn't an "OpenZiti" thing at all, it's how TLS connections are created. If you were to embed an OpenZiti SDK in the client and into the receiver, you wouldn't have a TLS connection but you would have an end-to-end-encrypted tunnel. Not the same thing, but it's certainly equivalent. The OpenZiti overlay in that case would be brokering that trust the way you want. Without embedding an OpenZiti SDK, you just can't avoid having a key/cert at the receiving side (at least), or at both sides (if you want to do mTLS between the client/server).

Hopefully that makes sense

1 Like

Got it makes sense. thanks!

I'm using OpenZiti in TCP mode to forward HTTPS requests from a remote client to a Kubernetes-hosted service (FastAPI) exposed via Traefik IngressRouteTCP with tls.passthrough=true. My service expects an mTLS handshake with a self-signed CA. When accessing it locally, the client certificate is accepted, and SNI routing works.

However, when accessed over Ziti:

  • The request reaches Traefik but falls back to the default certificate, meaning SNI routing or mTLS appears to fail.
  • My suspicion is that the TLS handshake is not preserved fully (either SNI or client cert is lost).

I'm using TCP mode — not SDK or HTTP-based — so I expected raw transport to be preserved. Does the Ziti tunneler in TCP mode:

  • Touch or re-initiate TLS?
  • Modify or strip SNI or client certificate?
  • Use a TLS proxy internally that might break the passthrough?

This is not the case. The far side establishes it's own TCP connection based on the address it's dialing. What does your host config look like? If you are 'forwarding', the far side will make a connection to the same hostname that was intercepted and SNI should work fine. If you are offloading to an IP address, there will be no SNI information as it's connecting to an IP address.

Ziti does not alter the bytes on the wire, it doesn't affect the handshake but as per above, the far side makes it's own TCP connection so if you are relying on SNI, you would want to offload to the proper hostname.

My expectation is that you're intercepting one hostname at the client side, but on the far side you're offloading to an IP address or a hostname that is not the same as what was intercepted. Remember that TCP connection on the far side is a new connection from the remote machine and I think that'll help you understand? hth

Thanks for the clarification!

In my case:

  • I'm intercepting mtls.local.ai (on the client via Ziti intercept.v1).
  • On the host side, the host.v1 config sets to mtls.local.ai.
  • The connection is forwarded to my IoT device IP (19x.xx.xx.xx:8444) — this is the IP of a k3s LoadBalancer.

My backend API expects SNI-based routing for mTLS, and it seems that Traefik always sees the default certificate being served instead of the one for mtls.local.ai.

My assumption is:
Since the Ziti edge router creates a new TCP connection to the IP (19x.xx.xx.xx), it's not preserving the original HostSNI (mtls.local.ai) in the TLS handshake.

:red_question_mark:My Questions:

  1. How can I ensure that SNI (mtls.local.ai) is preserved in the TLS handshake when the far-side router connects to the service?
  2. Is the only way to support this to configure the host config with FORWARD: YES and explicitly resolve mtls.local.ai (e.g., via /etc/hosts or internal DNS) so that the Ziti edge router connects to the hostname and not just the IP?
  3. Is there any other way within Ziti to preserve the SNI/host when the final connection is made?

My ultimate goal is to support mutual TLS for the internal API without breaking the SNI-based routing.

This sounds like a problem you're going to need to figure out with Traefik. If your host.v1 is using mtls.local.ai, that is what will be in TCP connection outbound from the router. The router doesn't "preserve" the SNI nor is it common (afaik) to proxy the SNI information. If host config is mtls.local.ai, that is what the SNI information in the TCP connection will contain.

This isn't an OpenZiti issue imo