authentik.providers.proxy.controllers.k8s.ingress

Kubernetes Ingress Reconciler

  1"""Kubernetes Ingress Reconciler"""
  2
  3from typing import TYPE_CHECKING
  4from urllib.parse import urlparse
  5
  6from kubernetes.client import (
  7    NetworkingV1Api,
  8    V1HTTPIngressPath,
  9    V1HTTPIngressRuleValue,
 10    V1Ingress,
 11    V1IngressSpec,
 12    V1IngressTLS,
 13    V1ServiceBackendPort,
 14)
 15from kubernetes.client.models.v1_ingress_backend import V1IngressBackend
 16from kubernetes.client.models.v1_ingress_rule import V1IngressRule
 17from kubernetes.client.models.v1_ingress_service_backend import V1IngressServiceBackend
 18
 19from authentik.outposts.controllers.base import FIELD_MANAGER
 20from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 21from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
 22from authentik.providers.proxy.models import ProxyMode, ProxyProvider
 23
 24if TYPE_CHECKING:
 25    from authentik.outposts.controllers.kubernetes import KubernetesController
 26
 27
 28class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
 29    """Kubernetes Ingress Reconciler"""
 30
 31    def __init__(self, controller: KubernetesController) -> None:
 32        super().__init__(controller)
 33        self.api = NetworkingV1Api(controller.client)
 34
 35    @staticmethod
 36    def reconciler_name() -> str:
 37        return "ingress"
 38
 39    def _check_annotations(self, current: V1Ingress, reference: V1Ingress):
 40        """Check that all annotations *we* set are correct"""
 41        for key, value in reference.metadata.annotations.items():
 42            if key not in current.metadata.annotations:
 43                raise NeedsUpdate()
 44            if current.metadata.annotations[key] != value:
 45                raise NeedsUpdate()
 46
 47    def reconcile(self, current: V1Ingress, reference: V1Ingress):
 48        super().reconcile(current, reference)
 49        self._check_annotations(current, reference)
 50        if current.spec.ingress_class_name != reference.spec.ingress_class_name:
 51            raise NeedsUpdate()
 52        # Create a list of all expected host and tls hosts
 53        expected_hosts = []
 54        expected_hosts_tls = []
 55        for proxy_provider in ProxyProvider.objects.filter(
 56            outpost__in=[self.controller.outpost],
 57        ):
 58            proxy_provider: ProxyProvider
 59            external_host_name = urlparse(proxy_provider.external_host)
 60            expected_hosts.append(external_host_name.hostname)
 61            if (
 62                external_host_name.scheme == "https"
 63                and self.controller.outpost.config.kubernetes_ingress_secret_name
 64            ):
 65                expected_hosts_tls.append(external_host_name.hostname)
 66        expected_hosts.sort()
 67        expected_hosts_tls.sort()
 68
 69        have_hosts = [rule.host for rule in current.spec.rules]
 70        have_hosts.sort()
 71
 72        have_hosts_tls = []
 73        if current.spec.tls:
 74            for tls_config in current.spec.tls:
 75                if not tls_config:
 76                    continue
 77                if tls_config.hosts:
 78                    have_hosts_tls += tls_config.hosts
 79                if (
 80                    tls_config.secret_name
 81                    != self.controller.outpost.config.kubernetes_ingress_secret_name
 82                ):
 83                    raise NeedsUpdate()
 84        have_hosts_tls.sort()
 85
 86        if have_hosts != expected_hosts:
 87            raise NeedsUpdate()
 88        if have_hosts_tls != expected_hosts_tls:
 89            raise NeedsUpdate()
 90        # If we have a current ingress, which wouldn't have any hosts, raise
 91        # NeedsRecreate() so that its deleted, and check hosts on create
 92        if len(have_hosts) < 1:
 93            raise NeedsRecreate()
 94
 95    def get_ingress_annotations(self) -> dict[str, str]:
 96        """Get ingress annotations"""
 97        annotations = {
 98            # Ensure that with multiple proxy replicas deployed, the same CSRF request
 99            # goes to the same pod
100            "nginx.ingress.kubernetes.io/affinity": "cookie",
101            "traefik.ingress.kubernetes.io/affinity": "true",
102            # Buffer sizes for large headers with JWTs
103            "nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
104            "nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
105            "nginx.ingress.kubernetes.io/proxy-busy-buffers-size": "32k",
106            # Enable TLS in traefik
107            "traefik.ingress.kubernetes.io/router.tls": "true",
108        }
109        annotations.update(self.controller.outpost.config.kubernetes_ingress_annotations)
110        return annotations
111
112    def get_reference_object(self) -> V1Ingress:
113        """Get deployment object for outpost"""
114        meta = self.get_object_meta(
115            name=self.name,
116            annotations=self.get_ingress_annotations(),
117        )
118        rules = []
119        tls_hosts = []
120        for proxy_provider in ProxyProvider.objects.filter(
121            outpost__in=[self.controller.outpost],
122        ):
123            proxy_provider: ProxyProvider
124            external_host_name = urlparse(proxy_provider.external_host)
125            if (
126                external_host_name.scheme == "https"
127                and self.controller.outpost.config.kubernetes_ingress_secret_name
128            ):
129                tls_hosts.append(external_host_name.hostname)
130            path_type = "Prefix"
131            if self.controller.outpost.config.kubernetes_ingress_path_type:
132                path_type = self.controller.outpost.config.kubernetes_ingress_path_type
133            if proxy_provider.mode in [
134                ProxyMode.FORWARD_SINGLE,
135                ProxyMode.FORWARD_DOMAIN,
136            ]:
137                rule = V1IngressRule(
138                    host=external_host_name.hostname,
139                    http=V1HTTPIngressRuleValue(
140                        paths=[
141                            V1HTTPIngressPath(
142                                backend=V1IngressBackend(
143                                    service=V1IngressServiceBackend(
144                                        name=self.name,
145                                        port=V1ServiceBackendPort(name="http"),
146                                    ),
147                                ),
148                                path="/outpost.goauthentik.io",
149                                path_type=path_type,
150                            )
151                        ]
152                    ),
153                )
154            else:
155                rule = V1IngressRule(
156                    host=external_host_name.hostname,
157                    http=V1HTTPIngressRuleValue(
158                        paths=[
159                            V1HTTPIngressPath(
160                                backend=V1IngressBackend(
161                                    service=V1IngressServiceBackend(
162                                        name=self.name,
163                                        port=V1ServiceBackendPort(name="http"),
164                                    ),
165                                ),
166                                path="/",
167                                path_type=path_type,
168                            )
169                        ]
170                    ),
171                )
172            rules.append(rule)
173        tls_config = None
174        if tls_hosts:
175            tls_config = [
176                V1IngressTLS(
177                    hosts=tls_hosts,
178                    secret_name=self.controller.outpost.config.kubernetes_ingress_secret_name,
179                )
180            ]
181        spec = V1IngressSpec(
182            rules=rules,
183            tls=tls_config,
184        )
185        if self.controller.outpost.config.kubernetes_ingress_class_name:
186            spec.ingress_class_name = self.controller.outpost.config.kubernetes_ingress_class_name
187        return V1Ingress(
188            metadata=meta,
189            spec=spec,
190        )
191
192    def create(self, reference: V1Ingress):
193        if len(reference.spec.rules) < 1:
194            self.logger.debug("No hosts defined, not creating ingress.")
195            return None
196        return self.api.create_namespaced_ingress(
197            self.namespace, reference, field_manager=FIELD_MANAGER
198        )
199
200    def delete(self, reference: V1Ingress):
201        return self.api.delete_namespaced_ingress(reference.metadata.name, self.namespace)
202
203    def retrieve(self) -> V1Ingress:
204        return self.api.read_namespaced_ingress(self.name, self.namespace)
205
206    def update(self, current: V1Ingress, reference: V1Ingress):
207        return self.api.patch_namespaced_ingress(
208            current.metadata.name,
209            self.namespace,
210            reference,
211            field_manager=FIELD_MANAGER,
212        )
 29class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
 30    """Kubernetes Ingress Reconciler"""
 31
 32    def __init__(self, controller: KubernetesController) -> None:
 33        super().__init__(controller)
 34        self.api = NetworkingV1Api(controller.client)
 35
 36    @staticmethod
 37    def reconciler_name() -> str:
 38        return "ingress"
 39
 40    def _check_annotations(self, current: V1Ingress, reference: V1Ingress):
 41        """Check that all annotations *we* set are correct"""
 42        for key, value in reference.metadata.annotations.items():
 43            if key not in current.metadata.annotations:
 44                raise NeedsUpdate()
 45            if current.metadata.annotations[key] != value:
 46                raise NeedsUpdate()
 47
 48    def reconcile(self, current: V1Ingress, reference: V1Ingress):
 49        super().reconcile(current, reference)
 50        self._check_annotations(current, reference)
 51        if current.spec.ingress_class_name != reference.spec.ingress_class_name:
 52            raise NeedsUpdate()
 53        # Create a list of all expected host and tls hosts
 54        expected_hosts = []
 55        expected_hosts_tls = []
 56        for proxy_provider in ProxyProvider.objects.filter(
 57            outpost__in=[self.controller.outpost],
 58        ):
 59            proxy_provider: ProxyProvider
 60            external_host_name = urlparse(proxy_provider.external_host)
 61            expected_hosts.append(external_host_name.hostname)
 62            if (
 63                external_host_name.scheme == "https"
 64                and self.controller.outpost.config.kubernetes_ingress_secret_name
 65            ):
 66                expected_hosts_tls.append(external_host_name.hostname)
 67        expected_hosts.sort()
 68        expected_hosts_tls.sort()
 69
 70        have_hosts = [rule.host for rule in current.spec.rules]
 71        have_hosts.sort()
 72
 73        have_hosts_tls = []
 74        if current.spec.tls:
 75            for tls_config in current.spec.tls:
 76                if not tls_config:
 77                    continue
 78                if tls_config.hosts:
 79                    have_hosts_tls += tls_config.hosts
 80                if (
 81                    tls_config.secret_name
 82                    != self.controller.outpost.config.kubernetes_ingress_secret_name
 83                ):
 84                    raise NeedsUpdate()
 85        have_hosts_tls.sort()
 86
 87        if have_hosts != expected_hosts:
 88            raise NeedsUpdate()
 89        if have_hosts_tls != expected_hosts_tls:
 90            raise NeedsUpdate()
 91        # If we have a current ingress, which wouldn't have any hosts, raise
 92        # NeedsRecreate() so that its deleted, and check hosts on create
 93        if len(have_hosts) < 1:
 94            raise NeedsRecreate()
 95
 96    def get_ingress_annotations(self) -> dict[str, str]:
 97        """Get ingress annotations"""
 98        annotations = {
 99            # Ensure that with multiple proxy replicas deployed, the same CSRF request
100            # goes to the same pod
101            "nginx.ingress.kubernetes.io/affinity": "cookie",
102            "traefik.ingress.kubernetes.io/affinity": "true",
103            # Buffer sizes for large headers with JWTs
104            "nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
105            "nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
106            "nginx.ingress.kubernetes.io/proxy-busy-buffers-size": "32k",
107            # Enable TLS in traefik
108            "traefik.ingress.kubernetes.io/router.tls": "true",
109        }
110        annotations.update(self.controller.outpost.config.kubernetes_ingress_annotations)
111        return annotations
112
113    def get_reference_object(self) -> V1Ingress:
114        """Get deployment object for outpost"""
115        meta = self.get_object_meta(
116            name=self.name,
117            annotations=self.get_ingress_annotations(),
118        )
119        rules = []
120        tls_hosts = []
121        for proxy_provider in ProxyProvider.objects.filter(
122            outpost__in=[self.controller.outpost],
123        ):
124            proxy_provider: ProxyProvider
125            external_host_name = urlparse(proxy_provider.external_host)
126            if (
127                external_host_name.scheme == "https"
128                and self.controller.outpost.config.kubernetes_ingress_secret_name
129            ):
130                tls_hosts.append(external_host_name.hostname)
131            path_type = "Prefix"
132            if self.controller.outpost.config.kubernetes_ingress_path_type:
133                path_type = self.controller.outpost.config.kubernetes_ingress_path_type
134            if proxy_provider.mode in [
135                ProxyMode.FORWARD_SINGLE,
136                ProxyMode.FORWARD_DOMAIN,
137            ]:
138                rule = V1IngressRule(
139                    host=external_host_name.hostname,
140                    http=V1HTTPIngressRuleValue(
141                        paths=[
142                            V1HTTPIngressPath(
143                                backend=V1IngressBackend(
144                                    service=V1IngressServiceBackend(
145                                        name=self.name,
146                                        port=V1ServiceBackendPort(name="http"),
147                                    ),
148                                ),
149                                path="/outpost.goauthentik.io",
150                                path_type=path_type,
151                            )
152                        ]
153                    ),
154                )
155            else:
156                rule = V1IngressRule(
157                    host=external_host_name.hostname,
158                    http=V1HTTPIngressRuleValue(
159                        paths=[
160                            V1HTTPIngressPath(
161                                backend=V1IngressBackend(
162                                    service=V1IngressServiceBackend(
163                                        name=self.name,
164                                        port=V1ServiceBackendPort(name="http"),
165                                    ),
166                                ),
167                                path="/",
168                                path_type=path_type,
169                            )
170                        ]
171                    ),
172                )
173            rules.append(rule)
174        tls_config = None
175        if tls_hosts:
176            tls_config = [
177                V1IngressTLS(
178                    hosts=tls_hosts,
179                    secret_name=self.controller.outpost.config.kubernetes_ingress_secret_name,
180                )
181            ]
182        spec = V1IngressSpec(
183            rules=rules,
184            tls=tls_config,
185        )
186        if self.controller.outpost.config.kubernetes_ingress_class_name:
187            spec.ingress_class_name = self.controller.outpost.config.kubernetes_ingress_class_name
188        return V1Ingress(
189            metadata=meta,
190            spec=spec,
191        )
192
193    def create(self, reference: V1Ingress):
194        if len(reference.spec.rules) < 1:
195            self.logger.debug("No hosts defined, not creating ingress.")
196            return None
197        return self.api.create_namespaced_ingress(
198            self.namespace, reference, field_manager=FIELD_MANAGER
199        )
200
201    def delete(self, reference: V1Ingress):
202        return self.api.delete_namespaced_ingress(reference.metadata.name, self.namespace)
203
204    def retrieve(self) -> V1Ingress:
205        return self.api.read_namespaced_ingress(self.name, self.namespace)
206
207    def update(self, current: V1Ingress, reference: V1Ingress):
208        return self.api.patch_namespaced_ingress(
209            current.metadata.name,
210            self.namespace,
211            reference,
212            field_manager=FIELD_MANAGER,
213        )

