Skip to content

Commit

Permalink
K8s autodiscovery (#453)
Browse files Browse the repository at this point in the history
* Add a new dependency on Kubernetes package

* Add and store a new flag about automatic nodes discovery from a pod

* Implement the listing of nodes

* Add tests to cover the k8s node listing

* Fix the k8s listing test to ensure the load incluster function is actually called

* Add more help to the k8s node discovery flags, and cross-reference them.

* Add a note on the Kubernetes auto-discovery in the main README file

* Move the kubernetes discovery from conf to modules/discovery

* When running with --pods, run the Kubernetes auto discovery

* Also mention that the auto discovery is always on when using --pod

Co-authored-by: Mikolaj Pawlikowski <[email protected]>
  • Loading branch information
seeker89 and Mikolaj Pawlikowski authored Jun 5, 2021
1 parent 0b90e0e commit 6689005
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 1 deletion.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ To specify interface scanning, you can use the `--interface` option (this will s
To specify a specific CIDR to scan, use the `--cidr` option. Example:
`kube-hunter --cidr 192.168.0.0/24`

4. **Kubernetes node auto-discovery**

Set `--k8s-auto-discover-nodes` flag to query Kubernetes for all nodes in the cluster, and then attempt to scan them all. By default, it will use [in-cluster config](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) to connect to the Kubernetes API. If you'd like to use an explicit kubeconfig file, set `--kubeconfig /location/of/kubeconfig/file`.

Also note, that this is always done when using `--pod` mode.

### Active Hunting

Active hunting is an option in which kube-hunter will exploit vulnerabilities it finds, to explore for further vulnerabilities.
Expand Down
4 changes: 3 additions & 1 deletion kube_hunter/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
quick=args.quick,
remote=args.remote,
statistics=args.statistics,
k8s_auto_discover_nodes=args.k8s_auto_discover_nodes,
kubeconfig=args.kubeconfig,
)
setup_logger(args.log, args.log_file)
set_config(config)
Expand Down Expand Up @@ -88,7 +90,7 @@ def list_hunters():

def main():
global hunt_started
scan_options = [config.pod, config.cidr, config.remote, config.interface]
scan_options = [config.pod, config.cidr, config.remote, config.interface, config.k8s_auto_discover_nodes]
try:
if args.list:
list_hunters()
Expand Down
2 changes: 2 additions & 0 deletions kube_hunter/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class Config:
remote: Optional[str] = None
reporter: Optional[Any] = None
statistics: bool = False
k8s_auto_discover_nodes: bool = False
kubeconfig: Optional[str] = None


_config: Optional[Config] = None
Expand Down
20 changes: 20 additions & 0 deletions kube_hunter/conf/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ def parser_add_arguments(parser):
help="One or more remote ip/dns to hunt",
)

parser.add_argument(
"--k8s-auto-discover-nodes",
action="store_true",
help="Enables automatic detection of all nodes in a Kubernetes cluster "
"by quering the Kubernetes API server. "
"It supports both in-cluster config (when running as a pod), "
"and a specific kubectl config file (use --kubeconfig to set this). "
"By default, when this flag is set, it will use in-cluster config. "
"NOTE: this is automatically switched on in --pod mode."
)

parser.add_argument(
"--kubeconfig",
type=str,
metavar="KUBECONFIG",
default=None,
help="Specify the kubeconfig file to use for Kubernetes nodes auto discovery "
" (to be used in conjuction with the --k8s-auto-discover-nodes flag."
)

parser.add_argument("--active", action="store_true", help="Enables active hunting")

parser.add_argument(
Expand Down
7 changes: 7 additions & 0 deletions kube_hunter/modules/discovery/hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from netifaces import AF_INET, ifaddresses, interfaces, gateways

from kube_hunter.conf import get_config
from kube_hunter.modules.discovery.kubernetes_client import list_all_k8s_cluster_nodes
from kube_hunter.core.events import handler
from kube_hunter.core.events.types import Event, NewHostEvent, Vulnerability
from kube_hunter.core.types import Discovery, InformationDisclosure, AWS, Azure
Expand Down Expand Up @@ -114,6 +115,9 @@ def __init__(self, event):

def execute(self):
config = get_config()
# Attempt to read all hosts from the Kubernetes API
for host in list_all_k8s_cluster_nodes(config.kubeconfig):
self.publish_event(NewHostEvent(host=host))
# Scan any hosts that the user specified
if config.remote or config.cidr:
self.publish_event(HostScanEvent())
Expand Down Expand Up @@ -298,6 +302,9 @@ def execute(self):
elif len(config.remote) > 0:
for host in config.remote:
self.publish_event(NewHostEvent(host=host))
elif config.k8s_auto_discover_nodes:
for host in list_all_k8s_cluster_nodes(config.kubeconfig):
self.publish_event(NewHostEvent(host=host))

# for normal scanning
def scan_interfaces(self):
Expand Down
27 changes: 27 additions & 0 deletions kube_hunter/modules/discovery/kubernetes_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import logging
import kubernetes


def list_all_k8s_cluster_nodes(kube_config=None, client=None):
logger = logging.getLogger(__name__)
try:
if kube_config:
logger.info("Attempting to use kubeconfig file: %s", kube_config)
kubernetes.config.load_kube_config(config_file=kube_config)
else:
logger.info("Attempting to use in cluster Kubernetes config")
kubernetes.config.load_incluster_config()
except kubernetes.config.config_exception.ConfigException:
logger.exception("Failed to initiate Kubernetes client")
return

try:
if client is None:
client = kubernetes.client.CoreV1Api()
ret = client.list_node(watch=False)
logger.info("Listed %d nodes in the cluster" % len(ret.items))
for item in ret.items:
for addr in item.status.addresses:
yield addr.address
except:
logger.exception("Failed to list nodes from Kubernetes")
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ install_requires =
packaging
dataclasses
pluggy
kubernetes==12.0.1
setup_requires =
setuptools>=30.3.0
setuptools_scm
Expand Down
31 changes: 31 additions & 0 deletions tests/discovery/test_k8s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from kube_hunter.conf import Config, set_config

set_config(Config())

from kube_hunter.modules.discovery.kubernetes_client import list_all_k8s_cluster_nodes
from unittest.mock import MagicMock, patch



def test_client_yields_ips():
client = MagicMock()
response = MagicMock()
client.list_node.return_value = response
response.items = [MagicMock(), MagicMock()]
response.items[0].status.addresses = [MagicMock(), MagicMock()]
response.items[0].status.addresses[0].address = "127.0.0.1"
response.items[0].status.addresses[1].address = "127.0.0.2"
response.items[1].status.addresses = [MagicMock()]
response.items[1].status.addresses[0].address = "127.0.0.3"

with patch('kubernetes.config.load_incluster_config') as m:
output = list(list_all_k8s_cluster_nodes(client=client))
m.assert_called_once()

assert output == ["127.0.0.1", "127.0.0.2", "127.0.0.3"]


def test_client_uses_kubeconfig():
with patch('kubernetes.config.load_kube_config') as m:
list(list_all_k8s_cluster_nodes(kube_config="/location", client=MagicMock()))
m.assert_called_once_with(config_file="/location")

0 comments on commit 6689005

Please sign in to comment.