{"id":3024,"date":"2025-12-01T20:27:50","date_gmt":"2025-12-01T20:27:50","guid":{"rendered":"https:\/\/brakkee.org\/site\/?p=3024"},"modified":"2026-03-08T14:30:14","modified_gmt":"2026-03-08T14:30:14","slug":"migrating-from-nginx-to-ha-proxy-ingress-controller","status":"publish","type":"post","link":"https:\/\/brakkee.org\/site\/2025\/12\/01\/migrating-from-nginx-to-ha-proxy-ingress-controller\/","title":{"rendered":"Migrating from nginx to ha-proxy ingress controller"},"content":{"rendered":"<p class=\"whitespace-break-spaces\">The NGINX Ingress Controller maintained by the Kubernetes project is being retired. Since I currently use this controller, I need to <span class=\"followup-block followup-block-hidden cursor-pointer outline-none static inline group-hover\/message:[--hover-opacity:1]\" tabindex=\"0\" data-question=\"What steps should I take to migrate from the Kubernetes NGINX Ingress Controller to a supported alternative?\" aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-_r_6k_\" data-state=\"closed\">migrate before updates stop. <\/span><\/p>\n<p><!--more--><\/p>\n<p class=\"whitespace-break-spaces\">It\u2019s important to note that there are two NGINX Ingress Controllers:<\/p>\n<ul>\n<li>One by the Kubernetes project (now deprecated).<\/li>\n<li>One by the NGINX project.<\/li>\n<\/ul>\n<p class=\"whitespace-break-spaces\">While these controllers are very similar, they are <span class=\"followup-block followup-block-hidden cursor-pointer outline-none static inline group-hover\/message:[--hover-opacity:1]\" tabindex=\"0\" data-question=\"What are the key differences between the Kubernetes and NGINX project\u2019s Ingress Controllers?\" aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-_r_6l_\" data-state=\"closed\">not identical<\/span>. You may notice <span class=\"followup-block followup-block-hidden cursor-pointer outline-none static inline group-hover\/message:[--hover-opacity:1]\" tabindex=\"0\" data-question=\"How can I troubleshoot issues caused by documentation differences between the two NGINX Ingress Controllers?\" aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-_r_6m_\" data-state=\"closed\">discrepancies when following documentation<\/span>, which can be confusing.<\/p>\n<h1>Choosing an ingress controller<\/h1>\n<p class=\"whitespace-break-spaces\">In addition to the NGINX project\u2019s controller, there are <span class=\"followup-block followup-block-hidden cursor-pointer outline-none static inline group-hover\/message:[--hover-opacity:1]\" tabindex=\"0\" data-question=\"Which alternative ingress controllers are recommended for replacing the deprecated Kubernetes NGINX Ingress Controller?\" aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-_r_6n_\" data-state=\"closed\">several other ingress controllers<\/span> available as alternatives.<\/p>\n<ul>\n<li>It should be based on mature software that has been handling network traffic for years<\/li>\n<li>it should support Gateway API or have clear roadmap towards full Gateway API support<\/li>\n<li>It should be open source as much as possible without features behind a paywall<\/li>\n<li>Built-in observability through prometheus metrics<\/li>\n<li>I want an SSLLabs A+ rating like I had before<\/li>\n<\/ul>\n<p class=\"whitespace-break-spaces\" dir=\"auto\">I want to use only the <span class=\"followup-block followup-block-hidden cursor-pointer outline-none static inline group-hover\/message:[--hover-opacity:1]\" tabindex=\"0\" data-question=\"What are the key differences between core Ingress and Gateway API features?\" aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-_r_7o_\" data-state=\"closed\">core Ingress and Gateway API features<\/span>. Convenience features\u2014such as built-in Let\u2019s Encrypt integration or API gateway functionality\u2014are not required. For example, I already use cert-manager for Let\u2019s Encrypt integration. Avoiding vendor-specific features also helps prevent lock-in to a particular ingress controller.<\/p>\n<table>\n<thead>\n<tr>\n<th>Ingress Controller<\/th>\n<th>Open Source Limitations<\/th>\n<th>Maturity (Underlying Tech)<\/th>\n<th>Gateway API Support<\/th>\n<th>Prometheus Metrics<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>NGINX Ingress<\/strong><\/td>\n<td>Advanced features require NGINX Plus.<\/td>\n<td>NGINX: 2004<\/td>\n<td>Yes<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><strong>Traefik<\/strong><\/td>\n<td>Enterprise features (WAF, advanced observability) require Traefik Enterprise.<\/td>\n<td>Traefik: 2015<\/td>\n<td>Yes<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><strong>HAProxy<\/strong><\/td>\n<td>Advanced LB and security require HAProxy Enterprise.<\/td>\n<td>HAProxy: 2001<\/td>\n<td>Yes<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><strong>Kong<\/strong><\/td>\n<td>Some plugins and scalability require Kong Enterprise.<\/td>\n<td>Kong: 2015 (based on NGINX)<\/td>\n<td>Yes<\/td>\n<td>Yes<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p class=\"whitespace-break-spaces\" dir=\"auto\">As shown, <span class=\"followup-block followup-block-hidden cursor-pointer outline-none static inline group-hover\/message:[--hover-opacity:1]\" tabindex=\"0\" data-question=\"What specific features make NGINX and HAProxy backends more mature compared to other controllers?\" aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-_r_8o_\" data-state=\"closed\">NGINX and HAProxy have the most mature backends<\/span>. Otherwise, the controllers appear comparable.Ultimately, the exact choice doesn\u2019t matter much, since I only plan to use the controller for Ingress and Gateway API\u2014not for any additional features they provide. Having had some issues in the past with the nginx controller from the ingress project. I decided to move to HA proxy.<\/p>\n<h1>HA proxy installation<\/h1>\n<pre>helm repo add haproxytech https:\/\/haproxytech.github.io\/helm-charts\r\nhelm repo update\r\nhelm upgrade --install haproxy haproxytech\/kubernetes-ingress \\\r\n--version MYVERSION \\\r\n--create-namespace \\\r\n--namespace haproxy-external \\\r\n-f values.yaml\r\n<\/pre>\n<p>The most important part is the <code>values.yaml<\/code><\/p>\n<pre>controller:\r\n  config:\r\n    response-set-header: |                                \r\n      Strict-Transport-Security \"max-age=31536000\"     #A\r\n      Server ssserver                                  #B \r\n    backend-config-snippet: |\r\n      option forwarded                                 #C\r\n    ssl-redirect-port: \"443\"                           #D\r\n  containerPort:\r\n    http: 80                                           #C\r\n    https: 443                                         #C     \r\n  allowPrivilegedPorts: true                           #C \r\n\r\n\r\n  ingressClass: external\r\n  ingressClassResource:\r\n    default: true\r\n    name: external\r\n  kind: DaemonSet                                     # E\r\n  service:\r\n    annotations:\r\n      metallb.io\/ip-allocated-from-pool: default\r\n      metallb.io\/loadBalancerIPs: 192.168.102.183     # F\r\n    externalTrafficPolicy: Local                      # E\r\n    type: LoadBalancer\r\n  serviceMonitor:\r\n    enabled: true                                     # G\r\n<\/pre>\n<ul>\n<li><strong># A<\/strong>: By default HA proxy give an SSLLabs A rating. With this setting it becomes A+<\/li>\n<li><strong># B<\/strong>: Override server header to hide the type of backend service that is being used<\/li>\n<li><strong># C<\/strong>: Keycloak related configuration. This makes the internal service ports and internal ports be identical for HTTP and HTTPS which makes running keyclock in the backend easier. Since these ports are privileged ports the privileged ports must be allowed. Also, make sure that the standardized Forwarded header (RFC 7239) is used instead of the non-standard X-Forwarded* headers. This is so backend servers can find out the original requested host and port.<\/li>\n<li><strong># D<\/strong>: This is required to make the HTTP to HTTPS redirect use the correct port.<\/li>\n<li><strong># E<\/strong>: I want to see the original IP addresses on the controller. For this we need ExternalTrafficPolicy Local and use a DaemonSet to avoid dropping traffic. The load balancer assigns an IP to the service and terminates that IP on one of the kubernetes nodes. It then forwards traffic to a backend pod for that service running on that node when ExternalTrafficPolicy Local is used. If no such pod is running on the same node then traffic is dropped. The DaemonSet prevents this. When ExternalTrafficPolicy Cluster is used, traffic can be forwarded to <strong>any<\/strong> backend pod on the cluster, losing the source IP in the process.<\/li>\n<li><strong># F<\/strong>: This is an annotation for metallb to configure the load balancer IP<\/li>\n<li><strong># G<\/strong>: Enable prometheus metrics.<\/li>\n<\/ul>\n<h1>Portability<\/h1>\n<h2>ExternalName services<\/h2>\n<p>There was one portability issue that I encountered which is that when using an ExternalName service for an Ingress resource, the service ports must also be specified.<\/p>\n<h2>Forwarded and X-Forwarded headers<\/h2>\n<p>Also, Keycloak cost quite a lot of time. In the end it was because in my old setup I used both the Forwarded and X-Forwarded headers and I had two paths to keycloak: one for logins and another for the admin console.The admun\u00a0 path relied on the Forwarded header and the login path worked also with the X-Forwarded headers. I have two paths because I have deployed two ingress classes with HA proxy (two helm installs), each one for a different ingress class. The Admin interface is hosted on another ingress class than the login interface. HA proxy apparently does not allow setting both the Forwarded and X-Forwarded headers at the same time.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The NGINX Ingress Controller maintained by the Kubernetes project is being retired. Since I currently use this controller, I need to migrate before updates stop.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[10,4],"tags":[],"_links":{"self":[{"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/posts\/3024"}],"collection":[{"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/comments?post=3024"}],"version-history":[{"count":20,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/posts\/3024\/revisions"}],"predecessor-version":[{"id":3046,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/posts\/3024\/revisions\/3046"}],"wp:attachment":[{"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/media?parent=3024"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/categories?post=3024"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/tags?post=3024"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}