<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[OpenZiti Tech Blog]]></title><description><![CDATA[Discussion of OpenZiti, the open source, modern, programmable, network overlay and edge components, for application-embedded, zero-trust networking.]]></description><link>https://blog.openziti.io</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1653315555269/iDjkqb44X.png</url><title>OpenZiti Tech Blog</title><link>https://blog.openziti.io</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 16:53:17 GMT</lastBuildDate><atom:link href="https://blog.openziti.io/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Comparing Open Source LLM Gateways]]></title><description><![CDATA[If you're running multiple LLM providers and want a single API in front of them, you have options. This post compares open source LLM gateways, including our own OpenZiti llm-gateway. We'll try to be ]]></description><link>https://blog.openziti.io/comparing-open-source-llm-gateways</link><guid isPermaLink="true">https://blog.openziti.io/comparing-open-source-llm-gateways</guid><category><![CDATA[#ai-tools]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[gateway]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Dave Hart]]></dc:creator><pubDate>Thu, 26 Mar 2026 18:03:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62a8cff00baf4306d94942af/5608a364-468a-4daa-b560-04d09c4dab2a.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're running multiple LLM providers and want a single API in front of them, you have options. This post compares open source LLM gateways, including our own <a href="https://github.com/openziti/llm-gateway">OpenZiti llm-gateway</a>. We'll try to be honest about where each one is strong and where ours fits.</p>
<h2>The Problem They All Solve</h2>
<p>You have applications that talk to LLMs. Maybe OpenAI for general tasks, Claude for coding, and a private inference cluster for sensitive workloads. Each provider has a different API, different auth, different SDKs, but you want one endpoint that handles routing, translation, and access control so your applications don't have to.</p>
<h2>The Options</h2>
<h3>LiteLLM</h3>
<p><a href="https://github.com/BerriAI/litellm">LiteLLM</a> is the most widely adopted open source LLM proxy. It supports 100+ providers, has a polished admin UI, and handles translation between API formats. If your primary need is broad provider support, LiteLLM is hard to beat.</p>
<p><strong>Strengths:</strong></p>
<ul>
<li><p>100+ provider integrations out of the box</p>
</li>
<li><p>Admin dashboard with spend tracking, rate limits, and team management</p>
</li>
<li><p>Virtual API keys with per-key budgets</p>
</li>
<li><p>Well-documented, large community</p>
</li>
<li><p>Python-based, easy to extend</p>
</li>
</ul>
<p><strong>Tradeoffs:</strong></p>
<ul>
<li><p>Security is at the application layer: API keys in headers, HTTPS for transport</p>
</li>
<li><p>Designed to be deployed on a network and accessed over HTTP</p>
</li>
<li><p>Requires a database (PostgreSQL) for the admin features</p>
</li>
<li><p>Python runtime and dependencies</p>
</li>
</ul>
<h3>Portkey</h3>
<p><a href="https://github.com/portkey-ai/gateway">Portkey</a> is a lightweight TypeScript gateway focused on reliability and observability. It supports caching, retries, fallbacks, and load balancing with a clean config format.</p>
<p><strong>Strengths:</strong></p>
<ul>
<li><p>Simple, focused feature set</p>
</li>
<li><p>Caching and automatic retries built in</p>
</li>
<li><p>Fallback chains across providers</p>
</li>
<li><p>Lightweight Node.js runtime</p>
</li>
<li><p>Good observability with logging and analytics</p>
</li>
</ul>
<p><strong>Tradeoffs:</strong></p>
<ul>
<li><p>Fewer provider integrations than LiteLLM</p>
</li>
<li><p>No built-in access control or API key management in the open source version (available in their cloud offering)</p>
</li>
<li><p>Security model is standard HTTPS</p>
</li>
</ul>
<h3>Kong AI Gateway</h3>
<p><a href="https://docs.konghq.com/gateway/latest/ai-gateway/">Kong</a> adds AI capabilities as plugins to the Kong API gateway. If you already run Kong, this is a natural extension.</p>
<p><strong>Strengths:</strong></p>
<ul>
<li><p>Builds on Kong's mature API gateway infrastructure</p>
</li>
<li><p>Rate limiting, auth, and observability from the Kong ecosystem</p>
</li>
<li><p>Enterprise support available</p>
</li>
<li><p>Plugin architecture for extensibility</p>
</li>
</ul>
<p><strong>Tradeoffs:</strong></p>
<ul>
<li><p>Requires running the full Kong gateway stack</p>
</li>
<li><p>Significant operational complexity if you don't already use Kong</p>
</li>
<li><p>AI features are plugins, not a purpose-built gateway</p>
</li>
<li><p>Community edition has limitations; some features require Enterprise license</p>
</li>
</ul>
<h3>Cloudflare AI Gateway</h3>
<p><a href="https://developers.cloudflare.com/ai-gateway/">Cloudflare AI Gateway</a> runs on Cloudflare's edge network. It's fast to set up if you're already in the Cloudflare ecosystem.</p>
<p><strong>Strengths:</strong></p>
<ul>
<li><p>Global edge deployment, low latency</p>
</li>
<li><p>Built-in caching and rate limiting</p>
</li>
<li><p>Simple setup if you use Cloudflare</p>
</li>
<li><p>Analytics dashboard</p>
</li>
</ul>
<p><strong>Tradeoffs:</strong></p>
<ul>
<li><p>Not self-hosted - your traffic goes through Cloudflare</p>
</li>
<li><p>Vendor lock-in to the Cloudflare ecosystem</p>
</li>
<li><p>Not open source</p>
</li>
<li><p>Limited customization compared to self-hosted options</p>
</li>
</ul>
<h3>llm-gateway (ours)</h3>
<p><a href="https://github.com/openziti/llm-gateway">llm-gateway</a> is our entry. It's an OpenAI-compatible proxy built on <a href="https://zrok.io">zrok</a> and <a href="https://github.com/openziti/ziti">OpenZiti</a> that adds zero-trust networking as a foundational layer.</p>
<p><strong>Strengths:</strong></p>
<ul>
<li><p>Dark by default: the gateway can run with zero listening ports, invisible to network scanners</p>
</li>
<li><p>End-to-end encryption through the OpenZiti/zrok overlay (not just HTTPS)</p>
</li>
<li><p>Private model mesh: connect to private inference instances on other machines without opening ports or VPN</p>
</li>
<li><p>Semantic routing with a three-layer cascade (heuristics, embeddings, LLM classifier)</p>
</li>
<li><p>Weighted load balancing across multiple inference instances with health checks and failover</p>
</li>
<li><p>Single Go binary, no runtime dependencies, no database required</p>
</li>
<li><p>Apache 2.0</p>
</li>
</ul>
<p><strong>Tradeoffs:</strong></p>
<ul>
<li><p>Fewer provider integrations than LiteLLM (currently OpenAI, Anthropic, OpenAI-compatible local inference)</p>
</li>
<li><p>No admin UI (CLI and config file only)</p>
</li>
<li><p>Newer project, smaller community</p>
</li>
<li><p>The zero-trust features require zrok, which adds a concept to learn</p>
</li>
</ul>
<h2>Where Each One Fits</h2>
<p>If you need broad provider support and an admin dashboard, <strong>LiteLLM</strong> is the pragmatic choice. It has the widest adoption and the most integrations.</p>
<p>If you want something lightweight with good retry/fallback behavior, <strong>Portkey</strong> keeps things simple.</p>
<p>If you already run Kong, the <strong>AI Gateway plugins</strong> are the path of least resistance.</p>
<p>If you're already on Cloudflare and don't need self-hosting, <strong>Cloudflare AI Gateway</strong> is quick to set up.</p>
<p>If your concern is security - specifically, if you don't want your LLM gateway to be a network-accessible endpoint, if you need to connect to models across networks without opening ports, or if you need cryptographic identity per client rather than shared API keys - that's where <strong>llm-gateway</strong> fits. The security model is fundamentally different from the others. The other gateways are good at what they do. We're not trying to be better at those same things. The difference is the connectivity layer underneath - that's where we bring something they don't have.</p>
<h2>The Security Gap in LLM Gateways</h2>
<p>This is the angle we think matters most, so it's worth expanding on.</p>
<p>Every other gateway on this list is designed to be deployed as an HTTP endpoint on a network. Clients connect to it over HTTPS, authenticate with API keys, and make requests. The security model is: put it behind a load balancer, use HTTPS, and manage API keys carefully.</p>
<p>This may be fine for a lot of scenarios, but it has some gaps. API keys can get committed to repos, leaked in logs, or shared among team members. The endpoint is scannable - anyone who can reach the network can find it and try to authenticate. If you're connecting to models on another network (e.g., an inference cluster on a different subnet), you need to either open firewall ports or set up a VPN.</p>
<p>llm-gateway takes a different approach. When you run it with zrok, the gateway doesn't listen on any port. It connects outbound to the zrok overlay and is reachable only by clients with the right cryptographic identity. You can connect to model instances on other machines the same way - no ports, no DNS, no VPN. The traffic is encrypted end to end through the overlay, not just to the gateway's TLS termination point.</p>
<p>This isn't better for everyone. If you're running a local development proxy, maybe you don't need this. But if you're deploying an LLM gateway for a team, connecting to models across networks, or operating in an environment where "put it on the network and secure it with API keys" doesn't meet your security requirements, the zero-trust model is worth looking at.</p>
<h2>Try It</h2>
<pre><code class="language-bash">go install github.com/openziti/llm-gateway/cmd/llm-gateway@latest
</code></pre>
<p>The <a href="https://github.com/openziti/llm-gateway/blob/main/docs/getting-started.md">getting started guide</a> walks through setup from a minimal Ollama config to a full deployment with semantic routing and zrok integration.</p>
<p>If you try it and have feedback, we'd appreciate it. Post on <a href="https://openziti.discourse.group">Discourse</a> or open an issue on the repo. And of course a star is always appreciated.</p>
]]></content:encoded></item><item><title><![CDATA[How to Automate OpenZiti SDK Integration Using a 4-Phase AI Skill]]></title><description><![CDATA[Embedding the OpenZiti SDK into an existing service is a good idea in theory and a grind in practice. You need to understand the SDK patterns for the language, find every call site where a raw TCP lis]]></description><link>https://blog.openziti.io/how-to-automate-openziti-sdk-integration-using-a-4-phase-ai-skill</link><guid isPermaLink="true">https://blog.openziti.io/how-to-automate-openziti-sdk-integration-using-a-4-phase-ai-skill</guid><dc:creator><![CDATA[Curt Tudor]]></dc:creator><pubDate>Wed, 25 Mar 2026 18:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/628901e6aad1d357809fe1b8/d1f04b28-7cae-4955-9e25-5752fc99fc8a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Embedding the OpenZiti SDK into an existing service is a good idea in theory and a grind in practice. You need to understand the SDK patterns for the language, find every call site where a raw TCP listener or HTTP client lives, replace each one correctly, wire up the Ziti context, update go.mod, and not break anything. For one service, that's an afternoon. For a fleet of microservices, it simply doesn't happen.</p>
<blockquote>
<p><strong>What is OpenZiti?</strong> OpenZiti is an open-source, zero-trust networking platform that embeds security directly into application connectivity. It replaces traditional VPNs and network perimeters with identity-aware, policy-driven service access — no open ports, no implicit trust. <em><strong>It's developed by</strong></em> <a href="https://netfoundry.io/"><em><strong>NetFoundry</strong></em></a><em><strong>,</strong></em></p>
</blockquote>
<p>The SKILL-driven auto-Zitification workflow changes that math. Running on <a href="https://opencode.ai">OpenCode</a> with Claude Sonnet 4.6 and <a href="https://blog.openziti.io/what-is-ziti-mcp-server-openzitis-entire-management-plane-available-to-any-ai-agent">ziti-mcp-server v0.9.0</a>, it took 82 Go files, found every transport candidate, generated reviewable diffs, transformed the code, and provisioned live network identities and policies, in under 5 minutes, with zero broken files.</p>
<p><strong>TL;DR</strong></p>
<ul>
<li><p>The auto-Zitification SKILL runs 4 phases: Detect, Plan, Transform, Provision, with a mandatory human approval gate before any file is written</p>
</li>
<li><p>Phase 1 uses <code>zitifier-detect-go</code>, a true AST analyzer (go/analysis framework, full type resolution), not grep</p>
</li>
<li><p>The review gate in Phase 2 caught two real issues in the demo target before touching a single file</p>
</li>
<li><p>Phase 4 provisions identities, a Ziti service, and dial/bind policies live via ziti-mcp-server v0.9.0</p>
</li>
<li><p>Claude Sonnet 4.6 reached the approve gate in 2:50; gpt-4o-mini never finished</p>
</li>
<li><p>Demo target: <a href="https://github.com/stefanprodan/podinfo">stefanprodan/podinfo</a>, 82 Go files, gRPC + HTTP + TCP, used by CNCF Flux and Flagger</p>
</li>
</ul>
<h2>What Is the Auto-Zitification SKILL?</h2>
<p>The SKILL is a 4-phase orchestration defined in a <code>SKILL.md</code> file and executed by <a href="https://opencode.ai">OpenCode</a>, an open-source, provider-agnostic AI coding agent. Each phase runs as a named subagent with declared tool permissions. No phase can exceed its scope: the detection agent can't write files, the provisioning agent can't touch source code.</p>
<pre><code class="language-mermaid">%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#e2e8f0', 'primaryBorderColor': '#3b82f6', 'lineColor': '#64748b', 'background': '#0f172a', 'mainBkg': '#1e293b', 'edgeLabelBackground': '#1e293b'}}}%%
flowchart LR
  classDef phase fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd,stroke-width:2px
  classDef gate fill:#3d1515,stroke:#ef4444,color:#fca5a5,stroke-width:2px,stroke-dasharray:4
  classDef prov fill:#14291f,stroke:#10b981,color:#6ee7b7,stroke-width:2px

  D["🔍 DETECT\nAST analysis\nzitifier-detect-go"]:::phase
  P["📋 PLAN\nPer-file diffs\nreview gate"]:::phase
  G["✋ HUMAN\nAPPROVAL\nREQUIRED"]:::gate
  T["✏️ TRANSFORM\nWrite SDK replacements\n.pre-ziti.bak backups"]:::phase
  V["⚙️ PROVISION\nIdentities · Service\nPolicies via MCP"]:::prov

  D --&gt; P --&gt; G --&gt; T --&gt; V
</code></pre>
<p><em>4 phases, one mandatory gate. No file is written until the human approves the plan.</em></p>
<h2>Why Doesn't Manual Zitification Scale?</h2>
<p>Embedding the OpenZiti SDK by hand requires knowing the idiomatic patterns for each language, following net.Conn assignments across variable assignments, and catching every call site, including the ones inside helper functions three layers deep. Miss one and you get a subtle fallback bug that only shows under load. Go, Node.js, and Python each have different SDK patterns, so there's no cross-language muscle memory to build on.</p>
<p>At microservice scale the math breaks. Hours per service multiplied by hundreds of services means this work simply doesn't get scheduled. The SKILL makes it schedulable.</p>
<h2>What Is True AST Detection and Why Does It Matter?</h2>
<p><code>zitifier-detect-go</code> uses the <code>go/analysis</code> framework, the same infrastructure behind <code>go vet</code>, <code>staticcheck</code>, and <code>gopls</code>. It performs full type resolution via <code>NeedTypesInfo</code>, which means it knows that <code>net.Listen</code> is <code>net.Listen</code> even when it's been aliased, assigned to an interface, or shadowed by a local variable. Grep-based detection cannot do this.</p>
<p>The practical difference shows in the false-positive rate. Grep flags <code>http.Get("github.com/some/import-path")</code> as a HIGH confidence external call. The AST detector reads the URL argument, classifies external domains as LOW confidence, and skips them. It also validates the network argument on <code>net.Dial</code>, Unix sockets and UDP get skipped automatically. Only TCP variants are candidates.</p>
<p>Detection output is structured JSON:</p>
<pre><code class="language-json">{ "file": "pkg/api/grpc/server.go",
  "line": 88,
  "type": "SERVER",
  "pattern": "net.Listen",
  "confidence": "HIGH" }
</code></pre>
<p>For podinfo's 82 Go files, the detector found 4 candidates across 3 transport patterns:</p>
<table>
<thead>
<tr>
<th>File</th>
<th>Type</th>
<th>Pattern</th>
<th>Confidence</th>
</tr>
</thead>
<tbody><tr>
<td><code>pkg/api/http/server.go</code></td>
<td>SERVER</td>
<td><code>http.ListenAndServe</code></td>
<td>HIGH</td>
</tr>
<tr>
<td><code>pkg/api/grpc/server.go</code></td>
<td>SERVER</td>
<td><code>net.Listen("tcp",...)</code></td>
<td>HIGH</td>
</tr>
<tr>
<td><code>pkg/api/http/echo.go</code></td>
<td>CLIENT</td>
<td><code>http.Client{Transport:...}</code></td>
<td>HIGH</td>
</tr>
<tr>
<td><code>cmd/podcli/check.go</code></td>
<td>CLIENT</td>
<td><code>net.Dial</code> / <code>http.Get</code></td>
<td>HIGH</td>
</tr>
<tr>
<td><code>pkg/api/http/server.go</code></td>
<td>AMBIGUOUS</td>
<td>gorilla/websocket Upgrader</td>
<td>MEDIUM</td>
</tr>
</tbody></table>
<p>No false positives. No missed candidates.</p>
<h2>What Did the Review Gate Actually Catch?</h2>
<p>Before a single file is written, Phase 2 generates per-file diffs and surfaces warnings. In the podinfo run, it caught two real issues.</p>
<p><strong>Warning 1, Per-request Ziti context.</strong> The initial plan placed <code>initZiti()</code> inside <code>echoHandler()</code>, a per-request HTTP handler. Ziti contexts are expensive to initialize and must be created once at startup, stored as a struct field, and reused. Creating one per request would crater performance under any real load. The gate flagged this before writing.</p>
<p><strong>Warning 2, Dropped transport wrapper.</strong> The echo.go client originally used <code>otelhttp.NewTransport(http.DefaultTransport)</code> for OpenTelemetry distributed tracing. The naive replacement would have discarded that wrapper entirely, silently breaking distributed traces. The correct fix composes the transports: wrap the Ziti transport with OTel, don't replace it.</p>
<p>Both issues were caught at the approve gate. Neither required finding a bug after the fact. This is the point of the gate, it's architectural, not advisory.</p>
<h2>What Does the Transformation Look Like?</h2>
<p>The gRPC server transformation is the cleanest example. <code>net.Listen</code> returns a <code>net.Listener</code> interface, and <code>zitiCtx.Listen</code> returns the same interface. It's a true drop-in, no gRPC-specific code changes needed anywhere else in the stack.</p>
<p><strong>Before:</strong></p>
<pre><code class="language-go">import (
  "fmt"
  "net"
)

func (s *Server) ListenAndServe() {
  listener, err := net.Listen(
    "tcp",
    fmt.Sprintf(":%v", s.config.Port),
  )
  // ...
}
</code></pre>
<p><strong>After:</strong></p>
<pre><code class="language-go">import (
  "os"
  "github.com/openziti/sdk-golang/ziti"
)

func (s *Server) ListenAndServe() {
  zitiCtx, _ := initZiti()
  listener, err := zitiCtx.Listen(
    os.Getenv("ZITI_SERVICE_NAME") + "-grpc")
  // ...
}
</code></pre>
<p>Every touched file gets a <code>.pre-ziti.bak</code> backup before writing. The <code>go.mod</code> update is handled automatically. The shared <code>initZiti()</code> helper is created once and referenced across all transformed files.</p>
<h2>How Does the Provisioning Phase Work?</h2>
<p>Phase 4 runs entirely through <a href="https://blog.openziti.io/what-is-ziti-mcp-server-openzitis-entire-management-plane-available-to-any-ai-agent">ziti-mcp-server v0.9.0</a>, which exposes the full Ziti Management API to the agent as 201 tools. The provisioner queries the controller version to confirm network type (the demo ran against a self-hosted OpenZiti quickstart), then:</p>
<ol>
<li><p>Creates <code>podinfo-client</code> and <code>podinfo-server</code> identities, writes enrollment JWTs to disk</p>
</li>
<li><p>Creates a Ziti service named per the <code>ZITI_SERVICE_NAME</code> env var, covering both HTTP and gRPC traffic</p>
</li>
<li><p>Wires a Dial policy for the client identity and a Bind policy for the server identity</p>
</li>
<li><p>Creates a Service Edge Router Policy for all routers</p>
</li>
<li><p>Emits <code>ziti edge enroll</code> commands for each JWT</p>
</li>
</ol>
<p>The whole provisioning step runs in the same 5-minute window as the code transformation. The <a href="https://blog.openziti.io/no-listening-ports">no-listening-ports</a> model applies immediately after enrollment; the service binds to the Ziti fabric, not to a TCP port.</p>
<p>Both self-hosted OpenZiti and NetFoundry-managed networks are supported.</p>
<h2>Which Model Should You Use?</h2>
<p>Model choice has a material impact on agentic workflows. Here's what the podinfo run showed:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Time to 'approve gate'</th>
<th>Code quality</th>
<th>Rate limits</th>
<th>Verdict</th>
</tr>
</thead>
<tbody><tr>
<td>gpt-4o-mini</td>
<td>Never finished</td>
<td>Compile error loops</td>
<td>Annoying</td>
<td>Wrong tool</td>
</tr>
<tr>
<td>gpt-4o</td>
<td>~5 min</td>
<td>Good</td>
<td>Tier 1 limited</td>
<td>Usable</td>
</tr>
<tr>
<td>Claude Sonnet 4.6</td>
<td><strong>2:50</strong></td>
<td>Excellent</td>
<td>No issues</td>
<td>Recommended</td>
</tr>
</tbody></table>
<p>Claude Sonnet 4.6 reached the approve gate in 2 minutes 50 seconds and completed the full run in under 5 minutes. The code quality was high enough that both review gate warnings were substantive, not noise, and the transformations required no manual corrections.</p>
<p>One practical note: running this required a personal Claude Pro account to get an API key. NetFoundry's Team plan doesn't distribute API keys directly to engineers. If your org is in the same situation, a personal Pro account is the current workaround until that changes.</p>
<h2>Why OpenCode for This Workflow?</h2>
<p>The initial evaluation ran on <a href="https://opencode.ai">OpenCode</a> rather than Claude Code for one specific reason: provider-agnostic validation. One of the goals was confirming the SKILL works across models, Claude, GPT, Gemini, local models. OpenCode handles that in a single config line change. Claude Code is provider-locked by design.</p>
<p>The other factor is the explicit subagent architecture. Each phase in the SKILL runs as a named agent with declared tool permissions:</p>
<pre><code class="language-mermaid">%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#e2e8f0', 'primaryBorderColor': '#3b82f6', 'lineColor': '#64748b', 'background': '#0f172a', 'mainBkg': '#1e293b', 'edgeLabelBackground': '#1e293b'}}}%%
flowchart TB
  classDef agent fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd,stroke-width:2px
  classDef scope fill:#1e293b,stroke:#64748b,color:#94a3b8,stroke-width:1px
  classDef mcp fill:#14291f,stroke:#10b981,color:#6ee7b7,stroke-width:2px

  SKILL["📋 SKILL.md\nZitifier Orchestrator"]

  subgraph D ["zitifier-detect-go · READ ONLY"]
    DA["read files\nrun binary\nJSON output"]:::scope
  end

  subgraph P ["zitifier-transform (plan) · READ + DIFF"]
    PA["read files\ngenerate diffs\nawait approval"]:::scope
  end

  subgraph T ["zitifier-transform (apply) · READ + WRITE"]
    TA["read files\nwrite files\nbackup (.bak)"]:::scope
  end

  subgraph V ["zitifier-provision · MCP ONLY"]
    VA["ziti-mcp-server\nidentities\nservices/policies"]:::mcp
  end

  SKILL --&gt; D --&gt; P --&gt; T --&gt; V