Kubernetes Ingress Reconciler

api
@staticmethod
def reconciler_name() -> str:
36    @staticmethod
37    def reconciler_name() -> str:
38        return "ingress"

A name this reconciler is identified by in the configuration

def reconcile( self, current: kubernetes.client.models.v1_ingress.V1Ingress, reference: kubernetes.client.models.v1_ingress.V1Ingress):
48    def reconcile(self, current: V1Ingress, reference: V1Ingress):
49        super().reconcile(current, reference)
50        self._check_annotations(current, reference)
51        if current.spec.ingress_class_name != reference.spec.ingress_class_name:
52            raise NeedsUpdate()
53        # Create a list of all expected host and tls hosts
54        expected_hosts = []
55        expected_hosts_tls = []
56        for proxy_provider in ProxyProvider.objects.filter(
57            outpost__in=[self.controller.outpost],
58        ):
59            proxy_provider: ProxyProvider
60            external_host_name = urlparse(proxy_provider.external_host)
61            expected_hosts.append(external_host_name.hostname)
62            if (
63                external_host_name.scheme == "https"
64                and self.controller.outpost.config.kubernetes_ingress_secret_name
65            ):
66                expected_hosts_tls.append(external_host_name.hostname)
67        expected_hosts.sort()
68        expected_hosts_tls.sort()
69
70        have_hosts = [rule.host for rule in current.spec.rules]
71        have_hosts.sort()
72
73        have_hosts_tls = []
74        if current.spec.tls:
75            for tls_config in current.spec.tls:
76                if not tls_config:
77                    continue
78                if tls_config.hosts:
79                    have_hosts_tls += tls_config.hosts
80                if (
81                    tls_config.secret_name
82                    != self.controller.outpost.config.kubernetes_ingress_secret_name
83                ):
84                    raise NeedsUpdate()
85        have_hosts_tls.sort()
86
87        if have_hosts != expected_hosts:
88            raise NeedsUpdate()
89        if have_hosts_tls != expected_hosts_tls:
90            raise NeedsUpdate()
91        # If we have a current ingress, which wouldn't have any hosts, raise
92        # NeedsRecreate() so that its deleted, and check hosts on create
93        if len(have_hosts) < 1:
94            raise NeedsRecreate()

