Skip to content

Commit

Permalink
MAJOR: add option to disable forwarding to ExternalName Services
Browse files Browse the repository at this point in the history
CVE-2021-25740
atack is possible to expose backend IPs to ExternalName Services

kubernetes/kubernetes#103675
  • Loading branch information
oktalz committed Jul 15, 2021
1 parent 5f4de9c commit 6cb1128
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 60 deletions.
4 changes: 2 additions & 2 deletions controller/controller.go
Expand Up @@ -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)
Expand Down
74 changes: 50 additions & 24 deletions controller/kubernetes.go
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -384,14 +392,20 @@ 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(),
Annotations: store.ConvertToMapStringW(data.ObjectMeta.Annotations),
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
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -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
Expand All @@ -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(),
Expand All @@ -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(),
Expand Down
65 changes: 33 additions & 32 deletions controller/utils/flags.go
Expand Up @@ -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"`
}
20 changes: 20 additions & 0 deletions documentation/controller.md
Expand Up @@ -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`
Expand Down Expand Up @@ -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
```

<p align='right'><a href='#haproxy-kubernetes-ingress-controller'>:arrow_up_small: back to top</a></p>

***

0 comments on commit 6cb1128

Please sign in to comment.