</code></pre>
<p><em>Each agent is a</em> <code>.md</code> <em>file in</em> <code>.opencode/agents/</code><em>, versioned and diffable. Tool scopes are declared per-agent, detect can't write, provision can't touch source files.</em></p>
<p>That audit trail matters for security-conscious teams. Every agent's capabilities are in a file you can read, diff, and review in a PR.</p>
<p>The next evaluation is Claude Code, the SKILL.md format is portable and the same 4-phase logic applies. That's the subject of a follow-up post.</p>
<h2>Frequently Asked Questions About Auto-Zitification</h2>
<h3>Does this work with languages other than Go?</h3>
<p>Go is the first supported language via <code>zitifier-detect-go</code>. A Node.js/TypeScript equivalent using the TypeScript Compiler API is in active development (<code>zitifier-detect-node</code>) and uses the same JSON output schema and confidence levels. Python and Java equivalents are on the roadmap but not yet scoped.</p>
<h3>What happens if the detector flags something incorrectly?</h3>
<p>The review gate in Phase 2 exists precisely for this. Every candidate appears in the diff with full context before any file is written. You can reject individual candidates at the approval step. The <code>.pre-ziti.bak</code> backups also mean you can revert any transformation after the fact without touching version control.</p>
<h3>Does it work with NetFoundry-managed networks?</h3>
<p>Yes. The provisioning phase uses <code>ziti-mcp-server</code>, which supports both self-hosted OpenZiti and NetFoundry-managed networks (V8 only). The <code>--profile</code> flag in ziti-mcp-server lets you pre-configure separate credentials for each environment.</p>
<h3>Is the SKILL open source?</h3>
<p>The component tools are open source: <code>zitifier-detect-go</code> is Apache-2.0 at <a href="https://github.com/openziti/zitifier-detect-go">github.com/openziti/zitifier-detect-go</a>, and <code>ziti-mcp-server</code> is Apache-2.0 at <a href="https://github.com/openziti/ziti-mcp-server">github.com/openziti/ziti-mcp-server</a>. The SKILL.md orchestration file and its sub-agents will be published alongside the component tools.</p>
<h3>What's next on the roadmap?</h3>
<p>Five items are scoped: <code>zitifier-detect-node</code> for TypeScript (testing now), a shared Ziti context helper to replace per-file <code>initZiti()</code> calls, transport composition to preserve OTel/Prometheus middleware, and scale validation against a large production Go codebase, Mattermost server at 500,000 lines is the current candidate.</p>
<h3>Thanks</h3>
<p>If you like OpenZiti, it's always very much appreciated if you take a moment to <a href="https://github.com/openziti/ziti">drop us a star</a>. We see and appreciate every repository star.</p>
<p>File issues, contribute tooling improvements, or drop into the <a href="https://openziti.discourse.group/"><strong>OpenZiti community Discourse</strong></a> if you run into anything unexpected.</p>
<hr />
<p>The full demo runs at <a href="https://youtu.be/b-G_zvmzaks">youtu.be/b-G_zvmzaks</a>, all 4 phases including live provisioning against a Ziti network.</p>
<p><code>zitifier-detect-go</code>: <a href="https://github.com/openziti/zitifier-detect-go">github.com/openziti/zitifier-detect-go</a></p>
<p><code>ziti-mcp-server</code>: <a href="https://github.com/openziti/ziti-mcp-server">github.com/openziti/ziti-mcp-server</a></p>
<p>OpenCode: <a href="https://opencode.ai">opencode.ai</a></p>
]]></content:encoded></item><item><title><![CDATA[What Is ziti-mcp-server? OpenZiti's Full Management API for AI Agents]]></title><description><![CDATA[AI agents are getting tool access to infrastructure faster than most security teams have thought through what that means. If you run OpenZiti, you now have a specific, well-scoped answer to "how should an AI agent interact with my network": ziti-mcp-...]]></description><link>https://blog.openziti.io/what-is-ziti-mcp-server-openzitis-full-management-api-for-ai-agents</link><guid isPermaLink="true">https://blog.openziti.io/what-is-ziti-mcp-server-openzitis-full-management-api-for-ai-agents</guid><dc:creator><![CDATA[Curt Tudor]]></dc:creator><pubDate>Wed, 25 Mar 2026 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/628901e6aad1d357809fe1b8/ba1a8642-0fc8-470c-94a2-89c7ddb91a81.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI agents are getting tool access to infrastructure faster than most security teams have thought through what that means. If you run OpenZiti, you now have a specific, well-scoped answer to "how should an AI agent interact with my network": <strong>ziti-mcp-server</strong> wraps the full Ziti Management API, 209 tools, and exposes it to any MCP-compatible client. Claude Desktop, Cursor, Windsurf, or anything else that speaks Model Context Protocol.</p>
<blockquote>
<p><strong>What is OpenZiti?</strong> OpenZiti is an open-source, zero-trust networking platform that embeds security directly into application connectivity. It replaces traditional VPNs and network perimeters with identity-aware, policy-driven service access — no open ports, no implicit trust. ziti-mcp-server is built on top of it, which means the agent's access to your management plane is governed by the same primitives you use for everything else in your network. It's developed by <a target="_blank" href="https://netfoundry.io/"><strong>NetFoundry</strong></a>,</p>
</blockquote>
<p><strong>TL;DR</strong></p>
<ul>
<li><p>ziti-mcp-server is an MCP server exposing 201 Ziti Management API tools plus 8 session meta-tools, full coverage of identities, services, routers, policies, configs, and fabric operations</p>
</li>
<li><p>Runs as a local process between your MCP client and your Ziti controller; credentials never leave your machine</p>
</li>
<li><p>For quick evaluation, UPDB (username/password) is the fastest path; for production, create a Ziti identity, enroll it, and authenticate using the identity file, the agent becomes a real Ziti identity subject to your access policies</p>
</li>
<li><p>Tested with OpenZiti v1.6+; requires Go 1.24+ if building from source</p>
</li>
<li><p>One gotcha: after any configuration change, restart your MCP client, ziti-mcp-server does not hot-reload</p>
</li>
</ul>
<h2 id="heading-what-is-ziti-mcp-server">What Is ziti-mcp-server?</h2>
<p>ziti-mcp-server is a <a target="_blank" href="https://modelcontextprotocol.io">Model Context Protocol</a> server that gives AI agents direct access to the Ziti Management API. It runs as a local process between your MCP client (Claude Desktop, Cursor, Windsurf) and your Ziti controller, exposing 209 tools: 201 wrapping the full REST management API and 8 for session lifecycle. Nothing is stubbed out or simplified — the tool surface is generated directly from the Ziti OpenAPI spec.</p>
<p>That 209-tool surface covers identities, services, edge routers, service policies, edge router policies, configurations, config types, authenticators, CAs, posture checks, and the fabric layer including terminators, circuits, and cluster operations.</p>
<pre><code class="lang-mermaid">%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#e2e8f0', 'primaryBorderColor': '#3b82f6', 'lineColor': '#64748b', 'background': '#0f172a', 'mainBkg': '#1e293b', 'edgeLabelBackground': '#1e293b'}}}%%
flowchart LR
  classDef client fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd,stroke-width:2px
  classDef server fill:#1e293b,stroke:#0ea5e9,color:#7dd3fc,stroke-width:2px
  classDef ctrl fill:#1a1f2e,stroke:#6366f1,color:#a5b4fc,stroke-width:2px

  MCP["🤖 MCP Client\n(Claude Desktop · Cursor · Windsurf)"]:::client
  ZMS["⚙️ ziti-mcp-server\n(local process)"]:::server
  Ctrl[("🔑 Ziti Controller\nManagement API")]:::ctrl

  MCP --&gt;|"tool calls via stdio"| ZMS
  ZMS --&gt;|"REST + mTLS"| Ctrl
</code></pre>
<p><em>ziti-mcp-server runs locally and proxies MCP tool calls to the Ziti controller's management API. Credentials stay on your machine.</em></p>
<h2 id="heading-what-can-you-actually-ask-it">What Can You Actually Ask It?</h2>
<p>Once connected, your MCP client can answer operational questions and execute management operations through natural language. The agent translates your request into one or more Ziti API calls and returns the result. Multi-step operations happen in a single conversational request, with no scripting required.</p>
<p>Some examples that work well:</p>
<ul>
<li><p><em>"Which identities have access to the payments-api service?"</em>, runs <code>listServiceIdentities</code> and formats the result</p>
</li>
<li><p><em>"Show me a network summary: total identities, services, and edge routers"</em>, combines <code>listIdentities</code>, <code>listServices</code>, and <code>listEdgeRouters</code> with count aggregation</p>
</li>
<li><p><em>"Create a new identity called ci-runner and return the enrollment JWT"</em>, runs <code>createIdentity</code> then <code>createEnrollment</code></p>
</li>
<li><p><em>"List all edge routers and flag any that haven't connected in the last 24 hours"</em>, uses <code>listEdgeRouters</code> with status filtering</p>
</li>
<li><p><em>"What version is the controller running?"</em>, calls <code>getVersion</code></p>
</li>
</ul>
<p>The agent handles multi-step operations naturally. Asking "create an identity, generate an enrollment JWT, and give me the one-time token" produces three sequential API calls without you scripting any of it.</p>
<h2 id="heading-how-do-you-get-running-in-5-minutes">How Do You Get Running in 5 Minutes?</h2>
<p>Pre-built binaries are available for macOS (Intel and ARM), Linux (amd64/arm64), and Windows. On macOS Apple Silicon:</p>
<pre><code class="lang-bash">curl -sL https://github.com/openziti/ziti-mcp-server/releases/latest/download/ziti-mcp-server_darwin_arm64.tar.gz | tar xz
sudo mv ziti-mcp-server /usr/<span class="hljs-built_in">local</span>/bin/
</code></pre>
<p>Or build from source with Go 1.24+:</p>
<pre><code class="lang-bash">go install github.com/openziti/ziti-mcp-server/cmd/ziti-mcp-server@latest
</code></pre>
<p>Register with Claude Desktop, this writes the server entry into Claude's config file, nothing more:</p>
<pre><code class="lang-bash">ziti-mcp-server install
</code></pre>
<p>For Cursor or Windsurf:</p>
<pre><code class="lang-bash">ziti-mcp-server install --client cursor
ziti-mcp-server install --client windsurf
</code></pre>
<p><strong>Now restart your MCP client.</strong> This is the step people miss. Config changes don't hot-reload, the client needs a full restart before it picks up the new server registration.</p>
<p>Once Claude Desktop is back up, you'll see the Ziti tools available. Authenticate at runtime using the <code>loginUpdb</code> tool, pass your controller host, username, and password when Claude asks. That's enough to start exploring.</p>
<h2 id="heading-what-authentication-mode-should-you-use">What Authentication Mode Should You Use?</h2>
<p>The right answer depends on your use case. ziti-mcp-server supports four modes:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Mode</td><td>How it works</td><td>Use it when</td></tr>
</thead>
<tbody>
<tr>
<td><strong>UPDB</strong></td><td>Username + password at runtime</td><td>Evaluating, dev environments</td></tr>
<tr>
<td><strong>Device auth</strong></td><td>Browser-based OAuth2 flow</td><td>Interactive sessions with SSO</td></tr>
<tr>
<td><strong>Client credentials</strong></td><td>OAuth2 client ID + secret</td><td>Automated pipelines, CI</td></tr>
<tr>
<td><strong>Identity file</strong></td><td>Enrolled Ziti identity JSON</td><td>Production, recommended</td></tr>
</tbody>
</table>
</div><p>For anything beyond kicking the tires, the identity file approach is the right model. Create a Ziti identity for the agent, enroll it, and configure ziti-mcp-server to authenticate using the resulting <code>identity.json</code>. Pre-configure it so no credentials are passed at runtime:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create the identity in your Ziti network</span>
ziti edge create identity mcp-agent --role-attributes mcp-agents -o mcp-agent.jwt

<span class="hljs-comment"># Enroll it to produce the identity file</span>
ziti edge enroll mcp-agent.jwt -o mcp-agent.json

<span class="hljs-comment"># Pre-configure ziti-mcp-server to use it</span>
ziti-mcp-server init \
  --auth-mode identity \
  --ziti-controller-host your-controller.example.com \
  --identity-file ./mcp-agent.json \
  --profile prod
</code></pre>
<p>The reason this matters: the agent now has a real Ziti identity. You can scope its access using role attributes and service policies exactly as you would for any other identity in your network. Want the agent to be read-only against production but fully operational against staging? That's a policy decision, not a server configuration. The <a target="_blank" href="https://blog.openziti.io/no-listening-ports">no listening ports model</a> OpenZiti applies to application traffic applies equally well to management plane access, and as of OpenZiti v1.8, the <a target="_blank" href="https://blog.openziti.io/openziti-drinks-its-own-champagne">controller APIs can themselves be OpenZiti services</a>, making this pattern even tighter.</p>
<h2 id="heading-how-do-you-restrict-what-the-agent-can-do">How Do You Restrict What the Agent Can Do?</h2>
<p>Two mechanisms: the <code>--read-only</code> flag and <code>--tools</code> glob filtering.</p>
<p><code>--read-only</code> strips all mutating tools at startup. The agent can query and inspect but cannot create, update, delete, or enroll anything:</p>
<pre><code class="lang-bash">ziti-mcp-server run --read-only
</code></pre>
<p><code>--tools</code> lets you filter by tool name patterns:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Identity operations only</span>
ziti-mcp-server run --tools <span class="hljs-string">'*Identity*'</span>

<span class="hljs-comment"># All list and get operations</span>
ziti-mcp-server run --tools <span class="hljs-string">'list*,get*'</span>

<span class="hljs-comment"># Combine: read-only identity tools only</span>
ziti-mcp-server run --tools <span class="hljs-string">'*Identity*'</span> --read-only
</code></pre>
<p>Multi-profile support lets you configure separate credentials and restrictions per network. A <code>--profile prod</code> can run read-only while <code>--profile staging</code> has full access. Switch between them at runtime using the <code>selectNetwork</code> tool without restarting the server.</p>
<h2 id="heading-frequently-asked-questions-about-ziti-mcp-server">Frequently Asked Questions About ziti-mcp-server</h2>
<h3 id="heading-does-the-llm-see-my-credentials">Does the LLM see my credentials?</h3>
<p>No. Credentials are stored in <code>~/.config/ziti-mcp-server/config.json</code> with <code>0600</code> permissions, readable only by your user. The LLM sees tool names and results, not the underlying auth tokens or identity file contents. When using UPDB at runtime, you pass credentials through the tool call, but they're handled by the local process and never included in the response the model receives.</p>
<h3 id="heading-which-mcp-clients-does-it-support">Which MCP clients does it support?</h3>
<p>Claude Desktop, Cursor, and Windsurf have first-class <code>install</code> support. For any other MCP-compatible client, add the server manually to the client's config:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"ziti"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"/usr/local/bin/ziti-mcp-server"</span>,
      <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"run"</span>]
    }
  }
}
</code></pre>
<p>Then restart the client.</p>
<h3 id="heading-what-openziti-version-does-it-require">What OpenZiti version does it require?</h3>
<p>ziti-mcp-server has been tested with OpenZiti v1.6 and later. The tool surface is generated from the Ziti OpenAPI spec, so older controllers may not support all 201 API tools, earlier API versions will return errors on unsupported operations rather than silently failing.</p>
<h3 id="heading-is-it-production-ready">Is it production-ready?</h3>
<p>The server is stable and the tool coverage is complete, but treat it as you would any agent with management plane access: scope its identity tightly, run <code>--read-only</code> against production where possible, and use role-attribute-based service policies to limit what the identity can reach. The security model is sound precisely because it uses OpenZiti's own access control primitives rather than inventing new ones.</p>
<h3 id="heading-can-i-manage-multiple-ziti-networks-from-one-session">Can I manage multiple Ziti networks from one session?</h3>
<p>Yes. Configure separate profiles with <code>ziti-mcp-server init --profile &lt;name&gt;</code> for each network. Use the <code>listNetworks</code> tool to see all configured profiles and their connection status, and <code>selectNetwork</code> to switch the active profile at runtime.</p>
<hr />
<p>ziti-mcp-server is available now at <a target="_blank" href="https://github.com/openziti/ziti-mcp-server">github.com/openziti/ziti-mcp-server</a> under Apache 2.0.</p>
<p>File issues, contribute tooling improvements, or drop into the <a target="_blank" href="https://openziti.discourse.group">OpenZiti community Discourse</a> if you run into anything unexpected.</p>
<p>If you like OpenZiti, it's always very much appreciated if you take a moment to <a target="_blank" href="https://github.com/openziti/ziti">drop us a star</a>. We see and appreciate every repository star.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing zrok v2.0]]></title><description><![CDATA[Today we're releasing zrok v2.0.0, and I'm really excited about it.
When we shipped v1.0 last year, it was about proving that zrok was production-ready... a solid foundation with a redesigned web cons]]></description><link>https://blog.openziti.io/introducing-zrok-v2-0</link><guid isPermaLink="true">https://blog.openziti.io/introducing-zrok-v2-0</guid><category><![CDATA[Security]]></category><category><![CDATA[networking]]></category><category><![CDATA[zerotrust]]></category><category><![CDATA[golang]]></category><category><![CDATA[internet]]></category><dc:creator><![CDATA[Michael Quigley]]></dc:creator><pubDate>Mon, 23 Mar 2026 18:38:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62e2ad6fd15a9fb24e5172e5/c3baa62e-bd9d-4c14-af5f-a6bb0f65e06a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today we're releasing zrok <code>v2.0.0</code>, and I'm really excited about it.</p>
<p>When we shipped v1.0 last year, it was about proving that zrok was production-ready... a solid foundation with a redesigned web console, a new zrok Agent, and all the infrastructure that a serious release needs. v2.0 is a different kind of milestone. This release is about rethinking the conceptual model where it was weakest and building a much more powerful foundation for what comes next.</p>
<p>In case you're not sure what zrok is, take a look at the <a href="https://blog.openziti.io/introducing-zrok">post introducing zrok</a>. zrok is all about making powerful, secure, peer-to-peer sharing work in a simple way... network resources, files, applications, all with a single command. zrok is open source, self-hostable, and built on top of <a href="https://openziti.io/">OpenZiti</a>. It's developed by <a href="https://netfoundry.io/">NetFoundry</a>, and there's a free hosted instance available at <a href="https://zrok.io/">zrok.io</a>.</p>
<p>For those of you who prefer video, I did a zrok Office Hours previewing the v2.0 changes a while back that is still very relevant:</p>
<p><a class="embed-card" href="https://www.youtube.com/watch?v=iJfWj7umdFI">https://www.youtube.com/watch?v=iJfWj7umdFI</a></p>

<p>Let's dig in...</p>
<h2>The One Main Issue: Namespaces and Names</h2>
<p>The headline change in v2.0 is that reserved sharing is gone. The <code>zrok reserve</code>, <code>zrok release</code>, and <code>zrok share reserved</code> commands have been removed entirely, replaced by a new system of namespaces and names.</p>
<p>This is a major conceptual shift, and I think it's the right one. Here's why.</p>
<p>In the old model, share tokens did double duty as public identifiers. Your share token <em>was</em> the name that the outside world used to reach your share. That seems fine until you realize that share tokens are tied to shares, and shares are tied to environments, which means they're tied to a specific host system and a specific account.</p>
<p>This created real operational problems. Want to move a named share to a different machine? You couldn't do it without disrupting the public name that people were using to reach it. If you stop and think about it, this is a pretty significant limitation for anything beyond casual use.</p>
<p>In v2.0, names are decoupled from shares. You create names, and then you attach those names to shares. If you need to move the sharing backend to a different environment, you just detach the name from the old share and attach it to the new one. The public-facing identity stays stable. Your users never notice.</p>
<p>Namespaces provide the organizational layer above names, giving you logical grouping and scoping. The combination is dramatically more flexible than what reserved sharing could ever offer.</p>
<p>For the full details on how namespaces and names work, see the <a href="https://docs.zrok.io">zrok v2 Migration Guide</a>. But here's the practical upshot: anything you could do with reserved sharing, you can do with namespaces and names... and a lot of things you <em>couldn't</em> do before are now straightforward.</p>
<h2>Try It, Then Keep It</h2>
<p>One of my favorite new features is <code>zrok2 modify name</code>. It solves a workflow problem that has always bugged me.</p>
<p>Previously, you had to decide up front whether a share was going to be ephemeral or reserved. If you shared something ephemerally and then realized you wanted to keep it around, you had to tear it down and recreate it as a reserved share. With <code>zrok2 modify name -r</code> you can just promote an ephemeral name to a reserved name on the fly. It persists indefinitely, or until you decide otherwise. Conversely, <code>zrok2 modify name -r=false</code> will schedule a reserved name for release when its associated share terminates.</p>
<p>Ephemeral by default, persistent by choice. That's the workflow I always envisioned.</p>
<h2>Private Share Allocation</h2>
<p>Now that reserved sharing has been replaced with namespaces, and all shares are effectively ephemeral (while reserved names are not)... where does this leave private sharing?</p>
<p>Private sharing is still one of zrok's super powers. Private shares still use share tokens as identifiers in the same way they always have. But in v1.0, the <code>zrok reserve</code> command was doing double-duty for both public share names and private share tokens. So how does v2.0 handle this?</p>
<p>In v2.0 you can use <code>zrok2 create share</code> and <code>zrok2 delete share</code> to pre-allocate and manage shares for private sharing. This is a streamlined replacement for v1.0's <code>zrok reserve</code> and <code>zrok release</code> commands. The <code>zrok2 share private</code> command includes a <code>--share-token</code> flag that lets you start sharing using an existing pre-allocated share, in the same way that <code>zrok share reserved</code> worked in v1.0. You can also use <code>--share-token</code> to give your private shares human-readable vanity names that are easy to remember and share with collaborators.</p>
<h2>Side-by-Side: Running v1 and v2 Together</h2>
<p>We made a deliberate decision with v2.0: no forced migration. You can run zrok v1 and zrok v2 on the same system simultaneously, with zero interference between them.</p>
<p>The binary is now called <code>zrok2</code> instead of <code>zrok</code>. The environment directory is <code>~/.zrok2</code> instead of <code>~/.zrok</code>. Environment variables use the <code>ZROK2_</code> prefix instead of <code>ZROK_</code>. Linux packages are <code>zrok2</code> and <code>zrok2-agent</code>, the systemd service is <code>zrok2-agent.service</code>, and configuration lives in <code>/etc/zrok2</code>.</p>
<p>This means you can install v2 alongside your existing v1 setup, experiment with the new namespace model, and migrate at your own pace. Your existing v1 environment is completely untouched. When you run <code>zrok2 enable</code>, it creates a fresh environment in <code>~/.zrok2</code>... nothing in <code>~/.zrok</code> is affected.</p>
<p>When you're comfortable and ready to make the switch, just stop using the <code>zrok</code> binary and switch to <code>zrok2</code>. That's it.</p>
<h2>Web Console Improvements</h2>
<p>The web console received a significant overhaul for v2.0. The node graph layout has been drastically improved... the visual network navigator is cleaner and more intuitive. Under the hood, the internals have been cleaned up and refactored.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62e2ad6fd15a9fb24e5172e5/095d7fc1-3425-40b6-9300-e89c162a0027.png" alt="The zrok web console now adapts to any browser size." style="display:block;margin:0 auto" />

<p>There are some nice new interaction features too. A new focus mode lets you press <code>F</code> on the keyboard to focus the graph on just the nodes reachable from your current selection, cutting through the noise when you're working with larger configurations. The detail panel can now be toggled on and off, giving you more screen real estate when you need it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62e2ad6fd15a9fb24e5172e5/6dbeab7a-6aad-4fe6-819a-a7c423d97dc0.png" alt="A smaller web console window." style="display:block;margin:0 auto" />

