<p>I use <a href="https://github.com/kubernetes/ingress-nginx" rel="nofollow noreferrer">Ingress-nginx</a> and I need to rewrite the <code>Host</code> header sent to the proxied Kubernetes Service based on the <code>path</code> of the original request. This must happen based on some variable or regular expression. I cannot hard code paths because this <strong>needs to work for any path</strong> automatically.</p>
<p>Basically I need something like this:</p>
<pre><code>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  namespace: my-ns
annotations:
    # below what's commented out almost works but adds a leading &quot;/&quot; to header
    # nginx.ingress.kubernetes.io/upstream-vhost: $request_uri.example.com
    nginx.ingress.kubernetes.io/upstream-vhost: $PATH.example.com
spec:
  ingressClassName: nginx
  rules:
    - host: 'example.com'
      http:
        paths:
        - backend:
            service:
              name: my-svc
              port:
                number: 80
          path: /$PATH
          pathType: Prefix
</code></pre>
<p>I wonder if this is somehow possible to implement with annotation <code>nginx.ingress.kubernetes.io/configuration-snippet</code> or <code>nginx.ingress.kubernetes.io/server-snippet</code> but I don't understand these annotations well enough. I've tried out these annotations by passing in a <code>location</code> and also attempted to use <code>map</code> module with no luck so far.</p>
<p>Context: This reverse proxy is set up in front of Knative and Knative's routing works through <code>Host</code> header.</p>

nichoio
<blockquote>
<p>without any <code>snippet</code> annotation</p>
</blockquote>
<p>Then... considering <a href="https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/lua/plugins/README.md" rel="nofollow noreferrer">NGINX Ingress annotation snippets contain LUA code execution</a> (which can be <a href="https://docs.bridgecrew.io/docs/prevent-nginx-ingress-annotation-snippets-which-contain-lua-code-execution" rel="nofollow noreferrer">a security issue</a>, by the way), you might consider using <a href="https://www.lua.org/" rel="nofollow noreferrer">Lua</a> through a <a href="https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/lua/plugins/README.md" rel="nofollow noreferrer">custom Lua plugin</a>.</p>
<p>A simple Lua script would be enough to capture the first segment of the request path and sets it as the <code>Host</code> header suffixed with <code>.example.com</code>.</p>
<p>Create a <code>dynamic-host-header/main.lua</code>: (inspired from <a href="https://github.com/ElvinEfendi/ingress-nginx-openidc/blob/master/rootfs/etc/nginx/lua/plugins/openidc/main.lua" rel="nofollow noreferrer">the official example</a>)</p>
<pre class="lang-lua prettyprint-override"><code>local _M = {}

function _M.rewrite()
    -- Check if this is the default backend, if so, skip processing.
    if ngx.var.proxy_upstream_name == &quot;upstream-default-backend&quot; then
        return
    end

    local path = ngx.var.uri
    local captured_path = string.match(path, &quot;^/([^/]+)&quot;)
    if captured_path then
        ngx.req.set_header(&quot;Host&quot;, captured_path .. &quot;.example.com&quot;)
    end
end

return _M
</code></pre>
<p>Before building a custom <code>ingress-nginx</code> image, you can start with testing that plugin into the <code>ingress-nginx</code> pod:</p>
<p>Create a ConfigMap with the content of your plugin:</p>
<pre class="lang-bash prettyprint-override"><code>kubectl create configmap dynamic-host-header-plugin --from-file=dynamic-host-header/main.lua -n &lt;NAMESPACE_WHERE_INGRESS_IS_RUNNING&gt;
</code></pre>
<p>And modify the <code>ingress-nginx</code> deployment to mount this ConfigMap to <code>/etc/nginx/lua/plugins/dynamic-host-header</code> (as mentioned in <a href="https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/lua/plugins/README.md#installing-a-plugin" rel="nofollow noreferrer">the &quot;installing plugin&quot; section</a>):</p>
<pre class="lang-bash prettyprint-override"><code>kubectl edit deployment -n &lt;NAMESPACE_WHERE_INGRESS_IS_RUNNING&gt; nginx-ingress-controller
</code></pre>
<p>Add the volume and <code>volumeMounts</code> as follows:</p>
<pre class="lang-yaml prettyprint-override"><code>...
spec:
  containers:
  - name: nginx-ingress-controller
    ...
    volumeMounts:
    - mountPath: /etc/nginx/lua/plugins/dynamic-host-header
      name: dynamic-host-header
    ...
  volumes:
  - name: dynamic-host-header
    configMap:
      name: dynamic-host-header-plugin
</code></pre>
<p>Now, you need to enable the plugin by updating the <code>nginx-ingress-controller</code>'s ConfigMap:</p>
<p>Edit the ConfigMap:</p>
<pre class="lang-bash prettyprint-override"><code>kubectl edit configmap nginx-configuration -n &lt;NAMESPACE_WHERE_INGRESS_IS_RUNNING&gt;
</code></pre>
<p>Add (or update) the <code>plugins</code> configuration setting:</p>
<pre class="lang-yaml prettyprint-override"><code>data:
  plugins: &quot;dynamic-host-header&quot;
