feat(kubelet): add 6 checks of Kubelet configuration files on the worker nodes (#3335)

Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Sergio Garcia
2024-02-28 18:32:45 +01:00
committed by GitHub
parent 3c4e5a14f7
commit 6197cf792d
22 changed files with 541 additions and 3 deletions
+60
View File
@@ -1,5 +1,7 @@
import grp
import json
import os
import pwd
import sys
import tempfile
from datetime import datetime
@@ -8,6 +10,7 @@ from io import TextIOWrapper
from ipaddress import ip_address
from os.path import exists
from time import mktime
from typing import Optional
from detect_secrets import SecretsCollection
from detect_secrets.settings import default_settings
@@ -102,3 +105,60 @@ def outputs_unix_timestamp(is_unix_timestamp: bool, timestamp: datetime):
else:
timestamp = timestamp.isoformat()
return timestamp
def get_file_permissions(file_path: str) -> Optional[str]:
"""
Retrieves the permissions of a file.
Args:
file_path (str): The path to the file.
Returns:
Optional[str]: The file permissions in octal format, or None if an error occurs.
"""
try:
# Get file status
file_stat = os.stat(file_path)
# Extract permission bits using bitwise AND and formatting as octal
permissions = oct(file_stat.st_mode & 0o777)
return permissions
except Exception as e:
logger.error(
f"{file_path}: {e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}"
)
return None
def is_owned_by_root(file_path: str) -> bool:
"""
Checks if a file is owned by the root user and group.
Args:
file_path (str): The path to the file.
Returns:
bool: True if owned by root, False otherwise or None if file does not exist.
"""
try:
# Get the file's status
file_stat = os.stat(file_path)
# Get the user and group names from their IDs
user_name = pwd.getpwuid(file_stat.st_uid).pw_name
group_name = grp.getgrgid(file_stat.st_gid).gr_name
# Check if both user and group are 'root'
return user_name == "root" and group_name == "root"
except FileNotFoundError as e:
logger.error(
f"{file_path}: {e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}"
)
return None
except Exception as e:
logger.error(
f"{file_path}: {e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}"
)
return False
@@ -11,9 +11,7 @@ class core_minimize_admission_windows_hostprocess_containers(Check):
report.resource_name = pod.name
report.resource_id = pod.uid
report.status = "PASS"
report.status_extended = (
f"Pod {pod.name} does not have the ability to run a Windows HostProcess."
)
report.status_extended = f"Pod {pod.name} does not have the ability to run a Windows HostProcess."
for container in pod.containers.values():
if (
@@ -1,3 +1,4 @@
import socket
from dataclasses import dataclass
from typing import List, Optional
@@ -19,6 +20,9 @@ class Core(KubernetesService):
self.__get_pods__()
self.config_maps = {}
self.__list_config_maps__()
self.nodes = {}
self.__list_nodes__()
self.__in_worker_node__()
def __get_pods__(self):
try:
@@ -93,6 +97,41 @@ class Core(KubernetesService):
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def __list_nodes__(self):
try:
response = self.client.list_node()
for node in response.items:
node_model = Node(
name=node.metadata.name,
uid=node.metadata.uid,
namespace=node.metadata.namespace
if node.metadata.namespace
else "cluster-wide",
labels=node.metadata.labels,
annotations=node.metadata.annotations,
unschedulable=node.spec.unschedulable,
node_info=node.status.node_info.to_dict()
if node.status.node_info
else None,
)
self.nodes[node.metadata.uid] = node_model
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def __in_worker_node__(self):
try:
hostname = socket.gethostname()
for node in self.nodes.values():
if hostname == node.name:
node.inside = True
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
@dataclass
class Container:
@@ -131,3 +170,14 @@ class ConfigMap(BaseModel):
labels: Optional[dict]
kubelet_args: list = []
annotations: Optional[dict]
class Node(BaseModel):
name: str
uid: str
namespace: str
labels: Optional[dict]
annotations: Optional[dict]
unschedulable: Optional[bool]
node_info: Optional[dict]
inside: bool = False
@@ -0,0 +1,36 @@
{
"Provider": "kubernetes",
"CheckID": "kubelet_conf_file_ownership",
"CheckTitle": "Ensure kubelet.conf file ownership is set to root:root",
"CheckType": [
"Security",
"Configuration"
],
"ServiceName": "kubelet",
"SubServiceName": "Config File Ownership",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KubernetesWorkerNode",
"Description": "Ensure that the kubelet.conf file, which is the kubeconfig file for the node, has its file ownership set to root:root. This check verifies the proper ownership settings to maintain the security and integrity of the node's configuration.",
"Risk": "Incorrect file ownership settings on kubelet.conf can lead to unauthorized access and potential security vulnerabilities.",
"RelatedUrl": "https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/kubelet-integration/",
"Remediation": {
"Code": {
"CLI": "chown root:root /etc/kubernetes/kubelet.conf",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Ensure kubelet.conf file ownership is correctly set to protect the node's configuration.",
"Url": "https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/"
}
},
"Categories": [
"Node Security",
"Compliance"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Regular checks of kubelet.conf file ownership are essential for maintaining node security."
}
@@ -0,0 +1,29 @@
from prowler.lib.check.models import Check, Check_Report_Kubernetes
from prowler.lib.utils.utils import is_owned_by_root
from prowler.providers.kubernetes.services.core.core_client import core_client
class kubelet_conf_file_ownership(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
for node in core_client.nodes.values():
report = Check_Report_Kubernetes(self.metadata())
report.namespace = node.namespace
report.resource_name = node.name
report.resource_id = node.uid
# It can only be checked if Prowler is being executed inside a worker node or if the file is the default one
if node.inside:
if is_owned_by_root("/etc/kubernetes/kubelet.conf") is None:
report.status = "MANUAL"
report.status_extended = f"kubelet.conf file not found in Node {node.name}, please verify kubelet.conf file ownership manually."
else:
report.status = "PASS"
report.status_extended = f"kubelet.conf file ownership is set to root:root in Node {node.name}."
if not is_owned_by_root("/etc/kubernetes/kubelet.conf"):
report.status = "FAIL"
report.status_extended = f"kubelet.conf file ownership is not set to root:root in Node {node.name}."
else:
report.status = "MANUAL"
report.status_extended = f"Prowler is not being executed inside Node {node.name}, please verify kubelet.conf file ownership manually."
findings.append(report)
return findings
@@ -0,0 +1,36 @@
{
"Provider": "kubernetes",
"CheckID": "kubelet_conf_file_permissions",
"CheckTitle": "Ensure kubelet.conf file permissions are set to 600 or more restrictive",
"CheckType": [
"Security",
"Configuration"
],
"ServiceName": "kubelet",
"SubServiceName": "Config File Permissions",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KubernetesWorkerNode",
"Description": "Ensure that the kubelet.conf file, which is the kubeconfig file for the node, has permissions set to 600 or more restrictive. This ensures the integrity and security of the node's configuration.",
"Risk": "Improper permissions on kubelet.conf can expose sensitive configuration data, potentially leading to cluster security compromises.",
"RelatedUrl": "https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/kubelet-integration/",
"Remediation": {
"Code": {
"CLI": "chmod 600 /etc/kubernetes/kubelet.conf",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Ensure kubelet.conf file permissions are correctly set to protect the node's configuration.",
"Url": "https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/"
}
},
"Categories": [
"Node Security",
"Compliance"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Regular checks of kubelet.conf file permissions are essential for maintaining node security."
}
@@ -0,0 +1,29 @@
from prowler.lib.check.models import Check, Check_Report_Kubernetes
from prowler.lib.utils.utils import get_file_permissions
from prowler.providers.kubernetes.services.core.core_client import core_client
class kubelet_conf_file_permissions(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
for node in core_client.nodes.values():
report = Check_Report_Kubernetes(self.metadata())
report.namespace = node.namespace
report.resource_name = node.name
report.resource_id = node.uid
# It can only be checked if Prowler is being executed inside a worker node or if the file is the default one
if node.inside:
if not get_file_permissions("/etc/kubernetes/kubelet.conf"):
report.status = "MANUAL"
report.status_extended = f"Kubelet.conf file not found in Node {node.name}, please verify kubelet.conf file permissions manually."
else:
report.status = "PASS"
report.status_extended = f"kubelet.conf file permissions are set to 600 or more restrictive in Node {node.name}."
if get_file_permissions("/etc/kubernetes/kubelet.conf") > 0o600:
report.status = "FAIL"
report.status_extended = f"kubelet.conf file permissions are not set to 600 or more restrictive in Node {node.name}."
else:
report.status = "MANUAL"
report.status_extended = f"Prowler is not being executed inside Node {node.name}, please verify kubelet.conf file permissions manually."
findings.append(report)
return findings
@@ -0,0 +1,36 @@
{
"Provider": "kubernetes",
"CheckID": "kubelet_config_yaml_ownership",
"CheckTitle": "Validate kubelet config.yaml File Ownership",
"CheckType": [
"Security",
"Configuration"
],
"ServiceName": "kubelet",
"SubServiceName": "Config File Ownership",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KubernetesWorkerNode",
"Description": "Ensure that if the kubelet refers to a configuration file with the --config argument, that file is owned by root:root. The kubelet config file contains various critical parameters for the kubelet service on worker nodes, and its ownership should be strictly controlled.",
"Risk": "Improper file ownership on kubelet config.yaml can expose sensitive data or allow unauthorized modifications.",
"RelatedUrl": "https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/",
"Remediation": {
"Code": {
"CLI": "chown root:root /var/lib/kubelet/config.yaml",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Secure the kubelet configuration by enforcing strict file ownership.",
"Url": "https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/"
}
},
"Categories": [
"Node Security",
"Compliance"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Regularly verify the file ownership of kubelet config files to ensure they are not altered unexpectedly."
}
@@ -0,0 +1,29 @@
from prowler.lib.check.models import Check, Check_Report_Kubernetes
from prowler.lib.utils.utils import is_owned_by_root
from prowler.providers.kubernetes.services.core.core_client import core_client
class kubelet_config_yaml_ownership(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
for node in core_client.nodes.values():
report = Check_Report_Kubernetes(self.metadata())
report.namespace = node.namespace
report.resource_name = node.name
report.resource_id = node.uid
# It can only be checked if Prowler is being executed inside a worker node or if the file is the default one
if node.inside:
if is_owned_by_root("/var/lib/kubelet/config.yaml") is None:
report.status = "MANUAL"
report.status_extended = f"Kubelet config.yaml file not found in Node {node.name}, please verify kubelet config.yaml file ownership manually."
else:
report.status = "PASS"
report.status_extended = f"kubelet config.yaml file ownership is set to root:root in Node {node.name}."
if not is_owned_by_root("/var/lib/kubelet/config.yaml"):
report.status = "FAIL"
report.status_extended = f"kubelet config.yaml file ownership is set to root:root in Node {node.name}."
else:
report.status = "MANUAL"
report.status_extended = f"Prowler is not being executed inside Node {node.name}, please verify kubelet config.yaml file permissions manually."
findings.append(report)
return findings
@@ -0,0 +1,36 @@
{
"Provider": "kubernetes",
"CheckID": "kubelet_config_yaml_permissions",
"CheckTitle": "Validate kubelet config.yaml File Permissions",
"CheckType": [
"Security",
"Configuration"
],
"ServiceName": "kubelet",
"SubServiceName": "Config File Permissions",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KubernetesWorkerNode",
"Description": "Ensure that if the kubelet refers to a configuration file with the --config argument, that file has permissions of 600 or more restrictive. The kubelet config file contains various critical parameters for the kubelet service on worker nodes, and its permissions should be strictly controlled.",
"Risk": "Improper file permissions on kubelet config.yaml can expose sensitive data or allow unauthorized modifications.",
"RelatedUrl": "https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/",
"Remediation": {
"Code": {
"CLI": "chmod 600 /var/lib/kubelet/config.yaml",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Secure the kubelet configuration by enforcing strict file permissions.",
"Url": "https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/"
}
},
"Categories": [
"Node Security",
"Compliance"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Regularly verify the file permissions of kubelet config files to ensure they are not altered unexpectedly."
}
@@ -0,0 +1,29 @@
from prowler.lib.check.models import Check, Check_Report_Kubernetes
from prowler.lib.utils.utils import get_file_permissions
from prowler.providers.kubernetes.services.core.core_client import core_client
class kubelet_config_yaml_permissions(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
for node in core_client.nodes.values():
report = Check_Report_Kubernetes(self.metadata())
report.namespace = node.namespace
report.resource_name = node.name
report.resource_id = node.uid
# It can only be checked if Prowler is being executed inside a worker node or if the file is the default one
if node.inside:
if not get_file_permissions("/var/lib/kubelet/config.yaml"):
report.status = "MANUAL"
report.status_extended = f"Kubelet config.yaml file not found in Node {node.name}, please verify kubelet config.yaml file permissions manually."
else:
report.status = "PASS"
report.status_extended = f"kubelet config.yaml file permissions are set to 600 or more restrictive in Node {node.name}."
if get_file_permissions("/var/lib/kubelet/config.yaml") > 0o600:
report.status = "FAIL"
report.status_extended = f"kubelet config.yaml file permissions are not set to 600 or more restrictive in Node {node.name}."
else:
report.status = "MANUAL"
report.status_extended = f"Prowler is not being executed inside Node {node.name}, please verify kubelet config.yaml file permissions manually."
findings.append(report)
return findings
@@ -0,0 +1,36 @@
{
"Provider": "kubernetes",
"CheckID": "kubelet_service_file_ownership_root",
"CheckTitle": "Ensure that the kubelet service file ownership is set to root:root",
"CheckType": [
"Security",
"Configuration"
],
"ServiceName": "kubelet",
"SubServiceName": "Service File Ownership",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KubernetesWorkerNode",
"Description": "This check ensures that the kubelet service file on each Node is owned by root. Proper file ownership is critical for the security and integrity of the kubelet service configuration.",
"Risk": "Incorrect ownership settings can lead to unauthorized modifications, potentially compromising the security and functionality of the kubelet service.",
"RelatedUrl": "https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/kubelet-integration/",
"Remediation": {
"Code": {
"CLI": "chown root:root /etc/systemd/system/kubelet.service.d/kubeadm.conf",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Set the kubelet service file ownership to root:root to maintain its integrity.",
"Url": "https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-config/"
}
},
"Categories": [
"Node Security",
"Compliance"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Regular checks for file ownership can prevent unauthorized changes."
}
@@ -0,0 +1,36 @@
from prowler.lib.check.models import Check, Check_Report_Kubernetes
from prowler.lib.utils.utils import is_owned_by_root
from prowler.providers.kubernetes.services.core.core_client import core_client
class kubelet_service_file_ownership_root(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
for node in core_client.nodes.values():
report = Check_Report_Kubernetes(self.metadata())
report.namespace = node.namespace
report.resource_name = node.name
report.resource_id = node.uid
# It can only be checked if Prowler is being executed inside a worker node or if the file is the default one
if node.inside:
if (
is_owned_by_root(
"/etc/systemd/system/kubelet.service.d/kubeadm.conf"
)
is None
):
report.status = "MANUAL"
report.status_extended = f"Kubelet service file not found in Node {node.name}, please verify Kubelet service file ownership manually."
else:
report.status = "PASS"
report.status_extended = f"Kubelet service file ownership is set to root:root in Node {node.name}."
if not is_owned_by_root(
"/etc/systemd/system/kubelet.service.d/kubeadm.conf"
):
report.status = "FAIL"
report.status_extended = f"Kubelet service file ownership is not set to root:root in Node {node.name}."
else:
report.status = "MANUAL"
report.status_extended = f"Prowler is not being executed inside Node {node.name}, please verify Kubelet service file ownership manually."
findings.append(report)
return findings
@@ -0,0 +1,36 @@
{
"Provider": "kubernetes",
"CheckID": "kubelet_service_file_permissions",
"CheckTitle": "Ensure that the kubelet service file permissions are set to 600 or more restrictive",
"CheckType": [
"Security",
"Configuration"
],
"ServiceName": "kubelet",
"SubServiceName": "Kubelet Service File Permission",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KubernetesNode",
"Description": "This check ensures that the kubelet service file on worker nodes has permissions set to 600 or more restrictive, limiting the file's write access to only system administrators. This measure is crucial to maintain the integrity and security of the kubelet service configuration.",
"Risk": "Improper file permissions on the kubelet service file could lead to unauthorized modifications, compromising node security and stability.",
"RelatedUrl": "https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-config/",
"Remediation": {
"Code": {
"CLI": "chmod 600 /etc/systemd/system/kubelet.service.d/kubeadm.conf",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Ensure the kubelet service file is securely configured with restrictive permissions.",
"Url": "https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/#44-joining-your-nodes"
}
},
"Categories": [
"Node Security",
"Configuration Management"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "The file location may vary based on the Kubernetes installation and should be verified for each cluster."
}
@@ -0,0 +1,36 @@
from prowler.lib.check.models import Check, Check_Report_Kubernetes
from prowler.lib.utils.utils import get_file_permissions
from prowler.providers.kubernetes.services.core.core_client import core_client
class kubelet_service_file_permissions(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
for node in core_client.nodes.values():
report = Check_Report_Kubernetes(self.metadata())
report.namespace = node.namespace
report.resource_name = node.name
report.resource_id = node.uid
# It can only be checked if Prowler is being executed inside a worker node or if the file is the default one
if node.inside:
if not get_file_permissions(
"/etc/systemd/system/kubelet.service.d/kubeadm.conf"
):
report.status = "MANUAL"
report.status_extended = f"Kubelet service file not found in Node {node.name}, please verify Kubelet service file permissions manually."
else:
report.status = "PASS"
report.status_extended = f"Kubelet service file permissions are set to 600 or more restrictive in Node {node.name}."
if (
get_file_permissions(
"/etc/systemd/system/kubelet.service.d/kubeadm.conf"
)
> 0o600
):
report.status = "FAIL"
report.status_extended = f"Kubelet service file permissions are not set to 600 or more restrictive in Node {node.name}."
else:
report.status = "MANUAL"
report.status_extended = f"Prowler is not being executed inside Node {node.name}, please verify Kubelet service file permissions manually."
findings.append(report)
return findings
+26
View File
@@ -9,7 +9,9 @@ from mock import patch
from prowler.lib.utils.utils import (
detect_secrets_scan,
file_exists,
get_file_permissions,
hash_sha512,
is_owned_by_root,
open_file,
outputs_unix_timestamp,
parse_json_file,
@@ -135,3 +137,27 @@ class Test_outputs_unix_timestamp:
def test_outputs_unix_timestamp_true(self):
time = datetime.now()
assert outputs_unix_timestamp(True, time) == mktime(time.timetuple())
class TestFilePermissions:
def test_get_file_permissions(self):
# Create a temporary file with known permissions
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
os.chmod(temp_file.name, 0o644) # Set permissions to 644 (-rw-r--r--)
permissions = get_file_permissions(temp_file.name)
assert permissions == "0o644"
os.unlink(temp_file.name)
assert not get_file_permissions("not_existing_file")
def test_is_owned_by_root(self):
# Create a temporary file with known permissions
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
os.chmod(temp_file.name, 0o644) # Set permissions to 644 (-rw-r--r--)
# Check ownership for the temporary file
is_root = is_owned_by_root(temp_file.name)
assert not is_root
os.unlink(temp_file.name)
assert not is_owned_by_root("not_existing_file")
assert is_owned_by_root("/etc/passwd")