<p>Overall the web console received a solid dose of attention and cleanup.</p>
<h2>The New <code>dynamicProxy</code> Frontend</h2>
<p>For self-hosters, v2.0 introduces <code>zrok2 access dynamicProxy</code>, a new frontend implementation designed to work with the namespace/names model.</p>
<p>The previous <code>publicProxy</code> frontend worked by parsing the <code>Host</code> header and extracting a share token. That approach was tightly coupled to the old model where share tokens <em>were</em> the public identifiers. The new <code>dynamicProxy</code> receives mapping updates directly from the zrok controller, allowing it to support any kind of mapped name, not just share tokens extracted from headers.</p>
<p><code>zrok2 access public</code> remains available for legacy-style setups, so existing self-hosted deployments aren't disrupted. See the <a href="https://docs.zrok.io">zrok dynamicProxy Guide</a> for details on setting up the new frontend.</p>
<h2>Agent Improvements</h2>
<p>The zrok Agent continues to mature. In v2.0, it includes significantly improved error handling for subordinate processes. When shares or accesses encounter errors, whether during agent reloading or during active runtime, they're now retried using an exponential backoff approach rather than failing hard. Errored processes get transient <code>err_XXXX</code> tokens, which you can use to manage and release them through the normal agent interface. This makes the agent much more resilient in production environments where transient failures are a fact of life.</p>
<p>The agent has also been updated for the v2 naming model. Any share with a reserved name will be automatically restarted by the agent, giving you the persistence behavior you'd expect without manual intervention.</p>
<h2>CLI Quality of Life</h2>
<p>v2.0 includes a set of new commands that make working with your zrok account much more pleasant.</p>
<p>New <code>zrok2 list</code> commands for names, namespaces, environments, shares, and accesses let you query everything in your account. They support filtering on activity, accesses, shares, descriptions, host, IP address, and other criteria. Human-readable tabular output by default, with <code>--json</code> when you need machine-readable output.</p>
<p><code>zrok2 overview</code> now defaults to a human-readable format that lays out your account details clearly. The classic JSON output is still available with <code>--json</code>.</p>
<p><code>zrok2 delete environment</code> lets you clean up environments other than your currently enabled one. Pair it with <code>zrok2 list environments --idle</code> to find and remove stale environments.</p>
<h2>Under the Hood</h2>
<p>For those who care about the internals... v2.0 includes a complete overhaul of the core OpenZiti automation logic. The legacy <code>controller/zrokEdgeSdk</code> package has been replaced with a much more streamlined <code>controller/automation</code> package. If you've ever had to read through the controller code, this is a significant clarity improvement.</p>
<p>All logging has been migrated from <code>pfxlog</code>/<code>logrus</code> to <code>df/dl</code> and <code>log/slog</code>. Use <code>DL_USE_JSON=true</code> for JSON output or <code>DL_USE_COLOR</code> for colorized output.</p>
<p>The root package path has moved to <code>github.com/openziti/zrok/v2</code> to follow Go's v2+ package naming conventions.</p>
<h2>Get Started</h2>
<p>To try zrok 2.0, grab a <code>zrok2</code> binary from the <a href="https://github.com/openziti/zrok/releases">releases page</a> and run <code>zrok2 enable</code> to create a new v2 environment. Your existing v1 setup will remain completely untouched.</p>
<p>For the full details on migrating from v1 to v2, see the <a href="https://docs.zrok.io">zrok v2 Migration Guide</a>.</p>
<h2>Thank You</h2>
<p>Thank you for supporting zrok. We're really excited about where the v2.0 roadmap takes us... namespaces and names open up a lot of possibilities that we're already exploring for future releases.</p>
<p>If you like zrok, it's always very much appreciated if you take a moment to drop a star onto the <a href="https://github.com/openziti/zrok">zrok repository on GitHub</a>. We literally see and appreciate every repository star. If zrok is an important part of your workflow, maybe consider getting a subscription on <a href="https://zrok.io">zrok.io</a>? Those subscriptions are an incredibly helpful signal that we're on the right track.</p>
<p>If there is anything we can do to improve zrok or if you've run into anything we can help with, please reach out to us at our <a href="https://openziti.discourse.group/">OpenZiti Discourse forum</a>. There is a zrok category, and we're standing by ready to help.</p>
]]></content:encoded></item><item><title><![CDATA[Using SPIRE as a Trusted Authority for OpenZiti Identities]]></title><description><![CDATA[Introduction
Identity management is central to modern network security. This post shows how to use short‑lived X.509 client certificates from SPIRE — the reference runtime for SPIFFE (Secure Productio]]></description><link>https://blog.openziti.io/using-spire-as-a-trusted-authority-for-openziti-identities</link><guid isPermaLink="true">https://blog.openziti.io/using-spire-as-a-trusted-authority-for-openziti-identities</guid><category><![CDATA[spiffe]]></category><category><![CDATA[spire]]></category><category><![CDATA[zerotrust]]></category><category><![CDATA[workload-identity]]></category><dc:creator><![CDATA[Kenneth Bingham]]></dc:creator><pubDate>Mon, 23 Mar 2026 15:07:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6287e0eb9490454c4d846d73/945e3374-9d53-4c64-bf3e-45787e651116.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2>
<p>Identity management is central to modern network security. This post shows how to use short‑lived X.509 client certificates from <a href="https://github.com/spiffe/spire?tab=readme-ov-file#readme">SPIRE</a> — the reference runtime for SPIFFE (<a href="https://spiffe.io/">Secure Production Identity Framework For Everyone</a>) — as OpenZiti identities. <a href="https://netfoundry.io/docs/openziti/learn/introduction/">OpenZiti</a> is an open‑source zero‑trust networking platform that represents workloads or devices with verifiable identities. Treating SPIRE as a CA lets you issue ephemeral, non‑interactive identities—ideal for workloads such as Kubernetes pods—without exposing services to public networks.</p>
<h2>How it works</h2>
<p>From OpenZiti’s perspective, SPIRE is another CA that you control. SPIRE issues a client certificate for <code>Workload1</code>. If the issuer is not a root CA, this must be presented with its supporting intermediate issuers. <code>Workload1</code> has a standard OpenZiti identity configuration, such as the following example, which the deployer must craft, referencing the client cert from SPIRE, the private key, and the trust bundle from the OpenZiti controller.</p>
<h3>OpenZiti identity configuration example: <code>work1.json</code></h3>
<pre><code class="language-json">{
  "ztAPI": "https://ziti.example.com:443",
  "id": {
    "cert": "file://work1.chain.pem",
    "key": "file://work1.key",
    "ca": "file://cas.pem"
  }
}
</code></pre>
<p>The client certificate in <code>work1.chain.pem</code> must have URI SAN <code>spiffe://spire1.ziti.example.com/workloads/work1</code>.</p>
<p>The OpenZiti Edge SDK can load this configuration file and connect to OpenZiti services. With this approach, there’s no need to enroll the OpenZiti identity. Each short-lived certificate from SPIRE can be used to obtain a session authorized to perform any operation granted to the OpenZiti identity for which its external ID maps to the SPIFFE ID.</p>
<h2>Configuring OpenZiti to trust SPIRE</h2>
<p>Let's walk through an example procedure for configuring OpenZiti to trust a SPIRE CA, define the OpenZiti identity roles for <code>Workload1</code>'s SPIFFE ID, and connect to an OpenZiti service with the client certificate from SPIRE.</p>
<h3>Step 1: Obtain the trust bundle from the OpenZiti Controller</h3>
<p>Each workload’s OpenZiti identity configuration file must reference this file in the <a href="http://id.ca"><code>id.ca</code></a> property.</p>
<pre><code class="language-bash">curl -skSf https://ziti.example.com:443/.well-known/est/cacerts \
| base64 -d \
| openssl pkcs7 -inform DER -outform PEM -print_certs \
| tee ./cas.pem
</code></pre>
<h3>Step 2: Prove ownership and register the SPIRE CA with OpenZiti</h3>
<p>You can use the <code>ziti</code> CLI if you have direct access to the SPIRE CA’s private key to verify it in a single step.</p>
<pre><code class="language-bash">ziti edge create ca "spire1" ./spire/certs/root.cert \
    --location "SAN_URI" \
    --matcher "SCHEME" \
    --matcher-criteria "spiffe" \
    --parser "NONE" \
    --auth

ziti edge verify ca "spire1" \
    --cacert ./spire/certs/root.cert \
    --cakey ./spire/keys/root.key
</code></pre>
<p>Alternatively, request a client cert from SPIRE with the verification token as the CN part of the DN. Obtain the verification token (e.g., <code>S6ZLJD7XL8</code>) by listing the newly created CA.</p>
<pre><code class="language-bash">ziti edge list cas
</code></pre>
<h3>Step 3: Issue a client certificate with the verification token</h3>
<p>Use the client certificate, with the verification token as the CN value of the subject DN, to prove ownership of the SPIRE CA.</p>
<pre><code class="language-bash">ziti edge verify ca "spire1" \
    --cert ./S6ZLJD7XL8.cert
</code></pre>
<p>Now, the OpenZiti controller trusts the SPIRE CA to issue client certificates for OpenZiti identities.</p>
<h2>Provisioning a workload identity</h2>
<p>Create an OpenZiti identity with the desired role and the SPIFFE ID that SPIRE will assign to this workload. The role must match the OpenZiti policies that authorize the identity to use OpenZiti services and routers.</p>
<pre><code class="language-bash">ziti edge create identity "work1" \
    --external-id spiffe://spire1.ziti.example.com/workloads/work1 \
    --role-attributes httpbin-clients
</code></pre>
<p>Finally, you only need the standard OpenZiti identity configuration JSON file described earlier, referencing the client cert from SPIRE with a matching SPIFFE ID in the URI SAN.</p>
<p>Any OpenZiti Edge SDK can load this standard identity configuration file and use OpenZiti services.</p>
<h2>Using the identity with the <code>zitify</code> tool</h2>
<p>Here's how it would look to use <a href="https://github.com/openziti/zitify">the <code>zitify</code> tool</a> on Linux—a shell script that invokes the OpenZiti Python SDK to “wrap” another program with Ziti sauce via <code>LD_PRELOAD</code>. We'll use <code>curl</code> in this example. The OpenZiti service address (i.e., the “intercept”) in this case is <code>httpbin.ziti.internal:80</code>.</p>
<pre><code class="language-bash">zitify -i work1.json curl --data ziti=works http://httpbin.ziti.internal/post
</code></pre>
<p>The response is JSON from Httpbin containing the request’s form data.</p>
<h2>Using the identity with a tunneler</h2>
<p>You can use an OpenZiti identity configuration file with OpenZiti tunnelers, like <code>ziti-edge-tunnel</code>, which load identities from a standard configuration file by placing the file in the identity directory before starting the tunnel. The tunneler must also be able to read the referenced cert, key, and CA files.</p>
<h2>Using the identity with the CLI</h2>
<p>Authentication works identically for a network administrator, so a SPIRE-authenticated workload can also operate on the OpenZiti network via the management API.</p>
<p>For example, you can provision an admin identity and use its configuration file with the <code>ziti edge</code> administrative CLI commands.</p>
<pre><code class="language-bash">ziti edge create identity "admin2" \
    --external-id spiffe://spire1.ziti.example.com/workloads/admin2 \
    --admin
</code></pre>
<p>Create an identity configuration file referencing the client certificate issued by SPIRE.</p>
<h3>OpenZiti identity configuration example: <code>admin2.json</code></h3>
<pre><code class="language-json">{
  "ztAPI": "https://ziti.example.com:443",
  "id": {
    "cert": "file://admin2.chain.pem",
    "key": "file://admin2.key",
    "ca": "file://cas.pem"
  }
}
</code></pre>
<p>The client certificate in <code>admin2.chain.pem</code> must have URI SAN <code>spiffe://spire1.ziti.example.com/workloads/admin2</code>.</p>
<p>Then, log in to the OpenZiti management API using the configuration file.</p>
<pre><code class="language-bash">ziti edge login --file admin2.json
</code></pre>
<p>...and perform administrative tasks.</p>
<pre><code class="language-bash">ziti edge list summary
</code></pre>
<h2>Conclusion</h2>
<p>By leveraging SPIRE as a trusted CA within OpenZiti, we can streamline the management of workload identities, especially for short-lived, non-interactive services. This integration enhances security and simplifies identity provisioning, making your zero-trust network more robust and easier to manage.</p>
<h2>References</h2>
<ul>
<li><a href="https://netfoundry.io/docs/openziti/learn/core-concepts/security/authentication/third-party-cas/#external-id-and-x509-claims">Using External CAs with OpenZiti</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Getting Metrics from Your OpenZiti Controller over WebSockets]]></title><description><![CDATA[A topic came up on the OpenZiti Discourse about subscribing to controller events over WebSockets. I've been doing exactly this in a side project that needed to show some usage data for a Ziti network,]]></description><link>https://blog.openziti.io/getting-metrics-from-your-openziti-controller-over-websockets</link><guid isPermaLink="true">https://blog.openziti.io/getting-metrics-from-your-openziti-controller-over-websockets</guid><category><![CDATA[Go Language]]></category><category><![CDATA[metrics]]></category><category><![CDATA[openziti]]></category><dc:creator><![CDATA[Dave Hart]]></dc:creator><pubDate>Mon, 16 Mar 2026 20:36:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62a8cff00baf4306d94942af/5a1b3c50-72ad-41a7-8463-a19d9afe298e.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A topic came up on the <a href="https://openziti.discourse.group/t/can-the-ziti-controller-broadcast-events-on-websocket-connections/4542">OpenZiti Discourse</a> about subscribing to controller events over WebSockets. I've been doing exactly this in a side project that needed to show some usage data for a Ziti network, so I figured it was worth writing up what I learned along the way.</p>
<p>The short version: the Ziti controller exposes a management WebSocket at <code>/fabric/v1/ws-api</code> that streams real-time events - usage metrics, session lifecycle, circuit events, etc. It speaks a binary "channel V2" protocol over the WebSocket, which is the same protocol the Ziti infrastructure components use to talk to each other. Once you know the wire format, it's pretty straightforward to connect, subscribe, and start processing events.</p>
<p>The Discourse thread above includes a working JavaScript implementation if that's your language of choice. The rest of this post focuses on Go, since that's what I used, but the concepts are the same regardless of language.</p>
<blockquote>
<p><strong>If you're using Go,</strong> you can skip the manual wire protocol handling entirely. The OpenZiti project provides the <a href="https://github.com/openziti/channel"><code>channel</code></a> library, which handles all the binary framing, message routing, and connection lifecycle. The Ziti CLI's own <code>stream events</code> command <a href="https://github.com/openziti/ziti/blob/main/ziti/cmd/fabric/stream_events.go">uses these libraries</a> - you build your subscription as JSON, send it as a channel message, and register a receive handler. The library takes care of everything else. The rest of this post covers the wire protocol directly, which is useful if you're not in Go or want to avoid the dependency.</p>
</blockquote>
<h2>Authenticating</h2>
<p>First you need a session token. The event stream endpoint uses the same auth as the rest of the management API. You have a couple options:</p>
<p><strong>Username/password:</strong></p>
<pre><code class="language-plaintext">POST https://&lt;controller&gt;/edge/management/v1/authenticate?method=password
Content-Type: application/json

{"username": "admin", "password": "your-password"}
</code></pre>
<p><strong>Certificate-based:</strong></p>
<pre><code class="language-plaintext">POST https://&lt;controller&gt;/edge/management/v1/authenticate?method=cert
</code></pre>
<p>No body needed - the client certificate from the TLS handshake is the credential. You'd use the cert and key from a Ziti identity file for this.</p>
<p>Either way, the response gives you a token in <code>data.token</code>. Hold on to it.</p>
<p>If you're using Go, the Ziti SDK handles this for you:</p>
<pre><code class="language-go">auth := rest_util.NewAuthenticatorUpdb("admin", "password")
auth.RootCas = caPool
session, err := auth.Authenticate(controllerURL)
token := *session.Token
</code></pre>
<h2>Opening the WebSocket</h2>
<p>Connect to <code>wss://&lt;controller&gt;/fabric/v1/ws-api</code> with your session token in a <code>zt-session</code> header.</p>
<p>One thing that tripped me up: the controller's WebSocket handler uses <code>http.Hijacker</code>, which isn't available over HTTP/2. You need to force HTTP/1.1 for the WebSocket handshake. In Go:</p>
<pre><code class="language-go">tlsCfg := &amp;tls.Config{
    RootCAs:    rootCAs,
    NextProtos: []string{"http/1.1"},
}
httpClient := &amp;http.Client{
    Transport: &amp;http.Transport{
        TLSClientConfig:  tlsCfg,
        ForceAttemptHTTP2: false,
    },
}

opts := &amp;websocket.DialOptions{
    HTTPClient: httpClient,
    HTTPHeader: http.Header{
        "zt-session":      []string{token},
        "Accept-Encoding": []string{"identity"},
    },
    CompressionMode: websocket.CompressionDisabled,
}
conn, _, err := websocket.Dial(ctx, "wss://controller:1280/fabric/v1/ws-api", opts)
</code></pre>
<p>I'm using <code>github.com/coder/websocket</code> here, but any WebSocket library that lets you set headers and force HTTP/1.1 will work.</p>
<h2>The Channel V2 Wire Protocol</h2>
<p>This is where it gets interesting. The WebSocket doesn't carry plain JSON - each binary message is a "channel V2" message with this layout:</p>
<pre><code class="language-plaintext">Offset  Size  Field
------  ----  -----
0       4     Magic bytes: 0x03 0x06 0x09 0x0C
4       4     Content type (uint32 LE)
8       4     Sequence number (uint32 LE)
12      4     Headers length (uint32 LE)
16      4     Body length (uint32 LE)
20      var   Headers
20+H    var   Body (JSON)
</code></pre>
<p>Headers are sequential key-value pairs: <code>[4B key LE] [4B value_len LE] [value bytes]</code>.</p>
<p>The content types you care about:</p>
<table>
<thead>
<tr>
<th>Content type</th>
<th>Value</th>
<th>What it is</th>
</tr>
</thead>
<tbody><tr>
<td>Result</td>
<td>2</td>
<td>Response to your subscription request</td>
</tr>
<tr>
<td>StreamEventsRequest</td>
<td>10040</td>
<td>Your subscription message</td>
</tr>
<tr>
<td>StreamEventsEvent</td>
<td>10041</td>
<td>An incoming event</td>
</tr>
</tbody></table>
<p>And two header keys:</p>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td>ReplyFor</td>
<td>1</td>
<td>Which sequence number this result is for</td>
</tr>
<tr>
<td>ResultSuccess</td>
<td>2</td>
<td>Single byte, 1 = success</td>
</tr>
</tbody></table>
<p>Building and parsing these messages is mechanical. Here's Go code for both:</p>
<pre><code class="language-go">var channelMagic = []byte{0x03, 0x06, 0x09, 0x0c}

func channelMarshal(contentType, sequence uint32, headers map[uint32][]byte, body []byte) []byte {
    var hdrs bytes.Buffer
    for k, v := range headers {
        binary.Write(&amp;hdrs, binary.LittleEndian, k)
        binary.Write(&amp;hdrs, binary.LittleEndian, uint32(len(v)))
        hdrs.Write(v)
    }
    hdrBytes := hdrs.Bytes()

    var buf bytes.Buffer
    buf.Write(channelMagic)
    binary.Write(&amp;buf, binary.LittleEndian, contentType)
    binary.Write(&amp;buf, binary.LittleEndian, sequence)
    binary.Write(&amp;buf, binary.LittleEndian, uint32(len(hdrBytes)))
    binary.Write(&amp;buf, binary.LittleEndian, uint32(len(body)))
    buf.Write(hdrBytes)
    buf.Write(body)
    return buf.Bytes()
}

func channelUnmarshal(data []byte) (contentType, sequence uint32, headers map[uint32][]byte, body []byte, err error) {
    if len(data) &lt; 20 || !bytes.Equal(data[:4], channelMagic) {
        return 0, 0, nil, nil, fmt.Errorf("invalid channel message")
    }

    contentType = binary.LittleEndian.Uint32(data[4:8])
    sequence = binary.LittleEndian.Uint32(data[8:12])
    hdrLen := binary.LittleEndian.Uint32(data[12:16])
    bodyLen := binary.LittleEndian.Uint32(data[16:20])

    headers = make(map[uint32][]byte)
    hdrData := data[20 : 20+hdrLen]
    for i := 0; i &lt; len(hdrData); {
        if i+8 &gt; len(hdrData) { break }
        key := binary.LittleEndian.Uint32(hdrData[i : i+4])
        vlen := binary.LittleEndian.Uint32(hdrData[i+4 : i+8])
        headers[key] = hdrData[i+8 : i+8+vlen]
        i += 8 + int(vlen)
    }

    body = data[20+hdrLen : 20+hdrLen+bodyLen]
    return
}
</code></pre>
<h2>Subscribing to Events</h2>
<p>Send a <code>StreamEventsRequest</code> (content type 10040) with a JSON body listing what you want:</p>
<pre><code class="language-go">sub := map[string]any{
    "format": "json",
    "subscriptions": []map[string]any{
        {"type": "fabric.usage", "options": map[string]any{"version": 3}},
        {"type": "edge.sessions"},
    },
}
body, _ := json.Marshal(sub)
msg := channelMarshal(10040, 1, nil, body)
conn.Write(ctx, websocket.MessageBinary, msg)
</code></pre>
<p>Then read messages until you get a Result (content type 2) that replies to your sequence number. Check that header key 2 has a first byte of <code>1</code> for success.</p>
<p>Available event types include <code>fabric.usage</code>, <code>edge.sessions</code>, <code>fabric.circuits</code>, <code>edge.routers</code>, <code>services</code>, <code>fabric.links</code>, and <code>fabric.routers</code>. For usage events, request version 3 - it gives you a cleaner format with separate <code>tags</code> and <code>usage</code> maps.</p>
<h2>Reading Events</h2>
<p>From here it's a read loop. Filter for content type 10041 and parse the JSON body:</p>
<pre><code class="language-go">for {
    _, data, err := conn.Read(ctx)
    if err != nil {
        break // reconnect
    }
    ct, _, _, body, err := channelUnmarshal(data)
    if err != nil || ct != 10041 {
        continue
    }

    var event struct {
        Namespace string            `json:"namespace"`
        Tags      map[string]string `json:"tags"`
        Usage     map[string]uint64 `json:"usage"`
    }
    json.Unmarshal(body, &amp;event)

    if event.Namespace == "fabric.usage" {
        fmt.Printf("service=%s client=%s tx=%d rx=%d\n",
            event.Tags["serviceId"], event.Tags["clientId"],
            event.Usage["ingress.tx"], event.Usage["ingress.rx"])
    }
}
</code></pre>
<p>Usage events include both client and host identity IDs in the tags (<code>clientId</code> and <code>hostId</code>), along with the <code>serviceId</code>. The usage map has directional counters: <code>ingress.tx</code> and <code>egress.rx</code> represent client upload, <code>ingress.rx</code> and <code>egress.tx</code> represent client download.</p>
<h2>Things to Know</h2>
<p><strong>The controller doesn't buffer events.</strong> If your WebSocket disconnects, events that occur during the gap are lost. Build your client to reconnect with backoff and re-subscribe. Session tokens expire, so you'll want to re-authenticate on reconnect too.</p>
<p><strong>Usage events arrive roughly every minute.</strong> The controller aggregates and emits them on an interval, so don't expect sub-second granularity.</p>
<p><strong>HTTP/2 will silently break the handshake.</strong> I learned this the hard way, but the controller requires HTTP/1.1 for the WebSocket upgrade. Force it explicitly in your client.</p>
<blockquote>
<p>For self-hosted controllers that use Ziti's internal CA for TLS, you can fetch the CA bundle from the controller's well-known endpoint at <code>/edge/client/v1/.well-known/est/cacerts</code> and add those CAs to your trust store alongside system CAs. The Go SDK has <code>rest_util.GetControllerWellKnownCas()</code> for this.</p>
</blockquote>
<blockquote>
<p><strong>A note on NetFoundry-hosted controllers.</strong> If you're connecting to a NetFoundry controller with a DNS name ending in <code>-p</code>, you're talking to a controller configured with publicly signed TLS certificates. Standard system CA trust stores will work - no custom CA bundles needed. Note that some NetFoundry environments require connecting to the management API through Ziti itself (i.e., the management API isn't directly reachable on the internet). Check with your NetFoundry contact if you're not sure which applies to you, but if you can't access the Ziti management API from a NetFoundry-hosted controller, this is probably why.</p>
</blockquote>
<p>That's pretty much it. The wire protocol looks intimidating at first, but it's simple once you've written the marshal/unmarshal functions. From there, it's just JSON.</p>
<p>If you have questions or run into issues, the <a href="https://openziti.discourse.group/">OpenZiti Discourse</a> is the best place to ask. And if you do something interesting with the event stream, I'd love to hear about it.</p>
]]></content:encoded></item><item><title><![CDATA[Host a dark service with Java, Spring Boot, and OpenZiti]]></title><description><![CDATA[Hide your application services to protect them against hackers.
This article’s headline mentions hosting a dark service. Huh? Why would you want your application services to be dark?
Open ports are everywhere. You need open ports, right? How are your...]]></description><link>https://blog.openziti.io/host-a-dark-service-with-java-spring-boot-and-openziti</link><guid isPermaLink="true">https://blog.openziti.io/host-a-dark-service-with-java-spring-boot-and-openziti</guid><category><![CDATA[Java]]></category><category><![CDATA[zerotrust]]></category><category><![CDATA[zero-trust]]></category><category><![CDATA[Springboot]]></category><dc:creator><![CDATA[Tod Burtchell]]></dc:creator><pubDate>Wed, 11 Feb 2026 12:07:53 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-hide-your-application-services-to-protect-them-against-hackers"><strong>Hide your application services to protect them against hackers.</strong></h2>
<p>This article’s headline mentions hosting a dark service. Huh? Why would you want your application services to be dark?</p>
<p>Open ports are everywhere. You <em>need</em> open ports, right? How are your users and applications going to connect to your services if there are no open ports? Well, they can still connect—but only if they are trusted.</p>
<p>A <em>dark service</em> is a service that’s had <em>zero trust</em> (ZT) principles applied across the board and, therefore, not only to the network layer. Indeed, the principles of ZT access are baked directly into the application itself.</p>
<p>Dark services have no listening ports, taking them off the internet (and even off the local network) where port scanners and nefarious actors are only an IP hop away.</p>
<h2 id="heading-what-is-zero-trust"><strong>What is zero trust?</strong></h2>
<p>The <em>zero trust</em> phrase has been thrown around a lot lately, but what does it really mean? Here’s how the concept is defined by <a target="_blank" href="https://web.archive.org/web/20240104173544/https://csrc.nist.gov/publications/detail/sp/800-207/final">NIST’s Computer Security Resource Center</a>.</p>
<p>Zero trust (ZT) is the term for an evolving set of cybersecurity paradigms that move defenses from static, network-based perimeters to focus on users, assets, and resources. A zero trust architecture (ZTA) uses zero trust principles to plan industrial and enterprise infrastructure and workflows. Zero trust assumes there is no implicit trust granted to assets or user accounts based solely on their physical or network location (i.e., local area networks versus the internet) or based on asset ownership (enterprise or personally owned). Authentication and authorization (both subject and device) are discrete functions performed before a session to an enterprise resource is established. Zero trust is a response to enterprise network trends that include remote users, bring your own device (BYOD), and cloud-based assets that are not located within an enterprise-owned network boundary. Zero trust focuses on protecting resources (assets, services, workflows, network accounts, etc.), not network segments, as the network location is no longer seen as the prime component to the security posture of the resource.</p>
<p>In practice, you can boil this down to five basic tenants that apply to everything on your network, including both human users and automated processes.</p>
<ul>
<li><p><strong>Deny access by default.</strong> Nobody can connect to the network unless you let them.</p>
</li>
<li><p><strong>Authorize on connect.</strong> Only allow connections to the network that use a strong, well-defined and known identity.</p>
</li>
<li><p><strong>Use explicit access grants.</strong> Even if they can connect, actors cannot do anything unless you let them. Limiting access prevents things such as traversal attacks and information leakage.</p>
</li>
<li><p><strong>Enforce least privilege access.</strong> Granting only permissions that are absolutely required for an actor to complete its tasks limits the damage that can be done when an account is compromised.</p>
</li>
<li><p><strong>Constantly monitor for security compliance.</strong> Zero trust is not just about the initial setup. It includes constantly monitoring policies, connections, and permissions to ensure that ZT principles continue to be enforced over time.</p>
</li>
</ul>
<h2 id="heading-zero-trust-principles-should-not-stop-at-the-network"><strong>Zero trust principles should not stop at the network</strong></h2>
<p>You can apply all the ZT principles to your network, but even after that your application is <em>still</em> listening on a port after that. If there is a port open, your application can be attacked. That’s why the concept of <em>app embedded zero trust</em> moves the edge of the network into the application. Bringing ZT into your app programmatically via an SDK has several benefits.</p>
<ul>
<li><p><strong>No listening ports.</strong> Your application can become totally dark with no open ports. When there are no open ports, the application cannot be scanned or attacked from any random person on the network.</p>
</li>
<li><p><strong>Zero trust of the entire network.</strong> When ZT is in the application, it extends across all networks, including the Internet, local-area networks, and even the operating system. Not trusting the network operating system makes applications immune to network-based side-channel attacks from malicious actors or ransomware that’s trying to attack the application from a stolen or infected device.</p>
</li>
<li><p><strong>Direct access.</strong> Applications are accessed directly from clients; they are not <em>discovered</em>. If the client doesn’t know where the application is, it can’t access it.</p>
</li>
<li><p><strong>Portability.</strong> Once ZT is configured and becomes part of the architectural overlay, your application and your clients need only outbound, commodity internet.</p>
</li>
<li><p><strong>Encrypted data.</strong> Application data is encrypted from the client all the way to the server and back. ZT enforces this.</p>
</li>
<li><p><strong>Micro segmentation.</strong> Applications cannot talk to other applications unless explicitly authorized to do so.</p>
</li>
</ul>
<p>In terms of portability, embedding ZT into an application can help in common use cases. If you want to change clouds or deploy your application into a new data center, no changes need to be made. Simply develop your app once and deploy it anywhere, and it just works. What about mobile clients that want access through a secure shell (<code>ssh</code>) from home, a client site, or the airport? No problem! It doesn’t matter where the client is; trusted connectivity works from anywhere.</p>
<p>How is this achieved? When using OpenZiti, every endpoint (SDK for in-app access, tunnelers for the operating system or edge routers for the network) must have an identity with provisioned certificates. The certificates allow Ziti to perform authentication and authorization before any data flows across secure communications channels. These endpoints reach out of the private network to talk to the controller (control plane) and make connections to join the network fabric mesh (data plane). Therefore, services and endpoints in your private networks only make outbound connections—and thus, no holes are opened for inbound traffic.</p>
<h2 id="heading-an-example-of-app-with-embedded-zero-trust"><strong>An example of app with embedded zero trust</strong></h2>
<p>This example will use <a target="_blank" href="https://web.archive.org/web/20240104173544/https://openziti.github.io/">OpenZiti</a> to provide the ZT overlay network and application SDKs. Spring Boot and Tomcat will host the service. The project will take about half an hour. You will need the following:</p>
<ul>
<li><p>Your favorite text editor or IDE</p>
</li>
<li><p>JDK 11 or later</p>
</li>
<li><p>Access to a Linux environment with the <code>bash</code> shell—or, for Windows users, a VM or Windows Subsystem for Linux 2 (WSL 2)</p>
</li>
</ul>
<p>If you don’t have access to a Linux environment but wish to use it, you can grab a Linux VM from the <a target="_blank" href="https://web.archive.org/web/20240104173544/https://www.oracle.com/cloud/free/">Oracle Cloud free tier</a>.</p>
<p>What is OpenZiti? It’s an open source project sponsored by <a target="_blank" href="https://web.archive.org/web/20240104173544/https://netfoundry.io/">NetFoundry</a>. Oracle embraces open source and zero trust security, so Oracle partnered with NetFoundry to provide zero trust connections to applications, including Java applications, running in Oracle Cloud Infrastructure (OCI). NetFoundry’s Edge Router software is staged within the OCI Marketplace for deployment within any OCI region.</p>
<p><strong>Get the code.</strong> The example code can be <a target="_blank" href="https://web.archive.org/web/20240104173544/https://github.com/netfoundry/openziti-spring-boot/releases">downloaded from here</a>. Alternatively, you can clone it using Git with the following shell command:</p>
<p><code>git clone https://github.com/netfoundry/openziti-spring-boot</code></p>
<p>As with most Spring guides, you can start from scratch and complete each step, or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.</p>
<p><strong>Create the test network.</strong> This example will use a very simple OpenZiti network, shown in <strong>Figure 1</strong>.</p>
<p><img src="https://web.archive.org/web/20240104173544im_/https://orasites-prodapp.cec.ocp.oraclecloud.com/content/published/api/v1.1/assets/CONTCE96E71C3E664627BFBDE93B0C3005BD/Medium?cb=_cache_76fd&amp;format=jpg&amp;channelToken=4d6a6a00a153413e9a7a992032379dbf" alt="A diagram of a simple OpenZiti network" /></p>
<p><strong>Figure 1.</strong> A simple OpenZiti network</p>
<p>For this article, it isn’t important for you to fully understand the components of the OpenZiti network; however, there are two important things to know.</p>
<ul>
<li><p>The controller manages the network, and it’s responsible for the configuration, authentication, and authorization of components that connect to the OpenZiti network.</p>
</li>
<li><p>The router delivers traffic from the client to the server and back again.</p>
</li>
</ul>
<p>To explore the architecture a little deeper, see “<a target="_blank" href="https://web.archive.org/web/20240104173544/https://openziti.github.io/ziti/overview.html#overview-of-a-ziti-network">Overview of a Ziti Network</a>” or watch this <a target="_blank" href="https://web.archive.org/web/20240104173544/https://youtu.be/a9C8nRlrWJ0?t=37/">54-minute video on YouTube</a>.</p>
<p>OpenZiti provides a script that contains set of shell functions that bootstrap the OpenZiti client and network. As with any script, it is a good idea to download it and look it over before adding it to your shell. After running the following instructions, leave this terminal window open, because you’ll need it to configure the network.</p>
<pre><code class="lang-plaintext"># Pull the shell extensions
wget -q https://raw.githubusercontent.com/openziti/ziti/release-next/quickstart/docker/image/ziti-cli-functions.sh

# Source the shell extensions
. ziti-cli-functions.sh

# Pull the latest Ziti CLI and put it on your shell's classpath
getLatestZiti yes
</code></pre>
<p>The shell script above includes a few functions to initialize a network. To start the OpenZiti network overlay, run the following in the same terminal window:</p>
<p><code>expressInstall</code><br /><code>startZitiController</code><br /><code>waitForController</code><br /><code>startExpressEdgeRouter</code></p>
<p>What do those functions do?</p>
<ul>
<li><p><code>expressInstall</code> creates cryptographic material and configuration files required to run an OpenZiti network.</p>
</li>
<li><p><code>startZitiController</code> starts the network controller.</p>
</li>
<li><p><code>startExpressEdgerouter</code> starts the edge router.</p>
</li>
</ul>
<p><strong>Log in to the new network.</strong> The OpenZiti network is now up and running. The next step is to log in to the controller and establish the administrative session that you will use to configure the example services and identities. <code>ziti-cli-functions</code> has the following function to do that:</p>
<p><code>zitiLogin</code></p>
<p><strong>Configure the new network.</strong> Use a script to configure the OpenZiti network. The code for the example contains a network directory. To configure the network, run the following command in the same terminal you used to start the OpenZiti network:</p>
<p><code>./express-network-config.sh</code></p>
<p>If the script produces errors with a lot of <code>ziti: command not found</code> statements, run the following shell command to put <code>ziti</code> in your terminal path:</p>
<p><code>getLatestZiti yes</code></p>
<p>The script will write out the two identity files (<code>client.json</code> and <code>private-service.json</code>) needed for the Java code you’ll write shortly. Note: The repository includes a file called <code>NETWORK-SETUP.md</code> that explains what the script is doing and why.</p>
<p><strong>Reset the Ziti demo network.</strong> If you wish to start over, these are the commands that need to be run to stop the Ziti network and clean up.</p>
<p><code>stopAllEdgeRouters</code><br /><code>stopZitiController</code><br /><code>unsetZitiEnv</code><br /><code>rm -rf ~/.ziti/quickstart</code></p>
<h2 id="heading-host-a-dark-service-using-spring-boot"><strong>Host a dark service using Spring Boot</strong></h2>
<p>Now, you’re at the good part! There are three things that need to be done to host an OpenZiti service in a Spring Boot application.</p>
<ul>
<li><p>Add the OpenZiti Spring Boot dependency.</p>
</li>
<li><p>Add two properties to the service to configure the service identity and service name.</p>
</li>
<li><p>Add an OpenZiti Tomcat customizer to the main application component scan.</p>
</li>
</ul>
<p>The example code contains an initial/server project. Pull that up in your favorite editor and follow along.</p>
<p><strong>Add the OpenZiti Spring Boot dependency.</strong> The OpenZiti Spring Boot dependency is hosted on Maven Central.</p>
<p>If you are using Gradle, add the following to <code>build.gradle</code>:</p>
<p><code>implementation 'org.openziti:ziti-springboot:0.23.12'</code></p>
<p>If you prefer Maven, add the following to <code>pom.xml</code>:</p>
<pre><code class="lang-plaintext">&lt;dependency&gt;
         &lt;groupid&gt;org.openziti&lt;/groupid&gt;
         &lt;artifactid&gt;ziti-springboot&lt;/artifactid&gt;
         &lt;version&gt;0.23.12&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p><strong>Add application properties.</strong> Open the application properties file: <code>src/main/resources/application.properties</code>. The Tomcat customizer provided by OpenZiti needs an identity and the name of the service that the identity will bind. If you followed along with the network setup above, the values will be the following:</p>
<p><code>ziti.id = ../../network/private-service.json</code><br /><code>ziti.serviceName = demo-service</code></p>
<p><strong>Configure the OpenZiti Tomcat customizer.</strong> The Tomcat customizer replaces the standard socket protocol with an OpenZiti protocol that knows how to bind a service to accept connections over the Ziti network. To enable this adapter, open the main application class: <code>com.example.restservice.RestServiceApplication</code>. Then replace</p>
<pre><code class="lang-plaintext">@SpringBootApplication (scanBasePackageClasses = {ZitiTomcatCustomizer.class, GreetingController.class})
</code></pre>
<p><strong>Run the application.</strong> The OpenZiti Java SDK will connect to the test network, authenticate, and bind your service so that other OpenZiti overlay network clients can connect to it.</p>
<p>If you use Gradle, enter the following in a terminal window in your project directory:</p>
<p><code>./gradlew bootRun</code></p>
<p>If you use Maven, run the following in a terminal window in your project directory:</p>
<p><code>./mvnw spring-boot:run</code></p>
<p><strong>Test the new Spring Boot service.</strong> The Spring Boot service you just created is now totally dark, with no listening ports. You can verify this by using the following command in a terminal window:</p>
<p><code>netstat -anp | grep 8080</code></p>
<p>You should find nothing marked as <code>LISTENING</code>. Now, the only way to access the service is via the OpenZiti network. Let’s write a simple client to connect to the service and check that everything is working correctly.</p>
<h2 id="heading-create-a-sample-java-client-application"><strong>Create a sample Java client application</strong></h2>
<p>This section will use the OpenZiti Java SDK to connect to the OpenZiti network. The example source code includes a project and a class that takes care of the boilerplate stuff for you.</p>
<p><strong>Connect to OpenZiti.</strong> The Java SDK needs to be initialized with an OpenZiti identity. It is polite to destroy the context once the code is done, so you will wrap it up in a <code>try-catch</code> construct with a <code>finally</code> block. Here is the code.</p>
<pre><code class="lang-plaintext">ZitiContext zitiContext = null;
try {
  zitiContext = Ziti.newContext(identityFile, "".toCharArray());
  long end = System.currentTimeMillis() + 10000;

  while (null == zitiContext.getService(serviceName) &amp;&amp; System.currentTimeMillis() &lt; end) {
    log.info("Waiting for {} to become available", serviceName);
    Thread.sleep(200);
  }

  if (null == zitiContext.getService(serviceName)) {
    throw new IllegalArgumentException(String.format("Service %s is not available on the OpenZiti network",serviceName));
  }
} catch (Throwable t) {
  log.error("OpenZiti network test failed", t);
}
finally {
  if( null != zitiContext ) zitiContext.destroy();
}
</code></pre>
<p>What’s going on here?</p>
<ul>
<li><p><code>Ziti.newContext</code> loads the OpenZiti identity and starts the connection process.</p>
</li>
<li><p><code>while()</code> inserts a delay. It can take a little while to establish the connection with the OpenZiti network fabric. For long-running applications, this is typically not a problem, but for this little client you need to give the network some time to get everything ready.</p>
</li>
<li><p><code>zitiContext.destroy()</code> disposes of the context and cleans up resources locally and on the OpenZiti network.</p>
</li>
</ul>
<p><strong>Send a request to the service.</strong> The client now has a connection to the test OpenZiti network. Now the client can ask OpenZiti to dial the service and send some data.</p>
<p><strong>Important:</strong> This client is for demonstration purposes only! You should never, ever write a raw HTTP request like this in a real app. <a target="_blank" href="https://web.archive.org/web/20240104173544/https://github.com/openziti/ziti-sdk-jvm/tree/main/samples">OpenZiti has a couple of examples on GitHub</a> that use <code>OKHttp</code> and <code>Netty</code> if you want to work up this code using a real HTTP client.</p>
<pre><code class="lang-plaintext">log.info("Dialing service");
ZitiConnection conn = zitiContext.dial(serviceName);
String request = "GET /greeting?name=MyName HTTP/1.1\n" +
"Accept: */*\n" +
"Host: example.web\n" +
"\n";
log.info("Sending request");
conn.write(request.getBytes(StandardCharsets.UTF_8));
</code></pre>
<p>Here’s an explanation of what the code does.</p>
<ul>
<li><p><code>ZitiConnection</code> is a socket connection over the OpenZiti network fabric that can be used to exchange data with a Ziti service.</p>
</li>
<li><p><code>zitiContext.dial</code> opens a connection through the OpenZiti network to the service.</p>
</li>
<li><p><code>request</code> is used because the connection is essentially a plain socket. The request string is a plain HTTP GET command to the greeting endpoint in the Spring Boot app.</p>
</li>
<li><p><code>conn.write</code> sends the request over the OpenZiti network.</p>
</li>
</ul>
<p><strong>Read the service response.</strong> The service will respond to the request with a JSON greeting. Read the greeting and write it to the log.</p>
<p><code>byte[] buff = new byte[1024];</code><br /><code>int i;</code><br /><code>log.info("Reading response");</code><br /><code>while (0 &lt; (i = conn.read(buff,0, buff.length))) {</code><br /> <code>log.info("=== " + new String(buff, 0, i) );</code><br /><code>}</code></p>
<p>What’s happening? <code>conn.read</code> reads the data sent back from the Spring Boot service via the OpenZiti connection.</p>
<p><strong>Run the client.</strong> If you use Gradle, run the following in a terminal window in the client project:</p>
<p><code>./gradlew build run</code></p>
<p>If you use Maven, run the following in a terminal window in the client project:</p>
<p><code>./mvnw package exec:java</code></p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this article, you’ve seen how the concept of zero trust enables secure computing and how embedding ZT into an application improves the application’s protection by letting it run as a dark service.</p>
<h2 id="heading-dig-deeper"><strong>Dig deeper</strong></h2>
<ul>
<li><p><a target="_blank" href="https://web.archive.org/web/20240104173544/https://blogs.oracle.com/cloud-infrastructure/post/protecting-against-log4shell-and-other-vulnerabilities-with-openziti">Protecting against Log4Shell and other vulnerabilities with OpenZiti</a></p>
</li>
<li><p><a target="_blank" href="https://web.archive.org/web/20240104173544/https://netfoundry.io/oracle-zero-trust-networking/">Oracle zero trust networking</a></p>
</li>
<li><p><a target="_blank" href="https://web.archive.org/web/20240104173544/https://blogs.oracle.com/cloud-infrastructure/post/zero-trust-network-access-with-netfoundry">Zero trust network access with NetFoundry</a></p>
</li>
<li><p><a target="_blank" href="https://web.archive.org/web/20240104173544/https://netfoundry.io/anvil/StopDatabasesBreachesSB.pdf">Stop database breaches with zero trust access to JDBC</a></p>
</li>
<li><p><a target="_blank" href="https://web.archive.org/web/20240104173544/https://blogs.oracle.com/javamagazine/post/quiz-yourself-security-threats-and-malicious-code-modifications">Quiz yourself: Security threats and malicious code modifications</a></p>
</li>
<li><p><a target="_blank" href="https://web.archive.org/web/20240104173544/https://blogs.oracle.com/javamagazine/post/quiz-yourself-using-the-securitymanager-class-in-java">Quiz yourself: Using the SecurityManager class in Java</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[OpenZiti Drinks Its Own Champagne]]></title><description><![CDATA[Management Plane Goes Zero-Trust
TL;DR: Starting with OpenZiti 1.8, controller APIs can bind as OpenZiti services. The same app-embedded zero-trust that secures your applications now secures the management plane itself.
Application-Embedded Zero-Trus...]]></description><link>https://blog.openziti.io/openziti-drinks-its-own-champagne</link><guid isPermaLink="true">https://blog.openziti.io/openziti-drinks-its-own-champagne</guid><category><![CDATA[appsec]]></category><category><![CDATA[zerotrust]]></category><category><![CDATA[zero-trust]]></category><category><![CDATA[openziti]]></category><dc:creator><![CDATA[Clint Dovholuk]]></dc:creator><pubDate>Tue, 25 Nov 2025 15:36:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763758660857/39e57ca6-c27e-4d7a-9469-606ddac80098.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-management-plane-goes-zero-trust">Management Plane Goes Zero-Trust</h1>
<p>TL;DR: Starting with OpenZiti 1.8, controller APIs can bind as OpenZiti services. The same app-embedded zero-trust that secures your applications now secures the management plane itself.</p>
<h2 id="heading-application-embedded-zero-trust-management">Application-Embedded Zero-Trust Management</h2>
<p>OpenZiti has been bringing zero trust to applications since 2019. Embed an OpenZiti SDK into your app, and it immediately gains significant security benefits—no need to open ports, no need to expose services to IP-based networks, no attack surface on the underlay. Your app becomes dark to the network, accessible only through authenticated identities.</p>
<p>You've always had options when securing the controller's management APIs. Split the APIs off that need to be more secure onto private IPs, use internal networks, and offload to the API through nearby tunnelers. These work great and remain solid approaches. But now there's a new option that takes it further with full application-embedded zero-trust for the controller itself.</p>
<h2 id="heading-what-changed-in-18">What Changed in 1.8</h2>
<p>Starting in 1.8, securing those critical APIs changes. Now, <a target="_blank" href="https://netfoundry.io/docs/openziti/reference/configuration/conventions#xweb">XWeb</a> supports overlay-based bindPoints directly! The APIs that shouldn't be exposed to the underlay can be removed from the underlay entirely. Have a look at this bit of controller configuration:</p>
<pre><code class="lang-plaintext">web:
  - name: ziti-management
    bindPoints:
      - identity:
          file: "/path/to/identity.json"
          service: "ziti-controller-api"
    apis:
      - binding: edge-management
      - binding: fabric
