diff --git a/controller/controller.go b/controller/controller.go index 8ee49267..d44ea472 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -92,13 +92,13 @@ func (c *HAProxyController) Start() { } // Get K8s client - c.k8s, err = GetKubernetesClient() + c.k8s, err = GetKubernetesClient(c.OSArgs.DisableServiceExternalName) if c.OSArgs.External { kubeconfig := filepath.Join(utils.HomeDir(), ".kube", "config") if c.OSArgs.KubeConfig != "" { kubeconfig = c.OSArgs.KubeConfig } - c.k8s, err = GetRemoteKubernetesClient(kubeconfig) + c.k8s, err = GetRemoteKubernetesClient(kubeconfig, c.OSArgs.DisableServiceExternalName) } if err != nil { logger.Panic(err) diff --git a/controller/kubernetes.go b/controller/kubernetes.go index 539b7bb0..be75faf1 100644 --- a/controller/kubernetes.go +++ b/controller/kubernetes.go @@ -45,12 +45,13 @@ var ErrIgnored = errors.New("ignored resource") // K8s is structure with all data required to synchronize with k8s type K8s struct { - API *kubernetes.Clientset - Logger utils.Logger + API *kubernetes.Clientset + Logger utils.Logger + DisableServiceExternalName bool // CVE-2021-25740 } // GetKubernetesClient returns new client that communicates with k8s -func GetKubernetesClient() (*K8s, error) { +func GetKubernetesClient(disableServiceExternalName bool) (*K8s, error) { k8sLogger := utils.GetK8sAPILogger() if !TRACE_API { k8sLogger.SetLevel(utils.Info) @@ -65,13 +66,14 @@ func GetKubernetesClient() (*K8s, error) { logger.Panic(err) } return &K8s{ - API: clientset, - Logger: k8sLogger, + API: clientset, + Logger: k8sLogger, + DisableServiceExternalName: disableServiceExternalName, }, nil } // GetRemoteKubernetesClient returns new client that communicates with k8s -func GetRemoteKubernetesClient(kubeconfig string) (*K8s, error) { +func GetRemoteKubernetesClient(kubeconfig string, disableServiceExternalName bool) (*K8s, error) { k8sLogger := utils.GetK8sAPILogger() if !TRACE_API { k8sLogger.SetLevel(utils.Info) @@ -85,13 +87,13 @@ func GetRemoteKubernetesClient(kubeconfig string) (*K8s, error) { // create the clientset clientset, err := kubernetes.NewForConfig(config) - if err != nil { logger.Panic(err) } return &K8s{ - API: clientset, - Logger: k8sLogger, + API: clientset, + Logger: k8sLogger, + DisableServiceExternalName: disableServiceExternalName, }, nil } @@ -104,7 +106,7 @@ func (k *K8s) EventsNamespaces(channel chan SyncDataEvent, stop chan struct{}, i k.Logger.Errorf("%s: Invalid data from k8s api, %s", NAMESPACE, obj) return } - var status = ADDED + status := ADDED if data.ObjectMeta.GetDeletionTimestamp() != nil { // detect services that are in terminating state status = DELETED @@ -126,7 +128,7 @@ func (k *K8s) EventsNamespaces(channel chan SyncDataEvent, stop chan struct{}, i k.Logger.Errorf("%s: Invalid data from k8s api, %s", NAMESPACE, obj) return } - var status = DELETED + status := DELETED item := &store.Namespace{ Name: data.GetName(), Endpoints: make(map[string]*store.Endpoints), @@ -149,7 +151,7 @@ func (k *K8s) EventsNamespaces(channel chan SyncDataEvent, stop chan struct{}, i k.Logger.Errorf("%s: Invalid data from k8s api, %s", NAMESPACE, newObj) return } - var status = MODIFIED + status := MODIFIED item1 := &store.Namespace{ Name: data1.GetName(), Status: status, @@ -349,7 +351,11 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf k.Logger.Errorf("%s: Invalid data from k8s api, %s", SERVICE, obj) return } - var status = ADDED + if data.Spec.Type == corev1.ServiceTypeExternalName && k.DisableServiceExternalName { + k.Logger.Tracef("forwarding to ExternalName Services for %v is disabled", data) + return + } + status := ADDED if data.ObjectMeta.GetDeletionTimestamp() != nil { // detect services that are in terminating state status = DELETED @@ -360,9 +366,11 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf Annotations: store.ConvertToMapStringW(data.ObjectMeta.Annotations), Selector: store.ConvertToMapStringW(data.Spec.Selector), Ports: []store.ServicePort{}, - DNS: data.Spec.ExternalName, Status: status, } + if data.Spec.Type == corev1.ServiceTypeExternalName { + item.DNS = data.Spec.ExternalName + } for _, sp := range data.Spec.Ports { item.Ports = append(item.Ports, store.ServicePort{ Name: sp.Name, @@ -384,7 +392,10 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf k.Logger.Errorf("%s: Invalid data from k8s api, %s", SERVICE, obj) return } - var status = DELETED + if data.Spec.Type == corev1.ServiceTypeExternalName && k.DisableServiceExternalName { + return + } + status := DELETED item := &store.Service{ Namespace: data.GetNamespace(), Name: data.GetName(), @@ -392,6 +403,9 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf Selector: store.ConvertToMapStringW(data.Spec.Selector), Status: status, } + if data.Spec.Type == corev1.ServiceTypeExternalName { + item.DNS = data.Spec.ExternalName + } if publishSvc != nil { if publishSvc.Namespace == item.Namespace && publishSvc.Name == item.Name { publishSvc.Status = DELETED @@ -406,21 +420,31 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf k.Logger.Errorf("%s: Invalid data from k8s api, %s", SERVICE, oldObj) return } + if data1.Spec.Type == corev1.ServiceTypeExternalName && k.DisableServiceExternalName { + k.Logger.Tracef("forwarding to ExternalName Services for %v is disabled", data1) + return + } data2, ok := newObj.(*corev1.Service) if !ok { k.Logger.Errorf("%s: Invalid data from k8s api, %s", SERVICE, newObj) return } - var status = MODIFIED + if data2.Spec.Type == corev1.ServiceTypeExternalName && k.DisableServiceExternalName { + k.Logger.Tracef("forwarding to ExternalName Services for %v is disabled", data2) + return + } + status := MODIFIED item1 := &store.Service{ Namespace: data1.GetNamespace(), Name: data1.GetName(), Annotations: store.ConvertToMapStringW(data1.ObjectMeta.Annotations), Selector: store.ConvertToMapStringW(data1.Spec.Selector), Ports: []store.ServicePort{}, - DNS: data1.Spec.ExternalName, Status: status, } + if data1.Spec.Type == corev1.ServiceTypeExternalName { + item1.DNS = data1.Spec.ExternalName + } for _, sp := range data1.Spec.Ports { item1.Ports = append(item1.Ports, store.ServicePort{ Name: sp.Name, @@ -435,9 +459,11 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf Annotations: store.ConvertToMapStringW(data2.ObjectMeta.Annotations), Selector: store.ConvertToMapStringW(data2.Spec.Selector), Ports: []store.ServicePort{}, - DNS: data1.Spec.ExternalName, Status: status, } + if data2.Spec.Type == corev1.ServiceTypeExternalName { + item2.DNS = data2.Spec.ExternalName + } for _, sp := range data2.Spec.Ports { item2.Ports = append(item2.Ports, store.ServicePort{ Name: sp.Name, @@ -469,7 +495,7 @@ func (k *K8s) EventsConfigfMaps(channel chan SyncDataEvent, stop chan struct{}, k.Logger.Errorf("%s: Invalid data from k8s api, %s", CONFIGMAP, obj) return } - var status = ADDED + status := ADDED if data.ObjectMeta.GetDeletionTimestamp() != nil { // detect services that are in terminating state status = DELETED @@ -489,7 +515,7 @@ func (k *K8s) EventsConfigfMaps(channel chan SyncDataEvent, stop chan struct{}, k.Logger.Errorf("%s: Invalid data from k8s api, %s", CONFIGMAP, obj) return } - var status = DELETED + status := DELETED item := &store.ConfigMap{ Namespace: data.GetNamespace(), Name: data.GetName(), @@ -510,7 +536,7 @@ func (k *K8s) EventsConfigfMaps(channel chan SyncDataEvent, stop chan struct{}, k.Logger.Errorf("%s: Invalid data from k8s api, %s", CONFIGMAP, newObj) return } - var status = MODIFIED + status := MODIFIED item1 := &store.ConfigMap{ Namespace: data1.GetNamespace(), Name: data1.GetName(), @@ -543,7 +569,7 @@ func (k *K8s) EventsSecrets(channel chan SyncDataEvent, stop chan struct{}, info k.Logger.Errorf("%s: Invalid data from k8s api, %s", SECRET, obj) return } - var status = ADDED + status := ADDED if data.ObjectMeta.GetDeletionTimestamp() != nil { // detect services that are in terminating state status = DELETED @@ -563,7 +589,7 @@ func (k *K8s) EventsSecrets(channel chan SyncDataEvent, stop chan struct{}, info k.Logger.Errorf("%s: Invalid data from k8s api, %s", SECRET, obj) return } - var status = DELETED + status := DELETED item := &store.Secret{ Namespace: data.GetNamespace(), Name: data.GetName(), @@ -584,7 +610,7 @@ func (k *K8s) EventsSecrets(channel chan SyncDataEvent, stop chan struct{}, info k.Logger.Errorf("%s: Invalid data from k8s api, %s", SECRET, newObj) return } - var status = MODIFIED + status := MODIFIED item1 := &store.Secret{ Namespace: data1.GetNamespace(), Name: data1.GetName(), diff --git a/controller/utils/flags.go b/controller/utils/flags.go index 4138cec7..6eba619f 100644 --- a/controller/utils/flags.go +++ b/controller/utils/flags.go @@ -65,36 +65,37 @@ func (n *LogLevelValue) UnmarshalFlag(value string) error { } // OSArgs contains arguments that can be sent to controller -type OSArgs struct { - Help []bool `short:"h" long:"help" description:"show this help message"` - Version []bool `short:"v" long:"version" description:"version"` - DefaultBackendService NamespaceValue `long:"default-backend-service" default:"" description:"default service to serve 404 page. If not specified HAProxy serves http 400"` - DefaultCertificate NamespaceValue `long:"default-ssl-certificate" default:"" description:"secret name of the certificate"` - ConfigMap NamespaceValue `long:"configmap" description:"configmap designated for HAProxy" default:""` - ConfigMapTCPServices NamespaceValue `long:"configmap-tcp-services" description:"configmap used to define tcp services" default:""` - ConfigMapErrorFiles NamespaceValue `long:"configmap-errorfiles" description:"configmap used to define custom error pages associated to HTTP error codes" default:""` - ConfigMapPatternFiles NamespaceValue `long:"configmap-patternfiles" description:"configmap used to provide a list of pattern files to use in haproxy configuration " default:""` - KubeConfig string `long:"kubeconfig" default:"" description:"combined with -e. location of kube config file"` - IngressClass string `long:"ingress.class" default:"" description:"ingress.class to monitor in multiple controllers environment"` - EmptyIngressClass bool `long:"empty-ingress-class" description:"empty-ingress-class manages the behavior in case an ingress has no explicit ingress class annotation. true: to process, false: to skip"` - PublishService string `long:"publish-service" default:"" description:"Takes the form namespace/name. The controller mirrors the address of this service's endpoints to the load-balancer status of all Ingress objects it satisfies"` - NamespaceWhitelist []string `long:"namespace-whitelist" description:"whitelisted namespaces"` - NamespaceBlacklist []string `long:"namespace-blacklist" description:"blacklisted namespaces"` - SyncPeriod time.Duration `long:"sync-period" default:"5s" description:"Sets the period at which the controller syncs HAProxy configuration file"` - CacheResyncPeriod time.Duration `long:"cache-resync-period" default:"10m" description:"Sets the underlying Shared Informer resync period: resyncing controller with informers cache"` - LogLevel LogLevelValue `long:"log" default:"info" description:"level of log messages you can see"` - PprofEnabled bool `short:"p" description:"enable pprof over https"` - External bool `short:"e" long:"external" description:"use as external Ingress Controller (out of k8s cluster)"` - Test bool `short:"t" description:"simulate running HAProxy"` - DisableIPV4 bool `long:"disable-ipv4" description:"toggle to disable the IPv4 protocol from all frontends"` - DisableIPV6 bool `long:"disable-ipv6" description:"toggle to disable the IPv6 protocol from all frontends"` - DisableHTTP bool `long:"disable-http" description:"toggle to disable the HTTP frontend"` - DisableHTTPS bool `long:"disable-https" description:"toggle to disable the HTTPs frontend"` - HTTPBindPort int64 `long:"http-bind-port" default:"80" description:"port to listen on for HTTP traffic"` - HTTPSBindPort int64 `long:"https-bind-port" default:"443" description:"port to listen on for HTTPS traffic"` - IPV4BindAddr string `long:"ipv4-bind-address" default:"0.0.0.0" description:"IPv4 address the Ingress Controller listens on (if enabled)"` - IPV6BindAddr string `long:"ipv6-bind-address" default:"::" description:"IPv6 address the Ingress Controller listens on (if enabled)"` - Program string `long:"program" description:"path to HAProxy program. NOTE: works only with External mode"` - CfgDir string `long:"config-dir" description:"path to HAProxy configuration directory. NOTE: works only in External mode"` - RuntimeDir string `long:"runtime-dir" description:"path to HAProxy runtime directory. NOTE: works only in External mode"` +type OSArgs struct { //nolint:maligned + Help []bool `short:"h" long:"help" description:"show this help message"` + Version []bool `short:"v" long:"version" description:"version"` + DefaultBackendService NamespaceValue `long:"default-backend-service" default:"" description:"default service to serve 404 page. If not specified HAProxy serves http 400"` + DefaultCertificate NamespaceValue `long:"default-ssl-certificate" default:"" description:"secret name of the certificate"` + ConfigMap NamespaceValue `long:"configmap" description:"configmap designated for HAProxy" default:""` + ConfigMapTCPServices NamespaceValue `long:"configmap-tcp-services" description:"configmap used to define tcp services" default:""` + ConfigMapErrorFiles NamespaceValue `long:"configmap-errorfiles" description:"configmap used to define custom error pages associated to HTTP error codes" default:""` + ConfigMapPatternFiles NamespaceValue `long:"configmap-patternfiles" description:"configmap used to provide a list of pattern files to use in haproxy configuration " default:""` + KubeConfig string `long:"kubeconfig" default:"" description:"combined with -e. location of kube config file"` + IngressClass string `long:"ingress.class" default:"" description:"ingress.class to monitor in multiple controllers environment"` + EmptyIngressClass bool `long:"empty-ingress-class" description:"empty-ingress-class manages the behavior in case an ingress has no explicit ingress class annotation. true: to process, false: to skip"` + PublishService string `long:"publish-service" default:"" description:"Takes the form namespace/name. The controller mirrors the address of this service's endpoints to the load-balancer status of all Ingress objects it satisfies"` + NamespaceWhitelist []string `long:"namespace-whitelist" description:"whitelisted namespaces"` + NamespaceBlacklist []string `long:"namespace-blacklist" description:"blacklisted namespaces"` + SyncPeriod time.Duration `long:"sync-period" default:"5s" description:"Sets the period at which the controller syncs HAProxy configuration file"` + CacheResyncPeriod time.Duration `long:"cache-resync-period" default:"10m" description:"Sets the underlying Shared Informer resync period: resyncing controller with informers cache"` + LogLevel LogLevelValue `long:"log" default:"info" description:"level of log messages you can see"` + PprofEnabled bool `short:"p" description:"enable pprof over https"` + External bool `short:"e" long:"external" description:"use as external Ingress Controller (out of k8s cluster)"` + Test bool `short:"t" description:"simulate running HAProxy"` + DisableIPV4 bool `long:"disable-ipv4" description:"toggle to disable the IPv4 protocol from all frontends"` + DisableIPV6 bool `long:"disable-ipv6" description:"toggle to disable the IPv6 protocol from all frontends"` + DisableHTTP bool `long:"disable-http" description:"toggle to disable the HTTP frontend"` + DisableHTTPS bool `long:"disable-https" description:"toggle to disable the HTTPs frontend"` + HTTPBindPort int64 `long:"http-bind-port" default:"80" description:"port to listen on for HTTP traffic"` + HTTPSBindPort int64 `long:"https-bind-port" default:"443" description:"port to listen on for HTTPS traffic"` + IPV4BindAddr string `long:"ipv4-bind-address" default:"0.0.0.0" description:"IPv4 address the Ingress Controller listens on (if enabled)"` + IPV6BindAddr string `long:"ipv6-bind-address" default:"::" description:"IPv6 address the Ingress Controller listens on (if enabled)"` + Program string `long:"program" description:"path to HAProxy program. NOTE: works only with External mode"` + CfgDir string `long:"config-dir" description:"path to HAProxy configuration directory. NOTE: works only in External mode"` + RuntimeDir string `long:"runtime-dir" description:"path to HAProxy runtime directory. NOTE: works only in External mode"` + DisableServiceExternalName bool `long:"disable-service-external-name" description:"disable forwarding to ExternalName Services due to CVE-2021-25740"` } diff --git a/documentation/controller.md b/documentation/controller.md index 18059aee..6f4d6882 100644 --- a/documentation/controller.md +++ b/documentation/controller.md @@ -35,6 +35,7 @@ Image can be run with arguments: | [`--program`](#--program) | `haproxy in PATH location` | | [`--config-dir`](#--config-dir) | `/tmp/haproxy-ingress/etc` | | [`--runtime-dir`](#--runtime-dir) | `/tmp/haproxy-ingress/run` | +| [`--disable-service-external-name`](#--disable-service-external-name) | `false` | ### `--configmap` @@ -625,3 +626,22 @@ args: *** +### `--disable-service-external-name` + + Disable forwarding to ExternalName Services due to CVE-2021-25740 + +Possible values: + +- Boolean value, just need to declare the flag to disable forwarding to ExternalName Services. + +Example: + +```yaml +args: + - --disable-service-external-name +``` + +

