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 )
class
IngressReconciler(authentik.outposts.controllers.k8s.base.KubernetesObjectReconciler[kubernetes.client.models.v1_ingress.V1Ingress]):
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
@staticmethod
def
reconciler_name() -> str:
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