Skip to content

Commit 89c104a

Browse files
committed
added network policies, added additional limitations (no root, no web, no FS), in /editor added vertical scroll
1 parent 49c40c9 commit 89c104a

File tree

7 files changed

+144
-8
lines changed

7 files changed

+144
-8
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ Svelte app includes:
195195
> [!TIP]
196196
> By limiting resources, we ensure fair usage and prevent any single script from hogging the system.
197197
198-
- **CPU & Memory Limits**: Each pod has caps to prevent overuse (128 Mi for RAM and 100m for CPU).
198+
- **CPU & Memory Limits**: Each pod has caps to prevent overuse (128 Mi for RAM and 1000m for CPU).
199199
- **Timeouts**: Scripts can't run forever—they'll stop after a set time (default: 5s).
200200
- **Disk Space**: Limited to prevent excessive storage use.
201201

backend/app/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ class Settings(BaseSettings):
2121
SERVER_PORT: int = 443
2222

2323
# Settings for Kubernetes resource limits and requests
24-
K8S_POD_CPU_LIMIT: str = "100m"
24+
K8S_POD_CPU_LIMIT: str = "1000m"
2525
K8S_POD_MEMORY_LIMIT: str = "128Mi"
26-
K8S_POD_CPU_REQUEST: str = "100m"
26+
K8S_POD_CPU_REQUEST: str = "1000m"
2727
K8S_POD_MEMORY_REQUEST: str = "128Mi"
2828
K8S_POD_EXECUTION_TIMEOUT: int = 5 # in seconds
2929
K8S_POD_PRIORITY_CLASS_NAME: Optional[str] = None

backend/app/services/kubernetes_service.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from app.core.logging import logger
1010
from app.runtime_registry import RUNTIME_REGISTRY
1111
from app.services.circuit_breaker import CircuitBreaker
12+
from app.services.network_policy import NetworkPolicyBuilder
1213
from app.services.pod_manifest_builder import PodManifestBuilder
1314
from fastapi import Depends, Request
1415
from kubernetes import client as k8s_client
@@ -61,13 +62,15 @@ class KubernetesService:
6162

6263
v1: Optional[k8s_client.CoreV1Api]
6364
apps_v1: Optional[k8s_client.AppsV1Api]
65+
networking_v1: Optional[k8s_client.NetworkingV1Api]
6466
version_api: Optional[k8s_client.VersionApi]
6567

6668
def __init__(self, manager: KubernetesServiceManager):
6769
self.settings = get_settings()
6870
self.manager = manager
6971
self.v1 = None
7072
self.apps_v1 = None
73+
self.networking_v1 = None
7174
self.version_api = None
7275
self._initialize_kubernetes_client()
7376

@@ -112,7 +115,7 @@ async def graceful_shutdown(self) -> None:
112115
return
113116
execution_id = pod_name[len("execution-"):]
114117
config_map_name = f"script-{execution_id}"
115-
await self._cleanup_resources(pod_name, config_map_name)
118+
await self._cleanup_resources(pod_name, config_map_name, execution_id)
116119
except Exception as e:
117120
logger.error(f"Error during pod cleanup on shutdown: {str(e)}")
118121

@@ -121,13 +124,15 @@ def _initialize_kubernetes_client(self) -> None:
121124
self._setup_kubernetes_config()
122125
self.v1 = k8s_client.CoreV1Api()
123126
self.apps_v1 = k8s_client.AppsV1Api()
127+
self.networking_v1 = k8s_client.NetworkingV1Api()
124128
self.version_api = k8s_client.VersionApi()
125129
self._test_api_connection()
126130
logger.info("Kubernetes client initialized successfully.")
127131
except Exception as e:
128132
logger.error(f"Failed to initialize Kubernetes client: {str(e)}")
129133
self.v1 = None
130134
self.apps_v1 = None
135+
self.networking_v1 = None
131136
self.version_api = None
132137
raise KubernetesConfigError(f"Failed to initialize Kubernetes client: {str(e)}") from e
133138