Check what operations should be done, should be raised as ReconcileTrigger

def get_ingress_annotations(self) -> dict[str, str]:
 96    def get_ingress_annotations(self) -> dict[str, str]:
 97        """Get ingress annotations"""
 98        annotations = {
 99            # Ensure that with multiple proxy replicas deployed, the same CSRF request
100            # goes to the same pod
101            "nginx.ingress.kubernetes.io/affinity": "cookie",
102            "traefik.ingress.kubernetes.io/affinity": "true",
103            # Buffer sizes for large headers with JWTs
104            "nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
105            "nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
106            "nginx.ingress.kubernetes.io/proxy-busy-buffers-size": "32k",
107            # Enable TLS in traefik
108            "traefik.ingress.kubernetes.io/router.tls": "true",
109        }
110        annotations.update(self.controller.outpost.config.kubernetes_ingress_annotations)
111        return annotations

Get ingress annotations

def get_reference_object(self) -> kubernetes.client.models.v1_ingress.V1Ingress:
113    def get_reference_object(self) -> V1Ingress:
114        """Get deployment object for outpost"""
115        meta = self.get_object_meta(
116            name=self.name,
117            annotations=self.get_ingress_annotations(),
118        )
119        rules = []
120        tls_hosts = []
121        for proxy_provider in ProxyProvider.objects.filter(
122            outpost__in=[self.controller.outpost],
123        ):
124            proxy_provider: ProxyProvider
125            external_host_name = urlparse(proxy_provider.external_host)
126            if (
127                external_host_name.scheme == "https"
128                and self.controller.outpost.config.kubernetes_ingress_secret_name
129            ):
130                tls_hosts.append(external_host_name.hostname)
131            path_type = "Prefix"
132            if self.controller.outpost.config.kubernetes_ingress_path_type:
133                path_type = self.controller.outpost.config.kubernetes_ingress_path_type
134            if proxy_provider.mode in [
135                ProxyMode.FORWARD_SINGLE,
136                ProxyMode.FORWARD_DOMAIN,
137            ]:
138                rule = V1IngressRule(
139                    host=external_host_name.hostname,
140                    http=V1HTTPIngressRuleValue(
141                        paths=[
142                            V1HTTPIngressPath(
143                                backend=V1IngressBackend(
144                                    service=V1IngressServiceBackend(
145                                        name=self.name,
146                                        port=V1ServiceBackendPort(name="http"),
147                                    ),
148                                ),
149                                path="/outpost.goauthentik.io",
150                                path_type=path_type,
151                            )
152                        ]
153                    ),
154                )
155            else:
156                rule = V1IngressRule(
157                    host=external_host_name.hostname,
158                    http=V1HTTPIngressRuleValue(
159                        paths=[
160                            V1HTTPIngressPath(
161                                backend=V1IngressBackend(
162                                    service=V1IngressServiceBackend(
163                                        name=self.name,
164                                        port=V1ServiceBackendPort(name="http"),
165                                    ),
166                                ),
167                                path="/",
168                                path_type=path_type,
169                            )
170                        ]
171                    ),
172                )
173            rules.append(rule)
174        tls_config = None
175        if tls_hosts:
176            tls_config = [
177                V1IngressTLS(
178                    hosts=tls_hosts,
179                    secret_name=self.controller.outpost.config.kubernetes_ingress_secret_name,
180                )
181            ]
182        spec = V1IngressSpec(
183            rules=rules,
184            tls=tls_config,
185        )
186        if self.controller.outpost.config.kubernetes_ingress_class_name:
187            spec.ingress_class_name = self.controller.outpost.config.kubernetes_ingress_class_name
188        return V1Ingress(
189            metadata=meta,
190            spec=spec,
191        )

