feat(k8s): Add kubeconfig content authentication (#5397)

Co-authored-by: Sergio <sergio@prowler.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Pedro Martín
2024-10-17 09:56:51 +02:00
committed by GitHub
parent a024ab31a0
commit 2b8b223403
3 changed files with 174 additions and 20 deletions
@@ -25,7 +25,11 @@ class KubernetesBaseException(ProwlerException):
"message": "An error occurred in the Kubernetes provider.",
"remediation": "Check the provider code and configuration to identify the issue. For more information on troubleshooting Kubernetes providers, refer to the Kubernetes documentation: https://kubernetes.io/docs/reference/",
},
(1936, "KubernetesInvalidKubeConfigFileError"): {
(1936, "KubernetesInvalidProviderIdError"): {
"message": "The provider ID is invalid.",
"remediation": "Check the provider ID and ensure it is correctly formatted. Refer to the Kubernetes documentation for guidance on provider IDs: https://kubernetes.io/docs/reference/access-authn-authz/rbac/",
},
(1937, "KubernetesInvalidKubeConfigFileError"): {
"message": "The provided kube-config is invalid.",
"remediation": "Review the kube-config and the attached error to get more details. Please, refer to the Kubernetes config documentation: https://kubernetes.io/docs/reference/config-api/kubeconfig.v1/#Config",
},
@@ -76,6 +80,11 @@ class KubernetesTimeoutError(KubernetesBaseException):
super().__init__(1934, file, original_exception, message)
class KubernetesInvalidKubeConfigFileError(KubernetesBaseException):
class KubernetesInvalidProviderIdError(KubernetesBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(1936, file, original_exception, message)
class KubernetesInvalidKubeConfigFileError(KubernetesBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(1937, file, original_exception, message)
@@ -4,7 +4,7 @@ from colorama import Fore, Style
from kubernetes.client.exceptions import ApiException
from kubernetes.config.config_exception import ConfigException
from requests.exceptions import Timeout
from yaml import safe_load
from yaml import parser, safe_load
from kubernetes import client, config
from prowler.config.config import get_default_mute_file_path
@@ -17,6 +17,7 @@ from prowler.providers.kubernetes.exceptions.exceptions import (
KubernetesCloudResourceManagerAPINotUsedError,
KubernetesError,
KubernetesInvalidKubeConfigFileError,
KubernetesInvalidProviderIdError,
KubernetesSetUpSessionError,
KubernetesTimeoutError,
)
@@ -146,7 +147,7 @@ class KubernetesProvider(Provider):
def setup_session(
kubeconfig_file: str = None,
kubeconfig_content: dict = None,
input_context: str = None,
context: str = None,
) -> KubernetesSession:
"""
Sets up the Kubernetes session.
@@ -154,7 +155,7 @@ class KubernetesProvider(Provider):
Args:
kubeconfig_file (str): Path to the kubeconfig file.
kubeconfig_content (dict): Content of the kubeconfig file.
input_context (str): Context name.
context (str): Context name.
Returns:
Tuple: A tuple containing the API client and the context.
@@ -163,8 +164,9 @@ class KubernetesProvider(Provider):
try:
if kubeconfig_content:
config.load_kube_config_from_dict(
kubeconfig_content, context=input_context
safe_load(kubeconfig_content), context=context
)
else:
try:
config.load_kube_config(
@@ -173,7 +175,7 @@ class KubernetesProvider(Provider):
if kubeconfig_file != "~/.kube/config"
else os.path.expanduser(kubeconfig_file)
),
context=input_context,
context=context,
)
except ConfigException:
# If the kubeconfig file is not found, try to use the in-cluster config
@@ -189,22 +191,36 @@ class KubernetesProvider(Provider):
return KubernetesSession(
api_client=client.ApiClient(), context=context
)
if input_context:
if context:
contexts = config.list_kube_config_contexts()[0]
for context_item in contexts:
if context_item["name"] == input_context:
if context_item["name"] == context:
context = context_item
else:
context = config.list_kube_config_contexts()[1]
return KubernetesSession(api_client=client.ApiClient(), context=context)
except parser.ParserError as parser_error:
logger.critical(
f"{parser_error.__class__.__name__}[{parser_error.__traceback__.tb_lineno}]: {parser_error}"
)
raise KubernetesInvalidKubeConfigFileError(
original_exception=parser_error, file=os.path.abspath(__file__)
)
except ConfigException as config_error:
logger.critical(
f"{config_error.__class__.__name__}[{config_error.__traceback__.tb_lineno}]: {config_error}"
)
raise KubernetesInvalidKubeConfigFileError(
original_exception=config_error, file=os.path.abspath(__file__)
)
if f"Expected object with name {context} in kube-config/contexts" in str(
config_error
):
raise KubernetesInvalidProviderIdError(
original_exception=config_error, file=os.path.abspath(__file__)
)
else:
raise KubernetesInvalidKubeConfigFileError(
original_exception=config_error, file=os.path.abspath(__file__)
)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -218,7 +234,7 @@ class KubernetesProvider(Provider):
kubeconfig_file: str = "~/.kube/config",
kubeconfig_content: dict = None,
namespace: str = None,
context: str = None,
provider_id: str = None,
raise_on_exception: bool = True,
) -> Connection:
"""
@@ -228,15 +244,16 @@ class KubernetesProvider(Provider):
kubeconfig_file (str): Path to the kubeconfig file.
kubeconfig_content (dict): Content of the kubeconfig file.
namespace (str): Namespace name.
context (str): Context name.
provider_id (str): Provider ID to use, in this case, the Kubernetes context.
raise_on_exception (bool): Whether to raise an exception on error.
Returns:
Connection: A Connection object.
"""
try:
kubeconfig_content = safe_load(kubeconfig_content)
KubernetesProvider.setup_session(
kubeconfig_file, kubeconfig_content, context
kubeconfig_file=kubeconfig_file,
kubeconfig_content=kubeconfig_content,
context=provider_id,
)
if namespace:
client.CoreV1Api().list_namespaced_pod(
@@ -245,6 +262,20 @@ class KubernetesProvider(Provider):
else:
client.CoreV1Api().list_namespace(timeout_seconds=2, _request_timeout=2)
return Connection(is_connected=True)
except KubernetesInvalidKubeConfigFileError as invalid_kubeconfig_error:
logger.critical(
f"KubernetesInvalidKubeConfigFileError[{invalid_kubeconfig_error.__traceback__.tb_lineno}]: {invalid_kubeconfig_error}"
)
if raise_on_exception:
raise invalid_kubeconfig_error
return Connection(error=invalid_kubeconfig_error)
except KubernetesInvalidProviderIdError as invalid_provider_id_error:
logger.critical(
f"KubernetesInvalidProviderIdError[{invalid_provider_id_error.__traceback__.tb_lineno}]: {invalid_provider_id_error}"
)
if raise_on_exception:
raise invalid_provider_id_error
return Connection(error=invalid_provider_id_error)
except KubernetesSetUpSessionError as setup_session_error:
logger.critical(
f"KubernetesSetUpSessionError[{setup_session_error.__traceback__.tb_lineno}]: {setup_session_error}"
@@ -7,6 +7,9 @@ from prowler.config.config import (
default_fixer_config_file_path,
load_and_validate_config_file,
)
from prowler.providers.kubernetes.exceptions.exceptions import (
KubernetesSetUpSessionError,
)
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
from prowler.providers.kubernetes.models import (
KubernetesIdentityInfo,
@@ -111,7 +114,7 @@ class TestKubernetesProvider:
connection = KubernetesProvider.test_connection(
kubeconfig_file=None,
kubeconfig_content=kubeconfig_content,
context="example-context",
provider_id="example-context",
raise_on_exception=False,
)
@@ -146,7 +149,7 @@ class TestKubernetesProvider:
connection = KubernetesProvider.test_connection(
kubeconfig_file="dummy_kubeconfig_path",
kubeconfig_content="",
context="test-context",
provider_id="test-context",
raise_on_exception=False,
)
@@ -182,10 +185,121 @@ class TestKubernetesProvider:
]
connection = KubernetesProvider.test_connection(
kubeconfig_file="dummy_kubeconfig_path",
kubeconfig_file="",
kubeconfig_content="",
namespace="test-namespace",
context="test-context",
provider_id="test-context",
raise_on_exception=False,
)
assert connection.is_connected
assert connection.error is None
@patch(
"prowler.providers.kubernetes.kubernetes_provider.client.CoreV1Api.list_namespace"
)
@patch("kubernetes.config.list_kube_config_contexts")
@patch("kubernetes.config.load_kube_config_from_dict")
def test_kubernetes_test_connection_with_kubeconfig_content_invalid_provider_id(
self,
mock_load_kube_config_from_dict,
mock_list_kube_config_contexts,
mock_list_namespace,
):
mock_load_kube_config_from_dict.return_value = None
mock_list_kube_config_contexts.return_value = (
[
{
"name": "example-context",
"context": {
"cluster": "example-cluster",
"user": "example-user",
},
}
],
None,
)
mock_list_namespace.return_value.items = [
client.V1Namespace(metadata=client.V1ObjectMeta(name="namespace-1")),
]
kubeconfig_content = {
"apiVersion": "v1",
"clusters": [
{
"cluster": {
"server": "https://kubernetes.example.com",
},
"name": "example-cluster",
}
],
"contexts": [
{
"context": {
"cluster": "example-cluster",
"user": "example-user",
},
"name": "example-context",
}
],
"current-context": "example-context",
"kind": "Config",
"preferences": {},
"users": [
{
"name": "example-user",
"user": {
"token": "EXAMPLE_TOKEN",
},
}
],
}
connection = KubernetesProvider.test_connection(
kubeconfig_file=None,
kubeconfig_content=kubeconfig_content,
provider_id="example-context-invalid",
raise_on_exception=False,
)
assert not connection.is_connected
assert connection.error is not None
assert isinstance(connection.error, KubernetesSetUpSessionError)
@patch(
"prowler.providers.kubernetes.kubernetes_provider.client.CoreV1Api.list_namespace"
)
@patch("kubernetes.config.list_kube_config_contexts")
@patch("kubernetes.config.load_kube_config_from_dict")
def test_kubernetes_test_connection_with_kubeconfig_content_valid_provider_id(
self,
mock_load_kube_config_from_dict,
mock_list_kube_config_contexts,
mock_list_namespace,
):
mock_load_kube_config_from_dict.return_value = None
mock_list_kube_config_contexts.return_value = (
[
{
"name": "example-context",
"context": {
"cluster": "example-cluster",
"user": "example-user",
},
}
],
None,
)
mock_list_namespace.return_value.items = [
client.V1Namespace(metadata=client.V1ObjectMeta(name="namespace-1")),
]
kubeconfig_content = '{"apiVersion": "v1", "clusters": [{"cluster": {"server": "https://kubernetes.example.com"}, "name": "example-cluster"}], "contexts": [{"context": {"cluster": "example-cluster", "user": "example-user"}, "name": "example-context"}], "current-context": "example-context", "kind": "Config", "preferences": {}, "users": [{"name": "example-user", "user": {"token": "EXAMPLE_TOKEN"}}]}'
connection = KubernetesProvider.test_connection(
kubeconfig_file=None,
kubeconfig_content=kubeconfig_content,
provider_id="example-context",
raise_on_exception=False,
)