@@ -203,14 +208,18 @@ async def create_execution_pod(
203208
pod_manifest = builder.build()
204209
await self._create_namespaced_pod(pod_manifest)
205210

211+
policy_builder = NetworkPolicyBuilder(execution_id, self.NAMESPACE)
212+
policy_manifest = policy_builder.build()
213+
await self._create_network_policy(policy_manifest)
214+
206215
self._active_pods[execution_id] = datetime.now(timezone.utc)
207216
logger.info(f"Successfully created pod '{pod_name}' with image '{image}'")
208217
self.circuit_breaker.record_success()
209218

210219
except Exception as e:
211220
logger.error(f"Failed to create execution pod '{execution_id}': {str(e)}", exc_info=True)
212221
self.circuit_breaker.record_failure()
213-
await self._cleanup_resources(pod_name, config_map_name)
222+
await self._cleanup_resources(pod_name, config_map_name, execution_id)
214223
raise KubernetesPodError(f"Failed to create execution pod: {str(e)}") from e
215224

216225
async def get_pod_logs(self, execution_id: str) -> tuple[dict, str]:
@@ -237,7 +246,7 @@ async def get_pod_logs(self, execution_id: str) -> tuple[dict, str]:
237246
return error_payload, pod_phase
238247
finally:
239248
logger.info(f"Initiating cleanup for execution '{execution_id}'...")
240-
await self._cleanup_resources(pod_name, config_map_name)
249+
await self._cleanup_resources(pod_name, config_map_name, execution_id)
241250
self._active_pods.pop(execution_id, None)
242251

243252
async def _wait_for_pod_completion(self, pod_name: str) -> k8s_client.V1Pod:
@@ -293,7 +302,7 @@ async def _create_namespaced_pod(self, pod_manifest: Dict[str, Any]) -> None:
293302
logger.error(f"Failed to create pod '{pod_name}': {e.status} {e.reason}")
294303
raise KubernetesPodError(f"Failed to create pod: {str(e)}") from e
295304

296-
async def _cleanup_resources(self, pod_name: str, config_map_name: str) -> None:
305+
async def _cleanup_resources(self, pod_name: str, config_map_name: str, execution_id: Optional[str] = None) -> None:
297306
if not self.v1:
298307
return
299308
try:
@@ -309,6 +318,38 @@ async def _cleanup_resources(self, pod_name: str, config_map_name: str) -> None:
309318
except ApiException as e:
310319
logger.error(f"Failed to delete config map '{config_map_name}': {e.reason}")
311320

321+
if execution_id:
322+
policy_name = f"deny-external-{execution_id}"
323+
await self._delete_network_policy(policy_name)
324+
325+
async def _create_network_policy(self, policy_manifest: Dict[str, Any]) -> None:
326+
if not self.networking_v1:
327+
raise KubernetesServiceError("NetworkingV1Api client not initialized.")
328+
policy_name = policy_manifest.get("metadata", {}).get("name", "unknown-policy")
329+
try:
330+
await asyncio.to_thread(
331+
self.networking_v1.create_namespaced_network_policy,
332+
body=policy_manifest,
333+
namespace=self.NAMESPACE
334+
)
335+
logger.info(f"NetworkPolicy '{policy_name}' created successfully.")
336+
except ApiException as e:
337+
logger.error(f"Failed to create NetworkPolicy '{policy_name}': {e.status} {e.reason}")
338+
raise KubernetesServiceError(f"Failed to create NetworkPolicy: {str(e)}") from e
339+
340+
async def _delete_network_policy(self, policy_name: str) -> None:
341+
if not self.networking_v1:
342+
return
343+
try:
344+
await asyncio.to_thread(
345+
self.networking_v1.delete_namespaced_network_policy,
346+
name=policy_name,
347+
namespace=self.NAMESPACE
348+
)
349+
logger.info(f"Deletion request sent for NetworkPolicy '{policy_name}'")
350+
except ApiException as e:
351+
logger.error(f"Failed to delete NetworkPolicy '{policy_name}': {e.reason}")
352+
312353
# DaemonSet: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/
313354
async def ensure_image_pre_puller_daemonset(self) -> None:
314355
if not self.apps_v1:
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import Any, Dict
2+
3+
4+
class NetworkPolicyBuilder:
5+
def __init__(self, execution_id: str, namespace: str = "default"):
6+
self.execution_id = execution_id
7+
self.namespace = namespace
8+
9+
def build(self) -> Dict[str, Any]:
10+
return {
11+
"apiVersion": "networking.k8s.io/v1",
12+
"kind": "NetworkPolicy",
13+
"metadata": {
14+
"name": f"deny-external-{self.execution_id}",
15+
"namespace": self.namespace,
16+
"labels": {
17+
"app": "script-execution",
18+
"execution-id": self.execution_id
19+
}
20+
},
21+
"spec": {
22+
"podSelector": {
23+
"matchLabels": {
24+
"execution-id": self.execution_id
25+
}
26+
},
27+
"policyTypes": ["Ingress", "Egress"],
28+
"ingress": [],
29+
"egress": []
30+
}
31+
}

backend/app/services/pod_manifest_builder.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,38 @@ def build(self) -> Dict[str, Any]:
5757
"volumeMounts": [
5858
{"name": "script-volume", "mountPath": "/scripts", "readOnly": True},
5959
{"name": "entrypoint-vol", "mountPath": "/entry", "readOnly": True},
60+
{"name": "writable-tmp", "mountPath": "/tmp"}
6061
],
6162
"terminationMessagePolicy": "FallbackToLogsOnError",
63+
"securityContext": {
64+
"readOnlyRootFilesystem": True,
65+
"allowPrivilegeEscalation": False,
66+
"runAsNonRoot": True,
67+
"runAsUser": 1000,
68+
"runAsGroup": 1000,
69+
"capabilities": {
70+
"drop": ["ALL"]
71+
}
72+
}
6273
}
6374
],
6475
"volumes": [
6576
{"name": "script-volume", "emptyDir": {}},
6677
{"name": "script-config", "configMap": {"name": self.config_map_name}},
6778
{"name": "entrypoint-vol", "configMap": {"name": self.config_map_name}},
79+
{"name": "writable-tmp", "emptyDir": {}}
6880
],
6981
"restartPolicy": "Never",
7082
"activeDeadlineSeconds": self.pod_execution_timeout + 1,
83+
"hostNetwork": False,
84+
"hostPID": False,
85+
"hostIPC": False,
86+
"dnsPolicy": "None",
87+
"dnsConfig": {
88+
"nameservers": ["127.0.0.1"],
89+
"searches": [],
90+
"options": []
91+
},
7192
}
7293