Get deployment object for outpost

def create(self, reference: kubernetes.client.models.v1_ingress.V1Ingress):
193    def create(self, reference: V1Ingress):
194        if len(reference.spec.rules) < 1:
195            self.logger.debug("No hosts defined, not creating ingress.")
196            return None
197        return self.api.create_namespaced_ingress(
198            self.namespace, reference, field_manager=FIELD_MANAGER
199        )

API Wrapper to create object

def delete(self, reference: kubernetes.client.models.v1_ingress.V1Ingress):
201    def delete(self, reference: V1Ingress):
202        return self.api.delete_namespaced_ingress(reference.metadata.name, self.namespace)

API Wrapper to delete object

def retrieve(self) -> kubernetes.client.models.v1_ingress.V1Ingress:
204    def retrieve(self) -> V1Ingress:
205        return self.api.read_namespaced_ingress(self.name, self.namespace)

API Wrapper to retrieve object

def update( self, current: kubernetes.client.models.v1_ingress.V1Ingress, reference: kubernetes.client.models.v1_ingress.V1Ingress):
207    def update(self, current: V1Ingress, reference: V1Ingress):
208        return self.api.patch_namespaced_ingress(
209            current.metadata.name,
210            self.namespace,
211            reference,
212            field_manager=FIELD_MANAGER,
213        )

API Wrapper to update object