</code></pre>
<p>Finally, restart the ingress-nginx pods to pick up the changes:</p>
<pre class="lang-bash prettyprint-override"><code>kubectl rollout restart deployment/nginx-ingress-controller -n &lt;NAMESPACE_WHERE_INGRESS_IS_RUNNING&gt;
</code></pre>
<p>With this Lua plugin enabled, the <code>Host</code> header should be dynamically set based on the request path, as required.</p>
<hr />
<p>Note: the <a href="https://stackoverflow.com/users/6312338/nichoio">OP nichoio</a> adds in <a href="https://stackoverflow.com/questions/76835282/ingress-nginx-set-host-header-dynamically-depending-on-path/76845902?noredirect=1#comment135618666_76845902">the comments</a>:</p>
<blockquote>
<p>Turns out it I can't get it to work yet.<br />
Using <code>ngx.req.set_header()</code>, I can add new headers and alter most existing headers (e.g. <code>user-agent</code>). Host however won't change.<br />
I'm debugging this with an Ingress pointing to a svc/pod running <code>mendhak/http-https-echo</code>.</p>
</blockquote>
<p>The behavior you have observed is because the <code>Host</code> header in Nginx is a bit special. When you change it with <a href="https://github.com/openresty/lua-nginx-module#ngxreqset_header" rel="nofollow noreferrer"><code>ngx.req.set_header()</code></a>, it does not necessarily reflect in the request sent to the upstream server. The value that gets sent as the <code>Host</code> header to the upstream is defined by the <a href="https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/#passing-request-headers" rel="nofollow noreferrer"><code>proxy_set_header Host</code> directive</a>.</p>
<p>However, there is a workaround using Lua.</p>
<p>Instead of setting the <code>Host</code> header directly in the Lua script, you can assign the new <code>Host</code> value to a variable.</p>
<pre class="lang-lua prettyprint-override"><code>local _M = {}

function _M.rewrite()
    -- Check if this is the default backend, if so, skip processing.
    if ngx.var.proxy_upstream_name == &quot;upstream-default-backend&quot; then
        return
    end

    local path = ngx.var.uri
    local captured_path = string.match(path, &quot;^/([^/]+)&quot;)
    if captured_path then
        ngx.var.dynamic_host_header = captured_path .. &quot;.example.com&quot;
    end
end

return _M
</code></pre>
<p>Here, instead of <code>ngx.req.set_header(&quot;Host&quot;, ...)</code>, a custom variable <code>ngx.var.dynamic_host_header</code> is assigned the desired value.</p>
<p>Then, you can use this variable in a custom <code>proxy_set_header</code> directive.</p>
<p>Use the <code>nginx.ingress.kubernetes.io/configuration-snippet</code> annotation: Add this to your Ingress resource. That lets you set the custom proxy header.</p>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  namespace: my-ns
  annotations:
    nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_set_header Host $dynamic_host_header;
</code></pre>
<p>The <code>proxy_set_header</code> directive in the annotation makes sure that the desired <code>Host</code> header is sent to the upstream server.</p>
<hr />
<p>The <a href="https://stackoverflow.com/users/6312338/nichoio">OP nichoio</a>, however, confirms <a href="https://stackoverflow.com/questions/76835282/ingress-nginx-set-host-header-dynamically-depending-on-path/76845902#comment135732838_76845902">in the comments</a> that:</p>
<blockquote>
<p>Unfortunately, <code>configuration-snippet</code> also didn't work.<br />
<a href="https://en.wiktionary.org/wiki/IIRC" rel="nofollow noreferrer">IIRC</a>, the <code>$dynamic_host_header</code> var is not available inside the snippet when following the alternative solution.</p>
<p>Workaround for now is to create one Ingress resource per path/header.</p>
</blockquote>
<p>Another approach you might consider is to use a service mesh like <a href="https://istio.io/" rel="nofollow noreferrer">Istio</a> to manage headers and routing rules more flexibly. Istio provides more fine-grained control over HTTP headers, including the Host header.<br />
While introducing a service mesh adds complexity, it might give you the control you need for this specific scenario.</p>
<p>In an Istio-based approach, you could leverage <a href="https://istio.io/latest/docs/reference/config/networking/virtual-service/" rel="nofollow noreferrer">Istio's <code>VirtualService</code></a> to specify the routing rules and header manipulation logic. Specifically, you can use the <a href="https://istio.io/latest/docs/reference/config/networking/virtual-service/#HTTPRoute" rel="nofollow noreferrer"><code>headers</code> field in the HTTP route</a> to rewrite the <code>Host</code> header based on the request path.</p>
<p>For instance:</p>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: my-virtualservice
spec:
  hosts:
  - &quot;*&quot;
  gateways:
  - my-gateway
  http:
  - match:
    - uri:
        prefix: &quot;/path1&quot;
    route:
    - destination:
        host: path1-service.default.svc.cluster.local
      headers:
        request:
          set:
            Host: &quot;path1.example.com&quot;
  - match:
    - uri:
        prefix: &quot;/path2&quot;
    route:
    - destination:
        host: path2-service.default.svc.cluster.local
      headers:
        request:
          set:
            Host: &quot;path2.example.com&quot;
  # additional path-based rules
</code></pre>
<ul>
<li><p>The <code>VirtualService</code> applies to all incoming requests (<code>hosts: [&quot;*&quot;]</code>) on a specific Istio gateway (<code>my-gateway</code>).</p>
</li>
<li><p>Two HTTP routes are defined; each matches a URI prefix (<code>/path1</code> and <code>/path2</code>).</p>
</li>
<li><p>For each HTTP route, the <code>destination</code> specifies which Kubernetes service to route to.</p>
</li>
<li><p>The <code>headers</code> field allows you to manipulate headers. In this case, the <code>Host</code> header is rewritten based on the matching path.</p>
</li>
</ul>
<p>This works with an Istio Gateway named <code>my-gateway</code> that is configured to handle the incoming traffic. You would also need to replace <code>path1-service.default.svc.cluster.local</code> and <code>path2-service.default.svc.cluster.local</code> with the actual service names you are using in your cluster.</p>
<p>That approach provides a way to conditionally rewrite the <code>Host</code> header based on the request path, and it could scale as you can define any number of such path-based rules in the <code>VirtualService</code> specification.</p>