7394
if self.priority_class_name:

cert-generator/setup-k8s.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ rules:
8989
verbs: ["create", "get", "list", "watch", "delete"]
9090
- apiGroups: ["apps"]
9191
resources: ["daemonsets"]
92-
verbs: ["get", "list", "watch", "create", "delete", "replace"]
92+
verbs: ["get", "list", "watch", "create", "delete", "replace", "update"]
93+
- apiGroups: ["networking.k8s.io"]
94+
resources: ["networkpolicies"]
95+
verbs: ["get", "list", "watch", "create", "delete"]
9396
EOF
9497
kubectl apply -f - <<EOF
9598
apiVersion: rbac.authorization.k8s.io/v1

frontend/src/routes/Editor.svelte

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,19 @@
188188
]),
189189
python(),
190190
EditorView.lineWrapping,
191+
EditorView.theme({
192+
"&": {
193+
height: "100%",
194+
maxHeight: "100%"
195+
},
196+
".cm-content": {
197+
minHeight: "100%"
198+
},
199+
".cm-scroller": {
200+
overflow: "auto",
201+
maxHeight: "100%"
202+
}
203+
}),
191204
EditorView.updateListener.of(update => {
192205
if (update.docChanged) {
193206
script.set(update.state.doc.toString());
@@ -945,6 +958,33 @@
945958
.editor-main-code {
946959
grid-row: 3 / 4;
947960
min-height: 0;
961+
display: flex;
962+
flex-direction: column;
963+
overflow: hidden;
964+
}
965+
966+
.editor-wrapper {
967+
flex: 1 1 auto;
968+
min-height: 0;
969+
overflow: hidden;
970+
position: relative;
971+
}
972+
973+
/* Ensure CodeMirror fills the wrapper */
974+
.editor-wrapper :global(.cm-editor) {
975+
height: 100% !important;
976+
max-height: 100% !important;
977+
}
978+
979+
.editor-wrapper :global(.cm-scroller) {
980+
overflow: auto !important;
981+
}
982+
983+
.editor-toolbar {
984+
position: sticky;
985+
top: 0;
986+
z-index: 10;
987+
background-color: inherit;
948988
}
949989
950990
.editor-main-output {

0 commit comments

Comments
 (0)