:arrow_up_small: back to top

+ +*** + diff --git a/documentation/doc.yaml b/documentation/doc.yaml index 1159d4c3..31debe06 100644 --- a/documentation/doc.yaml +++ b/documentation/doc.yaml @@ -365,6 +365,18 @@ image_arguments: args: - --external - --runtime-dir=/haproxy-ingress/run + - argument: --disable-service-external-name + description: Disable forwarding to ExternalName Services due to CVE-2021-25740 + values: + - Boolean value, just need to declare the flag to disable forwarding to ExternalName Services. + default: "false" + version_min: "1.6" + example: |- + args: + - --disable-service-external-name + helm: |- + helm install haproxy haproxytech/kubernetes-ingress \ + --set-string "controller.extraArgs={--disable-service-external-name}" groups: config-snippet: header: |- @@ -725,7 +737,7 @@ annotations: group: config-snippet dependencies: "" default: "" - description: + description: - Defines a group of configuration directives to insert in the main HTTP/HTTPS frontends. tip: - Because frontend-config-snippet is inserted in the main http/https frontends it will apply to all traffic. To apply configuration by Ingress, annotations should be privileged. @@ -771,7 +783,7 @@ annotations: - ingress - service version_min: "1.5" - example: + example: - |- backend-config-snippet: | http-send-name-header x-dst-server