diff --git a/docs/IS_CLUSTER_ADMIN.md b/docs/IS_CLUSTER_ADMIN.md new file mode 100644 index 0000000..2eb7d0a --- /dev/null +++ b/docs/IS_CLUSTER_ADMIN.md @@ -0,0 +1,23 @@ +# IS_CLUSTER_ADMIN + +### Overview + +This attack path aims to provide a route for nodes that are expected to grant cluster administrator access just due to the nature of the resource. + +### Description + +Compromise of certain resources within a cluster can be considered to grant cluster administrator due to the nature of the resource compromised. + +For example, compromise of a control plane node within a Kubernetes cluster that runs services such as the API server or etcd effectively grants cluster administrator access. + +### Defense + +Security of resources that are effectively cluster administrator should be reviewed and hardened. + +### Cypher Deep-Dive + +```cypher +MATCH (src:Node), (dest:ClusterRoleBinding)-[:GRANTS_PERMISSION]->(:ClusterRole {name: "cluster-admin"}) WHERE any(x in ["master", "control-plane"] WHERE x in src.node_roles) +``` + +The above query finds nodes `src` that have the `master` or `control-plane` role. The destination is set to a cluster role binding that binds to `cluster-admin`. diff --git a/icekube/attack_paths.py b/icekube/attack_paths.py index 7a541e4..f306081 100644 --- a/icekube/attack_paths.py +++ b/icekube/attack_paths.py @@ -137,4 +137,8 @@ def workload_query( }) """, ], + Relationship.IS_CLUSTER_ADMIN: """ + MATCH (src:Node), (dest:ClusterRoleBinding)-[:GRANTS_PERMISSION]->(:ClusterRole {name: "cluster-admin"}) + WHERE any(x in ["master", "control-plane"] WHERE x in src.node_roles) + """, } diff --git a/icekube/icekube.py b/icekube/icekube.py index 6c15d1f..6e0413d 100644 --- a/icekube/icekube.py +++ b/icekube/icekube.py @@ -72,6 +72,9 @@ def relationship_generator( with driver.session() as session: logger.info(f"Generating relationships for {resource}") for source, relationship, target in resource.relationships(initial): + logger.debug( + f"Creating relationship: {source} -> {relationship} -> {target}" + ) if isinstance(source, Resource): src_cmd, src_kwargs = get(source, prefix="src") else: diff --git a/icekube/models/node.py b/icekube/models/node.py index c232d87..738b3f1 100644 --- a/icekube/models/node.py +++ b/icekube/models/node.py @@ -1,9 +1,26 @@ from __future__ import annotations -from typing import List +from typing import Any, Dict, List from icekube.models.base import Resource +from pydantic import computed_field class Node(Resource): supported_api_groups: List[str] = [""] + + @computed_field # type: ignore + @property + def node_roles(self) -> List[str]: + return [ + x.split("/", 1)[1] + for x in self.labels.keys() + if x.startswith("node-role.kubernetes.io/") + ] + + @property + def db_labels(self) -> Dict[str, Any]: + return { + **super().db_labels, + "node_roles": self.node_roles, + } diff --git a/icekube/relationships.py b/icekube/relationships.py index 9dfe32f..061d629 100644 --- a/icekube/relationships.py +++ b/icekube/relationships.py @@ -11,6 +11,8 @@ class Relationship: in this direction in neo4j: (ObjectOne)-[:RELATIONSHIP]->(ObjectTwo) """ + IS_CLUSTER_ADMIN: ClassVar[str] = "IS_CLUSTER_ADMIN" + HOSTS_POD: ClassVar[str] = "HOSTS_POD" AUTHENTICATION_TOKEN_FOR: ClassVar[str] = "AUTHENTICATION_TOKEN_FOR"