Skip to content

Commit de4ccd8

Browse files
committed
MAJOR: add option to disable forwarding to ExternalName Services
CVE-2021-25740 atack is possible to expose backend IPs to ExternalName Services kubernetes/kubernetes#103675
1 parent 6688a1f commit de4ccd8

File tree

5 files changed

+119
-60
lines changed

5 files changed

+119
-60
lines changed

controller/controller.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,13 @@ func (c *HAProxyController) Start() {
9292
}
9393

9494
// Get K8s client
95-
c.k8s, err = GetKubernetesClient()
95+
c.k8s, err = GetKubernetesClient(c.OSArgs.DisableServiceExternalName)
9696
if c.OSArgs.External {
9797
kubeconfig := filepath.Join(utils.HomeDir(), ".kube", "config")
9898
if c.OSArgs.KubeConfig != "" {
9999
kubeconfig = c.OSArgs.KubeConfig
100100
}
101-
c.k8s, err = GetRemoteKubernetesClient(kubeconfig)
101+
c.k8s, err = GetRemoteKubernetesClient(kubeconfig, c.OSArgs.DisableServiceExternalName)
102102
}
103103
if err != nil {
104104
logger.Panic(err)

controller/kubernetes.go

+50-24
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ var ErrIgnored = errors.New("ignored resource")
4545

4646
// K8s is structure with all data required to synchronize with k8s
4747
type K8s struct {
48-
API *kubernetes.Clientset
49-
Logger utils.Logger
48+
API *kubernetes.Clientset
49+
Logger utils.Logger
50+
DisableServiceExternalName bool // CVE-2021-25740
5051
}
5152

5253
// GetKubernetesClient returns new client that communicates with k8s
53-
func GetKubernetesClient() (*K8s, error) {
54+
func GetKubernetesClient(disableServiceExternalName bool) (*K8s, error) {
5455
k8sLogger := utils.GetK8sAPILogger()
5556
if !TRACE_API {
5657
k8sLogger.SetLevel(utils.Info)
@@ -65,13 +66,14 @@ func GetKubernetesClient() (*K8s, error) {
6566
logger.Panic(err)
6667
}
6768
return &K8s{
68-
API: clientset,
69-
Logger: k8sLogger,
69+
API: clientset,
70+
Logger: k8sLogger,
71+
DisableServiceExternalName: disableServiceExternalName,
7072
}, nil
7173
}
7274

7375
// GetRemoteKubernetesClient returns new client that communicates with k8s
74-
func GetRemoteKubernetesClient(kubeconfig string) (*K8s, error) {
76+
func GetRemoteKubernetesClient(kubeconfig string, disableServiceExternalName bool) (*K8s, error) {
7577
k8sLogger := utils.GetK8sAPILogger()
7678
if !TRACE_API {
7779
k8sLogger.SetLevel(utils.Info)
@@ -85,13 +87,13 @@ func GetRemoteKubernetesClient(kubeconfig string) (*K8s, error) {
8587

8688
// create the clientset
8789
clientset, err := kubernetes.NewForConfig(config)
88-
8990
if err != nil {
9091
logger.Panic(err)
9192
}
9293
return &K8s{
93-
API: clientset,
94-
Logger: k8sLogger,
94+
API: clientset,
95+
Logger: k8sLogger,
96+
DisableServiceExternalName: disableServiceExternalName,
9597
}, nil
9698
}
9799

@@ -104,7 +106,7 @@ func (k *K8s) EventsNamespaces(channel chan SyncDataEvent, stop chan struct{}, i
104106
k.Logger.Errorf("%s: Invalid data from k8s api, %s", NAMESPACE, obj)
105107
return
106108
}
107-
var status = ADDED
109+
status := ADDED
108110
if data.ObjectMeta.GetDeletionTimestamp() != nil {
109111
// detect services that are in terminating state
110112
status = DELETED
@@ -126,7 +128,7 @@ func (k *K8s) EventsNamespaces(channel chan SyncDataEvent, stop chan struct{}, i
126128
k.Logger.Errorf("%s: Invalid data from k8s api, %s", NAMESPACE, obj)
127129
return
128130
}
129-
var status = DELETED
131+
status := DELETED
130132
item := &store.Namespace{
131133
Name: data.GetName(),
132134
Endpoints: make(map[string]*store.Endpoints),
@@ -149,7 +151,7 @@ func (k *K8s) EventsNamespaces(channel chan SyncDataEvent, stop chan struct{}, i
149151
k.Logger.Errorf("%s: Invalid data from k8s api, %s", NAMESPACE, newObj)
150152
return
151153
}
152-
var status = MODIFIED
154+
status := MODIFIED
153155
item1 := &store.Namespace{
154156
Name: data1.GetName(),
155157
Status: status,
@@ -349,7 +351,11 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf
349351
k.Logger.Errorf("%s: Invalid data from k8s api, %s", SERVICE, obj)
350352
return
351353
}
352-
var status = ADDED
354+
if data.Spec.Type == corev1.ServiceTypeExternalName && k.DisableServiceExternalName {
355+
k.Logger.Tracef("forwarding to ExternalName Services for %v is disabled", data)
356+
return
357+
}
358+
status := ADDED
353359
if data.ObjectMeta.GetDeletionTimestamp() != nil {
354360
// detect services that are in terminating state
355361
status = DELETED
@@ -360,9 +366,11 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf
360366
Annotations: store.ConvertToMapStringW(data.ObjectMeta.Annotations),
361367
Selector: store.ConvertToMapStringW(data.Spec.Selector),
362368
Ports: []store.ServicePort{},
363-
DNS: data.Spec.ExternalName,
364369
Status: status,
365370
}
371+
if data.Spec.Type == corev1.ServiceTypeExternalName {
372+
item.DNS = data.Spec.ExternalName
373+
}
366374
for _, sp := range data.Spec.Ports {
367375
item.Ports = append(item.Ports, store.ServicePort{
368376
Name: sp.Name,
@@ -384,14 +392,20 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf
384392
k.Logger.Errorf("%s: Invalid data from k8s api, %s", SERVICE, obj)
385393
return
386394
}
387-
var status = DELETED
395+
if data.Spec.Type == corev1.ServiceTypeExternalName && k.DisableServiceExternalName {
396+
return
397+
}
398+
status := DELETED
388399
item := &store.Service{
389400
Namespace: data.GetNamespace(),
390401
Name: data.GetName(),
391402
Annotations: store.ConvertToMapStringW(data.ObjectMeta.Annotations),
392403
Selector: store.ConvertToMapStringW(data.Spec.Selector),
393404
Status: status,
394405
}
406+
if data.Spec.Type == corev1.ServiceTypeExternalName {
407+
item.DNS = data.Spec.ExternalName
408+
}
395409
if publishSvc != nil {
396410
if publishSvc.Namespace == item.Namespace && publishSvc.Name == item.Name {
397411
publishSvc.Status = DELETED
@@ -406,21 +420,31 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf
406420
k.Logger.Errorf("%s: Invalid data from k8s api, %s", SERVICE, oldObj)
407421
return
408422
}
423+
if data1.Spec.Type == corev1.ServiceTypeExternalName && k.DisableServiceExternalName {
424+
k.Logger.Tracef("forwarding to ExternalName Services for %v is disabled", data1)
425+
return
426+
}
409427
data2, ok := newObj.(*corev1.Service)
410428
if !ok {
411429
k.Logger.Errorf("%s: Invalid data from k8s api, %s", SERVICE, newObj)
412430
return
413431
}
414-
var status = MODIFIED
432+
if data2.Spec.Type == corev1.ServiceTypeExternalName && k.DisableServiceExternalName {
433+
k.Logger.Tracef("forwarding to ExternalName Services for %v is disabled", data2)
434+
return
435+
}
436+
status := MODIFIED
415437
item1 := &store.Service{
416438
Namespace: data1.GetNamespace(),
417439
Name: data1.GetName(),
418440
Annotations: store.ConvertToMapStringW(data1.ObjectMeta.Annotations),
419441
Selector: store.ConvertToMapStringW(data1.Spec.Selector),
420442
Ports: []store.ServicePort{},
421-
DNS: data1.Spec.ExternalName,
422443
Status: status,
423444
}
445+
if data1.Spec.Type == corev1.ServiceTypeExternalName {
446+
item1.DNS = data1.Spec.ExternalName
447+
}
424448
for _, sp := range data1.Spec.Ports {
425449
item1.Ports = append(item1.Ports, store.ServicePort{
426450
Name: sp.Name,
@@ -435,9 +459,11 @@ func (k *K8s) EventsServices(channel chan SyncDataEvent, stop chan struct{}, inf
435459
Annotations: store.ConvertToMapStringW(data2.ObjectMeta.Annotations),
436460
Selector: store.ConvertToMapStringW(data2.Spec.Selector),
437461
Ports: []store.ServicePort{},
438-
DNS: data1.Spec.ExternalName,
439462
Status: status,
440463
}
464+
if data2.Spec.Type == corev1.ServiceTypeExternalName {
465+
item2.DNS = data2.Spec.ExternalName
466+
}
441467
for _, sp := range data2.Spec.Ports {
442468
item2.Ports = append(item2.Ports, store.ServicePort{
443469
Name: sp.Name,
@@ -469,7 +495,7 @@ func (k *K8s) EventsConfigfMaps(channel chan SyncDataEvent, stop chan struct{},
469495
k.Logger.Errorf("%s: Invalid data from k8s api, %s", CONFIGMAP, obj)
470496
return
471497
}
472-
var status = ADDED
498+
status := ADDED
473499
if data.ObjectMeta.GetDeletionTimestamp() != nil {
474500
// detect services that are in terminating state
475501
status = DELETED
@@ -489,7 +515,7 @@ func (k *K8s) EventsConfigfMaps(channel chan SyncDataEvent, stop chan struct{},
489515
k.Logger.Errorf("%s: Invalid data from k8s api, %s", CONFIGMAP, obj)
490516
return
491517
}
492-
var status = DELETED
518+
status := DELETED
493519
item := &store.ConfigMap{
494520
Namespace: data.GetNamespace(),
495521
Name: data.GetName(),
@@ -510,7 +536,7 @@ func (k *K8s) EventsConfigfMaps(channel chan SyncDataEvent, stop chan struct{},
510536
k.Logger.Errorf("%s: Invalid data from k8s api, %s", CONFIGMAP, newObj)
511537
return
512538
}
513-
var status = MODIFIED
539+
status := MODIFIED
514540
item1 := &store.ConfigMap{
515541
Namespace: data1.GetNamespace(),
516542
Name: data1.GetName(),
@@ -543,7 +569,7 @@ func (k *K8s) EventsSecrets(channel chan SyncDataEvent, stop chan struct{}, info
543569
k.Logger.Errorf("%s: Invalid data from k8s api, %s", SECRET, obj)
544570
return
545571
}
546-
var status = ADDED
572+
status := ADDED
547573
if data.ObjectMeta.GetDeletionTimestamp() != nil {
548574
// detect services that are in terminating state
549575
status = DELETED
@@ -563,7 +589,7 @@ func (k *K8s) EventsSecrets(channel chan SyncDataEvent, stop chan struct{}, info
563589
k.Logger.Errorf("%s: Invalid data from k8s api, %s", SECRET, obj)
564590
return
565591
}
566-
var status = DELETED
592+
status := DELETED
567593
item := &store.Secret{
568594
Namespace: data.GetNamespace(),
569595
Name: data.GetName(),
@@ -584,7 +610,7 @@ func (k *K8s) EventsSecrets(channel chan SyncDataEvent, stop chan struct{}, info
584610
k.Logger.Errorf("%s: Invalid data from k8s api, %s", SECRET, newObj)
585611
return
586612
}
587-
var status = MODIFIED
613+
status := MODIFIED
588614
item1 := &store.Secret{
589615
Namespace: data1.GetNamespace(),
590616
Name: data1.GetName(),

controller/utils/flags.go

+33-32
Original file line numberDiff line numberDiff line change
@@ -65,36 +65,37 @@ func (n *LogLevelValue) UnmarshalFlag(value string) error {
6565
}
6666

6767
// OSArgs contains arguments that can be sent to controller
68-
type OSArgs struct {
69-
Help []bool `short:"h" long:"help" description:"show this help message"`
70-
Version []bool `short:"v" long:"version" description:"version"`
71-
DefaultBackendService NamespaceValue `long:"default-backend-service" default:"" description:"default service to serve 404 page. If not specified HAProxy serves http 400"`
72-
DefaultCertificate NamespaceValue `long:"default-ssl-certificate" default:"" description:"secret name of the certificate"`
73-
ConfigMap NamespaceValue `long:"configmap" description:"configmap designated for HAProxy" default:""`
74-
ConfigMapTCPServices NamespaceValue `long:"configmap-tcp-services" description:"configmap used to define tcp services" default:""`
75-
ConfigMapErrorFiles NamespaceValue `long:"configmap-errorfiles" description:"configmap used to define custom error pages associated to HTTP error codes" default:""`
76-
ConfigMapPatternFiles NamespaceValue `long:"configmap-patternfiles" description:"configmap used to provide a list of pattern files to use in haproxy configuration " default:""`
77-
KubeConfig string `long:"kubeconfig" default:"" description:"combined with -e. location of kube config file"`
78-
IngressClass string `long:"ingress.class" default:"" description:"ingress.class to monitor in multiple controllers environment"`
79-
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"`
80-
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"`
81-
NamespaceWhitelist []string `long:"namespace-whitelist" description:"whitelisted namespaces"`
82-
NamespaceBlacklist []string `long:"namespace-blacklist" description:"blacklisted namespaces"`
83-
SyncPeriod time.Duration `long:"sync-period" default:"5s" description:"Sets the period at which the controller syncs HAProxy configuration file"`
84-
CacheResyncPeriod time.Duration `long:"cache-resync-period" default:"10m" description:"Sets the underlying Shared Informer resync period: resyncing controller with informers cache"`
85-
LogLevel LogLevelValue `long:"log" default:"info" description:"level of log messages you can see"`
86-
PprofEnabled bool `short:"p" description:"enable pprof over https"`
87-
External bool `short:"e" long:"external" description:"use as external Ingress Controller (out of k8s cluster)"`
88-
Test bool `short:"t" description:"simulate running HAProxy"`
89-
DisableIPV4 bool `long:"disable-ipv4" description:"toggle to disable the IPv4 protocol from all frontends"`
90-
DisableIPV6 bool `long:"disable-ipv6" description:"toggle to disable the IPv6 protocol from all frontends"`
91-
DisableHTTP bool `long:"disable-http" description:"toggle to disable the HTTP frontend"`
92-
DisableHTTPS bool `long:"disable-https" description:"toggle to disable the HTTPs frontend"`
93-
HTTPBindPort int64 `long:"http-bind-port" default:"80" description:"port to listen on for HTTP traffic"`
94-
HTTPSBindPort int64 `long:"https-bind-port" default:"443" description:"port to listen on for HTTPS traffic"`
95-
IPV4BindAddr string `long:"ipv4-bind-address" default:"0.0.0.0" description:"IPv4 address the Ingress Controller listens on (if enabled)"`
96-
IPV6BindAddr string `long:"ipv6-bind-address" default:"::" description:"IPv6 address the Ingress Controller listens on (if enabled)"`
97-
Program string `long:"program" description:"path to HAProxy program. NOTE: works only with External mode"`
98-
CfgDir string `long:"config-dir" description:"path to HAProxy configuration directory. NOTE: works only in External mode"`
99-
RuntimeDir string `long:"runtime-dir" description:"path to HAProxy runtime directory. NOTE: works only in External mode"`
68+
type OSArgs struct { //nolint:maligned
69+
Help []bool `short:"h" long:"help" description:"show this help message"`
70+
Version []bool `short:"v" long:"version" description:"version"`
71+
DefaultBackendService NamespaceValue `long:"default-backend-service" default:"" description:"default service to serve 404 page. If not specified HAProxy serves http 400"`
72+
DefaultCertificate NamespaceValue `long:"default-ssl-certificate" default:"" description:"secret name of the certificate"`
73+
ConfigMap NamespaceValue `long:"configmap" description:"configmap designated for HAProxy" default:""`
74+
ConfigMapTCPServices NamespaceValue `long:"configmap-tcp-services" description:"configmap used to define tcp services" default:""`
75+
ConfigMapErrorFiles NamespaceValue `long:"configmap-errorfiles" description:"configmap used to define custom error pages associated to HTTP error codes" default:""`
76+
ConfigMapPatternFiles NamespaceValue `long:"configmap-patternfiles" description:"configmap used to provide a list of pattern files to use in haproxy configuration " default:""`
77+
KubeConfig string `long:"kubeconfig" default:"" description:"combined with -e. location of kube config file"`
78+
IngressClass string `long:"ingress.class" default:"" description:"ingress.class to monitor in multiple controllers environment"`
79+
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"`
80+
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"`
81+
NamespaceWhitelist []string `long:"namespace-whitelist" description:"whitelisted namespaces"`
82+
NamespaceBlacklist []string `long:"namespace-blacklist" description:"blacklisted namespaces"`
83+
SyncPeriod time.Duration `long:"sync-period" default:"5s" description:"Sets the period at which the controller syncs HAProxy configuration file"`
84+
CacheResyncPeriod time.Duration `long:"cache-resync-period" default:"10m" description:"Sets the underlying Shared Informer resync period: resyncing controller with informers cache"`
85+
LogLevel LogLevelValue `long:"log" default:"info" description:"level of log messages you can see"`
86+
PprofEnabled bool `short:"p" description:"enable pprof over https"`
87+
External bool `short:"e" long:"external" description:"use as external Ingress Controller (out of k8s cluster)"`
88+
Test bool `short:"t" description:"simulate running HAProxy"`
89+
DisableIPV4 bool `long:"disable-ipv4" description:"toggle to disable the IPv4 protocol from all frontends"`
90+
DisableIPV6 bool `long:"disable-ipv6" description:"toggle to disable the IPv6 protocol from all frontends"`
91+
DisableHTTP bool `long:"disable-http" description:"toggle to disable the HTTP frontend"`
92+
DisableHTTPS bool `long:"disable-https" description:"toggle to disable the HTTPs frontend"`
93+
HTTPBindPort int64 `long:"http-bind-port" default:"80" description:"port to listen on for HTTP traffic"`
94+
HTTPSBindPort int64 `long:"https-bind-port" default:"443" description:"port to listen on for HTTPS traffic"`
95+
IPV4BindAddr string `long:"ipv4-bind-address" default:"0.0.0.0" description:"IPv4 address the Ingress Controller listens on (if enabled)"`
96+
IPV6BindAddr string `long:"ipv6-bind-address" default:"::" description:"IPv6 address the Ingress Controller listens on (if enabled)"`
97+
Program string `long:"program" description:"path to HAProxy program. NOTE: works only with External mode"`
98+
CfgDir string `long:"config-dir" description:"path to HAProxy configuration directory. NOTE: works only in External mode"`
99+
RuntimeDir string `long:"runtime-dir" description:"path to HAProxy runtime directory. NOTE: works only in External mode"`
100+
DisableServiceExternalName bool `long:"disable-service-external-name" description:"disable forwarding to ExternalName Services due to CVE-2021-25740"`
100101
}

documentation/controller.md

+20
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Image can be run with arguments:
3535
| [`--program`](#--program) | `haproxy in PATH location` |
3636
| [`--config-dir`](#--config-dir) | `/tmp/haproxy-ingress/etc` |
3737
| [`--runtime-dir`](#--runtime-dir) | `/tmp/haproxy-ingress/run` |
38+
| [`--disable-service-external-name`](#--disable-service-external-name) | `false` |
3839

3940

4041
### `--configmap`
@@ -625,3 +626,22 @@ args:
625626

626627
***
627628

629+
### `--disable-service-external-name`
630+
631+
Disable forwarding to ExternalName Services due to CVE-2021-25740
632+
633+
Possible values:
634+
635+
- Boolean value, just need to declare the flag to disable forwarding to ExternalName Services.
636+
637+
Example:
638+
639+
```yaml
640+
args:
641+
- --disable-service-external-name
642+
```
643+
644+
<p align='right'><a href='#haproxy-kubernetes-ingress-controller'>:arrow_up_small: back to top</a></p>
645+
646+
***
647+

0 commit comments

Comments
 (0)