</code></pre>
<p>Instead of your bindPoint using an interface and a port like you've been doing for years, now you can specify an OpenZiti identity and bind the Controller APIs directly as OpenZiti services! Now, just like you do with your other services, these critical APIs can be accessed through the OpenZiti overlay itself with no need to bind the services to a tunneler and offload back into private IP space. Naturally, we expect most users will make an identity from their own OpenZiti network; however, you're not limited to using the same overlay you're protecting. If you need or want to use a different OpenZiti overlay to protect the controller's APIs, that's fine. You only need to supply the proper configuration, and the controller will bind the services to the overlay you specify.</p>
<h2 id="heading-why-this-changes-everything">Why This Changes Everything</h2>
<p>If you're reading this blog, you probably understand already why this is so important.</p>
<p><strong>No attack surface.</strong> Your controller doesn't need internet-facing interfaces for sensitive APIs. It's not in DNS. It can't be scanned. Can't be found. Even if an attacker can make local network requests on that private network, those requests can never reach those sensitive APIs.</p>
<p><strong>Manage from anywhere.</strong> Only authenticated and authorized identities get access. Period. No VPN. No bastion host. No "are you on the right network" nonsense.</p>
<p><strong>True zero-trust.</strong> The network that provides identity-based segmentation for applications now uses identity-based segmentation for its own management plane.</p>
<h2 id="heading-the-bigger-picture">The Bigger Picture</h2>
<p>This isn't just a feature. It's philosophical consistency. We tell users: "Don't trust networks. Explicitly trust connections using strong identities. Authenticate and authorize <strong>before</strong> allowing inbound connections. Hide everything. Verify always."</p>
<p>Routers already authenticate and authorize before connecting. You should be able to do the same with your sensitive management plane APIs, and now with 1.8, you can.</p>
<p>The entire management plane can live behind zero-trust segmentation.</p>
<h2 id="heading-the-ziti-cli-goes-app-embedded-too">The <code>ziti</code> CLI Goes App-Embedded Too</h2>
<p>With controller APIs now exposed as OpenZiti services, the <code>ziti</code> CLI is now able to connect through the overlay just like any other zero-trust application. Enable the <code>ziti</code> CLI to use an overlay network by providing the <code>--network-identity</code> parameter during login, then just use the <code>ziti</code> CLI normally:</p>
<pre><code class="lang-bash">ziti edge login mgmt.ziti \
  --network-identity /path/to/network-identity.json \
  --username admin \
  --password admin \
</code></pre>
<p>Now, the CLI becomes a zero-trust client. It doesn't need network routes to the controller, doesn't need to know the controller's IP address, and doesn't need firewall exceptions. It just needs a valid identity with the right permissions. Manage your network securely from anywhere. The overlay handles the rest.</p>
<p><strong>No special underlay rules required. Just OpenZiti.</strong></p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>If you're running 1.8+ and want to secure your APIs using an identity, follow these steps:</p>
<ol>
<li><p>Create an identity for your controller to bind API access with</p>
</li>
<li><p>Define a service for your controller APIs and authorize the identity created above</p>
</li>
<li><p>Split the sensitive APIs off into a separate <code>web</code> section</p>
</li>
<li><p>Update the <code>web</code> section with overlay bindPoints by adding an <code>identity</code> accordingly</p>
</li>
<li><p>Create and authorize an identity to use to access (dial) the management plane APIs once secured</p>
</li>
</ol>
<p>The network is the security. Now for everything.</p>
<h2 id="heading-share-the-project"><strong>Share the Project</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702330572628/7bb2b76c-af3f-45c6-83ab-d519f183024d.png?auto=compress,format&amp;format=webp&amp;auto=compress,format&amp;format=webp" alt class="image--center mx-auto" /></p>
<p>If you find this interesting, please consider <a target="_blank" href="https://github.com/openziti/ziti/"><strong>starring us on GitHub</strong></a>. It really does help to support the project! And if you haven't seen it yet, check out <a target="_blank" href="https://zrok.io/"><strong>zrok.io</strong></a><strong>. It's totally free sharing platform built on OpenZiti! It uses the OpenZiti Go SDK since it's a ziti-native application. It's also</strong> <a target="_blank" href="https://github.com/openziti/zrok/"><strong>all open source too!</strong></a></p>
<p>Tell us how you're using OpenZiti on <a target="_blank" href="https://twitter.com/openziti"><strong>X <s>twitter</s></strong></a>, <a target="_blank" href="https://www.reddit.com/r/openziti/"><strong>reddit</strong></a>, or over at our <a target="_blank" href="https://openziti.discourse.group/"><strong>Discourse</strong></a>. Or you prefer, check out <a target="_blank" href="https://youtube.com/openziti"><strong>our content on YouTube</strong></a> if that's more your speed. Regardless of how, we'd love to hear from you.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing zrok 1.0]]></title><description><![CDATA[What does version 1.0 mean?
It’s always a big deal when a project releases “version 1.0”. Our version 1.0 release validates all of the good things about zrok while improving a few areas that needed it, and also brings several exciting new features.
T...]]></description><link>https://blog.openziti.io/introducing-zrok-10</link><guid isPermaLink="true">https://blog.openziti.io/introducing-zrok-10</guid><category><![CDATA[Announcement]]></category><category><![CDATA[Security]]></category><category><![CDATA[Developer]]></category><category><![CDATA[tools]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Michael Quigley]]></dc:creator><pubDate>Wed, 09 Apr 2025 19:48:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744209956458/656a2fe0-41e5-4628-bd06-a9b3267df001.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-what-does-version-10-mean">What does version 1.0 mean?</h1>
<p>It’s always a big deal when a project releases “version 1.0”. Our version 1.0 release validates all of the good things about zrok while improving a few areas that needed it, and also brings several exciting new features.</p>
<p>The initial release of 1.0 includes major new features like:</p>
<ul>
<li><p>A new polished API console (web interface)</p>
</li>
<li><p>A new “zrok Agent” designed to streamline the management of your zrok resources and provide a web interface making zrok accessible to users who aren’t command-line natives</p>
</li>
<li><p>The “zrok Canary” a new suite of tests designed to focus on both performance and reliability for production zrok environment</p>
</li>
</ul>
<p>We’re going to take a look at these highlights of zrok 1.0 in this blog post. For those of you who prefer videos, I recently did a zrok Office Hours covering this same information:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=cIqkbnv-xAQ">https://www.youtube.com/watch?v=cIqkbnv-xAQ</a></div>
<p> </p>
<h1 id="heading-history">History</h1>
<p>I keep a detailed journal of my work. I like to save screenshots documenting interesting points in the development of the projects that I work on. Sometimes it’s fun to go back and look at them. Here’s a little look into zrok screenshots over the years.</p>
<p>First, starting in 2022, here is a <code>v0.2</code> screenshot. <code>v0.2</code> predates <a target="_blank" href="https://zrok.io/">zrok.io</a> and the hosted service. This is the very first version of zrok that had a “console” and integrated metrics:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744133087807/9da95d37-6436-49c6-967a-21f74f292e92.png" alt class="image--center mx-auto" /></p>
<p><code>v0.2</code> also predates <code>private</code> sharing in zrok. This was just a proof-of-concept exercise to see what a secure public reverse proxy solution on top of OpenZiti might look like.</p>
<p>Then we developed <code>v0.3</code>, which added private sharing and introduced the concept of a “backend mode”:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744133265428/52aa2f76-017c-4336-8a54-b1775041c49b.gif" alt class="image--center mx-auto" /></p>
<p>The “visualizer” concept was first introduced in <code>v0.3</code>. This version of the visualizer is similar to what we ended up with in <code>v0.4</code>.</p>
<p>The main goal of the <code>v0.4</code> series was to develop zrok into a platform that could support a zrok as a service (<a target="_blank" href="https://zrok.io/">zrok.io</a>). A lot of refinement and polish went into making <code>v0.4</code> usable and useful for a wider range of users:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744133390425/30237eb1-fa20-483c-86e9-25b1be9a8b68.png" alt class="image--center mx-auto" /></p>
<p>Every time there is a major refresh of the zrok user interface, I feel like the new version feels like a substantial leap forward.</p>
<p>This brings us to the first major feature of zrok <code>v1.0</code>, which is the updated API console.</p>
<h1 id="heading-the-new-api-console">The New API Console</h1>
<p>The new zrok API console (the primary “web interface”) has received a major refresh for <code>v1.0</code>. In addition to new branding, the technology under the covers has been streamlined as well. Here are some screenshots so you can get the vibes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744134630638/d2d3ba1c-f4e7-4f93-b094-1e9c013c7052.png" alt class="image--center mx-auto" /></p>
<p>These things are subjective, but I very much look at this new user interface and feel the same way about it that I’ve felt about previous iterations… it looks like a substantial improvement in polish and usability versus the previous version.</p>
<p>Under the covers the API console has been re-developed in Typescript using Vite. It’s still a React application, but the stack has been radically cleaned up and simplified. This new stack will give us a solid platform to extend and build on going forward.</p>
<p>The new API console retains the same visual approach to navigating your zrok resources as the previous release… but in addition, the new API console provides a “tabular” mode to easily filter and sort the contents of your account:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744134838383/6ac1f878-3e82-4fef-b862-c6ca674c14ce.png" alt class="image--center mx-auto" /></p>
<p>When sorted by activity, the tabular mode operates kind of like a “top” command for your zrok account.</p>
<p>For users with large, active accounts, the sorting and filtering features can help you to zero in on the specific resources you’re looking for. The selection state is maintained between the visualizer and tabular modes, allowing you to search and then quickly visualize.</p>
<p>The forms and dialogs have also received a lot of attention and polish:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744134927112/36f17c1f-ebc7-4d88-8537-7d5dcc6d7eca.png" alt class="image--center mx-auto" /></p>
<p>We’re excited about the new streamlined look-and-feel, too.</p>
<h1 id="heading-zrok-agent">zrok Agent</h1>
<p>If you’re a zrok power user who has ever used more than a single <code>zrok share</code> or <code>zrok access</code>, you’ve surely run into the issue where you’ve needed to manage a number of zrok processes. Some of them might go in <code>systemd</code>. Some of them might sit in terminal windows. You might even <code>nohup</code> some of them into the background.</p>
<p>zrok v1.0 introduces a new “zrok Agent” to make this situation much simpler. The Agent is a kind of intelligent “process manager” for all of your zrok shares and accesses. It’s designed to work well for both end-user environments (for developers and end-users) and also for headless production environments.</p>
<p>When you’re not running the zrok Agent, the v1.0 command-line works the way it always has… If you run a <code>zrok share</code>, you’ll end up with a single process for that share, like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744135682541/8178eb22-1424-4dfb-8f32-4bea6987dc6b.png" alt class="image--center mx-auto" /></p>
<p>But when I start the new zrok Agent using the <code>zrok agent start</code> command, we get a different operating mode. With the zrok Agent running, if I do a <code>zrok share</code>, I get a different result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744135820048/21f79d74-b5bd-4cfe-becf-4d1454bcf487.png" alt class="image--center mx-auto" /></p>
<p>Notice that the <code>zrok share public</code> command above returned immediately, displaying the share token and the URL for my new share. When I ran <code>zrok share public</code> with the Agent running, the Agent was detected and the process for my new share was managed by the Agent automatically.</p>
<p>With the zrok Agent you’ll have a single process to manage. You can put your <code>zrok agent start</code> command into <code>systemd</code> using the <code>zrok-agent</code> package for Linux. You can <code>nohup</code> it. You can just stash it away in a terminal window somewhere. You can run it as a Windows service. But once you’ve managed that single process, your whole zrok experience becomes simpler.</p>
<p>You might also notice the <code>zrok agent status</code> command in the terminal above. That command is used to display the shares and accesses being managed by the Agent.</p>
<p>There are additional commands under the <code>zrok agent</code> tree for managing shares and accesses:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744135943261/4141ac00-4a3d-4801-aa52-6d6d0c19b6bf.png" alt class="image--center mx-auto" /></p>
<p>And the <code>zrok agent release share</code> command can direct the agent to release the share.</p>
<p>The zrok Agent also includes a <code>localhost</code> web interface, which can be used to create and remove shares from the agent… yes, when you’re running the zrok Agent you can create new shares <em>without using the command-line</em> interface!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744136149257/08b3caad-b8c5-4741-9e95-ec497afd5fff.png" alt class="image--center mx-auto" /></p>
<p>The zrok “Agent console” is an early preview and a work-in-progress. There are currently a limited subset of share types that can be created through the Agent console, but we expect to elaborate this into a first-class interface over the next handful of releases.</p>
<p>We expect the zrok Agent console to grow into a super useful desktop assistant for managing your local zrok shares and accesses. This will be especially helpful for non-CLI users.</p>
<h2 id="heading-remote-agent-administration">Remote Agent Administration</h2>
<p>We’re not quite there yet, but we’re very excited about one of the features we’re actively developing for the zrok Agent… opt-in “remote administration”. Imagine you have a headless system somewhere with an enabled zrok environment running a zrok Agent. Sure, you could <code>ssh</code> into that system to create and manage shares. But what if you could simply enroll your zrok Agent so that it can be managed from the central zrok API console… from anywhere?</p>
<p>This kind of remote administration would allow you to create and release shares and accesses on remote environments through the zrok API console. Need to get into a remote system without direct access? This could be a way to make that very simple. This also allows you to shut down shares and accesses when you’re not using them, increasing security.</p>
<p>Remote administration will be completely opt-in and will be the kind of capability you can enable only when you need it. If you do not enroll your Agent, then that Agent is an island unto itself that you can only access locally. You’ll be in control.</p>
<h1 id="heading-zrok-canary">zrok Canary</h1>
<p>We take the reliability and performance of zrok very seriously. So seriously that we’re developing a set of performance and reliability tooling into zrok that we collectively refer to as the “zrok Canary”.</p>
<p>For the initial 1.0 release, we’ve re-worked the old <code>zrok test loop public</code> infrastructure into a more polished set of tools underneath <code>zrok test canary</code>.</p>
<p>We expect to continue elaborating these tools into a wide suite of monitors and tests to properly validate all of the major operating conditions of a zrok instance. And we expect to be rolling these tools out across <a target="_blank" href="https://zrok.io/">zrok.io</a> as they’re available to give us deeper and better visibility into how zrok is operating.</p>
<h1 id="heading-switching-to-10">Switching to 1.0</h1>
<p>To switch to 1.0, simply obtain a zrok binary at <code>v1.0.2</code> or later, and your existing environments will automatically be migrated to the new version, transparently.</p>
<h1 id="heading-the-medium-term-roadmap">The Medium-term Roadmap</h1>
<p>There are a lot of exciting new features and capabilities lined up for the <code>v1.0.x</code> release series!</p>
<h2 id="heading-zrok-for-openziti-networks">zrok for OpenZiti Networks</h2>
<p>Have an existing OpenZiti network? Want to quickly and easily add zrok capabilities to your OpenZiti network on-demand? We’re taking a good look at how best to add and remove zrok as a capability of an existing OpenZiti network.</p>
<h2 id="heading-remote-agent-administration-1">Remote Agent Administration</h2>
<p>Enroll your zrok Agent for remote administration through the zrok API console. Un-enroll the instant you’re done. All communications between your zrok Agent and the zrok instance will be handled securely through the OpenZiti overlay.</p>
<h2 id="heading-a-complete-zrok-agent-console">A Complete zrok Agent Console</h2>
<p>Do everything through the zrok Agent console that you can do through the command-line. Keep this interface on your desktop and never touch the zrok CLI again.</p>
<h2 id="heading-reserved-namespace-improvements">Reserved Namespace Improvements</h2>
<p>Everyone wants the same names for their reserved shares: <code>staging</code>, <code>testing</code>, etc. We’ll be taking a good hard look at how to give users better, more specific namespaces allowing them to use the names that they want to use.</p>
<h2 id="heading-more-zrok-drives-making-drives-more-useful">More zrok Drives; Making Drives More Useful</h2>
<p>Magic wormholes. Drive-by upload boxes. Ephemeral downloads. We’ll be looking at ways to build on the proof-of-concept that is the current zrok Drives implementation, making it useful for all kinds of data-in-motion use cases.</p>
<h2 id="heading-api-console-timeline-and-notifications">API Console Timeline and Notifications</h2>
<p>See how your zrok resources have changed over time. Answer questions like “when exactly did I remove that share?” We’ll also be looking at notifications and working on providing better mechanisms for communicating with users.</p>
<h2 id="heading-openziti-ha-control-plane-integration">OpenZiti HA Control Plane Integration</h2>
<p>OpenZiti has an HA control plane and we’re going to teach zrok how best to take advantage of that. zrok is one of our examples of best practices around “Ziti Native Applications” and we’re going to continue and expand that tradition to include HA.</p>
<h1 id="heading-thank-you">Thank you!</h1>
<p>Thank you for supporting zrok. We’re really looking forward to seeing where the 1.0 roadmap takes us.</p>
<p>If you like zrok, it’s always very much appreciated if you take a moment to drop a star onto the <a target="_blank" href="https://github.com/openziti/zrok">zrok repository on GitHub</a>. We literally see and appreciate every repository star.</p>
<p>If zrok is an important part of your workflow, maybe you would consider getting a subscription on <a target="_blank" href="https://zrok.io/">zrok.io</a>? Those subscriptions are an incredibly helpful signal that we’re on the right track.</p>
<p>If there is anything we can do to improve zrok or if you’ve run into anything we can help with, please reach out to us at our <a target="_blank" href="https://openziti.discourse.group/c/zrok/24">OpenZiti Discourse forum</a>. There is a <a target="_blank" href="https://openziti.discourse.group/c/zrok/24">zrok category</a>, and we’re standing by ready to help.</p>
]]></content:encoded></item><item><title><![CDATA[zrok Unleashed: Top 10 Uses Explored]]></title><description><![CDATA[As we roll into 2025, we’re entering into an exciting time for the world of zrok. We’ve just recently added support for custom domains, and with the 1.0 release right around the corner, zrok is more powerful than it has ever been. We thought this was...]]></description><link>https://blog.openziti.io/zrok-unleashed-top-10-uses-explored</link><guid isPermaLink="true">https://blog.openziti.io/zrok-unleashed-top-10-uses-explored</guid><category><![CDATA[zrok]]></category><dc:creator><![CDATA[Mike Guthrie]]></dc:creator><pubDate>Wed, 05 Mar 2025 14:23:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741184699506/3d770cf5-535b-4f7d-8bc7-9ebbba03ebcd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As we roll into 2025, we’re entering into an exciting time for the world of zrok. We’ve just recently added <a target="_blank" href="https://blog.openziti.io/zrok-custom-domains">support for custom domains</a>, and with the 1.0 release right around the corner, zrok is more powerful than it has ever been. We thought this was a good time to zoom out and take a look at how users are actually using our <a target="_blank" href="https://myzrok.io">hosted zrok as a service</a>.</p>
<p>As part of the larger open source <a target="_blank" href="https://github.com/openziti/zrok/">OpenZiti project</a>, zrok uses an <a target="_blank" href="https://openziti.io/docs/learn/introduction/">OpenZiti overlay network</a> for any secure communications. When you start an instance of zrok, zrok will create a secure zero trust connection to the OpenZiti overlay. This connection is also how zrok can provide truly end-to-end encrypted tunnels with the <code>--tcpTunnel</code> <a target="_blank" href="https://docs.zrok.io/docs/getting-started#share-backend-modes">backend mode</a>. Given its nature, it’s difficult to know what services zrok is providing, but we were able to run some statistics based on the terminating port and make some inferences on whether or not the port was exposed as a public or private share. Here’s the list that we came up with. I found some of these use cases particularly cool, and I discovered some use cases in the process I’d never actually thought of. We’re hoping this post gives you some fresh eyes with zrok and you discover some new possibilities for how powerful it can be for resource sharing!</p>
<h2 id="heading-1-local-development-servers">#1. Local Development Servers</h2>
<p><em>Ports: 80,8000,8080,3000,3001,5173</em></p>
<p>No surprise here! This is probably the original use case that zrok was built around, and zrok just makes it so easy to share your local environment and alleviates the need to stand up an entire shared environment just to be able to show off a local demo, collaborate on a pre-release, or share resources in a distributed developer environment. Whether you’re running apache, nginx, nodejs, or Vite, zrok allows you to easily share any of these technologies.</p>
<p><code>zrok share public 8080</code></p>
<h2 id="heading-2-minecraft-servers">#2. Minecraft Servers</h2>
<p><em>Port: 25565</em></p>
<p>Still one of the best games of all time, and considering how easy it is to run your own server and add custom mods, zrok provides an easy way to share a private game server with your friends without having to open up your firewall ports to the world.</p>
<p>See <a target="_blank" href="https://blog.openziti.io/minecraft-over-zrok">this tutorial</a> for full details on how to run your minecraft server over zrok!</p>
<h2 id="heading-3-aws-sagemaker-notebooks">#3. AWS Sagemaker Notebooks</h2>
<p><em>Port: 7860</em></p>
<p>About a year ago we had a huge amount of buzz from the community that outlined how to integrate zrok with <a target="_blank" href="https://aws.amazon.com/sagemaker/">AWS Sagemaker</a> notebooks. <a target="_blank" href="https://www.youtube.com/@pogscafe">Pogs Cafe</a> put together some great video content that shows zrok integrated with their launcher for ephemeral AI image generation. <a target="_blank" href="https://youtu.be/Tl5eHI_AMmw?si=c9JEfWi0T-Gord2s&amp;t=56">See zrok in action here</a>, or check out their <a target="_blank" href="https://www.pogs.cafe/software/tunneling-sagemaker-kaggle">blog post</a>.</p>
<h2 id="heading-4-bluebubbles">#4. BlueBubbles</h2>
<p><em>Port: 1234</em></p>
<p>BlueBubbles is a free, open-source app ecosystem that allows users to send and receive iMessages on non-Apple devices. <a target="_blank" href="https://newreleases.io/project/github/BlueBubblesApp/bluebubbles-server/release/v1.9.6">BlueBubbles leverages zrok</a> as one of its built-in proxy services so that you can run BlueBubbles server without the need for port-forwarding.</p>
<h2 id="heading-5-irc-internet-relay-chat">#5. IRC - Internet Relay Chat</h2>
<p><em>Port: 6666</em></p>
<p>The original chat server, has been alive since 1988. zrok provides a way for you to easily put modern security around your chat server with end-to-end encryption, and private access tokens with the use of <a target="_blank" href="https://docs.zrok.io/docs/concepts/tunnels/">private TCP shares</a>.</p>
<h2 id="heading-6-ssh">#6. SSH</h2>
<p><em>Port: 22</em></p>
<p>Who needs a Bastion anyway? That’s so 2010! Just leave 22 closed to the world and put a zrok private TCP share on the host and call it a day. No open ports. Simply fire up a <a target="_blank" href="https://docs.zrok.io/docs/guides/linux-user-share/">zrok share as a systemd service</a></p>
<h2 id="heading-7-jupyter-notebooks-ai-notebooks">#7 - Jupyter Notebooks / AI Notebooks</h2>
<p><em>Port: 8188</em></p>
<p>This one was a little bit ambiguous because there are multiple AI Notebook tools that all rely on this same default port. We know that <a target="_blank" href="https://jupyter.org/">Jupyter Notebooks</a> is one of the biggest uses in this bucket, and some cloud providers like Google Cloud AI and Hauwei Cloud AI also make use of these tools.</p>
<h2 id="heading-8-squid-proxy">#8. Squid Proxy</h2>
<p><em>Port: 3128</em></p>
<p><a target="_blank" href="https://www.squid-cache.org/">Squid proxy</a> is an easy-to-use caching and proxy server that can be used to give greater control over outbound traffic whether you’re testing compatibility with a corporate proxy for your application, or you need funnel traffic through a remote server’s address, zrok gives you an easy way to set this up. Just create a share that terminates on your proxy service and set your HTTP_PROXY/HTTPS_PROXY variables in your environment, and all http-based traffic will now filter through your zrok proxy.</p>
<h2 id="heading-9-foundryvtt">#9. FoundryVTT</h2>
<p><em>Port: 30000</em></p>
<p>Probably my personal favorite use case for zrok. <a target="_blank" href="https://foundryvtt.com/">FoundryVTT is a virtual table top game server</a>, and zrok provides a fantastic way to host a virtual game night from your own PC while still protecting your home network. Check out this <a target="_blank" href="https://www.youtube.com/watch?v=x-3PODwEdDM">YouTube tutorial here!</a></p>
<h2 id="heading-10-nomachine-remote-desktop">#10. NoMachine Remote Desktop</h2>
<p><em>Port 4000</em></p>
<p>Remote desktop is incredibly handy, but it’s also something you would never want to expose as an open port on the internet. <a target="_blank" href="https://www.nomachine.com/">NoMachine is a free multi-platform remote desktop tool</a>. With the use of <a target="_blank" href="https://docs.zrok.io/docs/concepts/tunnels/">private TCP shares</a>, zrok enables you to maintain secure remote access from anywhere without ever exposing a port to the internet.</p>
<h2 id="heading-you-tell-us">#??? - You Tell Us!!</h2>
<p><em>Port: ???</em></p>
<p>Got a killer idea for zrok that we missed? Post it in the comments! We always love to hear new use cases for how people are using zrok so that we can make this technology better. And if you haven’t already, help us spread the word about zrok by <a target="_blank" href="https://github.com/openziti/zrok">giving us a star on github!</a></p>
]]></content:encoded></item><item><title><![CDATA[Securing Ziti Identities with HSM/TPM]]></title><description><![CDATA[Regular readers of this blog know that OpenZiti provides secure overlay networking between Ziti identities. You can improve security of your OpenZiti edge identities by using hardware-based private keys. This guide provides step-by-step instructions ...]]></description><link>https://blog.openziti.io/securing-ziti-identities-with-hsmtpm</link><guid isPermaLink="true">https://blog.openziti.io/securing-ziti-identities-with-hsmtpm</guid><category><![CDATA[zerotrust]]></category><category><![CDATA[hardware]]></category><category><![CDATA[Security]]></category><category><![CDATA[TPM]]></category><category><![CDATA[hsm]]></category><dc:creator><![CDATA[Eugene Kobyakov]]></dc:creator><pubDate>Wed, 26 Feb 2025 15:27:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740433372226/36a74b0f-3dfc-496f-8907-00e6fb0aabfd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Regular readers of this blog know that OpenZiti provides secure overlay networking between Ziti identities. You can improve security of your OpenZiti edge identities by using hardware-based private keys. This guide provides step-by-step instructions on integrating hardware security with OpenZiti. It uses Linux built-in TPM as a hardware security device. Similar steps will also work with other HSM devices.</p>
<h2 id="heading-this-is-how-i-enroll">This is How I (en)Roll</h2>
<p>On my Linux(Ubuntu) laptop I check that the kernel detected TPM hardware and created a device for it.</p>
<pre><code class="lang-bash">ziggy@hermes:~$ ls -l /dev/tpm*
crw-rw---- 1 tss root  10,   224 Apr 26 14:41 /dev/tpm0
crw-rw---- 1 tss tss  253, 65536 Apr 26 14:41 /dev/tpmrm0
</code></pre>
<p>Device <code>/dev/tpmrm0</code> is there, so I can proceed.</p>
<p>Add my user to the <code>tss</code> group so that the TPM device can be accessed. You will probably need to restart your login shell for that to take effect, check your groups with <code>id</code> command.</p>
<pre><code class="lang-bash">ziggy@hermes:~$ sudo usermod -G tss -a ziggy
</code></pre>
<p>Install some useful software packages to interact with the TPM device</p>
<ul>
<li><p><code>libtpm2-pkcs11-tools</code> - includes <code>tmp2_ptool</code> to initialize PKCS#11 token on TPM device</p>
</li>
<li><p><code>libtpm2-pkcs11-1</code> - TPM PKCS#11 library</p>
</li>
<li><p><code>opensc</code> - includes <code>pkcs11-tool</code> for interacting with PKCS#11 token. This is not needed for OpenZiti enrollment but is useful for inspecting tokens</p>
</li>
</ul>
<pre><code class="lang-bash">ziggy@hermes:~$ sudo apt install libtpm2-pkcs11-tools libtpm2-pkcs11-1 opensc
</code></pre>
<p>Now that I have access to the device and the needed software I can initialize the token and test PKCS#11 driver access to it</p>
<pre><code class="lang-bash">ziggy@hermes:~$ tpm2_ptool init
action: Created
id: 1

