OpenZiti: Zero Trust Networking from Application Code
Imagine building a micro‑service that talks to a database, a cache, and a third‑party API, all without ever opening a firewall port or managing VPN credentials. That’s the promise of OpenZiti: a zero‑trust networking layer that lives inside your application code, turning every connection into an authenticated, encrypted tunnel on demand.
Zero‑trust networking flips the classic perimeter model on its head. Instead of assuming everything inside a network is safe, each request is verified, encrypted, and authorized right at the source. OpenZiti makes this model practical for developers by exposing a simple SDK that abstracts away the underlying mesh, so you can focus on business logic, not network plumbing.
What Is OpenZiti?
OpenZiti is an open‑source, application‑layer overlay network that implements zero‑trust principles. It creates “Ziti tunnels” between endpoints, which are established only after mutual authentication using identity certificates. Because the tunnels are created on demand, there’s no need for static IP whitelisting or NAT traversal tricks.
Under the hood, OpenZiti consists of three core components: the controller (policy engine), the edge router (traffic forwarder), and the SDK (client library). The controller stores identity certificates, service definitions, and access control policies. Edge routers form a resilient mesh that forwards encrypted packets, while the SDK runs inside your process and handles tunnel creation, encryption, and authentication automatically.
Zero‑Trust Principles in One Sentence
- Never trust – always verify every connection.
- Least‑privilege – grant only the exact resources an identity needs.
- Encrypt by default – all traffic is end‑to‑end encrypted.
- Assume breach – design for rapid revocation and isolation.
Why Embed Networking in Application Code?
Traditional networking relies on perimeter firewalls, VPNs, and static routing tables. Those approaches are brittle in cloud‑native environments where services spin up and down constantly. By embedding networking directly into the application, you get:
- Dynamic discovery: Services find each other via the controller, not DNS hacks.
- Fine‑grained access control: Policies can be tied to specific identities or even code signatures.
- Reduced attack surface: No open ports mean fewer vectors for attackers.
- Seamless multi‑cloud: The same SDK works across on‑prem, AWS, Azure, or edge devices.
Because the SDK handles TLS handshakes, key rotation, and tunnel lifecycles, developers can write code as if they were calling a local function, while the traffic travels securely across the Ziti fabric.
Getting Started: The Minimal Setup
First, spin up a Ziti controller and at least one edge router. The easiest way is using Docker Compose:
version: '3.8'
services:
controller:
image: openziti/controller:latest
ports:
- "6262:6262"
environment:
- ZITI_CONTROLLER_ADMIN_PASSWORD=admin
edge-router:
image: openziti/router:latest
depends_on:
- controller
environment:
- ZITI_CONTROLLER_URL=https://controller:6262
- ZITI_ROUTER_NAME=edge-router
ports:
- "3022:3022"
Once the containers are up, use the ziti CLI to enroll an identity for your application. This creates a JSON file containing the client certificate and private key, which the SDK will load at runtime.
ziti edge enroll --jwt my-app.jwt --output my-app.json
Now you have a portable identity file that can be bundled with your binary or stored securely in a secret manager.
First Code Example: Secure HTTP Client
Let’s replace a plain requests call with a Ziti‑enabled HTTP client. The ziti-sdk-py library provides a ZitiContext that you can wrap around any socket‑based library.
import ziti
import requests
# Load the Ziti identity
ziti_ctx = ziti.ZitiContext.from_file('my-app.json')
# Create a Ziti‑enabled session
session = requests.Session()
adapter = ziti.ZitiAdapter(ziti_ctx)
session.mount('https://', adapter)
# Perform a request to a Ziti‑exposed service
response = session.get('https://myservice.internal/api/data')
print(response.json())
Behind the scenes, the ZitiAdapter intercepts the socket creation, establishes a tunnel to the edge router, and encrypts the payload. The service you’re calling must be registered in the controller with a matching service name (myservice.internal) and a policy that allows your identity to connect.
Pro tip: When deploying to Kubernetes, store the identity JSON in a Secret and mount it as a read‑only volume. This avoids hard‑coding credentials and lets you rotate keys without rebuilding images.
Second Code Example: Bidirectional TCP Echo Service
Many legacy applications use raw TCP sockets. OpenZiti can expose a TCP service without changing the client code at all. First, register the service in the controller:
ziti edge create service echo-service \
--protocol tcp \
--port 9999 \
--terminator-address 10.0.0.5 \
--terminator-port 9999
Now write a simple Python echo server that binds to 0.0.0.0:9999. The server sees normal TCP traffic; the Ziti fabric handles the encryption.
import socket
HOST = '0.0.0.0'
PORT = 9999
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
print('Echo server listening on', PORT)
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
On the client side, use the same Ziti SDK to dial the service by name. No IP address or port exposure is required.
import ziti
import socket
ziti_ctx = ziti.ZitiContext.from_file('my-app.json')
ziti_sock = ziti_ctx.dial('echo-service')
with socket.socket(fileno=ziti_sock.fileno()) as s:
s.sendall(b'Hello, Ziti!\n')
print('Received:', s.recv(1024).decode())
The dial call resolves the service name through the controller, negotiates a tunnel, and returns a regular Python socket object. Your code can stay unchanged, yet all traffic is now zero‑trust.
Pro tip: For high‑throughput services, enable sessionResumption in the Ziti config. This reuses TLS sessions across connections, cutting handshake latency by up to 30 %.
Real‑World Use Cases
1. Secure Database Access from Edge Devices
Edge gateways often need to read/write to a central PostgreSQL instance. By exposing the database as a Ziti service, you eliminate the need for public IPs or SSH tunnels. Each device authenticates with its own certificate, and the controller enforces per‑device read/write policies.
2. SaaS API Protection
A SaaS provider can expose its public API only through Ziti. Partner applications receive a Ziti identity and can call the API without ever seeing a public endpoint. If a partner’s credentials are compromised, revoking the identity instantly cuts off access without touching firewall rules.
3. Micro‑service Mesh in Multi‑Cloud Deployments
When services span AWS, GCP, and on‑prem data centers, traditional service meshes struggle with cross‑cloud traffic encryption. OpenZiti’s edge routers run in each environment, forming a global mesh. Policies can be written once in the controller, ensuring consistent security across clouds.
Designing Policies with the Controller
Policies in OpenZiti are declarative JSON objects that bind identities to services. A typical policy might look like this:
{
"type": "servicePolicy",
"service": "myservice.internal",
"identityRoles": ["frontend", "mobile"],
"allow": [
{
"protocol": "tcp",
"port": 443,
"sourcePorts": [0, 65535]
}
]
}
This policy grants any identity with the role frontend or mobile permission to connect to myservice.internal over TCP port 443. The controller evaluates policies on every connection attempt, ensuring that the least‑privilege principle is always enforced.
Roles are assigned to identities during enrollment, either manually via the CLI or automatically through an identity provider integration (e.g., Okta, Azure AD). This makes it easy to align network access with existing RBAC structures.
Advanced Topics: Service Discovery & Load Balancing
OpenZiti supports multiple terminators for a single service, enabling built‑in load balancing. When you register a service, you can add several terminators with different IPs and ports. The controller then distributes client connections using a round‑robin algorithm.
For dynamic environments, you can use the ziti edge create terminator command inside a CI/CD pipeline to register new service instances as they spin up. When an instance is terminated, a corresponding delete terminator call removes it from the pool, preventing stale connections.
Service discovery can also be driven by DNS. By configuring a DNS server to resolve Ziti service names to a special .ziti domain, applications that rely on DNS lookups can transparently discover services without code changes.
Pro tip: Pair OpenZiti with Consul or etcd for automated service registration. A small watcher script can listen for container start/stop events and call the Ziti CLI to keep the mesh in sync.
Performance Considerations
Because OpenZiti encrypts traffic end‑to‑end, there is a modest CPU overhead for TLS and packet framing. In practice, the overhead is comparable to using HTTPS directly. However, you can tune performance by:
- Enabling
mtuoptimization on edge routers to match the underlying network. - Adjusting
maxConnectionsin the SDK config to reuse sockets. - Using hardware acceleration (AES‑NI) on modern CPUs for faster cryptography.
Benchmarking a simple echo service over Ziti on a 2 GHz CPU showed a latency increase of ~5 ms per request compared to a plain TCP socket—acceptable for most web‑scale workloads.
Operational Best Practices
1. Centralize Identity Management
Store all identity JSON files in a secure vault (e.g., HashiCorp Vault). Rotate certificates regularly and enforce short TTLs (e.g., 30 days) to limit exposure if a file is leaked.
2. Monitor Edge Router Health
Edge routers expose Prometheus metrics for tunnel count, latency, and error rates. Set up alerts for sudden spikes, which could indicate a misconfiguration or a denial‑of‑service attempt.
3. Use Immutable Infrastructure
Treat the Ziti controller and routers as immutable services. Deploy updates via rolling upgrades, and keep configuration in version‑controlled YAML files to ensure reproducibility.
Integrating OpenZiti with Existing CI/CD Pipelines
Most teams already have pipelines that build Docker images, run tests, and push artifacts. Adding OpenZiti is as simple as injecting the identity file and a step to register services.
# Example GitHub Actions snippet
- name: Enroll Ziti Identity
run: |
curl -sSL https://ziti.io/cli | sudo bash
ziti edge enroll --jwt ${{ secrets.ZITI_JWT }} --output /tmp/app.json
- name: Register Service Terminator
run: |
ziti edge create terminator myservice \
--address ${{ env.HOST_IP }} \
--port 8080 \
--protocol tcp
After the deployment step, your service is instantly reachable via Ziti, and the pipeline can run integration tests that call the service through the Ziti SDK, ensuring end‑to‑end security from the first commit.
Common Pitfalls and How to Avoid Them
Missing Role Assignment – If an identity lacks the required role, the controller will reject the connection with a generic “access denied” error. Always verify role bindings in the identity JSON or via the CLI.
Port Conflicts on Edge Routers – Edge routers listen on a single port for inbound Ziti traffic (default 3022). If you run multiple routers on the same host, ensure each uses a distinct listenPort to avoid binding errors.
Certificate Expiry – Unlike short‑lived JWTs, Ziti certificates have longer lifetimes. Schedule a cron job that checks ziti edge list identities for expiry dates and re‑enrolls automatically.
Pro tip: Enable ziti edge watch in a background container. It streams policy changes in real time, allowing your application to react instantly to new access rules.
Future Roadmap
The OpenZiti community is actively adding features that make zero‑trust networking even more developer‑friendly. Upcoming releases aim to provide:
- Native support for WebSockets and gRPC streams.
- Automatic sidecar injection for Kubernetes, turning any pod into a Ziti‑enabled service without code changes.
- Enhanced observability with OpenTelemetry integration.
These enhancements will further blur the line between application code and networking, allowing developers to think of “service calls” as a single abstraction regardless of where the service lives.
Conclusion
OpenZiti brings zero‑trust networking directly into the hands of developers, removing the need for complex VPNs, static firewalls, and ad‑hoc tunneling solutions. By embedding the SDK into your code, you gain dynamic service discovery, fine‑grained access control, and end‑to‑end encryption with only a few lines of Python (or Go, Java, etc.). Real‑world use cases—from edge device database access to multi‑cloud micro‑service meshes—demonstrate that zero‑trust can be both practical and performant.
Adopt OpenZiti early, automate identity lifecycle, and integrate policy management into your CI/CD pipeline. The result is a resilient, secure network fabric that scales with your application, not against it. Happy coding, and welcome to the future of zero‑trust networking!