From 909dc865c599680299765c3f340dab49545c379f Mon Sep 17 00:00:00 2001 From: Maciej Skuratowski Date: Tue, 5 May 2026 21:46:02 +0200 Subject: [PATCH] [K8s] Make pod and container securityContext configurable (closes #1432) --- docs/source/compute_config/kubernetes.md | 27 +++++++++++++++++++++++ lithops/serverless/backends/k8s/config.py | 17 ++++++++++++++ lithops/serverless/backends/k8s/k8s.py | 16 ++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/docs/source/compute_config/kubernetes.md b/docs/source/compute_config/kubernetes.md index 796f67d89..671ba56f6 100644 --- a/docs/source/compute_config/kubernetes.md +++ b/docs/source/compute_config/kubernetes.md @@ -76,6 +76,33 @@ k8s: |k8s | runtime_memory | 512 |no | Memory limit in MB. Default 512MB | |k8s | runtime_timeout | 600 |no | Runtime timeout in seconds. Default 600 seconds | |k8s | master_timeout | 600 |no | Master pod timeout in seconds. Default 600 seconds | +|k8s | container_security_context | PSS Baseline (drop ALL caps, no privilege escalation, RuntimeDefault seccomp) | no | Mapping injected as the container `securityContext` on every Lithops pod. Set to `null` to disable. | +|k8s | pod_security_context | | no | Mapping injected as the pod-level `securityContext`. Required for clusters enforcing Pod Security Standards Restricted (e.g. EGI Rancher, GKE Autopilot, OpenShift). Requires a non-root runtime image. | + +## Running on Pod Security Standards Restricted clusters + +Clusters enforcing the [Pod Security Standards "Restricted"](https://kubernetes.io/docs/concepts/security/pod-security-standards/) profile (Rancher with EGI policies, GKE Autopilot, OpenShift, AKS with Azure Policy, EKS with admission controllers) require pods to run as a non-root user with additional hardening. Set `pod_security_context` and use a runtime image that has a non-root `USER` directive: + +```yaml +k8s: + runtime: /: + pod_security_context: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + container_security_context: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault +``` + +Providing `container_security_context` fully replaces the defaults — copy the snippet above and adjust if you want to extend rather than override. ## Test Lithops diff --git a/lithops/serverless/backends/k8s/config.py b/lithops/serverless/backends/k8s/config.py index ed6f4a6e1..6e8f46363 100644 --- a/lithops/serverless/backends/k8s/config.py +++ b/lithops/serverless/backends/k8s/config.py @@ -26,6 +26,14 @@ 'docker_server': 'docker.io' } +# Pod Security Standards "Baseline"-aligned container defaults; safe to +# enable without runtime image changes. Override via `container_security_context`. +DEFAULT_CONTAINER_SECURITY_CONTEXT = { + 'allowPrivilegeEscalation': False, + 'capabilities': {'drop': ['ALL']}, + 'seccompProfile': {'type': 'RuntimeDefault'}, +} + DEFAULT_GROUP = "batch" DEFAULT_VERSION = "v1" MASTER_NAME = "lithops-master" @@ -142,6 +150,15 @@ def load_config(config_data): if key not in config_data['k8s']: config_data['k8s'][key] = DEFAULT_CONFIG_KEYS[key] + if 'container_security_context' not in config_data['k8s']: + config_data['k8s']['container_security_context'] = DEFAULT_CONTAINER_SECURITY_CONTEXT + config_data['k8s'].setdefault('pod_security_context', None) + + for key in ('container_security_context', 'pod_security_context'): + value = config_data['k8s'][key] + if value is not None and not isinstance(value, dict): + raise Exception(f"'{key}' under 'k8s' must be a mapping or null, got {type(value).__name__}") + if 'runtime' in config_data['k8s']: runtime = config_data['k8s']['runtime'] registry = config_data['k8s']['docker_server'] diff --git a/lithops/serverless/backends/k8s/k8s.py b/lithops/serverless/backends/k8s/k8s.py index c8e3881ca..a880676bc 100644 --- a/lithops/serverless/backends/k8s/k8s.py +++ b/lithops/serverless/backends/k8s/k8s.py @@ -125,6 +125,16 @@ def _get_default_runtime_image_name(self): self.name, self.k8s_config, 'lithops-kubernetes-default' ) + def _apply_security_context(self, job_res): + """Inject pod- and container-level securityContext from config (if any).""" + pod_spec = job_res['spec']['template']['spec'] + pod_sc = self.k8s_config.get('pod_security_context') + if pod_sc: + pod_spec['securityContext'] = pod_sc + container_sc = self.k8s_config.get('container_security_context') + if container_sc: + pod_spec['containers'][0]['securityContext'] = container_sc + def build_runtime(self, docker_image_name, dockerfile, extra_args=[]): """ Builds a new runtime from a Docker file and pushes it to the registry @@ -458,6 +468,8 @@ def _start_master(self, docker_image_name): master_res['metadata']['labels']['user'] = self.user master_res['spec']['activeDeadlineSeconds'] = self.k8s_config['master_timeout'] + self._apply_security_context(master_res) + container = master_res['spec']['template']['spec']['containers'][0] container['image'] = docker_image_name container['env'][0]['value'] = 'run_master' @@ -648,6 +660,8 @@ def invoke(self, docker_image_name, runtime_memory, job_payload): job_res['spec']['activeDeadlineSeconds'] = self.k8s_config['runtime_timeout'] job_res['spec']['parallelism'] = total_workers + self._apply_security_context(job_res) + container = job_res['spec']['template']['spec']['containers'][0] container['image'] = docker_image_name if not docker_image_name.endswith(':latest'): @@ -694,6 +708,8 @@ def _generate_runtime_meta(self, docker_image_name): job_res['metadata']['labels']['version'] = 'lithops_v' + __version__ job_res['metadata']['labels']['user'] = self.user + self._apply_security_context(job_res) + container = job_res['spec']['template']['spec']['containers'][0] container['image'] = docker_image_name container['imagePullPolicy'] = 'Always'