ziggy@hermes:~$ tpm2_ptool addtoken --pid 1 --sopin 1111 --userpin ziggy1 --label ziggy-tpm

ziggy@hermes:~/ziti$ pkcs11-tool --module /usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so --list-slots
WARNING: Getting tokens from fapi backend failed.
Available slots:
Slot 0 (0x1): ziggy-tpm
  token label        : ziggy-tpm
  token manufacturer : Infineon
  token model        : SLB9665
  token flags        : login required, rng, token initialized, PIN initialized
  hardware version   : 1.16
  firmware version   : 5.62
  serial num         : 0000000000000000
  pin min/max        : 0/128
Slot 1 (0x2): 
  token state:   uninitialized
</code></pre>
<p>Note: fapi backend warning can be <a target="_blank" href="https://github.com/tpm2-software/tpm2-pkcs11/issues/655#issuecomment-781500908">ignored</a>.</p>
<p>To enroll I need to get an enrollment token from my Ziti controller. Once I have that I can enroll. Let’s enroll with <code>ziti-edge-tunnel</code></p>
<pre><code class="lang-bash">
<span class="hljs-comment"># enroll identity with ziti-edge-tunnel</span>
ziggy@hermes:~$ ziti-edge-tunnel enroll -j ./ziggy.jwt -i ziggy.json -k <span class="hljs-string">'pkcs11:///usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so?label=ziggy-key&amp;pin=ziggy1'</span>
(1676722)[        0.000]    INFO ziti-sdk:utils.c:198 ziti_log_set_level() <span class="hljs-built_in">set</span> <span class="hljs-built_in">log</span> level: root=3/INFO
(1676722)[        0.000]    INFO ziti-sdk:utils.c:167 ziti_log_init() Ziti C SDK version 1.5.0 @ga39db85(HEAD) starting at (2025-02-24T15:21:31.292)
(1676722)[        0.000]    INFO ziti-sdk:ziti_enroll.c:112 ziti_enroll() Ziti C SDK version 1.5.0 @ga39db85(HEAD) starting enrollment at (2025-02-24T15:21:31.294)
(1676722)[        0.000]    INFO ziti-sdk:ziti_enroll.c:221 start_enrollment() pkcs11 key not found. trying to generate
</code></pre>
<p>ZIti identity is stores in the JSON file -- <code>ziggy.json</code> in my case. Let take a look</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"ztAPI"</span>: <span class="hljs-string">"https://&lt;my-controller-address&gt;:443"</span>,
  <span class="hljs-attr">"id"</span>: {
    <span class="hljs-attr">"cert"</span>: <span class="hljs-string">"-----BEGIN CERTIFICATE-----...."</span>,
    <span class="hljs-attr">"key"</span>: <span class="hljs-string">"pkcs11:///usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so?label=ziggy-key&amp;pin=ziggy1"</span>,
    <span class="hljs-attr">"ca"</span>: <span class="hljs-string">"-----BEGIN CERTIFICATE-----...."</span>
   }
}
</code></pre>
<p>As you can see the .id.key is referencing the private key stored in my laptop's TPM hardware. I can inspect it using <code>pkcs11-tool</code></p>
<pre><code class="lang-bash">ziggy@hermes:~$ pkcs11-tool --module /usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so --list-objects --pin ziggy1          1 ↵
Using slot 0 with a present token (0x1)
Public Key Object; EC  EC_POINT 384 bits
  EC_POINT:   04610442fa885e120e9c63227aa7c53aeb84a52400264f3452fcf4dc4dbf4ade9ca46e7e15c4d37cc7a317edd860887ccd5746eea70cdea1d106b36b59297ecb93d43c57f4e8aef7029fa55f52cd740292d8de92fbce35c7788d3cc00f528cac2d0e4d
  EC_PARAMS:  06052b81040022 (OID 1.3.132.0.34)
  label:      ziggy-key
  ID:         dad9d0f20cc1d9ec
  Usage:      encrypt, verify, wrap
  Access:     <span class="hljs-built_in">local</span>
Private Key Object; EC
  label:      ziggy-key
  ID:         dad9d0f20cc1d9ec
  Usage:      decrypt, sign, unwrap
  Access:     sensitive, always sensitive, never extractable, <span class="hljs-built_in">local</span>
  Allowed mechanisms: ECDSA,ECDSA-SHA1,ECDSA-SHA256,ECDSA-SHA384,ECDSA-SHA512
</code></pre>
<p>As you can see my TPM token <code>ziggy-tpm</code> now has two objects private, and public keys with same label and id. OpenZiti SDK will use that hardware key to provide identity.</p>
<p>Now I can start <code>ziti-edge-tunnel</code> with that identity:</p>
<pre><code class="lang-plaintext">ziggy@hermes:~$ sudo -E ziti-edge-tunnel run -i ziggy.json
</code></pre>
<p>Note: <code>-E</code> flag is required since I initialized TPM token as user <code>ziggy</code> and there are some support files created in the user’s <code>$HOME</code> directory.</p>
]]></content:encoded></item><item><title><![CDATA[zrok Custom Domains]]></title><description><![CDATA[With the latest release of myzrok.io we are excited to announce the introduction of custom domains!
TL;DR
You can now use your own domain when creating zrok shares! With a zrok custom domain you’ll be able to create shares like <token>.your.domain.co...]]></description><link>https://blog.openziti.io/zrok-custom-domains</link><guid isPermaLink="true">https://blog.openziti.io/zrok-custom-domains</guid><category><![CDATA[myzrok]]></category><category><![CDATA[zrok]]></category><category><![CDATA[dns]]></category><dc:creator><![CDATA[Nick Pieros]]></dc:creator><pubDate>Wed, 12 Feb 2025 20:17:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1739389834988/18400a05-53fc-45ca-b458-c4c56b7b4b81.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With the latest release of <a target="_blank" href="http://myzrok.io">myzrok.io</a> we are excited to announce the introduction of <a target="_blank" href="https://docs.zrok.io/docs/myzrok/custom-domains/">custom domains</a>!</p>
<h2 id="heading-tldr">TL;DR</h2>
<p>You can now use your own domain when creating <a target="_blank" href="https://zrok.io">zrok</a> shares! With a zrok custom domain you’ll be able to create shares like <code>&lt;token&gt;.your.domain.com</code> instead of <code>&lt;token&gt;.share.zrok.io</code>. You’ll need to bring your own domain and have a <a target="_blank" href="http://myzrok.io">myzrok.io</a> Pro plan to get started. Creating a zrok custom domain via myzrok will create a zrok frontend for you. This will allow you to create <a target="_blank" href="https://docs.zrok.io/docs/concepts/sharing-public/">ephemeral shares</a> or <a target="_blank" href="https://docs.zrok.io/docs/concepts/sharing-reserved/">reserved shares</a> using your domain name instead of zrok’s!</p>
<h2 id="heading-what-is-myzrok">What is myzrok?</h2>
<p><a target="_blank" href="http://myzrok.io">myzrok</a> is the self-service billing portal for your zrok account. This is where you can upgrade your zrok to the different plans outlined on our <a target="_blank" href="https://zrok.io/pricing/">pricing page</a> to receive increased data, share, and environment limits based on your needs. This is also where you will now be able to create and manage custom domains if you have a Pro plan!</p>
<h2 id="heading-why-custom-domains">Why Custom Domains?</h2>
<p>Up to this point, all <a target="_blank" href="https://zrok.io">zrok</a> hosted shares have been created using the <code>share.zrok.io</code> domain. This is great when you want to get something hosted quickly and easily. However, there are situations where it would be really nice to host a share on your own domain to make it identifiably yours. This is where custom domains come into play.</p>
<h2 id="heading-how-does-it-work">How Does it Work?</h2>
<p>You can set up your custom domain in just a few easy steps:</p>
<ol>
<li><p>Bring your own custom domain name</p>
</li>
<li><p>Create DNS records for certificate validation and traffic routing</p>
</li>
<li><p>Wait for zrok to validate your records and finalize configuration</p>
</li>
<li><p>Start sharing!</p>
</li>
</ol>
<p>You only need to do this once, after that you can create your shares like normal but utilize your domain instead of zrok’s!</p>
<p>Can’t wait to get started? Check out the step by step instructions in our <a target="_blank" href="https://docs.zrok.io/docs/myzrok/custom-domains/">guide here</a>!</p>
<h2 id="heading-custom-domains-in-action">Custom Domains in Action</h2>
<p>Let’s take a look at how custom domains make zrok even more useful. Let’s say that I own a company <code>acme-corp</code> and I’ve got an upcoming convention I would really like to demo my latest software at. Rather than deploying to an environment, I could run my demo locally and use zrok to create a reverse proxy, which would allow others to access it. This works great, but by sharing my demo via a <code>zrok share</code> command, I would end up with a share link like <code>https://sjy8zgif4s8w.share.zrok.io/</code>. While this is nice and simple, the link generated doesn’t really identify my company, making it harder to leave a lasting impact. This is where a zrok custom domain comes in!</p>
<p>To do this I first create a zrok custom domain <code>demo.acme-corp.com</code> using the <code>acme-corp.com</code> domain.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739205451276/20f27da7-7f4e-4f77-90da-eb3ec06b4cd8.png" alt class="image--center mx-auto" /></p>
<p>Once the custom domain is created, I simply create a few DNS records for <code>acme-corp.com</code>. Within a few minutes I’m able to use the <code>demo.acme-corp.com</code> domain in my zrok shares!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739205561400/54eb0324-fcfa-4fff-9ef4-afc54bd352c8.png" alt class="image--center mx-auto" /></p>
<p>By creating a custom domain, I have also created a <a target="_blank" href="https://docs.zrok.io/docs/guides/self-hosting/personalized-frontend/">zrok frontend</a>. This will create shares in the form of <code>&lt;token&gt;.demo.acme-corp.com</code>. This is much more identifiable to my company however, it still leaves me with the share token at the start of my share. I can improve this by creating a <a target="_blank" href="https://docs.zrok.io/docs/concepts/sharing-reserved/">reserved share</a>. This will reserve a unique identifier for me to use in place of my share token. Not only can I make my share even more branded towards my specific use case, but I can also start and stop it and still maintain the same link!</p>
<p>I’ll first set my frontend as the default for my environment, then I’ll create a reserved share for the resource I want to share. In this case it’s a demo app running on localhost:4000</p>
<pre><code class="lang-bash">zrok config <span class="hljs-built_in">set</span> defaultFrontend demo-acme-corp_YS6RyLSod
zrok configuration updated


zrok reserve public --unique-name <span class="hljs-string">"summit"</span> http://localhost:4000
[   0.388]    INFO main.(*reserveCommand).run: your reserved share token is <span class="hljs-string">'summit'</span>
[   0.388]    INFO main.(*reserveCommand).run: reserved frontend endpoint: https://summit.demo.acme-corp.com

