<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Elchi Blog</title>
        <link>https://www.elchi.io/docs/blog</link>
        <description>Engineering notes on Envoy control planes, xDS and API security.</description>
        <lastBuildDate>Fri, 13 Nov 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>© 2026 CloudNativeWorks</copyright>
        <item>
            <title><![CDATA[What Is an Envoy Control Plane? xDS Explained]]></title>
            <link>https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane</link>
            <guid>https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane</guid>
            <pubDate>Fri, 13 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An Envoy control plane serves configuration over the xDS APIs. Learn LDS, RDS, CDS, EDS, SDS, ADS ordering, delta xDS, ACK/NACK and snapshot caches.]]></description>
            <content:encoded><![CDATA[<p>An Envoy control plane is a gRPC server that tells Envoy proxies what to run: which listeners to open, how to route requests, which upstream clusters exist, which endpoints are behind them and which certificates to use. Envoy fetches all of this over a family of discovery APIs called <strong>xDS</strong> and applies each update without a restart. The proxy is the data plane; the server that answers its xDS requests is the control plane.</p>
<p>This post explains the xDS APIs, how Aggregated Discovery Service (ADS) orders updates, the difference between state-of-the-world and incremental (delta) xDS, how ACK and NACK work, and what a snapshot cache does. At the end we show where Elchi fits and which of these choices it makes.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="static-bootstrap-vs-dynamic-configuration">Static bootstrap vs dynamic configuration<a href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane#static-bootstrap-vs-dynamic-configuration" class="hash-link" aria-label="Direct link to Static bootstrap vs dynamic configuration" title="Direct link to Static bootstrap vs dynamic configuration" translate="no">​</a></h2>
<p>Every Envoy starts from a bootstrap file. A bootstrap can hold the whole configuration under <code>static_resources</code>, which works until you need to change something: then you edit the file and restart or hot restart the process. The alternative is <code>dynamic_resources</code>, where the bootstrap only says where the management server is and which resource types to fetch from it. From then on, the management server owns the configuration and Envoy keeps a stream open to receive changes.</p>
<p>That management server is the control plane. It is not part of the request path; if it goes away, Envoy keeps serving with the last configuration it accepted.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-xds-apis">The xDS APIs<a href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane#the-xds-apis" class="hash-link" aria-label="Direct link to The xDS APIs" title="Direct link to The xDS APIs" translate="no">​</a></h2>
<p>Each xDS API carries one resource type. The ones every Envoy operator meets are:</p>



































<table><thead><tr><th>API</th><th>Resource</th><th>What it holds</th></tr></thead><tbody><tr><td><strong>LDS</strong> (Listener Discovery Service)</td><td><code>Listener</code></td><td>Addresses and ports to bind, filter chains, network filters such as the HTTP connection manager or TCP proxy</td></tr><tr><td><strong>RDS</strong> (Route Discovery Service)</td><td><code>RouteConfiguration</code></td><td>Virtual hosts and routes that the HTTP connection manager uses</td></tr><tr><td><strong>CDS</strong> (Cluster Discovery Service)</td><td><code>Cluster</code></td><td>Upstream clusters: load balancing policy, timeouts, health checks, TLS to the upstream</td></tr><tr><td><strong>EDS</strong> (Endpoint Discovery Service)</td><td><code>ClusterLoadAssignment</code></td><td>The endpoints (hosts, ports, weights, health) behind a cluster</td></tr><tr><td><strong>SDS</strong> (Secret Discovery Service)</td><td><code>Secret</code></td><td>TLS certificates, private keys and validation contexts</td></tr></tbody></table>
<p>Two more appear in larger setups. <strong>ECDS</strong> (Extension Config Discovery Service) delivers the configuration of a single filter as a <code>TypedExtensionConfig</code>, so a filter inside a listener can be updated without resending the whole listener. <strong>VHDS</strong> (Virtual Host Discovery Service) delivers virtual hosts on demand instead of as part of a full route configuration.</p>
<p>The resources reference each other by name. A listener's HTTP connection manager names a route configuration, a route names a cluster, an EDS cluster names its endpoint assignment, and a TLS context names a secret. That reference graph is why the order of updates matters.</p>
<p>The authoritative description of the protocol is the <a href="https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol" target="_blank" rel="noopener noreferrer" class="">xDS REST and gRPC protocol</a> page in the Envoy documentation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="separate-streams-vs-ads">Separate streams vs ADS<a href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane#separate-streams-vs-ads" class="hash-link" aria-label="Direct link to Separate streams vs ADS" title="Direct link to Separate streams vs ADS" translate="no">​</a></h2>
<p>Envoy can fetch each resource type on its own gRPC stream, possibly from different management servers. That is simple but gives no ordering guarantee between types. If a new route arrives before the cluster it points at, requests that hit that route fail until the cluster shows up.</p>
<p><strong>ADS (Aggregated Discovery Service)</strong> puts all resource types on a single gRPC stream to a single management server. Because one server sequences everything on one stream, it can apply the ordering the Envoy documentation recommends to avoid dropping traffic during a change:</p>
<ol>
<li class="">CDS updates first.</li>
<li class="">EDS updates for those clusters after CDS.</li>
<li class="">LDS updates after the clusters and endpoints they need.</li>
<li class="">RDS updates for new listeners after LDS.</li>
<li class="">Only then remove clusters and endpoints that nothing references any more.</li>
</ol>
<p>This is "make before break": a resource is only referenced once what it depends on is in place, and nothing is removed while it is still referenced.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="state-of-the-world-vs-incremental-delta-xds">State of the world vs incremental (delta) xDS<a href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane#state-of-the-world-vs-incremental-delta-xds" class="hash-link" aria-label="Direct link to State of the world vs incremental (delta) xDS" title="Direct link to State of the world vs incremental (delta) xDS" translate="no">​</a></h2>
<p>xDS exists in two flavours, and each works with separate streams or ADS, which gives four variants.</p>
<p><strong>State of the world (SotW)</strong> uses <code>DiscoveryRequest</code> and <code>DiscoveryResponse</code>. For listeners and clusters, every response must carry the complete set of resources the client needs, even the ones that did not change. If a listener is missing from an LDS response, Envoy removes it. With hundreds of listeners or thousands of clusters, one small change means resending all of them.</p>
<p><strong>Incremental, or delta, xDS</strong> uses <code>DeltaDiscoveryRequest</code> and <code>DeltaDiscoveryResponse</code>. The client subscribes and unsubscribes to resource names (<code>resource_names_subscribe</code>, <code>resource_names_unsubscribe</code>), the server sends only resources that were added or changed, and it lists removed ones explicitly in <code>removed_resources</code>. Each resource carries its own version. On reconnect, the client reports the versions it already has in <code>initial_resource_versions</code>, so the server does not have to resend unchanged resources.</p>
<p>The choice is made in the bootstrap: <code>api_type: GRPC</code> for SotW and <code>api_type: DELTA_GRPC</code> for delta, both under <code>ads_config</code> when ADS is used.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ack-and-nack">ACK and NACK<a href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane#ack-and-nack" class="hash-link" aria-label="Direct link to ACK and NACK" title="Direct link to ACK and NACK" translate="no">​</a></h2>
<p>xDS is not fire-and-forget. Every response from the server carries a <code>nonce</code>, and Envoy answers each one with a new request on the same stream:</p>
<ul>
<li class=""><strong>ACK</strong>: Envoy applied the update. In SotW, the request carries the <code>version_info</code> of the accepted response and its <code>response_nonce</code>.</li>
<li class=""><strong>NACK</strong>: Envoy rejected the update, for example because a listener failed validation. The request carries the <code>response_nonce</code> of the rejected response, an <code>error_detail</code> with the reason, and, in SotW, the <code>version_info</code> of the last version it did accept. Envoy keeps running the previous configuration.</li>
</ul>
<p>Delta requests carry no <code>version_info</code> at all; ACK and NACK are told apart by whether <code>error_detail</code> is set, and the nonce says which response it answers.</p>
<p>For an operator this is the most useful signal a control plane has. A NACK is the only place where "the proxy refused this configuration, and here is why" shows up. A control plane that does not record NACKs leaves you comparing what you pushed with what <code>/config_dump</code> shows.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="snapshot-caches-and-go-control-plane">Snapshot caches and go-control-plane<a href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane#snapshot-caches-and-go-control-plane" class="hash-link" aria-label="Direct link to Snapshot caches and go-control-plane" title="Direct link to Snapshot caches and go-control-plane" translate="no">​</a></h2>
<p>Most control planes are not written from scratch. <a href="https://github.com/envoyproxy/go-control-plane" target="_blank" rel="noopener noreferrer" class="">envoyproxy/go-control-plane</a> is the reference Go implementation: it ships the generated Go types for the Envoy API, a gRPC server for every xDS variant, and caches that the server reads from.</p>
<p>The most common cache is the <strong>snapshot cache</strong>. The control plane builds a snapshot, a consistent set of listeners, routes, clusters, endpoints, secrets and other resources for one node, and stores it under a key derived from the Envoy node (the <code>NodeHash</code>). The server streams that snapshot to every Envoy that maps to the key, answers ACKs and NACKs, and sends a new update whenever the control plane sets a new snapshot. The cache is created with an <code>ads</code> flag; when it is set, the cache waits until a request has named all the resources it wants before it responds, which suits ADS clients.</p>
<p>That leaves the hard part to you: turning your own data model into correct Envoy resources, deciding which node gets which snapshot, and keeping references between resources consistent.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-elchi-fits">Where Elchi fits<a href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane#where-elchi-fits" class="hash-link" aria-label="Direct link to Where Elchi fits" title="Direct link to Where Elchi fits" translate="no">​</a></h2>
<p>Elchi is a control plane for Envoy fleets with a UI and an API in front of it. Here is how it answers each of the choices above, with the code behind it in the <code>elchi-backend</code> and <code>elchi-client</code> repositories.</p>
<p><strong>ADS over delta xDS.</strong> The bootstrap Elchi generates for a deployed listener fetches LDS and CDS through ADS and sets <code>api_type: DELTA_GRPC</code> with transport API v3. The other resource types follow from there: route configurations, endpoints, secrets and HTTP filter configurations are referenced from listeners and clusters and fetched over the same aggregated stream. HTTP filters inside the HTTP connection manager are stored as ECDS <code>config_discovery</code> references, not inline configuration, so a filter can change without rewriting the listener. The control plane also serves VHDS.</p>
<p><strong>A snapshot cache keyed by node ID.</strong> The control-plane process builds a go-control-plane snapshot cache with ADS enabled and keys it by the Envoy node ID. Every deployed listener has its own node ID in the form <code>listener::project::ip</code>, so each listener gets its own snapshot. The xDS server listens on port 18000 behind the platform's front door. See <a class="" href="https://www.elchi.io/docs/getting-started/architecture">Architecture</a> for the full wiring.</p>
<p><strong>NACKs are recorded, not just logged.</strong> When a delta request carries <code>error_detail</code>, the control plane stores the error per proxy together with the revision it rejected, and the UI shows it on the service. An ACK records which revision the proxy now runs. One detail we document because it surprises people: the delta server does not resend a resource that was NACKed until its content changes, so the fix is to correct the resource and publish again.</p>
<p><strong>One control plane per Envoy version.</strong> Envoy's API changes between releases: fields are added, deprecated and removed. A control plane compiled against one release's protobuf definitions can produce configuration that an older proxy does not understand. Elchi builds against <code>versioned-go-control-plane</code>, our fork of go-control-plane with one module version per Envoy release, and the release pipeline produces one control-plane binary per supported Envoy version. Each Envoy sends its node ID and an <code>envoy-version</code> header on its ADS stream. The registry, which runs as an <code>ext_proc</code> filter on the platform's front door, reads both and routes the stream to the control plane built for that version. This is what lets several Envoy versions run side by side in one project; <a class="" href="https://www.elchi.io/docs/guides/run-multiple-envoy-versions">Run several Envoy versions side by side</a> explains the setup and the listener upgrade.</p>
<p><strong>What you edit.</strong> Each resource form in the UI is generated from the protobuf schema of the Envoy version it targets, and Elchi assembles the references (ECDS pointers, SDS secrets, EDS assignments) for you. You never write the snapshot by hand. You can still check what a node is served in the <a class="" href="https://www.elchi.io/docs/envoy-configuration/snapshot-dump">snapshot dump</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next steps<a href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane#next-steps" class="hash-link" aria-label="Direct link to Next steps" title="Direct link to Next steps" translate="no">​</a></h2>
<ul>
<li class="">Read the <a class="" href="https://www.elchi.io/docs/getting-started/architecture">Architecture</a> page for every process and connection in an Elchi installation.</li>
<li class="">Follow <a class="" href="https://www.elchi.io/docs/guides/manage-envoy-without-yaml">Manage Envoy without YAML</a> to build a listener, route and cluster from the UI.</li>
<li class="">See <a class="" href="https://www.elchi.io/docs/envoy-configuration/versions-and-upgrades">Versions &amp; Upgrades</a> and <a class="" href="https://www.elchi.io/docs/guides/run-multiple-envoy-versions">Run several Envoy versions side by side</a> for version routing.</li>
<li class="">Learn more about <a href="https://www.elchi.io/products/core.html" target="_blank" rel="noopener noreferrer" class="">Elchi Core</a>, or <a href="https://www.elchi.io/contact.html" target="_blank" rel="noopener noreferrer" class="">book a demo</a>.</li>
</ul>]]></content:encoded>
            <category>Envoy</category>
            <category>xDS</category>
        </item>
        <item>
            <title><![CDATA[Adding the OWASP Core Rule Set to Envoy with ext_proc]]></title>
            <link>https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc</link>
            <guid>https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc</guid>
            <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Run the OWASP CRS as an Envoy ext_proc sidecar: UDS wiring, per-route body buffering, the HTTP-method trap, and a detect, shadow, block rollout.]]></description>
            <content:encoded><![CDATA[<p>Envoy does not ship a request-inspecting WAF. If you want the OWASP Core Rule Set (CRS) in front of your APIs, you have to bring an engine and connect it to Envoy. Envoy's <strong>external processing</strong> filter (<code>ext_proc</code>) is a clean way to do that: the WAF runs as a separate process with its own release cycle, and Envoy stays a standard build.</p>
<p>This post walks through the Envoy side of the wiring, the design choices that make ext_proc workable for a WAF, and the rollout sequence that keeps the CRS from blocking legitimate traffic on day one. The examples use Shield, Elchi's ext_proc sidecar, but the Envoy configuration works for any ext_proc service.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-ext_proc-works">How ext_proc works<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#how-ext_proc-works" class="hash-link" aria-label="Direct link to How ext_proc works" title="Direct link to How ext_proc works" translate="no">​</a></h2>
<p><code>envoy.filters.http.ext_proc</code> opens a <strong>bidirectional gRPC stream per HTTP transaction</strong> to an external service. Envoy sends messages for request headers, request body, response headers and response body, as selected by the filter's <code>processing_mode</code>. The service answers each message: continue, mutate, or return an immediate response (a <code>403</code>, for a WAF).</p>
<p>Three properties matter for a WAF:</p>
<ul>
<li class=""><strong>Body handling is configurable per phase</strong>, and when the filter sets <code>allow_mode_override</code>, the external service can change it per request with <code>mode_override</code>. You don't have to buffer every body because some routes need inspection.</li>
<li class=""><strong>Failure behavior is explicit.</strong> <code>failure_mode_allow</code> decides what happens when the service cannot be reached.</li>
<li class=""><strong>The transport can be a Unix domain socket</strong>, which keeps the hop on the host.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-envoy-side">The Envoy side<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#the-envoy-side" class="hash-link" aria-label="Direct link to The Envoy side" title="Direct link to The Envoy side" translate="no">​</a></h2>
<p>The external service is a gRPC server, so its cluster must use HTTP/2. A Unix domain socket is the preferred transport:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">clusters</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> elchi_shield</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> STATIC</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">connect_timeout</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 0.25s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">typed_extension_protocol_options</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">envoy.extensions.upstreams.http.v3.HttpProtocolOptions</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">"@type"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">explicit_http_config</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">http2_protocol_options</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">load_assignment</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">cluster_name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> elchi_shield</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">endpoints</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">lb_endpoints</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">endpoint</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token key atrule" style="color:#00a4db">address</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                  </span><span class="token key atrule" style="color:#00a4db">pipe</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token key atrule" style="color:#00a4db">path</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> /run/elchi</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">shield/extproc.sock</span><br></div></code></pre></div></div>
<p>The filter goes in the HTTP filter chain <strong>before the router</strong>:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">http_filters</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> envoy.filters.http.ext_proc</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">typed_config</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">"@type"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">failure_mode_allow</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">allow_mode_override</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">request_attributes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"xds.node.id"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">grpc_service</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">envoy_grpc</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">cluster_name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> elchi_shield </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">timeout</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 0.2s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">processing_mode</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">request_header_mode</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> SEND</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">response_header_mode</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> SKIP</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">request_body_mode</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> NONE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">response_body_mode</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> NONE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> envoy.filters.http.router</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">typed_config</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">"@type"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> type.googleapis.com/envoy.extensions.filters.http.router.v3.Router</span><br></div></code></pre></div></div>
<p>Three lines deserve attention.</p>
<p><code>request_body_mode: NONE</code> looks wrong for a WAF that needs bodies. It is deliberate. Envoy sends <strong>headers only</strong> by default, and Shield upgrades the body mode to <code>BUFFERED</code> through <code>mode_override</code> only for the routes whose policy inspects bodies. Routes that need no body inspection never pay for buffering.</p>
<p><code>allow_mode_override: true</code> is what lets Envoy accept that per-request upgrade. Without it, Envoy ignores <code>mode_override</code> and the processor never sees a body.</p>
<p><code>failure_mode_allow: true</code> is the availability backstop <em>outside</em> the WAF. If the sidecar process is down or the gRPC call fails, traffic continues uninspected instead of failing. Set it to <code>false</code> only if you accept an outage when the sidecar is down. This is a separate decision from how the WAF treats its <em>own</em> internal errors, covered below.</p>
<p>One more listener setting is not optional: the WAF needs a client IP it can trust.</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># HttpConnectionManager</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">use_remote_address</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">xff_num_trusted_hops</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><br></div></code></pre></div></div>
<p>Shield reads the client IP from the right-hand end of <code>X-Forwarded-For</code>. Without <code>use_remote_address</code>, that entry is whatever the client sent, and every IP-based rule (reputation lists, GeoIP, per-IP rate limits) acts on an attacker-controlled value. Full reference: <a class="" href="https://www.elchi.io/docs/shield/envoy-wiring">Wiring Shield into Envoy</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="running-the-crs-inside-the-processor">Running the CRS inside the processor<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#running-the-crs-inside-the-processor" class="hash-link" aria-label="Direct link to Running the CRS inside the processor" title="Direct link to Running the CRS inside the processor" translate="no">​</a></h2>
<p>Shield embeds <strong>Coraza</strong> (the OWASP Go WAF library, which runs SecLang rules) with the <strong>CRS compiled into the binary</strong>. There are no rule files to ship to the edge. A policy turns it on per host and route:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">apiVersion</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> sentinel.elchi.io/v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">kind</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> SecurityPolicy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">metadata</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> api</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">coraza</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">spec</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">defaults</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">mode</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> block</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">fail_mode</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> fail_open</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">inspect_request_body</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">max_request_body_bytes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1048576</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">domains</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">hosts</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"app.example.com"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">routes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">match</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token key atrule" style="color:#00a4db">path_prefix</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/api/"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">policy</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token key atrule" style="color:#00a4db">mode</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> detect</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token key atrule" style="color:#00a4db">engines</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token key atrule" style="color:#00a4db">coraza</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token key atrule" style="color:#00a4db">include_owasp</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><br></div></code></pre></div></div>
<p>The CRS uses <strong>anomaly scoring</strong>: each matching rule adds to a per-transaction score, and the blocking rule fires when the score crosses a threshold. Shield exposes the CRS's usual controls as policy fields: <code>paranoia_level</code> (1 to 4, default 1), <code>detection_paranoia_level</code> (run stricter rules in detection only), <code>inbound_anomaly_threshold</code> (default 5) and <code>outbound_anomaly_threshold</code> (default 4), plus <code>exclude_rule_ids</code> for tuning out noisy rules by ID.</p>
<p>The directive bundle is assembled in a fixed order: the recommended Coraza config, <code>SecRuleEngine On</code>, the CRS setup, your tuning, the CRS rules, your own SecLang <code>directives</code>, and finally the rule exclusions. The WAF is compiled once and swapped in atomically, so a config reload never races a request that is being inspected.</p>
<p>One trap catches almost everyone who puts the CRS in front of an API. On its own, the CRS allows only <code>GET HEAD POST OPTIONS</code>. Rule 911100 is critical (+5), which is enough to block at the default threshold, so every <code>PUT</code>, <code>PATCH</code> and <code>DELETE</code> would get a 403. Shield always writes an explicit method list into the CRS settings for that reason, defaulting to <code>GET HEAD POST OPTIONS PUT PATCH DELETE</code>. If you wire the CRS up yourself, set <code>tx.allowed_methods</code> before your first deploy.</p>
<p>Engine reference: <a class="" href="https://www.elchi.io/docs/shield/engines/coraza-waf">Coraza WAF (OWASP CRS)</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="header-phase-vs-body-phase">Header phase vs body phase<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#header-phase-vs-body-phase" class="hash-link" aria-label="Direct link to Header phase vs body phase" title="Direct link to Header phase vs body phase" translate="no">​</a></h2>
<p>The CRS is a <strong>body-phase</strong> engine. Shield runs each request as an ordered pipeline: policy resolution, cheap pre-checks (method, host, headers, content type, source IP) and header-phase engines such as JWT or rate limiting, and only then, if the policy needs it, the body gate and the body-phase engines. A policy whose engines don't need the body never buffers it just to run the WAF. Response inspection is a separate pipeline that reuses the request's policy, and it is where the CRS's outbound rules run. See <a class="" href="https://www.elchi.io/docs/shield/how-it-works">How Shield Works</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rolling-out-without-breaking-traffic">Rolling out without breaking traffic<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#rolling-out-without-breaking-traffic" class="hash-link" aria-label="Direct link to Rolling out without breaking traffic" title="Direct link to Rolling out without breaking traffic" translate="no">​</a></h2>
<p>The CRS will produce false positives on real APIs, so plan the rollout around that. Shield policies have four modes:</p>

























<table><thead><tr><th>Mode</th><th>Effect</th></tr></thead><tbody><tr><td><code>detect</code></td><td>Evaluate everything, record would-blocks, allow the request</td></tr><tr><td><code>shadow</code></td><td>Evaluate as if blocking, log what would have been blocked, allow</td></tr><tr><td><code>block</code></td><td>A finding returns an immediate 403</td></tr><tr><td><code>off</code></td><td>Skip inspection</td></tr></tbody></table>
<p><code>detect</code> and <code>shadow</code> never modify traffic, and they record the <strong>full trail</strong> of findings rather than stopping at the first, which is what you need for tuning. The recommended sequence is <strong>detect, then shadow, then block</strong>, per route if needed: establish a baseline, exclude the rule IDs that fire on legitimate traffic, rehearse enforcement, then enforce.</p>
<p>Keep the two failure settings apart:</p>
<ul>
<li class=""><code>fail_mode: fail_open | fail_close</code> (policy) governs <strong>engine errors and timeouts inside the WAF</strong>. It never lets a confirmed finding through.</li>
<li class=""><code>failure_mode_allow</code> (Envoy) governs <strong>the sidecar being unreachable</strong>.</li>
</ul>
<p>Details: <a class="" href="https://www.elchi.io/docs/shield/policies/modes-and-postures">Modes &amp; Fail Postures</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="getting-policies-to-every-edge">Getting policies to every edge<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#getting-policies-to-every-edge" class="hash-link" aria-label="Direct link to Getting policies to every edge" title="Direct link to Getting policies to every edge" translate="no">​</a></h2>
<p>A WAF on one host is a demo. On a fleet, the question is how policy changes reach every sidecar safely. Shield's answer is that its configuration is <strong>files in a watched directory</strong>. It hot-reloads on change, without a restart, and an invalid config never replaces the last good one.</p>
<p>In Elchi, the controller merges a project's policies into one bundle and the edge agent applies it in phases: it stages each file, validates the staged set with the local <code>elchi-shield validate</code> binary (the same version that will run it), and only then renames the files into place. The agent then confirms that Shield's active config version changed. If Shield rejected the config, the deploy reports Shield's file-and-field error instead of a false success. See <a class="" href="https://www.elchi.io/docs/shield/deployment">Deploying Policies to Edges</a>.</p>
<p>The client installer puts Shield on each edge by default, next to Envoy: <a class="" href="https://www.elchi.io/docs/installation/client/shield-sidecar">The Bundled Shield Sidecar</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="verifying-it">Verifying it<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#verifying-it" class="hash-link" aria-label="Direct link to Verifying it" title="Direct link to Verifying it" translate="no">​</a></h2>
<p>With the policy in <code>detect</code>, a SQL-injection probe still gets the upstream's response and shows up in Security Events as a would-block, with the CRS rule that fired. Move the route to <code>block</code>, repeat the request, and it returns <code>403 Forbidden</code>, while a clean request still gets <code>200</code>. The end-to-end walkthrough, including discovering the API first and drafting the policy from its observed traffic, is in <a class="" href="https://www.elchi.io/docs/tutorials/secure-an-api-with-shield">Secure an API with Shield</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="takeaways">Takeaways<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#takeaways" class="hash-link" aria-label="Direct link to Takeaways" title="Direct link to Takeaways" translate="no">​</a></h2>
<ul>
<li class="">ext_proc gives you a WAF without a custom Envoy build. Use a Unix domain socket, send headers only, and let the processor upgrade the body mode per route (<code>allow_mode_override</code>).</li>
<li class="">Make the client IP trustworthy (<code>use_remote_address</code>) before any IP-based rule means anything.</li>
<li class="">Allow your API's methods in the CRS before the first deploy.</li>
<li class="">Roll out detect, shadow, block, and keep sidecar-down behavior separate from engine-error behavior.</li>
<li class="">Treat WAF config as files that are validated before they go live, on every edge.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next steps<a href="https://www.elchi.io/docs/blog/owasp-crs-waf-envoy-ext-proc#next-steps" class="hash-link" aria-label="Direct link to Next steps" title="Direct link to Next steps" translate="no">​</a></h2>
<ul>
<li class="">Follow the how-to: <a class="" href="https://www.elchi.io/docs/guides/add-waf-to-envoy-ext-proc">Add a WAF (OWASP Core Rule Set) to Envoy with ext_proc</a>.</li>
<li class="">Read the Shield docs: <a class="" href="https://www.elchi.io/docs/shield/overview">Shield overview</a>.</li>
<li class="">Product overview: <a href="https://www.elchi.io/products/api-security.html" target="_blank" rel="noopener noreferrer" class="">Elchi API Security</a>.</li>
<li class="">Want to see it against your own APIs? <a href="https://www.elchi.io/contact.html" target="_blank" rel="noopener noreferrer" class="">Book a demo</a>.</li>
</ul>]]></content:encoded>
            <category>Envoy</category>
            <category>WAF</category>
        </item>
        <item>
            <title><![CDATA[Running an Envoy Fleet Without Hand-Writing xDS]]></title>
            <link>https://www.elchi.io/docs/blog/envoy-fleet-without-xds</link>
            <guid>https://www.elchi.io/docs/blog/envoy-fleet-without-xds</guid>
            <pubDate>Wed, 11 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Hand-written xDS breaks on references, validation and versions, not syntax. What an Envoy control plane must do, and how Elchi handles each part.]]></description>
            <content:encoded><![CDATA[<p>Envoy is easy to start with and hard to operate at fleet scale. One proxy with a static YAML bootstrap is a weekend project. Twenty proxies on VMs, a few on Kubernetes, two Envoy versions in flight and a change window every week is a different job. At that point most teams reach the same conclusion: Envoy is meant to be driven by a control plane over <strong>xDS</strong>, and somebody has to own that control plane.</p>
<p>This post covers what that control plane has to do, why hand-written xDS breaks down, and how we approached each part in Elchi. The problems apply to any Envoy deployment, whichever control plane you end up with.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="envoys-configuration-is-an-api-not-a-file">Envoy's configuration is an API, not a file<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#envoys-configuration-is-an-api-not-a-file" class="hash-link" aria-label="Direct link to Envoy's configuration is an API, not a file" title="Direct link to Envoy's configuration is an API, not a file" translate="no">​</a></h2>
<p>Every Envoy object (listener, route configuration, cluster, endpoint assignment, secret) is a protobuf message. The xDS protocols (LDS, RDS, CDS, EDS and SDS, aggregated over ADS) stream those messages to a proxy, and Envoy applies them <strong>without a restart</strong>. The static bootstrap only has to contain enough for Envoy to find its control plane.</p>
<p>The resources form a reference graph:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Listener ─▶ filter chain (HTTP connection manager) ─▶ route config ─▶ virtual host ─▶ route ─▶ cluster ─▶ endpoints</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">   └─ transport socket ─▶ secret (TLS)                                              cluster ─▶ transport socket ─▶ secret</span><br></div></code></pre></div></div>
<p>A typo in a cluster name inside a route does not fail when the config loads. The route points at nothing, and requests get a 503 at runtime. Multiply that by every reference in a non-trivial config and the main cost of hand-written xDS becomes clear: <strong>the references are the hard part, not the fields</strong>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-a-control-plane-has-to-do">What a control plane has to do<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#what-a-control-plane-has-to-do" class="hash-link" aria-label="Direct link to What a control plane has to do" title="Direct link to What a control plane has to do" translate="no">​</a></h2>
<p>If you build your own on top of a library such as go-control-plane, you take on a list of responsibilities. These are the ones that tend to cause trouble:</p>
<ol>
<li class=""><strong>Model resources and their references.</strong> Know that deleting a cluster breaks three routes before it happens.</li>
<li class=""><strong>Validate before pushing.</strong> Protobuf type checking is not enough. Envoy's API carries validation rules (required fields, ranges, enum constraints) that should be enforced before anything leaves the control plane.</li>
<li class=""><strong>Stage changes and ship them together.</strong> Operators want to change several resources and ship them as one unit, not one push per field edit.</li>
<li class=""><strong>Build and serve snapshots per node.</strong> Each proxy needs a consistent set of resources, keyed by its node identity.</li>
<li class=""><strong>Handle more than one Envoy version.</strong> The API changes between releases. During an upgrade window part of the fleet runs the old version and part runs the new one.</li>
<li class=""><strong>Show what actually shipped.</strong> "I published it" and "the proxy is running it" are different statements.</li>
</ol>
<p>None of this is exotic, but together it is a product. Elchi is our attempt to build that product so platform teams don't have to.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="typed-resources-instead-of-yaml">Typed resources instead of YAML<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#typed-resources-instead-of-yaml" class="hash-link" aria-label="Direct link to Typed resources instead of YAML" title="Direct link to Typed resources instead of YAML" translate="no">​</a></h2>
<p>In Elchi, everything pushed to Envoy is modeled as a <strong>resource</strong>: listeners, clusters, routes, endpoints, secrets and the bootstrap. HTTP, network, listener and UDP filters and reusable extension configurations are managed alongside them, versioned per Envoy release.</p>
<p>Every resource is created for one Envoy version, and its editor form is <strong>generated from that version's protobuf definitions</strong>. That has two practical effects. When a new Envoy release adds a field, the field appears in the editor for that version without hand-built UI work. And the form offers exactly the fields that version supports, so you don't write <code>@type</code> URLs or nest messages by hand.</p>
<p>Details: <a class="" href="https://www.elchi.io/docs/envoy-configuration/config-model">Resources &amp; the Config Model</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="two-step-validation">Two-step validation<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#two-step-validation" class="hash-link" aria-label="Direct link to Two-step validation" title="Direct link to Two-step validation" translate="no">​</a></h2>
<p>Every change is checked twice:</p>
<ol>
<li class=""><strong>In the editor</strong>: types generated from the Envoy API catch shape and type errors as you edit.</li>
<li class=""><strong>On the server</strong>: the controller runs the Envoy API's validation rules before the resource is persisted.</li>
</ol>
<p>The second step is the one that protects the fleet. A resource that fails it is not stored and never reaches a snapshot, so a proxy is not the first place the error shows up.</p>
<p>Validation has a limit worth stating. A value can be valid against the schema and still be refused by a filter at runtime. The verification step further down is how you catch that case.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="draft-save-publish">Draft, save, publish<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#draft-save-publish" class="hash-link" aria-label="Direct link to Draft, save, publish" title="Direct link to Draft, save, publish" translate="no">​</a></h2>
<p>Edits are kept as <strong>drafts</strong>, so you can stage changes across several resources (say, a new route plus the cluster it targets) and publish them together. On publish, the controller validates the changes, persists them, and pushes a new snapshot to the control plane, which streams it to Envoy over ADS. Envoy applies it in place.</p>
<p>Config changes are also recorded in an audit trail, so "who changed this route, and when" has an answer. See <a class="" href="https://www.elchi.io/docs/observability/audit-and-syslog">Audit &amp; Syslog Forwarding</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="seeing-the-reference-graph">Seeing the reference graph<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#seeing-the-reference-graph" class="hash-link" aria-label="Direct link to Seeing the reference graph" title="Direct link to Seeing the reference graph" translate="no">​</a></h2>
<p>The reference problem from the start of this post gets two views:</p>
<ul>
<li class="">A <strong>dependency graph</strong> for any resource shows what it links to (clusters, routes, secrets, filters). It is the quickest way to spot an orphaned or broken reference before you publish. See <a class="" href="https://www.elchi.io/docs/envoy-configuration/dependency-graph">Dependency Graph</a>.</li>
<li class="">A <strong>route map</strong> shows how requests flow through a listener's routes to upstreams. See <a class="" href="https://www.elchi.io/docs/envoy-configuration/route-map">Route Map</a>.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="verifying-what-envoy-received">Verifying what Envoy received<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#verifying-what-envoy-received" class="hash-link" aria-label="Direct link to Verifying what Envoy received" title="Direct link to Verifying what Envoy received" translate="no">​</a></h2>
<p>Authoring tells you what you <em>intended</em>. The <strong>Snapshot Dump</strong> reads back the xDS snapshot the control plane is serving a specific node, with one tab per resource type and the full JSON. If a resource is not in the dump, the node is not running it, whatever the editor shows. A stale snapshot can be cleared for one node, which forces a rebuild and a re-push.</p>
<p>When a change "isn't taking effect", this check splits the problem in two: either the config never reached the node, or it did and the cause is elsewhere. See <a class="" href="https://www.elchi.io/docs/envoy-configuration/snapshot-dump">Snapshot Dump</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="several-envoy-versions-at-once">Several Envoy versions at once<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#several-envoy-versions-at-once" class="hash-link" aria-label="Direct link to Several Envoy versions at once" title="Direct link to Several Envoy versions at once" translate="no">​</a></h2>
<p>Elchi runs a control-plane instance for each Envoy version you deploy. When a proxy connects, a <strong>registry</strong> matches it to the control plane that speaks the version it reports, so a mixed fleet is a normal state, not an emergency.</p>
<p>Moving listeners to a newer version is a tracked background job. You select the listeners, pick a target version and can run it as a dry run first. The controller:</p>
<ol>
<li class="">runs a per-listener dependency analysis to find everything that has to move together,</li>
<li class="">recreates the dependencies, then the listeners, in topological order,</li>
<li class="">regenerates snapshots and bootstrap configs for the target version.</li>
</ol>
<p>Managed listeners switch to the new version through an Envoy hot restart, with no traffic interruption. Once nothing is left on the old version, a cleanup removes that version's leftover resources. See <a class="" href="https://www.elchi.io/docs/envoy-configuration/versions-and-upgrades">Versions &amp; Upgrades</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-the-proxies-run">Where the proxies run<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#where-the-proxies-run" class="hash-link" aria-label="Direct link to Where the proxies run" title="Direct link to Where the proxies run" translate="no">​</a></h2>
<p>A central platform does not help if getting Envoy onto hosts is still manual. Each edge host runs a small agent, <strong>elchi-client</strong>. It has <strong>no inbound listener</strong>: it dials out to the controller, registers the node, runs the lifecycle commands the controller sends, and ships logs. Envoy's admin interface stays on loopback.</p>
<p>On the central side, one public TLS listener on <code>:443</code> carries the UI, the REST API, the agents' command stream, xDS and access logs. An ext_proc filter in front inspects each stream and routes it to the right internal service, so the internal ports never have to be exposed. The full wire map is in <a class="" href="https://www.elchi.io/docs/getting-started/architecture">Architecture</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-minimal-end-to-end-path">A minimal end-to-end path<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#a-minimal-end-to-end-path" class="hash-link" aria-label="Direct link to A minimal end-to-end path" title="Direct link to A minimal end-to-end path" translate="no">​</a></h2>
<p>The shortest route from nothing to traffic is on Kubernetes:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">helm repo </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> elchi https://charts.elchi.io</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">helm repo update</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">helm </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> elchi elchi/elchi-stack </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --set-string </span><span class="token assign-left variable" style="color:#36acaa">global.mainAddress</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">192.168</span><span class="token plain">.1.10 </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">--namespace</span><span class="token plain"> elchi-stack </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --create-namespace</span><br></div></code></pre></div></div>
<p><code>192.168.1.10</code> stands for the IP of one of your nodes or your load balancer. Then install the agent on one Linux host and build the four resources every HTTP path needs: endpoints, cluster, route configuration and listener. Publish them and send a request. The full walkthrough, including the client install, is in <a class="" href="https://www.elchi.io/docs/tutorials/zero-to-envoy">From Install to First Listener</a>.</p>
<p><a class="" href="https://www.elchi.io/docs/installation/docker-swarm/overview">Docker Swarm</a>, <a class="" href="https://www.elchi.io/docs/installation/kind-quickstart">single-host</a> and <a class="" href="https://www.elchi.io/docs/installation/bare-metal/overview">bare-metal (systemd)</a> installs bring up the same control plane.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-you-dont-need-this">When you don't need this<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#when-you-dont-need-this" class="hash-link" aria-label="Direct link to When you don't need this" title="Direct link to When you don't need this" translate="no">​</a></h2>
<p>If you run a handful of proxies whose config changes rarely, a static bootstrap in version control is a fine choice and much simpler. A control plane earns its keep when the number of proxies, the rate of change, or the number of Envoy versions you run at once outgrows reviewing YAML diffs by hand. That is roughly when the six responsibilities above stop being theoretical.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<p>Hand-written xDS fails on references, validation and versions, not on syntax. A control plane that models resources as typed objects per Envoy version, validates them twice, publishes them as a unit and lets you read back what each node received removes most of those failure modes before a proxy ever sees them.</p>
<p>Without a license, Elchi runs a free tier, Elchi Core with a single managed client, which is enough to try the workflow end to end (<a class="" href="https://www.elchi.io/docs/administration/licensing">Licensing</a>).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next steps<a href="https://www.elchi.io/docs/blog/envoy-fleet-without-xds#next-steps" class="hash-link" aria-label="Direct link to Next steps" title="Direct link to Next steps" translate="no">​</a></h2>
<ul>
<li class="">Follow the how-to: <a class="" href="https://www.elchi.io/docs/guides/manage-envoy-without-yaml">Manage Envoy proxies without hand-writing xDS or YAML</a>.</li>
<li class="">Running more than one Envoy version? See <a class="" href="https://www.elchi.io/docs/guides/run-multiple-envoy-versions">Run multiple Envoy versions</a>.</li>
<li class="">Read the product overview: <a href="https://www.elchi.io/products/core.html" target="_blank" rel="noopener noreferrer" class="">Elchi Core</a>.</li>
<li class="">Want to see it on your own topology? <a href="https://www.elchi.io/contact.html" target="_blank" rel="noopener noreferrer" class="">Book a demo</a>.</li>
</ul>]]></content:encoded>
            <category>Envoy</category>
            <category>xDS</category>
        </item>
        <item>
            <title><![CDATA[Running Envoy on VMs Without Kubernetes]]></title>
            <link>https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes</link>
            <guid>https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes</guid>
            <pubDate>Tue, 10 Nov 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Envoy runs well on plain VMs if you cover bootstrap, xDS, process supervision, upgrades and telemetry. Here is what each needs and how Elchi automates it.]]></description>
            <content:encoded><![CDATA[<p>Envoy does not need Kubernetes. It is a single static binary that reads a bootstrap file and, if you let it, fetches the rest of its configuration from a control plane over xDS. Many teams run it on virtual machines or bare metal as an edge proxy, an API gateway or a load balancer in front of services that will never move into a cluster.</p>
<p>What Kubernetes and a service mesh normally give you for free is everything around the binary: a bootstrap for each proxy, a connection to a control plane, a supervisor that restarts it, a way to upgrade it, and a path for its logs and metrics. This post lists what you have to provide yourself on a VM, and then shows how the Elchi edge agent, <code>elchi-client</code>, provides each piece.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-a-vm-based-envoy-needs">What a VM-based Envoy needs<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#what-a-vm-based-envoy-needs" class="hash-link" aria-label="Direct link to What a VM-based Envoy needs" title="Direct link to What a VM-based Envoy needs" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-bootstrap-per-proxy">A bootstrap per proxy<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#a-bootstrap-per-proxy" class="hash-link" aria-label="Direct link to A bootstrap per proxy" title="Direct link to A bootstrap per proxy" translate="no">​</a></h3>
<p>Every Envoy process starts from a bootstrap file. At minimum it sets a node identity (<code>node.id</code>, <code>node.cluster</code>), the admin interface, and either the full configuration under <code>static_resources</code> or a <code>dynamic_resources</code> section that points at a control plane. If you run several Envoy processes on one host, each needs its own bootstrap, its own admin port and its own <code>--base-id</code>, the value Envoy uses to keep the shared memory of separate processes apart.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-connection-to-a-control-plane">A connection to a control plane<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#a-connection-to-a-control-plane" class="hash-link" aria-label="Direct link to A connection to a control plane" title="Direct link to A connection to a control plane" translate="no">​</a></h3>
<p>With a static bootstrap, every change means editing a file on every host and restarting. With <code>dynamic_resources</code> and ADS, Envoy keeps a gRPC stream open to a management server and applies listener, route, cluster, endpoint and secret changes live. Our post <a class="" href="https://www.elchi.io/docs/blog/what-is-an-envoy-control-plane">What is an Envoy control plane?</a> explains the xDS side.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="process-supervision">Process supervision<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#process-supervision" class="hash-link" aria-label="Direct link to Process supervision" title="Direct link to Process supervision" translate="no">​</a></h3>
<p>On a VM, something has to start Envoy at boot, restart it when it crashes, limit its resources and stop it cleanly. On Linux that is systemd. You also need the unit to fail early on a broken bootstrap instead of crash-looping; <code>envoy --mode validate</code> checks a bootstrap without starting the proxy.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="binary-distribution-and-upgrades">Binary distribution and upgrades<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#binary-distribution-and-upgrades" class="hash-link" aria-label="Direct link to Binary distribution and upgrades" title="Direct link to Binary distribution and upgrades" translate="no">​</a></h3>
<p>Envoy releases often. You need a way to get a given version onto each host, verify it, keep the previous version around, and move a running proxy to the new binary. A plain <code>systemctl restart</code> closes the listening sockets and the connections on them. Envoy's answer is <strong>hot restart</strong>: a new process is started with <code>--restart-epoch</code> one higher, takes over the listening sockets from the old process over a Unix domain socket, and the old process drains its connections for <code>--drain-time-s</code> and is shut down after <code>--parent-shutdown-time-s</code>. Hot restart only works between two binaries that report the same <code>--hot-restart-version</code>, and Envoy itself does not supervise this; the upstream project ships a small Python wrapper, <code>hot-restarter.py</code>, that forks the new epoch when it receives SIGHUP. See the <a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/hot_restart" target="_blank" rel="noopener noreferrer" class="">hot restart documentation</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="logs-and-metrics">Logs and metrics<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#logs-and-metrics" class="hash-link" aria-label="Direct link to Logs and metrics" title="Direct link to Logs and metrics" translate="no">​</a></h3>
<p>Envoy writes access logs, exposes statistics on its admin interface and can push both through stats sinks and the access log service (ALS). On a VM there is no sidecar collector by default, so you choose where each signal goes and wire it into every bootstrap.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-elchi-does-it">How Elchi does it<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#how-elchi-does-it" class="hash-link" aria-label="Direct link to How Elchi does it" title="Direct link to How Elchi does it" translate="no">​</a></h2>
<p>Elchi splits this work between the central platform and an agent on each host. The control plane builds configuration and serves it over xDS; <code>elchi-client</code> runs on every edge VM, registers with the platform, and does the host-side work.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="registration">Registration<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#registration" class="hash-link" aria-label="Direct link to Registration" title="Direct link to Registration" translate="no">​</a></h3>
<p><code>elchi-client</code> has no inbound listener. It dials out to the platform's controller over gRPC (through the platform's single front door on port 443) and registers with a token of the form <code>&lt;uuid&gt;--&lt;projectId&gt;</code>, which ties the host to one project. On every registration it also reports the listeners it already runs, so the control plane can reconcile what it expects with what is on the box. Installation is one command; see <a class="" href="https://www.elchi.io/docs/installation/client/installation">Client installation</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="envoy-binaries">Envoy binaries<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#envoy-binaries" class="hash-link" aria-label="Direct link to Envoy binaries" title="Direct link to Envoy binaries" translate="no">​</a></h3>
<p>The agent downloads Envoy binaries from the release archive (<code>https://archive.elchi.io/index.json</code> by default, configurable for a mirror), verifies each download's checksum, and stores every version under its own directory in <code>/var/lib/elchi/envoys/</code>. Several versions can be present at once, and the client's <strong>Apps</strong> tab in the UI shows which ones are downloaded and which can be installed. See <a class="" href="https://www.elchi.io/docs/operations/clients">Clients</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-systemd-unit-per-listener">One systemd unit per listener<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#one-systemd-unit-per-listener" class="hash-link" aria-label="Direct link to One systemd unit per listener" title="Direct link to One systemd unit per listener" translate="no">​</a></h3>
<p>When you deploy a listener from the UI, the control plane generates its bootstrap. That bootstrap sets the node ID (<code>listener::project::ip</code>), fetches LDS and CDS over ADS with delta xDS, and sends the Envoy version as gRPC metadata so the platform routes the stream to the control plane built for that version.</p>
<p>The agent writes the bootstrap to <code>/var/lib/elchi/bootstraps/</code> and runs the listener as its own systemd unit, <code>&lt;listener&gt;-&lt;adminPort&gt;.service</code>. One Envoy process per listener means a bad change, a crash or an upgrade affects one listener, not every listener on the host. Each unit:</p>
<ul>
<li class="">validates the bootstrap with the same binary (<code>--mode validate</code>) before starting;</li>
<li class="">uses the admin port as <code>--base-id</code>, so the processes on one host never share hot-restart memory;</li>
<li class="">runs Envoy through a hot-restart wrapper, with <code>ExecReload</code> sending SIGHUP;</li>
<li class="">restarts 3 seconds after Envoy exits, and stops trying after 10 starts in 5 minutes;</li>
<li class="">adds the listener's address to the host only once Envoy reports it is live and ready, and removes it when the unit stops, so a crashed or held listener stops attracting traffic;</li>
<li class="">caps memory with <code>MemoryHigh</code> and the Envoy overload manager that the agent adds to the bootstrap, rather than a hard kill.</li>
</ul>
<p>Deployed listeners appear in the UI under <a class="" href="https://www.elchi.io/docs/operations/services">Services</a>, with their status per client.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="zero-downtime-version-upgrades">Zero-downtime version upgrades<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#zero-downtime-version-upgrades" class="hash-link" aria-label="Direct link to Zero-downtime version upgrades" title="Direct link to Zero-downtime version upgrades" translate="no">​</a></h3>
<p>Upgrading a listener to a newer Envoy version is a job in the UI or API. The control plane copies the listener's dependencies into the target version and moves the listener and its bootstrap; the agent then switches the running process.</p>
<p>The agent does not point the unit at a versioned binary path. The unit runs a per-listener symlink in <code>/var/lib/elchi/envoys/current/</code>, and the wrapper re-runs that same path on SIGHUP. To upgrade, the agent compares the <code>--hot-restart-version</code> of the running binary with that of the target binary. When they match, it repoints the symlink and reloads the unit: Envoy's hot restart starts the next epoch, which takes over the listening sockets while the old process drains. When they do not match, the agent falls back to a full restart and says so in the job result.</p>
<p>In our end-to-end tests, the listener upgrade runs against a control plane serving at least two Envoy versions, with a client on a separate VM. The recorded run moved a listener from Envoy 1.38.3 to 1.39.0 under a steady load of 20 requests per second: 1,499 requests were sent and 0 were dropped. The service kept the same systemd main PID, the restart epoch went up by one, and the agent reported "hot restarted (epoch 0 -&gt; 1), zero connection loss". A later run of the same test group, upgrading from 1.39.1 to 1.39.3, again lost no request. The test judges a dropped request with Envoy's own request counters, read before and after the load window, not only with the load generator's view.</p>
<p>The conditions matter. The result applies when the two binaries are hot-restart compatible, the target version is already on the agent, and every client of the service is connected; otherwise the job fails or the agent falls back to a full restart, which drops the open connections. The step-by-step guide is <a class="" href="https://www.elchi.io/docs/guides/run-multiple-envoy-versions">Run several Envoy versions side by side</a>, and the details are in <a class="" href="https://www.elchi.io/docs/envoy-configuration/versions-and-upgrades">Versions &amp; Upgrades</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="logs-and-metrics-1">Logs and metrics<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#logs-and-metrics-1" class="hash-link" aria-label="Direct link to Logs and metrics" title="Direct link to Logs and metrics" translate="no">​</a></h3>
<p>Envoy statistics go through a stats sink to an OpenTelemetry Collector and on to VictoriaMetrics, with dashboards in the UI and Grafana. Access logs can stream over ALS to <code>elchi-collector</code> for API discovery. Service logs are searchable in the UI's log viewer, and the agent can forward them to Syslog or Elastic/Logstash. See <a class="" href="https://www.elchi.io/docs/observability/metrics-and-logs">Metrics &amp; Logs</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="high-availability-without-kubernetes">High availability without Kubernetes<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#high-availability-without-kubernetes" class="hash-link" aria-label="Direct link to High availability without Kubernetes" title="Direct link to High availability without Kubernetes" translate="no">​</a></h2>
<p>A single VM is a single point of failure. Elchi offers two ways to remove it.</p>
<p>With BGP, several edge hosts can announce the same listener address, and each unit's address handling means a host whose listener is down stops announcing it.</p>
<p>Without BGP, use an <strong>HA pair</strong>: two <a class="" href="https://www.elchi.io/docs/elchi-os/overview">Elchi OS</a> appliances that serve the same services on the same virtual IPs. Both members receive the same xDS configuration and run the same Envoy listeners; only the active member holds the VIPs. The boxes elect the active member themselves with a heartbeat on UDP 8650 and probes of the peer's data plane, so a control-plane outage never moves a VIP. On failover, the standby adds the VIP and sends gratuitous ARP. An Envoy hot restart or an xDS update does not cause a failover. HA pairs need Elchi OS on both members; they are not available for a client installed on a general-purpose Linux host. See <a class="" href="https://www.elchi.io/docs/ha-pairs/overview">HA Pairs Overview</a> for the failover rules and the measured takeover times.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next steps<a href="https://www.elchi.io/docs/blog/envoy-on-vms-without-kubernetes#next-steps" class="hash-link" aria-label="Direct link to Next steps" title="Direct link to Next steps" translate="no">​</a></h2>
<ul>
<li class="">Install the agent: <a class="" href="https://www.elchi.io/docs/installation/client/installation">Elchi Client installation</a>.</li>
<li class="">Deploy and upgrade listeners: <a class="" href="https://www.elchi.io/docs/guides/run-multiple-envoy-versions">Run several Envoy versions side by side</a> and <a class="" href="https://www.elchi.io/docs/guides/manage-envoy-without-yaml">Manage Envoy without YAML</a>.</li>
<li class="">Remove the single box: <a class="" href="https://www.elchi.io/docs/ha-pairs/overview">HA Pairs Overview</a> and <a class="" href="https://www.elchi.io/docs/elchi-os/overview">Elchi OS</a>.</li>
<li class="">Learn more about <a href="https://www.elchi.io/products/core.html" target="_blank" rel="noopener noreferrer" class="">Elchi Core</a>, or <a href="https://www.elchi.io/contact.html" target="_blank" rel="noopener noreferrer" class="">book a demo</a>.</li>
</ul>]]></content:encoded>
            <category>Envoy</category>
            <category>Virtual machines</category>
        </item>
    </channel>
</rss>