zrok share reserved <span class="hljs-string">"summit"</span>
</code></pre>
<p>I can then have people visit <code>summit.demo.acme-corp.com</code> to check my demo</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739206842262/d9f16273-9aa1-4d66-bbce-eb4f744a36f2.png" alt class="image--center mx-auto" /></p>
<p>Success!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739206905400/6c6c4029-af97-4ece-9f7a-d95d4fab185e.png" alt class="image--center mx-auto" /></p>
<p>This is a relatively simple example of how custom domains can be used to enhance your zrok experience. There are many times where I might want to share something a bit more permanent than a demo, such as a production app. This is where I could take advantage of <a target="_blank" href="https://docs.zrok.io/docs/guides/frontdoor/">zrok frontdoor</a> alongside my custom domain. A zrok frontdoor would allow me to run zrok alongside my production app, enabling me to publicly expose access to it via a reserved share!</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>If you’re ready to dive in and create your own custom domain head over to <a target="_blank" href="http://myzrok.io">myzrok.io</a>, sign up for a Pro plan and navigate to the custom domains page ( icon). For a more in-depth look at how to setup a custom domain you can <a target="_blank" href="https://docs.zrok.io/docs/myzrok/custom-domains/">check out the guide here</a>.</p>
<p>If you find this interesting and like what we’re doing, <a target="_blank" href="https://github.com/openziti/zrok">consider starring the project on GitHub</a>! zrok is open source and built on <a target="_blank" href="https://github.com/openziti/ziti">OpenZiti</a>, which is another great project to check out and star if you haven’t done so already!</p>
<p>If you want to show us how zrok or ziti have been improving your workflow or have feedback you’d like to share with the team we’d love to hear from you over at  <a target="_blank" href="https://twitter.com/openziti">X</a>, <a target="_blank" href="https://www.reddit.com/r/openziti/">reddit</a>, or at our <a target="_blank" href="https://openziti.discourse.group/">Discourse</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Golang Aha! Moments: OOP]]></title><description><![CDATA[Moving to Go as my primary development language was a surprisingly easy transition. Coming from a language with strong OOP roots, like Java, I quickly found many analogs for the OOP constructs I was used to, but also had to adjust my thinking.
This a...]]></description><link>https://blog.openziti.io/golang-aha-moments-oop</link><guid isPermaLink="true">https://blog.openziti.io/golang-aha-moments-oop</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Java]]></category><category><![CDATA[Object Oriented Programming]]></category><dc:creator><![CDATA[Paul Lorenz]]></dc:creator><pubDate>Fri, 01 Nov 2024 14:05:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730383460910/9b2d0062-8b76-47c6-b68d-24810fca9f8b.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Moving to Go as my primary development language was a surprisingly easy transition. Coming from a language with strong OOP roots, like Java, I quickly found many analogs for the OOP constructs I was used to, but also had to adjust my thinking.</p>
<p>This article walks through some of Go’s object oriented features and also discusses how some patterns common in other languages can be written in a way that’s closer to idiomatic Go. It also covers some features of Go that surprised and (in some cases) delighted me.</p>
<h1 id="heading-is-go-object-oriented">Is Go Object Oriented?</h1>
<p>Go allows you to group data into structs, and to associate logic to those structs.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Beans <span class="hljs-keyword">struct</span> {
    Count <span class="hljs-keyword">int</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(beans *Beans)</span> <span class="hljs-title">Spill</span><span class="hljs-params">(n <span class="hljs-keyword">int</span>)</span></span> {
    beans.Count = beans.Count - n
    fmt.Printf(<span class="hljs-string">"spilled %d beans\n"</span>, n)
}
</code></pre>
<p>Since Go supports these simple features, it meets certain bare-minimum standard for object oriented coding.</p>
<p>However, as we’ll cover below, many features like inheritance, method overriding, and abstract types work differently or not at all.</p>
<h3 id="heading-inheritance-vs-composition">Inheritance vs Composition</h3>
<p>Even in languages with a strong object oriented bias, people often give the advice to favor composition over inheritance. Large complicated type hierarchies can lead to fragile designs and unexpected behavior.</p>
<p>Go is strongly biased towards composition. This, in my experience, leads to cleaner, simpler designs, that are easier to refactor and have less surprising behavior.</p>
<p><strong>Note:</strong> Most of the patterns explored below aren’t exclusive to Go. Go just encourages them more than other languages may.</p>
<h3 id="heading-fun-with-functions-nil-receivers">Fun with functions: Nil Receivers</h3>
<p>One difference from languages like Java, is that a nil receiver is allowed and can be handled in Go code. This is the equivalent of being able to call a method on a <code>null</code> object in Java without a <code>NullPointerException</code> and have the method check if <code>this</code> is null.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Beans <span class="hljs-keyword">struct</span> {
    Count <span class="hljs-keyword">int</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(beans *Beans)</span> <span class="hljs-title">Spill</span><span class="hljs-params">(n <span class="hljs-keyword">int</span>)</span></span> {
    <span class="hljs-keyword">if</span> beans == <span class="hljs-literal">nil</span> {
        fmt.Printf(<span class="hljs-string">"you've got no beans to spill!\n"</span>)
     } <span class="hljs-keyword">else</span> {    
        beans.Count = beans.Count - n
        fmt.Printf(<span class="hljs-string">"spilled %d beans\n"</span>, n)
    }
}
</code></pre>
<h1 id="heading-what-do-we-want-to-do">What do we want to do?</h1>
<p>Rather than just look at language features, we’re going to look at what we’re trying to accomplish. Then we can see how to achieve our goals in Go, and how that might be different than a pure object-oriented approach.</p>
<h2 id="heading-handle-different-types-with-a-common-set-of-methods">Handle different types with a common set of methods</h2>
<p>For polymorphic types, Go uses interfaces.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Spillable <span class="hljs-keyword">interface</span> {
    Spill(amount <span class="hljs-keyword">int</span>)
}

<span class="hljs-keyword">type</span> Beans <span class="hljs-keyword">struct</span> {
    Count <span class="hljs-keyword">int</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(beans *Beans)</span> <span class="hljs-title">Spill</span><span class="hljs-params">(n <span class="hljs-keyword">int</span>)</span></span> {
    beans.Count = beans.Count - n
    fmt.Printf(<span class="hljs-string">"spilled %d beans\n"</span>, n)
}

<span class="hljs-keyword">type</span> Tea <span class="hljs-keyword">struct</span> {
    Volume <span class="hljs-keyword">int</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(t *Tea)</span> <span class="hljs-title">Spill</span><span class="hljs-params">(n <span class="hljs-keyword">int</span>)</span></span> {
    t.Volume = t.Volume - n
    fmt.Printf(<span class="hljs-string">"spilled %d ml of tea\n"</span>, n)
}
</code></pre>
<p>You may have noticed that the types don’t have to declare that they are <code>Spillable</code>.</p>
<h3 id="heading-duck-ish-typing">Duck(-ish) Typing</h3>
<p>Unlike many other languages, Go types do not have to explicitly state which interfaces they implement. If the type has the correct methods, it can be used as that interface type.</p>
<p>I’m not sure if it’s accurate to call this duck typing, since you still need to define interfaces. But from where I’m standing, it looks a lot like duck typing and sounds like duck typing, so I’m going to call it duck typing.</p>
<p>This focuses the attention on where the interface is used, not where it’s implemented. In cases where a type is used in multiple contexts, Go pushes you towards multiple interfaces, one for each context. In other languages, I’ve seen types end up with one massive interface, covering all the possible contexts where it might be needed.</p>
<p>Consider an HR application. It might have various entities</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> employees

<span class="hljs-keyword">type</span> Employee <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// has name, email, address, salary, etc</span>
}
</code></pre>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> vendors

<span class="hljs-keyword">type</span> Vendor <span class="hljs-keyword">struct</span> {
   <span class="hljs-comment">// has company name, email, address</span>
}
</code></pre>
<p>You might have a couple of modules, one for sending out email notifications and another for payroll.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> emailer

<span class="hljs-keyword">type</span> Contact <span class="hljs-keyword">interface</span> {
   Email() <span class="hljs-keyword">string</span>
   Name() <span class="hljs-keyword">string</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SendEmail</span><span class="hljs-params">(contact Contact, subj, body <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> { ... }
</code></pre>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> payroll

<span class="hljs-keyword">type</span> Payee <span class="hljs-keyword">interface</span> {
    Name() <span class="hljs-keyword">string</span>
    HomeAddress() <span class="hljs-keyword">string</span>
    Salary() <span class="hljs-keyword">float32</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SendPaychecks</span><span class="hljs-params">(payee Payee)</span> <span class="hljs-title">error</span></span> { ... }
</code></pre>
<p>Note that the payroll and email packages don’t know anything about the employee and vendor packages.</p>
<h3 id="heading-package-coherency">Package Coherency</h3>
<p>Go encourages keeping interfaces in the package where they are consumed. Let’s try adding an <code>Email</code> method to <code>Employee</code>.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(emp* Employee)</span> <span class="hljs-title">Email</span><span class="hljs-params">(<span class="hljs-keyword">string</span> subj, <span class="hljs-keyword">string</span> body)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> email.Email(emp.EmailAddress, subj, body)
}
</code></pre>
<p>If the <code>Contact</code> interface was defined in the <code>employee</code> package, we’d end up with a circular package dependency error. If the <code>emailer</code> package didn’t have a <code>Contact</code> interface, and was able to take <code>Employee</code> instances directly, that would also cause a circular package dependency error.</p>
<p>I found circular package dependency errors to be very frustrating when I first started with Go. Now however, having gotten more comfortable with Go style interfaces, I’ve realized the following:</p>
<ol>
<li><p>It’s OK to have smaller, highly specific interfaces per package. There may be some overlap, but each package will end up with exactly what’s needed. This makes the use clearer for developers using the package.</p>
</li>
<li><p>Having the interfaces in the package will also decouple the package from other packages, eliminating circular dependency issues.</p>
</li>
</ol>
<p>I’m still occasionally annoyed by circular dependency errors, but overall feel that the limitation promotes package decoupling and coherency.</p>
<h3 id="heading-requiring-type-interface-conformance"><strong>Requiring Type Interface Conformance</strong></h3>
<p>In most cases, if your type doesn’t match up with your interface, you’ll get a compile error. In some cases, where you may be doing runtime checks before converting, you might not notice that your type no longer fits the interface.</p>
<p>If you wish to guarantee at compile time that a specific type implements a given interface, you can do this as follows:</p>
<pre><code class="lang-go"><span class="hljs-keyword">var</span> _ Spillable = (*Tea)(<span class="hljs-literal">nil</span>)
</code></pre>
<h3 id="heading-fun-with-functions-function-vs-interface">Fun with functions: Function Vs Interface</h3>
<p>Sometimes you may have an API that only requires a single method. Something like:</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> MessageHandler <span class="hljs-keyword">interface</span> {
    Handle(msg []<span class="hljs-keyword">byte</span>) error
}

<span class="hljs-keyword">type</span> MessageHandlers <span class="hljs-keyword">interface</span> {
    AddHandler(msgType <span class="hljs-keyword">string</span>, handler MessageHandler)
}
</code></pre>
<p>This could also be implemented using a function type.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> MessageHandler <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(msg []<span class="hljs-keyword">byte</span>)</span> <span class="hljs-title">error</span></span>

<span class="hljs-keyword">type</span> MessageHandlers <span class="hljs-keyword">interface</span> {
    AddHandler(msgType <span class="hljs-keyword">string</span>, handler MessageHandler)
}
</code></pre>
<p>Which to use? For some library consumers a function may be more convenient. For others, an interface to implement would be better. Fortunately Go lets you handle both. In Go a function type can have methods defined on it.</p>
<p>This broke my brain slightly when I first encountered it, but it works well to allow both kinds of API.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> MessageHandler <span class="hljs-keyword">interface</span> {
    Handle(msg []<span class="hljs-keyword">byte</span>) error
}

<span class="hljs-keyword">type</span> MessageHandlerF <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(msg []<span class="hljs-keyword">byte</span>)</span> <span class="hljs-title">error</span></span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(f MessageHandlerF)</span> <span class="hljs-title">Handle</span><span class="hljs-params">(msg []<span class="hljs-keyword">byte</span>)</span> <span class="hljs-title">error</span></span> {
    f(msg)
}

<span class="hljs-comment">// example use</span>
handlers.AddHandler(MessageHandlerF(<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(msg []<span class="hljs-keyword">byte</span>)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}))
</code></pre>
<h3 id="heading-extending-interfaces">Extending Interfaces</h3>
<p>Go interfaces can extend other interfaces. The classic examples are the types in the <code>io</code> package.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Closer <span class="hljs-keyword">interface</span> {
    Close() error
}

<span class="hljs-keyword">type</span> Reader <span class="hljs-keyword">interface</span> {
    Read(b []<span class="hljs-keyword">byte</span>) (n, error)
}

<span class="hljs-keyword">type</span> ReadCloser() {
   Closer
   Reader
}
</code></pre>
<p>Older versions of Go used to complain if you extended multiple interfaces that had overlapping method signatures. Recent versions of Go no longer have this limitation, as long as method signatures with the same name match exactly.</p>
<h3 id="heading-anonymous-interfaces">Anonymous Interfaces</h3>
<p>There may be times when you want to invoke a method on a variable, but the method isn’t on the interface you’ve got. You can use an anonymous interface to check if the method exists, and if it does, invoke it.</p>
<p>I’m not sure there’s ever a strong reason to use an anonymous interface over declaring a named interface. I’ve used this feature when an interface was only used in a single method.</p>
<p>In this example we’re looking for a non-standard close method.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">CloseIt</span><span class="hljs-params">(v any)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">if</span> closeable, ok := v.(<span class="hljs-keyword">interface</span>{ CloseMe() error }); ok {
        <span class="hljs-keyword">return</span> closeable.CloseMe()
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<h2 id="heading-we-want-to-share-functionality-across-types">We Want to Share Functionality Across Types</h2>
<p>In object-oriented languages, extending a type (or sometimes multiple types) to create a new type is very common. Go does not allow structs to extend other structs in exactly the same way as most object oriented languages.</p>
<p>What it does offer is type composition that can look something like inheritance. We’ll look at how it works, and the potential pitfalls from this approach.</p>
<p>Go allows embedding both structs and interfaces.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Knocker <span class="hljs-keyword">interface</span> {
    Knock()
}

<span class="hljs-keyword">type</span> Door <span class="hljs-keyword">struct</span> {
    lock sync.Mutex
    knocker Knocker
}
</code></pre>
<p>The Door type has a lock and knocker. As they are, they are just struct members. Since they are private, they won’t be accessible outside the package. If you try to <code>Lock</code> the door or <code>Knock</code> at the door, the compiler will complain.</p>
<p>A door isn’t a lock or a knocker, it has a lock and and a knocker. However, it still makes sense to be able to Lock or Knock the door itself. We can do that as follows.</p>
<pre><code class="lang-go"> <span class="hljs-keyword">type</span> Knocker <span class="hljs-keyword">interface</span> {
    Knock()
}

<span class="hljs-keyword">type</span> Door <span class="hljs-keyword">struct</span> {
    sync.Mutex
    Knocker
}
</code></pre>
<p>When we embed types in our struct this way, the methods and fields of the embedded types become accessible at the top-level of the struct. The embedded types can still be referenced individually using the type name.</p>
<pre><code class="lang-go">door := &amp;Door{
    Knocker : &amp;BrassKnocker{},
}  

door.Mutex.Lock()
door.Unlock()
door.Knocker.Knock()
door.Knock()
</code></pre>
<p>Note that we could also embed a pointer type.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Door <span class="hljs-keyword">struct</span> {
    *sync.Mutex
    Knocker
}
</code></pre>
<h3 id="heading-method-overriding-vs-method-shadowing">Method Overriding vs Method Shadowing</h3>
<p>Methods from embedded types can’t be overridden, in an OO sense, but they can be shadowed. Here’s an example to highlight the difference.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> A <span class="hljs-keyword">struct</span> {}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(a *A)</span> <span class="hljs-title">String</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">return</span> a.Name() 
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(a *A)</span> <span class="hljs-title">Name</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
   <span class="hljs-keyword">return</span> <span class="hljs-string">"A"</span>
}

<span class="hljs-keyword">type</span> B <span class="hljs-keyword">struct</span> {
    A
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(b *B)</span> <span class="hljs-title">Name</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">return</span> b.A.Name() + <span class="hljs-string">" or B"</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span> <span class="hljs-params">()</span></span> {
    a := &amp;A{}
    b := &amp;B{}
    fmt.Println(a.Name())    <span class="hljs-comment">// output: "A"</span>
    fmt.Println(a.String())  <span class="hljs-comment">// output: "A"</span>
    fmt.Println(b.Name())    <span class="hljs-comment">// output: "A or B"</span>
    fmt.Println(b.String())  <span class="hljs-comment">// output: "A"      &lt;-- Maybe unexpected</span>
}
</code></pre>
<p>Because <code>A</code> doesn’t know that it’s embedded by <code>B</code>, it won’t call <code>B</code>'s versions of methods that have been overridden. As long as you use type embedding with a composition mindset instead of an inheritance mindset, you should be OK.</p>
<h3 id="heading-i-want-abstract-types">I Want Abstract Types!</h3>
<p>Abstract types can be a very useful way of providing some base functionality that relies on methods to be supplied by a sub-type. When I first started using Go, I was generally happy and productive with the language. Two things I missed from Java were generics and abstract data types. Generics have since come to <a target="_blank" href="https://go.dev/blog/intro-generics">Go in version 1.18</a>, and I’ve since learned patterns better suited to Go to replace abstract data types.</p>
<p>Here’s a java example of an abstract data type:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AbstractDataStore</span>&lt;<span class="hljs-title">T</span>&gt; </span>{

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> T <span class="hljs-title">LoadEntity</span><span class="hljs-params">(Row r)</span></span>;

    <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;T&gt; <span class="hljs-title">ListEntities</span><span class="hljs-params">(Database db, String query)</span> </span>{
         Cursor c = db.ExecuteQuery(query);
         List&lt;T&gt; result = <span class="hljs-keyword">new</span> ArrayList&lt;T&gt;(); 
         <span class="hljs-keyword">while</span> (c.HasNext()) {
             Row row = c.Next();
             T entity = <span class="hljs-keyword">this</span>.LoadEntity(row);
             result.Add(entity);
         }
         <span class="hljs-keyword">return</span> result;
    }
}
</code></pre>
<p>We’ve got a data store that’s got some generic functionality to iterate over database results and build an entity list. Each concrete implementation would manage the details specific to that entity type.</p>
<h3 id="heading-the-wrong-way">The Wrong Way</h3>
<p>The first time I tried modeling something like this in Go, I came up with a pattern similar to abstract types, but significantly worse because the underlying abstractions weren’t there to support it.</p>
<p>The idea was that if you defined the full API as an interface, the abstract type could keep a reference to the concrete type, and call it as necessary.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> DataStore[T any] <span class="hljs-keyword">interface</span> {
    LoadEntity(r *Row) T
    ListEntities(db Database, query <span class="hljs-keyword">string</span>) []T
}

<span class="hljs-keyword">type</span> BaseStore[T any] <span class="hljs-keyword">struct</span> {
    impl DataStore[T]
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(store *BaseStore[T])</span> <span class="hljs-title">ListEntities</span><span class="hljs-params">(db Database, query <span class="hljs-keyword">string</span>)</span> []<span class="hljs-title">T</span></span> {
    cursor = db.ExecuteQuery(query)
    <span class="hljs-keyword">var</span> result []T
    <span class="hljs-keyword">for</span> cursor.HasNext() {
        row := cursor.Next()
        entity := store.impl.LoadEntity(r)
        result = <span class="hljs-built_in">append</span>(result, entity)
    }
    <span class="hljs-keyword">return</span> result
}
</code></pre>
<p>While this does work, it has a downside. If you treat it as an actual abstract type and try to override any of the base methods in the embedding type, they will only be shadowed. If you then ever forget to call the concrete version on the embedded <code>impl</code>, you’ll get the default version. This kind of bug can be easy to miss and hard to track down.</p>
<p>Subjectively, it also doesn’t feel right. It feels like we’re working against the language, rather than with it.</p>
<h3 id="heading-a-better-way">A Better Way</h3>
<p>The solution above is not that far off from something that feels more in line with Go’s ethos.</p>
<p>What is it we’re trying to accomplish? We’ve got a core set of functionality that needs to delegate some logic. There’s another pattern that fits this description:</p>
<h4 id="heading-strategies">Strategies</h4>
<p>Instead of trying to implement abstract types, we can use strategies. Let’s look at what a strategy oriented version of the above code would look like:</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> EntityLoader[T any] <span class="hljs-keyword">interface</span> {
    LoadEntity(r *Row) T
}

<span class="hljs-keyword">type</span> Store[T any] <span class="hljs-keyword">struct</span> {
    entityLoader EntityLoader[T]
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(store *Store[T])</span> <span class="hljs-title">ListEntities</span><span class="hljs-params">(db Database, query <span class="hljs-keyword">string</span>)</span> []<span class="hljs-title">T</span></span> {
    cursor = db.ExecuteQuery(query)
    <span class="hljs-keyword">var</span> result []T
    <span class="hljs-keyword">for</span> cursor.HasNext() {
        row := cursor.Next()
        entity := store.entityLoader.LoadEntity(r)
        result = <span class="hljs-built_in">append</span>(result, entity)
    }
    <span class="hljs-keyword">return</span> result
}
</code></pre>
<p>It looks very similar, but the intent is different. This is a composition-oriented approach. Rather than extending a BaseStore, then overriding pieces, we compose the various strategies we might need.</p>
<p>This approach also lends itself to sharing functionality in a clean way. You might have three or four different strategies. Some of them might have default implementations that are rarely replaced. Some might have only a small set of implementations that are used across many instances. Some might be different for every instance.</p>
<p>Rather than trying to shape these as overrides in a convoluted type hierarchy, each instance can mix and match as needed.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Every language has some inbuilt preferences. If you’re coming from a heavily object-oriented language, it can take time to adapt how you design your data types in Go. However, I think Go pushes you towards code that is less fragile and cleaner. If I were to write Java code again, it would be positively influenced by the habits I’ve picked up from Go.</p>
<h2 id="heading-about-openziti"><strong>About OpenZiti</strong></h2>
<p><a target="_blank" href="http://github.com/openziti/ziti">OpenZiti</a> is an open-source platform for providing secure and reliable access to network applications. It does this using strong, certificate-based identities, end-to-end encryption, mesh networking, policy-based access control, and app-embedded SDKs. If you find this interesting, please consider <a target="_blank" href="https://github.com/openziti/ziti/"><strong>starring us on GitHub</strong></a>. It helps to support the project! And if you haven't seen it yet, check out <a target="_blank" href="https://zrok.io"><strong>zrok.io</strong></a>. It's a free sharing platform built on OpenZiti! It uses the OpenZiti Go SDK since it's a ziti-native application. It's also <a target="_blank" href="https://github.com/openziti/zrok/"><strong>all open source too!</strong></a></p>
<p>Tell us how you're using OpenZiti on <a target="_blank" href="https://twitter.com/openziti"><strong>X</strong></a><strong><s>Twitter</s></strong>, <a target="_blank" href="https://www.reddit.com/r/openziti/"><strong>Reddit</strong></a>, or over at our <a target="_blank" href="https://openziti.discourse.group/"><strong>Discourse</strong></a>. Or you can check out <a target="_blank" href="https://youtube.com/openziti"><strong>our content on YouTube</strong></a> if that's more your speed. Regardless of how, we'd love to hear from you.</p>
]]></content:encoded></item><item><title><![CDATA[The safest way to make Portainer Internet accessible]]></title><description><![CDATA[If you run Portainer, and you seek a modern, flexible recipe for how to make it secure while also providing flexible access to your authorized users, this article is for you.
A question that sometimes]]></description><link>https://blog.openziti.io/the-safest-way-to-make-portainer-internet-accessible</link><guid isPermaLink="true">https://blog.openziti.io/the-safest-way-to-make-portainer-internet-accessible</guid><category><![CDATA[ziti]]></category><category><![CDATA[Web Security]]></category><category><![CDATA[zerotrust]]></category><category><![CDATA[Security]]></category><category><![CDATA[Portainer]]></category><dc:creator><![CDATA[Curt Tudor]]></dc:creator><pubDate>Thu, 03 Oct 2024 16:15:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727532608880/5fb5b813-b9cf-4fe7-9d36-ffb921cda67a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you run Portainer, and you seek a modern, flexible recipe for how to make it secure while also providing flexible access to your authorized users, this article is for you.</p>
<p>A question that sometimes gets asked is: <em>What types of companies, self-hosters, or general use cases, will gain the greatest benefit from using</em> <a href="https://blog.openziti.io/introducing-openziti-browzer"><em>OpenZiti BrowZer</em></a>?</p>
<p>To help answer the “<em>who benefits</em>” question, much like I did in a <a href="https://blog.openziti.io/effortless-docker-management-with-private-web-access">previous article</a>, this article will frame the answer in the context of a web application named <a href="https://www.portainer.io/">Portainer</a> (a web-accessible container management platform) when it needs to be internet-facing.</p>
<p><code>NOTE: Upcoming articles will describe how similar techniques employing BrowZer can be used to protect and secure other popular web applications. So be sure to subscribe to this blog to be notified when those articles are published.</code></p>
<h1>Why is Portainer interesting?</h1>
<p>Portainer can be used to manage Docker, Kubernetes, and other container environments. It is primarily a web-based management interface, designed to be accessed via a browser. The web-based approach provides convenient access and centralized management, allowing users to interact with their container infrastructure …<em><strong>from anywhere</strong></em>.</p>
<p>Companies benefiting from Portainer include teams with limited in-house container management expertise, those with rapid deployment requirements, those having resource constraints/constrained IT budgets, or teams with members of varying skills, technical and non-technical. It also benefits those with a security and compliance focus.</p>
<p>Hobbyists and IT enthusiasts can also benefit from Portainer to manage containerized services for personal projects, home automation, media servers, and more.</p>
<p>Need to check on something in your docker fleet… while you are standing in line at the coffee shop? Just open a browser on your phone, and surf to your Portainer instance. Easy. Convenient. Powerful.</p>
<p>However…as the old saying goes: <em>Don’t run with scissors</em>.</p>
<p>Portainer is an exceptionally privileged piece of software, and it has near root-level access to your Container infrastructure, so you had better make sure your Portainer instance is safe from malicious actors.</p>
<h1>How to secure Portainer (without BrowZer)</h1>
<p>Getting Portainer up and running is very straightforward using their standard deployment scripts.</p>
<p>When I decided to kick the tires, I used the following code in a <code>docker compose</code> file:</p>
<pre><code class="language-yaml">  portainer:
    image: portainer/portainer-ce:2.21.0
    restart: unless-stopped
    ports:
      - "8000:8000"
      - "8080:9000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./portainer_data:/data
</code></pre>
<p>But when you've decided to allow access to Portainer from the public internet, things require more work.</p>
<p>Various <a href="https://www.portainer.io/blog/how-to-correctly-secure-portainer-when-presented-on-the-internet">writeups</a> have been <a href="https://www.portainer.io/blog/how-to-secure-your-portainer-installation">published</a> by the friendly folks on the Portainer team to provide advice on how to secure Portainer. While not wrong, the instructions are (in my opinion) inflexible and antiquated.</p>
<p>They mention <code>port forwarding</code> the Portainer UI ports (http:9000/https:9443) from a public IP to Portainer.</p>
<blockquote>
<p>This implies you must open some ports to the internet. Having ports open to the internet is something we on the <a href="https://openziti.io/">OpenZiti project</a>, as well as all zero-trust advocates in general, will advise against.</p>
</blockquote>
<p>They strongly recommend network ACLs on your firewall, so you only allow access from known trusted IP addresses.</p>
<blockquote>
<p>This is painful and tedious to set up correctly. It is technically out of reach for many. And, it is also incredibly brittle and inflexible (recall my coffee shop reference — this use case would be precluded in this scenario).</p>
</blockquote>
<p>They even question if you want to expose it to the internet directly at all, and instead, suggest you set up a VPN.</p>
<blockquote>
<p>VPN deployment and configuration is a non-starter for many because of complexity, and inflexibility.</p>
</blockquote>
<p>They assert that Portainer's internal authentication system should <strong>never</strong> be used when presenting Portainer to the internet, but instead you should configure an authentication system to use a suitably secure external mechanism, such as "LDAP" or "OAuth" (the latter of which supports 2FA/MFA).</p>
<blockquote>
<p>Again, this is getting to be a heavy lift for many teams.</p>
</blockquote>
<p>Understandably, they suggest you force/enable the <strong>HTTPS only</strong> option, and upload your own x509 certificates.</p>
<blockquote>
<p>Many would have given up before this :(</p>
</blockquote>
<h1>How to secure Portainer (with BrowZer)</h1>
<p>A better topology is one where Portainer resides in a <a href="https://en.wikipedia.org/wiki/Virtual_private_cloud"><strong>VPC</strong></a>, and the host has NO ports open to the internet. In this kind of deployment, Portainer will be completely invisible to the internet.</p>
<p>I hear you thinking: <em>How do I access Portainer over the web if it’s invisible</em>?</p>
<p>The answer is via a zero-trust overlay network that requires authentication before you (or anyone) can connect to it.</p>
<h3><strong>How to Easily Deploy a Zero-Trust Overlay Network</strong></h3>
<p>The component that implements the zero-trust overlay network solution is OpenZiti.</p>
<p><a href="https://openziti.io/"><strong>OpenZiti</strong></a> is a free, open-source zero-trust networking platform that makes services invisible to unauthorized users. Add zero trust to existing applications with tunnelers, or embed SDKs directly for the strongest posture - either way, every connection is authenticated, authorized, and encrypted end to end.</p>
<h3><strong>BrowZer</strong></h3>
<p>The next component involved in the solution is <code>BrowZer</code>.</p>
<p>The <code>Z</code> in this component's name (within the word normally spelled "<em>browser</em>") is not a typo. It is a purposeful indication that this solution, unique in today's technology offerings for securing browser-based applications, is built as part of the <a href="https://github.com/openziti/"><strong>OpenZiti project</strong></a>.</p>
<p>BrowZer enables you and your organization, enterprises, and self-hosting enthusiasts alike, in the cloud or at home, to operate private-to-the-internet web applications while still easily providing secure access for your authorized internet-based remote users.</p>
<p>I previously published a lengthy article that introduced the <a href="https://blog.openziti.io/introducing-openziti-browzer"><strong>concept of browZer</strong></a>. I recommend giving it a read.</p>
<p>There is a vast amount of documentation, as well as <a href="https://openziti.io/docs/learn/quickstarts/browzer/example/"><strong>quick starts for BrowZer</strong></a>, on the OpenZiti site.</p>
<p>There is also some related ZitiTV content:</p>
<p><a class="embed-card" href="https://www.youtube.com/watch?v=ZPkOQbVEnW0&amp;t=816s">https://www.youtube.com/watch?v=ZPkOQbVEnW0&amp;t=816s</a></p>

<p>NOTE: If you like what you read about OpenZiti, or what you saw in the above ZitiTV episode, but prefer not to self-host it yourself, <a href="https://netfoundry.io/"><strong>we</strong></a> also offer a <a href="https://netfoundry.io/">zero-trust networking platform</a>. If that interests you, <a href="https://netfoundry.io/lets-talk/"><strong>reach out to us for more discussion.</strong></a></p>
<p>Here is a diagram that describes at a high level how browZer operates:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727968501738/3fa80598-cf57-48f0-9fe3-c3dae201a33d.png" alt="" style="display:block;margin:0 auto" />

<h3>Example of Accessing Portainer over BrowZer</h3>
<p>Here is what a user would experience using browZer to access a private-to-the-internet instance of Portainer:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727804563586/4b5e5d9a-5a4f-461a-8bb7-cfe4232d091a.gif" alt="" style="display:block;margin:0 auto" />

<p>Here's what happens above:</p>
<ul>
<li><p>Brave web browser hits the public URL representing the protected instance of Portainer. This is where the BrowZer Bootstrapper runs.</p>
</li>
<li><p>The BrowZer runtime is loaded into Brave by the BrowZer Bootstrapper</p>
</li>
<li><p>Brave is redirected by BrowZer runtime to Auth0, then federated to Google (with 2FA) for authentication</p>
</li>
<li><p>Using the OIDC auth token received from Auth0, the BrowZer runtime then (transparently) authenticates onto the Ziti overlay network and then bootstraps the necessary x509 certs into the Brave tab, and the BrowZer runtime then loads the Portainer web app over the <a href="https://en.wikipedia.org/wiki/Mutual_authentication#mTLS">mTLS</a>-based zero-trust overlay network from the Portainer server which is invisible to the internet.</p>
</li>
<li><p>User is presented with Portainer login screen, and user authenticates using Portainer’s internal authentication system (no need to integrate or setup an external "LDAP" or "OAuth" system)</p>
</li>
<li><p>User clicks on BrowZer button, looks at various BrowZer settings, HTTP throughput chart, BrowZer changelog, BrowZer feedback form, etc</p>
</li>
<li><p>User is presented with Portainer GUI welcome screen showing that 2 Containers are running (one is Portainer, the other is an instance of the BrowZer Bootstrapper for a staging environment)</p>
</li>
<li><p>User clicks around, looks at logs for a running container, also removes an old Docker image, and then logs out</p>
</li>
</ul>
<h1>The BrowZer Difference</h1>
<p>In the above section, we discussed how difficult it can be to properly secure Portainer using “<em>traditional techniques</em>”.</p>
<p>BrowZer uses more modern, game-changing techniques that can not only properly secure your Portainer, but do it more easily, more flexibly, and more thoroughly.</p>
<p>With BrowZer there is:</p>
<ul>
<li><p>no need to expose Portainer ports to the internet or mess around with port forwarding.</p>
</li>
<li><p>no need to fuss with network ACLs on your firewall, or only allow access from statically configured IP addresses.</p>
</li>
<li><p>no need to install VPN software on clients.</p>
</li>
<li><p>remote access from ANY device that has a browser.</p>
</li>
<li><p>remote access from ANY location on the internet.</p>
</li>
<li><p>no need to alter Portainer AT ALL (use it off the shelf).</p>
</li>
<li><p>retained ability to use Portainer’s built-in auth system.</p>
</li>
<li><p>2FA/MFA for free (use whatever IdP you want)</p>
</li>
<li><p>not only is there HTTPS/TLS, but also automatic mTLS, and even end-to-end XChaCha20-Poly1305 encryption <em><strong>before</strong></em> data hits the mTLS wire.</p>
</li>
</ul>
<h1><strong>Wrap up</strong></h1>
<p>Do you host a web app (like Portainer) and want to be invisible to malicious intruders?</p>
<p>Do you want your users to have easy access from anywhere with no additional software on their client devices?</p>
<p>Do you want to do all this without making any modifications to the web app?</p>
<p>If so, then we hope you'll <a href="https://netfoundry.io/lets-talk/"><strong>reach out for a conversation</strong></a> about BrowZer.</p>
]]></content:encoded></item><item><title><![CDATA[Multifactor Zero Trust ssh]]></title><description><![CDATA[The previous post revisited the zssh project and demonstrated how to implement a simple ssh client using the extended modules provided by the Golang project. It also modified that simple program and showed what it takes to incorporate OpenZiti, creat...]]></description><link>https://blog.openziti.io/multifactor-zero-trust-ssh</link><guid isPermaLink="true">https://blog.openziti.io/multifactor-zero-trust-ssh</guid><category><![CDATA[zero-trust]]></category><category><![CDATA[zero trust security]]></category><category><![CDATA[zerotrust]]></category><category><![CDATA[openziti]]></category><category><![CDATA[ssh]]></category><dc:creator><![CDATA[Clint Dovholuk]]></dc:creator><pubDate>Mon, 09 Sep 2024 00:10:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1725453901119/410a39f8-eaf7-4c9f-bbba-ae547eb78d94.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://blog.openziti.io/zero-trust-sshclient">The previous post</a> revisited the <code>zssh</code> project and demonstrated how to implement a simple ssh client using the extended modules provided by the Golang project. It also modified that simple program and showed what it takes to incorporate OpenZiti, creating a zero trust ssh client. This post focuses on another aspect of <code>zssh</code> and OpenZiti: multi-factor authentication.</p>
<p><a target="_blank" href="https://openziti.io/docs/learn/introduction/">OpenZiti</a> has a powerful and flexible authentication system. It allows an OpenZiti overlay network operator to decide what authentication mechanisms any given identity must satisfy before being authenticated to the overlay network. OpenZiti is a zero trust overlay network; authentication is only half of the equation. Once authenticated, identities still require authorization before accessing services secured by the overlay following the zero trust pillar of "least privilege".</p>
<h2 id="heading-certificate-based-authentication">Certificate-based Authentication</h2>
<p>Perhaps the most common type of authentication, OpenZiti supports authentication to the the OpenZiti overlay network using certificates. It might not be obvious, but a <code>zssh</code> user using an <a target="_blank" href="https://openziti.io/docs/learn/core-concepts/identities/enrolling/">enrolled an OpenZiti identity</a> to authenticate to the overlay network <strong>is already implicitly</strong> using multi-factor authentication. The first authentication factor is OpenZiti itself. OpenZiti requires connections to be both authenticated and authorized before being allowed to connect to the target service. Once authenticated and authorized, <code>zssh</code> can connect to <code>sshd</code> and attempt to authenticate. Just by using <code>zssh</code>, users are protected with two factors of authentication but <code>zssh</code> (and OpenZiti) offers other factors of authentication as well.</p>
<p>When <a target="_blank" href="https://openziti.io/docs/learn/core-concepts/identities/enrolling/">enrolling an identity</a>, the output of the enrollment flow will be an OpenZiti identity file containing a certificate, key, and CA bundle. This identity can then be used to authenticate connections to the target OpenZiti overlay network. If you are interested in learning how this process works, you can read about it in <a target="_blank" href="https://blog.openziti.io/bootstrapping-trust-part-1-encryption-everywhere">Andrew's five-part series about bootstrapping trust</a>. Using <code>zssh</code> with an identity file and certificate-based authentication looks something like this (examples are taken <a target="_blank" href="https://github.com/openziti-test-kitchen/zssh?tab=readme-ov-file#identity-based-certificate-authentication">directly from the GitHub repo</a>):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725132017881/75590748-1fb6-4616-bf1a-4fc59b31975b.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-go">zssh \
  -i <span class="hljs-string">"${private_key}"</span> \
  -s <span class="hljs-string">"${service_name}"</span> \
  -c <span class="hljs-string">"${client_identity}.json"</span> \
  <span class="hljs-string">"${user_id}@${server_identity}"</span>
</code></pre>
<p>In this example, <code>zssh</code> is accepting the ssh key to use to authenticate to <code>sshd</code> (<code>-i</code>), the OpenZiti service to dial (<code>-s</code>), the OpenZiti identity file (<code>-c</code>). Somewhat hidden within this example is the OpenZiti target identity that is binding the service. All <code>zssh</code> connections are made with the expectation that the target identity is provided to the <code>zssh</code> command where one would normal 'host' would be. This allows the OpenZiti overlay network operator to have a single service usable by any identity looking to provide ssh access.</p>
<h2 id="heading-oidc-based-authentication-only">OIDC-based Authentication Only</h2>
<p>The <code>zssh</code> executable was recently enhanced to support OIDC-only-based authentication. Using <a target="_blank" href="https://openziti.io/docs/learn/core-concepts/security/authentication/external-jwt-signers/">external JWT signers</a>, you can configure an OpenZIti overlay network to trust JWTs from configured IdPs as authentication tokens. This allows the operator to create <a target="_blank" href="https://openziti.io/docs/learn/core-concepts/security/authentication/authentication-policies/">authentication policies</a> allowing for external, OIDC-based integrations. This is interesting because it allows you to authenticate to the OpenZiti overlay network without needing to enroll the client ahead of time. Instead, <code>zssh</code> users complete an <a target="_blank" href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce">Authorization Code Flow with PKCE</a>. In this scenario, the user will see the familiar flow of a browser window popping up and asking the user to authenticate to an identity provider configured to be trusted by OpenZiti. When the flow completes, the <code>zssh</code> binary will have a JWT that can be used to authenticate to the OpenZiti controller.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725132084008/92208900-0593-410c-9305-b4b3b63821ce.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-go">zssh \
  -i <span class="hljs-string">"${private_key}"</span> \
  -s <span class="hljs-string">"${service_name}"</span> \
  -o \
  -a <span class="hljs-string">"${oidc_issuer}"</span> \
  -n openziti-client \
  --oidcOnly \
  --controllerUrl https:<span class="hljs-comment">//localhost:1280 \</span>
  <span class="hljs-string">"${user_id}@${server_identity}"</span>
</code></pre>
<p>In this example, <code>zssh</code> is given the same <code>-i</code> and <code>-s</code> parameters as the certificate-based example above as well as the <code>userid@identity</code> but there are a few others supplied. The <code>-o</code> flag is passed to indicate <code>zssh</code> should obtain a JWT from the specified (<code>-a</code>) OIDC provider. In this example, Keycloak is used as a federated IdP to federate authentication to GitHub. To authenticate to Keycloak, a client id (<code>-n</code>) will need to be provided. This Keycloak client minimally needs to be configured with URLs it's allowed to redirect to after successful authentication. This example uses the <code>--oidcOnly</code> flag, indicating no OpenZiti certificate will be used for authentication. An identity will need to exist in the controller and <a target="_blank" href="https://github.com/openziti-test-kitchen/zssh?tab=readme-ov-file#create-an-external-jwt-signer-and-auth-policies">a matching auth-policy</a> to allow the identity to authenticate. Finally, as no OpenZiti identity is used in this example, <code>zssh</code> must be told where to send the authentication request with the <code>--controllerUrl</code> flag.</p>
<h2 id="heading-certificate-based-oidc-based-authentication">Certificate-based + OIDC-based Authentication</h2>
<p>OpenZiti can also be configured to support OIDC as a secondary form of authentication. Accordingly, the <code>zssh</code> binary can be configured to use certificate-based authentication as a primary authentication source and an OIDC-based flow for secondary authentication. In the scenario, connecting to the OpenZiti overlay itself would require multiple forms of authentication. These mechanisms are a great way to prove there's both a human and a device. The device provides the certificate, while the human interacts with Keycloak/GitHub's OIDC to verify a human is indeed in the loop.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725132167930/c0f03fe3-0d55-4046-846c-0a8df3715e50.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-go">zssh \
  -i <span class="hljs-string">"${private_key}"</span> \
  -s <span class="hljs-string">"${service_name}"</span> \
  -o \
  -a <span class="hljs-string">"${oidc_issuer}"</span> \
  -n openziti-client \
  -c <span class="hljs-string">"${client_identity}.json"</span> \
  <span class="hljs-string">"${user_id}@${server_identity}"</span>
</code></pre>
<p>In this example, <code>zssh</code> is given the all the same parameters as the OIDC-only example but because it specifies an OpenZiti identity with <code>-c</code>, the <code>--controllerUrl</code> and <code>--oidcOnly</code> flags are not necessary.</p>
<h2 id="heading-totp-instead-of-oidc">TOTP Instead of OIDC</h2>
<p>Also added in this release was support for OpenZiti's <a target="_blank" href="https://en.wikipedia.org/wiki/Time-based_one-time_password">TOTP-based</a> authentication. Users can now be required to enter their TOTP code before making a connection, allowing for multi-factor authentication to the OpenZiti overlay without using an IdP. Or, if using the OIDC-only based flow with an IdP that doesn't support TOTP (for reasons), OpenZiti's TOTP can be used as a secondary form of authentication to the overlay. For TOTP example usage have a look at <a target="_blank" href="https://github.com/openziti-test-kitchen/zssh?tab=readme-ov-file#adding-totp">the readme on the repository</a>. There, you’ll find the commands shown in the example gif below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725543173600/84bb2bc3-e5ba-45d4-9f3c-dccc08e6b0d6.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-download-zsshzscp">Download zssh/zscp</h2>
<p>If you are interested in trying out <code>zssh</code> and it's partner <code>zscp</code>, you can download the latest releases from GitHub at <a target="_blank" href="https://github.com/openziti-test-kitchen/zssh/releases/latest">https://github.com/openziti-test-kitchen/zssh/releases/latest</a>, right next to the source code for the project.</p>
<h2 id="heading-share-the-project"><strong>Share the Project</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702330572628/7bb2b76c-af3f-45c6-83ab-d519f183024d.png?auto=compress,format&amp;format=webp" alt class="image--center mx-auto" /></p>
<p>If you find this interesting, please consider <a target="_blank" href="https://github.com/openziti/ziti/"><strong>starring the projects on GitHub</strong></a>. It really does help to support the project! And if you haven't seen it yet, check out <a target="_blank" href="https://github.com/openziti/ziti/"><strong>https://zrok.io</strong></a>. It's a totally free sharing platform built on OpenZiti and uses the OpenIti SDK! It uses the OpenZiti Go SDK since it's a ziti-native application. It's also <a target="_blank" href="https://github.com/openziti/zrok/"><strong>all open source too!</strong></a></p>
<p>Tell us how you're using OpenZiti on <a target="_blank" href="https://twitter.com/openziti"><strong>X <s>twitter</s></strong></a>, <a target="_blank" href="https://www.reddit.com/r/openziti/"><strong>reddit</strong></a>, or over at our <a target="_blank" href="https://openziti.discourse.group/"><strong>Discourse</strong></a>. Or, if you prefer, check out <a target="_blank" href="https://youtube.com/openziti"><strong>our content on YouTube</strong></a> if that's more your speed. Regardless of how, we'd love to hear from you.</p>
]]></content:encoded></item><item><title><![CDATA[Zero Trust *ssh.Client]]></title><description><![CDATA[A few years ago, the OpenZiti project developed and published two client tools to make ssh and scp available over an OpenZiti overlay network without requiring the sshd port to be exposed to the internet. If interested, read the original posts about ...]]></description><link>https://blog.openziti.io/zero-trust-sshclient</link><guid isPermaLink="true">https://blog.openziti.io/zero-trust-sshclient</guid><category><![CDATA[zero-trust]]></category><category><![CDATA[golang]]></category><category><![CDATA[ssh]]></category><category><![CDATA[openziti]]></category><dc:creator><![CDATA[Clint Dovholuk]]></dc:creator><pubDate>Mon, 09 Sep 2024 00:09:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1725370192508/92d59b16-3273-4213-9918-01a0dc5ba343.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few years ago, the <a target="_blank" href="https://github.com/openziti/ziti">OpenZiti project</a> developed and published two client tools to make ssh and scp available over an OpenZiti overlay network without requiring the sshd port to be exposed to the internet. If interested, read the original posts <a target="_blank" href="https://blog.openziti.io/zitifying-ssh">about zssh</a> and <a target="_blank" href="https://blog.openziti.io/zitifying-scp">zscp</a>. Continuing with the belief that security-related code should be open source and auditable, the project is <a target="_blank" href="https://github.com/openziti-test-kitchen/zssh">available on GitHub</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725040827487/7810a66b-c739-43d9-b7a8-fd607adb04e6.gif" alt class="image--center mx-auto" /></p>
<p>The <a target="_blank" href="https://openziti.io/docs/learn/introduction/">OpenZiti</a> project provides SDKs that developers can use to create secure connections. The <code>zssh</code> client demonstrates that adopting an OpenZiti SDK into an application is no harder than developing any application that uses traditional IP-based, underlay connectivity.</p>
<p>Secondly, though uncommon, there are still vulnerabilities found in <code>ssh</code>. Just this year (2024) <a target="_blank" href="https://en.wikipedia.org/wiki/RegreSSHion">RegreSSHion</a> was discovered and was given a staggering <a target="_blank" href="https://www.first.org/cvss/">CVSS score</a> of 9.8. Scores greater than 9 are generally deemed a "patch this as soon as possible" type of CVE. Yes, today's <code>ssh</code> clients are incredibly robust, but if it's easy to remove a substantial portion of attackers from attacking a service by using a zero trust overlay network like OpenZiti, why wouldn't you?</p>
<h2 id="heading-using-sshclient">Using <code>*ssh.Client</code></h2>
<p>The go ecosystem provides extended packages from the <code>golang.org/x/*</code> modules. One of these modules is <code>golang.org/x/crypto</code>. Within this module, there is an <code>ssh</code> package that provides everything needed to make a functional ssh client. In there is <code>ssh.Client</code>, the main thing you'll interact with. This struct, along with <code>ssh.NewClientConn</code> and the Golang standard library, provides all the functionality needed to create a simple ssh client.</p>
<p>Shown below is all the code needed to make a very contrived ssh example. Any errors are ignored, and all values are hard-coded or expected as arguments to the program to keep the example small. In total, there are fewer than 30 total lines. Hopefully, the example is straightforward enough to understand.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"golang.org/x/crypto/ssh"</span>
    <span class="hljs-string">"os"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    key, _ := os.ReadFile(os.Args[<span class="hljs-number">1</span>])
    signer, _ := ssh.ParsePrivateKey(key)
    config := &amp;ssh.ClientConfig{
        User:            <span class="hljs-string">"ubuntu"</span>,
        Auth:            []ssh.AuthMethod{ssh.PublicKeys(signer)},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    sshClient, _ := ssh.Dial(<span class="hljs-string">"tcp"</span>, os.Args[<span class="hljs-number">2</span>], config)
    <span class="hljs-keyword">defer</span> sshClient.Close()
    session, _ := sshClient.NewSession()
    <span class="hljs-keyword">defer</span> session.Close()
    session.RequestPty(<span class="hljs-string">"xterm"</span>, <span class="hljs-number">80</span>, <span class="hljs-number">40</span>, ssh.TerminalModes{})
    session.Stdout = os.Stdout
    session.Stderr = os.Stderr
    session.Stdin = os.Stdin
    session.Shell()
    session.Wait()
}
</code></pre>
<p>Try it out! That's really all there is to it. You'll be able to ssh to any machine that uses key-based authentication. Although it's not a robust example, it demonstrates the overall idea and shows off how amazing the go ecosystem can be. Note that <code>ssh.InsecureIgnoreHostKey</code> is used for the <code>HostKeyCallback</code>, to keep the example short. See <a target="_blank" href="https://github.com/openziti-test-kitchen/zssh/blob/main/zsshlib/ssh.go#L471"><code>zssh</code>'s implementation</a> if interested</p>
<h2 id="heading-layering-in-zero-trust-connectivity">Layering in Zero Trust Connectivity</h2>
<p>The Golang standard library is well thought out. The <a target="_blank" href="https://blog.openziti.io/go-is-amazing-for-zero-trust">abstractions in place make it amazing for building applications embedding zero trust</a>. Adapting an application that uses normal IP-based connectivity (like the ssh example shown above that uses "tcp") to use an OpenZiti SDK is generally straightforward. From the example above, a single line needs to be changed: the line that creates the <code>ssh.Client</code>. This line:</p>
<pre><code class="lang-go">    sshClient, _ := ssh.Dial(<span class="hljs-string">"tcp"</span>, host, config)
</code></pre>
<p>Creating the <code>sshClient</code> needs to be adapted away from using IP-based underlay networking. Instead of "tcp" and "remote-machine-name:22", it needs to use a zero trust connection provided by the OpenZiti Golang SDK. Below is a simplified function that uses an OpenZiti identity file to create an OpenZiti context and dial an OpenZiti service, creating a Golang <code>net.Conn</code> that can be used to create an <code>ssh.Client</code>. Again, the example omits error handling and robustness for simplicity's sake, and looks like this:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">obtainZitiConn</span><span class="hljs-params">()</span> <span class="hljs-title">net</span>.<span class="hljs-title">Conn</span></span> {
    cfg, _ := ziti.NewConfigFromFile(os.Args[<span class="hljs-number">3</span>])
    ctx, _ := ziti.NewContext(cfg)
    dialOptions := &amp;ziti.DialOptions{
        Identity:       host,
    }
    c, _ := ctx.DialWithOptions(<span class="hljs-string">"zsshSvc"</span>, dialOptions)
    <span class="hljs-keyword">return</span> c
}
</code></pre>
<p>With the IP-based underlay <code>net.Conn</code> connection replaced with a zero trust connection, an <code>ssh.Client</code> can be created by replacing the call to <code>ssh.Dial</code>, and instead using a call to <code>ssh.NewClientConn</code> combined with a call to <code>ssh.NewClient</code>. With the <code>ssh.Dial</code> line adapted, it looks like this:</p>
<pre><code class="lang-go">    <span class="hljs-comment">//adapted sshClient, _ := ssh.Dial("tcp", host, config)</span>
    c, chans, reqs, _ := ssh.NewClientConn(obtainZitiConn(), <span class="hljs-string">""</span>, config)
    sshClient := ssh.NewClient(c, chans, reqs)
</code></pre>
<p>Everything else in the example remains identical; these are the only lines that need to change! The full source for <code>zssh</code> is available on GitHub at <a target="_blank" href="https://github.com/openziti-test-kitchen/zssh">https://github.com/openziti-test-kitchen/zssh</a>. You'll find the examples shown above in that repo as individual, compilable go files available in <a target="_blank" href="https://github.com/openziti-test-kitchen/zssh/tree/main/zssh/example">the <code>example</code> folder</a>.</p>
<p>If you're interested in <code>zssh</code>, the OpenZiti project and zero trust in general, check out <a target="_blank" href="https://blog.openziti.io/multifactor-zero-trust-ssh">the next article</a>. It focuses on using <code>zssh</code> and using OpenZiti's OIDC-based authentication mechanisms and uses <a target="_blank" href="https://www.keycloak.org/">Keycloak</a> for federated authentication to GitHub or Google.</p>
<h2 id="heading-share-the-project"><strong>Share the Project</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702330572628/7bb2b76c-af3f-45c6-83ab-d519f183024d.png?auto=compress,format&amp;format=webp" alt class="image--center mx-auto" /></p>
<p>If you find this interesting, please consider <a target="_blank" href="https://github.com/openziti/ziti/"><strong>starring the projects on GitHub</strong></a>. It really does help to support the project! And if you haven't seen it yet, check out <a target="_blank" href="https://github.com/openziti/ziti/"><strong>https://zrok.io</strong></a>. It's a totally free sharing platform built on OpenZiti and uses the OpenZiti Golang SDK and is also <a target="_blank" href="https://github.com/openziti/zrok/"><strong>all open source!</strong></a></p>
<p>Tell us how you're using OpenZiti on <a target="_blank" href="https://twitter.com/openziti"><strong>X <s>twitter</s></strong></a>, <a target="_blank" href="https://www.reddit.com/r/openziti/"><strong>reddit</strong></a>, or over at our <a target="_blank" href="https://openziti.discourse.group/"><strong>Discourse</strong></a>. Or, if you prefer, check out <a target="_blank" href="https://youtube.com/openziti"><strong>our content on YouTube</strong></a> if that's more your speed. Regardless of how, we'd love to hear from you.</p>
]]></content:encoded></item><item><title><![CDATA[Effortless Docker Management]]></title><description><![CDATA[Overcoming Common Headaches
If you're involved with software creation or deployment, you likely use Docker. If so, the following saga probably rings some bells with you:

A common, painful scenario
Yo]]></description><link>https://blog.openziti.io/effortless-docker-management-with-private-web-access</link><guid isPermaLink="true">https://blog.openziti.io/effortless-docker-management-with-private-web-access</guid><category><![CDATA[Web Security]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[openziti]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Curt Tudor]]></dc:creator><pubDate>Thu, 05 Sep 2024 15:00:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1725481068115/9bc5870f-011f-4890-8487-23369499b5da.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Overcoming Common Headaches</h1>
<p>If you're involved with software creation or deployment, you likely use Docker. If so, the following <em>saga</em> probably rings some bells with you:</p>
<hr />
<h3>A common, painful scenario</h3>
<p>You become aware that something's not working in your Docker fleet. Maybe a service is down? You <code>ssh</code> to the the host where the containers run, and do a <code>docker compose ps</code>. Yep, it's that buggy microservice that has crashed.</p>
<p>No problem, I'll restart it: <code>docker compose restart</code>. Okay now let's try again. Hmm... The issue is still there. <code>docker compose ps</code> again. Sigh... the service must have just crashed immediately after starting.</p>
<p>I probably would have known what was happening had I been reading the log stream, but there is a lot of clutter from other services. I could get the logs for just that one service via <code>docker compose logs --follow myservice</code> but that dies every time the service dies so I'd need to run that command every time I restart the service.</p>
<p>I could alternatively run <code>docker compose up myservice</code> and in that terminal window if the service is down I could just <code>up</code> it again, but now I've got one service hogging a terminal window even after I no longer care about its logs.</p>
<p>I guess when I want to reclaim the terminal real estate I can <code>ctrl+P,Q</code>, but... wait, that's not working for some reason. Should I use <code>ctrl+C</code> instead? I can't remember if that closes the foreground process or kills the actual service...</p>
<hr />
<p>OK. You get the picture. Ugh... What a headache!</p>
<p>Of course, it makes no sense to manage a large scale Docker deployment by connecting to each container individually or restarting processes (an orchestrator like K8S will serve that large scale use case much better). But if you've got a few containers running in a self-hosted (home?) network, the above story probably sounds familiar.</p>
<h3>A Better way</h3>
<p>Memorizing docker commands is hard. Memorizing <code>alias</code>'s isn't much easier. Keeping track of your containers across multiple terminal windows is untenable.</p>
<p>But hang on. What if you had all the information you needed in a single <em>magical</em> terminal window where every common docker command was one keypress away?</p>
<p>Picture this:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725034531527/33cfddf1-4849-4995-a5c4-18ee4df9e8a8.gif" alt="" style="display:block;margin:0 auto" />

<p>If that got your attention, Now, imagine if you could access the <em>magical</em> terminal window by simply opening a web browser, on any laptop or mobile device, and then navigating to a URL representing the remote host where your containers run. Everything is right there in the browser tab.</p>
<p>Bye-bye <code>ssh</code>.</p>
<p>Intrigued?</p>
<p>As nice as that is, It gets even better. Now, imagine the URL was internet-accessible, but <em><strong>only</strong></em> visible to you, invisible to others, thus protecting your containers from malicious actors.</p>
<p>This is not a fever dream. In this article, I'll show you how all this is possible today.</p>
<p>I'll discuss the following components that collectively implement the solution:</p>
<ul>
<li><p>Isaiah</p>
</li>
<li><p>OpenZiti</p>
</li>
<li><p>BrowZer</p>
</li>
</ul>
<h1>The Isaiah Service</h1>
<p>One component involved in the solution is Isaiah. <a href="https://github.com/will-moss/isaiah">Isaiah</a> is a relatively new open-source project (<em>it first appeared in early 2024</em>). It is a self-hostable service that enables you to manage all your Docker resources on a remote server. It is an attempt at recreating the <code>lazydocker</code> command-line application while making everything available as a <em><strong>web application</strong></em>.</p>
<p>The screencap in the above "better way" section shows Isaiah in action.</p>
<h3>Isaiah Deployment</h3>
<p>You run Isaiah on the host where your containers execute -- the host you would previously <code>ssh</code> to in the '<em>painful scenario</em>' described at the top of this article.</p>
<p><em>(NOTE: Ensure that Docker 23.0.0+ is installed on your host before proceeding)</em></p>
<p>There are several ways to deploy Isaiah, but perhaps the easiest is via <code>docker compose</code>. Here is a <code>compose.yml</code> file you can use:</p>
<pre><code class="language-yaml">services:
  isaiah:
    image: mosswill/isaiah:latest
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      SERVER_PORT: "80"
      AUTHENTICATION_SECRET: "some-very-long-and-mysterious-secret"
</code></pre>
<p>Then simply run <code>docker compose up -d</code></p>
<p><em>(NOTE: if you already have a compose file on your host, you can simply add the above</em> <code>isaiah</code> <em>section to the existing</em> <code>services</code> <em>section of your compose file)</em></p>
<p>As you can see, this compose file will have Isaiah listening on HTTP, without TLS, on your LAN or perhaps even on the open internet. This is certainly sub-optimal from a security standpoint, but read on and I'll describe how to lock things down.</p>
<p>If the <code>AUTHENTICATION_SECRET</code> env var is configured, it tells Isaiah to require visitors to enter the secret when they arrive, like this:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725117677807/06d61b16-4a94-4116-b9f3-87466ad91d82.png" alt="" style="display:block;margin:0 auto" />

<p>But even with the <code>AUTHENTICATION_SECRET</code> in place, and HTTP visitors hitting your host being prompted as shown above...this is still not as secure as we need it to be.</p>
<p>A better topology is one where your Docker host, and Isaiah, reside in a <a href="https://en.wikipedia.org/wiki/Virtual_private_cloud">VPC</a>, and the host has NO ports open to the internet. In this kind of deployment, the host will be completely invisible to the internet. The best (most secure) way to access the host is via a zero-trust overlay network.</p>
<h1>How to Easily Deploy a Zero-Trust Overlay Network</h1>
<p>The next component involved in the solution is OpenZiti. <a href="https://openziti.io/">OpenZiti</a> is a free, open-source zero-trust networking platform that makes services invisible to unauthorized users. Add zero trust to existing applications with tunnelers, or embed SDKs directly for the strongest posture - either way, every connection is authenticated, authorized, and encrypted end to end.</p>
<p>There is a vast amount of documentation, as well as <a href="https://openziti.io/docs/learn/quickstarts/">quick starts</a>, on the OpenZiti site, so I will not replicate it here.</p>
<p>If you like what you read about OpenZiti, but you prefer not to self-host it yourself, <a href="https://netfoundry.io/">we</a> also offer a zero trust networking platform. If that interests you, <a href="https://netfoundry.io/lets-talk/">reach out to us for more discussion.</a></p>
<h1>BrowZer</h1>
<p>The next component involved in the solution is <code>BrowZer</code>.</p>
<p>The <code>Z</code> in this component's name (within the word normally spelled "<em>browser</em>") is not a typo. It is a purposeful indication that this solution, unique in today's technology offerings for securing browser-based applications, is built as part of the <a href="https://github.com/openziti/"><strong>OpenZiti</strong> project</a>.</p>
<p>BrowZer enables you and your organization, enterprises and self-hosting enthusiasts alike, in the cloud or at home, to operate private-to-the-internet web applications while still easily providing secure access for your authorized internet-based remote users.</p>
<p>I previously published a lengthy article that introduced the <a href="https://blog.openziti.io/introducing-openziti-browzer">concept of browZer</a>. I recommend giving it a read.</p>
<h2>Forward Proxy Authentication / Trusted SSO</h2>
<p>With OpenZiti and BrowZer now in your mind, I want to return to Isaiah for a moment.</p>
<p>Isaiah has a feature known as <em>Forward Proxy Authentication</em>. This feature enables you to log in to an authentication portal, and then connect to Isaiah without having to type your <code>AUTHENTICATION_SECRET</code> every time.</p>
<p>In this mode, you protect Isaiah using your authentication portal rather than a cleartext / hashed password.</p>
<p>To use this mechanism, configure Isaiah using the following variables:</p>
<ul>
<li><p>Set <code>FORWARD_PROXY_AUTHENTICATION_ENABLED</code> to <code>true</code>.</p>
</li>
<li><p>Set <code>FORWARD_PROXY_AUTHENTICATION_HEADER_KEY</code> to the name of the forwarded authentication header your auth proxy sends to Isaiah.</p>
</li>
<li><p>Set <code>FORWARD_PROXY_AUTHENTICATION_HEADER_VALUE</code> to the value of the header that Isaiah should expect (or use <code>*</code> if all values are accepted).</p>
</li>
</ul>
<p>By the way, by default, Isaiah is configured to work with <a href="https://www.authelia.com/">Authelia</a> out of the box. So if you are using the Authelia IdP as your auth proxy, you can just set <code>FORWARD_PROXY_AUTHENTICATION_ENABLED</code> to <code>true</code> and be done with it (no need to configure the other variables).</p>
<h2>Trusted SSO Between BrowZer and Isaiah</h2>
<p>Much like Authelia, BrowZer also works with Isaiah's <em>Forward Proxy Authentication</em> out of the box.</p>
<p>As you read in the browZer article linked above, browZer requires you to authenticate with an IdP before connecting to your zero-trust overlay network. A common setup is to use Auth0 as the browZer IdP and have Auth0 federate to Google. You can read about how to do this in our <a href="https://openziti.io/docs/identity-providers-for-browZer-auth0">browZer IdP documentation</a>.</p>
<p>Once you have set up your Ziti overlay network, and have set up browZer to enable web access to Isaiah, you are then using browZer as your auth proxy.</p>
<p>Now you can configure Isaiah with:</p>
<ul>
<li><p><code>FORWARD_PROXY_AUTHENTICATION_ENABLED</code> to <code>true</code>, and</p>
</li>
<li><p><code>FORWARD_PROXY_AUTHENTICATION_HEADER_VALUE</code> to the value of your Google email address.</p>
</li>
</ul>
<p>For example:</p>
<pre><code class="language-yaml">services:
  isaiah:
    image: mosswill/isaiah:latest
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      SERVER_PORT: "80"
      FORWARD_PROXY_AUTHENTICATION_ENABLED: "true"
      FORWARD_PROXY_AUTHENTICATION_HEADER_VALUE: "you@gmail.com"
</code></pre>
<p>Here is what it looks like to use browZer to access a private-to-the-internet instance of Isaiah:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725469319491/f2d0112a-e120-41da-8030-95624e4ff713.gif" alt="" style="display:block;margin:0 auto" />

<p>Here's what happens above:</p>
<ul>
<li><p>Brave web browser hits the URL representing the protected instance of Isaiah</p>
</li>
<li><p>Brave is redirected by BrowZer to Auth0, then federated to Google (with 2FA) for authentication</p>
</li>
<li><p>BrowZer bootstraps the necessary OpenZiti software into Brave tab, and the OpenZiti software loads the Isaiah web app over the zero-trust overlay network</p>
</li>
<li><p>Isaiah sees that BrowZer has done the proper SSO by passing necessary info from Google to Isaiah, so no login prompt is rendered by Isaiah</p>
</li>
<li><p>User is presented with Isaiah GUI welcome screen showing that "2 Containers" are running (one is Isaiah, the other is an instance of the BrowZer Bootstrapper for a staging environment)</p>
</li>
<li><p>User clicks around, looks at logs for a running container, also removes an old Docker image</p>
</li>
</ul>
<h1>Wrap up</h1>
<p>Do you host a web app (like Isaiah) and want to be invisible to malicious intruders?</p>
<p>Do you want your users to have easy access from anywhere with no additional software on their client devices?</p>
<p>Do you want to do all this without making any modifications to the web app?</p>
<p>If so, then we hope you'll <a href="https://netfoundry.io/lets-talk/">reach out for a conversation</a> about BrowZer.</p>
]]></content:encoded></item><item><title><![CDATA[zrok is Growing Up]]></title><description><![CDATA[zrok has been growing pretty steadily throughout 2024. As we've grown, we've started having to deal with abuse, phishing, and other similar types of issues showing up on the service at zrok.io.
Over the last weeks, the team has worked together to fig...]]></description><link>https://blog.openziti.io/zrok-is-growing-up</link><guid isPermaLink="true">https://blog.openziti.io/zrok-is-growing-up</guid><category><![CDATA[network]]></category><category><![CDATA[Security]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Browsers]]></category><dc:creator><![CDATA[Michael Quigley]]></dc:creator><pubDate>Fri, 02 Aug 2024 20:07:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722357921994/c2ddeddd-f52e-41a9-ace8-26ce7bb0259c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>zrok has been growing pretty steadily throughout 2024. As we've grown, we've started having to deal with abuse, phishing, and other similar types of issues showing up on the service at zrok.io.</p>
<p>Over the last weeks, the team has worked together to figure out the best compromise to continue offering our users the best free sharing solution possible, while also offering a reasonable level of protection against abuse. We've batted around a number of different ideas.</p>
<p>The solution we've settled on is to introduce an "interstitial page" for free-tier accounts using public sharing. The interstitial page will be shown the first time an internet user visits a zrok public share (with a timer resetting every week). The page is there to make sure that the user visiting your share is very clear that the web resource they're visiting is shared through zrok, and very likely <em>not</em> a financial institution (or at least that they should be careful with personal information).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722361596170/898a7bec-d03b-4aef-80bc-5860547001ef.png" alt class="image--center mx-auto" /></p>
<p>The interstitial page is designed to only be displayed to interactive clients (web browsers presenting with a <code>User-Agent</code> header starting with <code>Mozilla/5.0</code>). Other clients like <code>curl</code>, or HTTP clients from various programming langues will bypass the interstitial page.</p>
<p>zrok users who upgrade to any paid tier (see <a target="_blank" href="https://zrok.io/pricing/">pricing</a>) will not have an interstitial page presented on their shares. The interstitial pages feature does not impact private zrok sharing in any way. When you <code>zrok access private</code> a share and create a private frontend, that frontend does not present interstitial pages. In those cases, we're pretty confident the user understands what they're accessing.</p>
<p>We're also actively working on bringing some additional capabilities to the zrok paid offerings, including "bring your own domain" support, which will allow users to create public shares using their own domain name. So, we're kind of hoping you might want to consider one of those options outside of removing the interstitial page.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">We're expecting to enable the interstitial pages feature globally for all free-tier zrok users starting on Thursday, August 8th.</div>
</div>

<p>If you're a self-hoster, you'll have extensive options to customize the interstitial page configuration. Each public frontend can enable or disable interstitial support, and interstitial pages can be disabled on a per-account basis.</p>
<p>I recently did a short zrok Office Hours video about the interstitial pages feature:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=NYjec5mIXTE&amp;list=PLMUj_5fklasLuM6XiCNqwAFBuZD1t2lO2&amp;index=24">https://www.youtube.com/watch?v=NYjec5mIXTE&amp;list=PLMUj_5fklasLuM6XiCNqwAFBuZD1t2lO2&amp;index=24</a></div>
<p> </p>
<p>We appreciate every zrok user. Thank you for your support and attention. If you appreciate what we're doing with zrok, it always means a lot when you can drop a star on the <a target="_blank" href="https://github.com/openziti/zrok">zrok repo on GitHub</a>.</p>
<p>If you have any questions or concerns, feel free to reach out on the <a target="_blank" href="https://openziti.discourse.group/c/zrok/24">OpenZiti discourse using the zrok topic</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Easy Steps to Install a Private VPN on Linux with zrok]]></title><description><![CDATA[The great thing about the zrok-share.service is that it comes with the zrok binary and is always on in the background, so it's a good fit for a reliable VPN connection. Let's install the service and configure it to auto-start after a reboot.
curl -sS...]]></description><link>https://blog.openziti.io/zrok-vpn-linux-service</link><guid isPermaLink="true">https://blog.openziti.io/zrok-vpn-linux-service</guid><category><![CDATA[vpn]]></category><dc:creator><![CDATA[Kenneth Bingham]]></dc:creator><pubDate>Fri, 02 Aug 2024 17:10:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722617478239/b985c30b-aae1-42c1-a450-b3fd255b14a3.avif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The great thing about the <code>zrok-share.service</code> is that it comes with the <code>zrok</code> binary and is always on in the background, so it's a good fit for a reliable VPN connection. Let's install the service and configure it to auto-start after a reboot.</p>
<pre><code class="lang-bash">curl -sSLf https://get.openziti.io/install.bash | sudo bash -s zrok-share
</code></pre>
<p>Edit <code>/opt/openziti/etc/zrok/zrok-share.env</code>.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># env</span>
ZROK_ENABLE_TOKEN=<span class="hljs-string">"w9pEuX3Gb750"</span>  <span class="hljs-comment"># account token from the console</span>
ZROK_ENVIRONMENT_NAME=<span class="hljs-string">"my zrok vpn service on hostname"</span>

<span class="hljs-comment"># share</span>
ZROK_UNIQUE_NAME=<span class="hljs-string">"mysecretsharetoken"</span>
ZROK_BACKEND_MODE=<span class="hljs-string">"vpn"</span>
ZROK_TARGET=<span class="hljs-string">"172.21.71.1/24"</span>  <span class="hljs-comment"># allocate ip in some private subnet</span>
ZROK_FRONTEND_MODE=<span class="hljs-string">"reserved-private"</span>
</code></pre>
<p>You can control access to your VPN by keeping <code>ZROK_UNIQUE_NAME</code> a secret (the share token) or adding the following to restrict zrok accounts by email. Only your account can use the share in closed mode if you don't add grants.</p>
<pre><code class="lang-bash">ZROK_PERMISSION_MODE=<span class="hljs-string">"closed"</span>
ZROK_ACCESS_GRANTS=<span class="hljs-string">"alice@example.com bob@example.org"</span>
</code></pre>
<p>Grant kernel capability <code>NET_ADMIN</code> to the service.</p>
<pre><code class="lang-bash">sudo sed -Ei <span class="hljs-string">'s/.*AmbientCapabilities=CAP_NET_ADMIN/AmbientCapabilities=CAP_NET_ADMIN/'</span> /etc/systemd/system/zrok-share.service.d/override.conf
sudo systemctl daemon-reload
</code></pre>
<p>Start the service now and auto-start after a reboot.</p>
<pre><code class="lang-bash">sudo systemctl <span class="hljs-built_in">enable</span> --now zrok-share.service
</code></pre>
<p>Check the logs.</p>
<pre><code class="lang-bash">sudo journalctl -lfu zrok-share.service
</code></pre>
<p>The logs should look like this, confirming this device is allocated 172.21.71.1 on the VPN.</p>
<pre><code class="lang-json">Aug <span class="hljs-number">02</span> <span class="hljs-number">16</span>:<span class="hljs-number">33</span>:<span class="hljs-number">08</span> ubuntu zrok-share.bash[<span class="hljs-number">6243</span>]: {<span class="hljs-attr">"level"</span>:<span class="hljs-string">"info"</span>,<span class="hljs-attr">"ts"</span>:<span class="hljs-number">1722616388.2556849</span>,<span class="hljs-attr">"msg"</span>:<span class="hljs-string">"interface created tun0"</span>}
Aug <span class="hljs-number">02</span> <span class="hljs-number">16</span>:<span class="hljs-number">33</span>:<span class="hljs-number">08</span> ubuntu zrok-share.bash[<span class="hljs-number">6243</span>]: {<span class="hljs-attr">"level"</span>:<span class="hljs-string">"info"</span>,<span class="hljs-attr">"ts"</span>:<span class="hljs-number">1722616388.255752</span>,<span class="hljs-attr">"msg"</span>:<span class="hljs-string">"exec /sbin/ip [link set dev tun0 mtu 16384]"</span>}
Aug <span class="hljs-number">02</span> <span class="hljs-number">16</span>:<span class="hljs-number">33</span>:<span class="hljs-number">08</span> ubuntu zrok-share.bash[<span class="hljs-number">6243</span>]: {<span class="hljs-attr">"level"</span>:<span class="hljs-string">"info"</span>,<span class="hljs-attr">"ts"</span>:<span class="hljs-number">1722616388.2595484</span>,<span class="hljs-attr">"msg"</span>:<span class="hljs-string">"exec /sbin/ip [addr add 172.21.71.1/24 dev tun0]"</span>}
Aug <span class="hljs-number">02</span> <span class="hljs-number">16</span>:<span class="hljs-number">33</span>:<span class="hljs-number">08</span> ubuntu zrok-share.bash[<span class="hljs-number">6243</span>]: {<span class="hljs-attr">"level"</span>:<span class="hljs-string">"info"</span>,<span class="hljs-attr">"ts"</span>:<span class="hljs-number">1722616388.2627363</span>,<span class="hljs-attr">"msg"</span>:<span class="hljs-string">"exec /sbin/ip [-6 addr add fd00:7a72:6f6b::1/64 dev tun0]"</span>}
Aug <span class="hljs-number">02</span> <span class="hljs-number">16</span>:<span class="hljs-number">33</span>:<span class="hljs-number">08</span> ubuntu zrok-share.bash[<span class="hljs-number">6243</span>]: {<span class="hljs-attr">"level"</span>:<span class="hljs-string">"info"</span>,<span class="hljs-attr">"ts"</span>:<span class="hljs-number">1722616388.2659466</span>,<span class="hljs-attr">"msg"</span>:<span class="hljs-string">"exec /sbin/ip [link set dev tun0 up]"</span>}
Aug <span class="hljs-number">02</span> <span class="hljs-number">16</span>:<span class="hljs-number">33</span>:<span class="hljs-number">08</span> ubuntu zrok-share.bash[<span class="hljs-number">6243</span>]: {<span class="hljs-attr">"level"</span>:<span class="hljs-string">"info"</span>,<span class="hljs-attr">"ts"</span>:<span class="hljs-number">1722616388.2711568</span>,<span class="hljs-attr">"msg"</span>:<span class="hljs-string">"interface configured tun0"</span>}
</code></pre>
<h2 id="heading-join-the-vpn-from-another-device">Join the VPN from Another Device</h2>
<p>The zrok console looks like this for an account with two environments, one per VPN peer, and one VPN share.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722617910042/3972fae3-e826-402f-82e9-9c3514f0bfe4.png" alt class="image--center mx-auto" /></p>
<p>To temporarily join the zrok VPN you only need to run this command.</p>
<pre><code class="lang-bash">sudo -E zrok access private mysecretsharetoken
</code></pre>
<p>You will see zrok's terminal user interface (TUI) telling you what is happening. This second device will be allocated 172.21.71.2 and subsequent devices will get unique IP allocations when they join. This is a network-layer VPN. You can send ICMP, TCP, UDP, etc.</p>
<p>Now, this second device is joined to the VPN, so it has a dashed line to the VPN share indicating a zrok "private access."</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722618841867/969509da-0a39-4a4b-ad76-0c0dc0a524c9.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-share-the-project"><strong>Share the Project</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702330572628/7bb2b76c-af3f-45c6-83ab-d519f183024d.png?auto=compress,format&amp;format=webp" alt class="image--center mx-auto" /></p>
<p>If you find this interesting, please <a target="_blank" href="https://github.com/openziti/zrok">give zrok a star on GitHub</a>!</p>
<p>Let us know if you found a good use for this or have an improvement or question in mind on <a target="_blank" href="https://twitter.com/openziti"><strong>X <s>twitter</s></strong></a>, in <a target="_blank" href="https://www.reddit.com/r/openziti/">/r/openziti</a>, or the <a target="_blank" href="https://openziti.discourse.group/">Discourse forum</a>. We upload and stream <a target="_blank" href="https://youtube.com/openziti"><strong>on YouTube</strong></a> too. We'd love to hear from you!</p>
]]></content:encoded></item><item><title><![CDATA[Jitsi, meet zrok]]></title><description><![CDATA[Jitsi Meet is an open-source video conferencing server. I've wanted to run Jitsi behind zrok and someone asked about it today in the forum and we got it working! Here's how I conveniently self-host Jitsi with zrok.
Why zrok?
zrok.io makes it easy to ...]]></description><link>https://blog.openziti.io/jitsi-meet-zrok</link><guid isPermaLink="true">https://blog.openziti.io/jitsi-meet-zrok</guid><category><![CDATA[self-hosted]]></category><dc:creator><![CDATA[Kenneth Bingham]]></dc:creator><pubDate>Wed, 31 Jul 2024 18:25:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722450203675/ee7c2e5c-4ef2-46e7-a05d-767cf2e7ab58.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://jitsi.github.io/handbook/">Jitsi Meet</a> is an open-source video conferencing server. I've wanted to run Jitsi behind zrok and <a target="_blank" href="https://openziti.discourse.group/t/local-jitsi-meet-zrok/2900/4?u=qrkourier">someone asked about it today in the forum</a> and we got it working! Here's how I conveniently self-host Jitsi with zrok.</p>
<h2 id="heading-why-zrok">Why zrok?</h2>
<p><a target="_blank" href="https://zrok.io">zrok.io</a> makes it easy to self-host web applications while obscuring your real public IP and protecting your private network. Your visitors will see a trusted certificate with the name <code>*.share.zrok.io</code> provided by zrok.io as a service when they visit your Jitsi Meet instance's public URL.</p>
<p>Jitsi Meet requires a trusted certificate for WebRTC, so zrok.io also spares you the chore of issuing, renewing, and configuring a TLS server certificate for Jitsi.</p>
<h2 id="heading-orientation">Orientation</h2>
<p>You only need Docker to follow this tutorial. There's nothing else to install. You can use this guide with a free account from <a target="_blank" href="https://zrok.io">zrok.io</a> or a <a target="_blank" href="https://docs.zrok.io/docs/guides/self-hosting/docker/">self-hosted zrok instance</a>.</p>
<p>Jitsi Meet's Docker Compose project is pre-configured to publish the container ports to the Docker Host's external interfaces. Typically, that is necessary so they are reachable by clients.</p>
<p>zrok works differently and does not need the ports to be published externally. Instead, zrok runs inside the Compose project on the <code>meet.jitsi</code> bridge network. zrok will proxy the traffic securely to the containers' internal ports, so we'll override the forwarded, published ports, exposing them only inside the Compose project.</p>
<h2 id="heading-the-steps">The Steps</h2>
<ol>
<li><p>Follow <a target="_blank" href="https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/#quick-start">Jitsi's Docker Quickstart</a> and wait to run <code>docker compose up</code>.</p>
</li>
<li><p>In your terminal, change to the directory where you have created these Jitsi quickstart files: <code>.env</code> and <code>docker-compose.yml</code>.</p>
</li>
<li><p>Download the zrok <a target="_blank" href="https://docs.zrok.io/zrok-public-reserved/compose.yml">public share compose example</a> and save it as the filename <code>docker-compose.zrok.yml</code> in the same directory.</p>
</li>
<li><p>Add the following YAML as the filename <code>docker-compose.override.yml</code> in the same directory.</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">services:</span>
   <span class="hljs-attr">web:</span>
     <span class="hljs-attr">ports:</span> <span class="hljs-type">!override</span> []
   <span class="hljs-attr">jicofo:</span>
     <span class="hljs-attr">ports:</span> <span class="hljs-type">!override</span> []
   <span class="hljs-attr">jvb:</span>
     <span class="hljs-attr">ports:</span> <span class="hljs-type">!override</span> []
   <span class="hljs-attr">zrok-share:</span>
     <span class="hljs-attr">networks:</span>
       <span class="hljs-attr">meet.jitsi:</span>
</code></pre>
</li>
<li><p>Think of a name for your self-hosted Jitsi Meet instance. You will use it in the next step to define the unique name of the zrok share which is part of the public URL. The name must be 4-32 lowercase letters or numbers.</p>
</li>
<li><p>Save the following variable assignments as the filename <code>.env.zrok</code> in the same directory.</p>
<pre><code class="lang-bash"> PUBLIC_URL=<span class="hljs-string">"https://myjitsi.share.zrok.io"</span>  <span class="hljs-comment"># subdomain must match ZROK_UNIQUE_NAME</span>
 ZROK_UNIQUE_NAME=<span class="hljs-string">"myjitsi"</span>                  <span class="hljs-comment"># must match PUBLIC_URL subdomain</span>
 ZROK_ENABLE_TOKEN=<span class="hljs-string">"ix9XrvQt13Rf"</span>            <span class="hljs-comment"># zrok account token from console</span>
 ZROK_ENVIRONMENT_NAME=<span class="hljs-string">"jitsi-zrok-compose"</span>  <span class="hljs-comment"># name for the environment in the console graph</span>
 ZROK_API_ENDPOINT=<span class="hljs-string">"https://api.zrok.io"</span>     <span class="hljs-comment"># must be set to the zrok API you're using</span>
 ZROK_TARGET=<span class="hljs-string">"https://web:443"</span>               <span class="hljs-comment"># this is correct for the web container's internal port</span>
 ZROK_INSECURE=<span class="hljs-string">"--insecure"</span>                  <span class="hljs-comment"># let zrok skip cert verification for the internal web:443 target</span>
</code></pre>
</li>
<li><p>Optionally, turn on OAuth for this Jitsi Meet instance with zrok. Add the following to the <code>.env.zrok</code> file (<a target="_blank" href="https://docs.zrok.io/docs/guides/docker-share/docker_public_share_guide/">Docker public share guide has more info</a>).</p>
<pre><code class="lang-bash"> ZROK_OAUTH_PROVIDER=<span class="hljs-string">"google"</span>  <span class="hljs-comment"># google, github</span>
 <span class="hljs-comment"># space-separated list email patterns verified by the provider</span>
 ZROK_OAUTH_EMAILS=<span class="hljs-string">"alice.example@gmail.com *@acme.example.com"</span>
</code></pre>
</li>
<li><p>Save the following script as the filename <code>compose.bash</code> in the same directory. This script configures the compose project and environment files.</p>
<pre><code class="lang-bash">
 <span class="hljs-built_in">export</span> COMPOSE_FILE=<span class="hljs-string">"docker-compose.yml:docker-compose.zrok.yml:docker-compose.override.yml"</span>
 <span class="hljs-built_in">export</span> COMPOSE_ENV_FILES=<span class="hljs-string">".env,.env.zrok"</span>

 docker compose  <span class="hljs-string">"<span class="hljs-variable">${@}</span>"</span>
</code></pre>
</li>
<li><p>Ensure you have all the necessary files.</p>
<ol>
<li><p><code>docker-compose.yml</code></p>
</li>
<li><p><code>docker-compose.zrok.yml</code></p>
</li>
<li><p><code>docker-compose.override.yml</code></p>
</li>
<li><p><code>.env</code></p>
</li>
<li><p><code>.env.zrok</code></p>
</li>
<li><p><code>compose.bash</code></p>
</li>
</ol>
</li>
<li><p>Run Jitsi and zrok.</p>
<pre><code class="lang-bash">bash ./compose.bash up
</code></pre>
</li>
<li><p>Open Jitsi in a web browser at the address from the <code>PUBLIC_URL</code> environment variable, e.g., <code>https://myjitsi.share.zrok.io</code> .</p>
</li>
<li><p>If you need to change the name, authentication, etc. you can delete the environment in the zrok web console and delete the Docker volumes like this to start over. It's also possible to make surgical changes if you don't want to start over. <a target="_blank" href="https://openziti.discourse.group/">Ask for help in Discourse</a>.</p>
<pre><code class="lang-bash">bash ./compose.bash down --volumes
</code></pre>
</li>
</ol>
<h2 id="heading-zrok-frontdoor">zrok frontdoor</h2>
<p>This tutorial for Jitsi Meet is a great example of <a target="_blank" href="https://docs.zrok.io/docs/guides/frontdoor/?os=Docker">zrok frontdoor</a>. zrok frontdoor brings many advantages for self-hosters and is always enabled when using zrok.io as a service with a production-ready service like this zrok public share in Docker. zrok.io users enjoy additional shielding for their Jitsi Meet public URL.</p>
<h2 id="heading-relatedly">Relatedly</h2>
<p>zrok is built with OpenZiti. Here's <a target="_blank" href="https://medium.com/netfoundry/tunneling-voip-over-openziti-69d6487605e4">another post about running an Asterisk PBX without published ports</a>, just as your Jitsi Meet instance has no open ports on the Docker Host's outward-facing interfaces.</p>
<h2 id="heading-share-the-project"><strong>Share the Project</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702330572628/7bb2b76c-af3f-45c6-83ab-d519f183024d.png?auto=compress,format&amp;format=webp" alt class="image--center mx-auto" /></p>
<p>If you find this interesting, please consider <a target="_blank" href="https://github.com/openziti/ziti/"><strong>starring us on GitHub</strong></a>. It helps. Let us know if you found a good use for this or have an improvement or question in mind on <a target="_blank" href="https://twitter.com/openziti"><strong>X <s>twitter</s></strong></a>, in <a target="_blank" href="https://www.reddit.com/r/openziti/">/r/openziti</a>, or the <a target="_blank" href="https://openziti.discourse.group/">Discourse forum</a>. We upload and stream <a target="_blank" href="https://youtube.com/openziti"><strong>on YouTube</strong></a> too. We'd love to hear from you!</p>
]]></content:encoded></item></channel></rss>