Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions backend/app/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import jwt
from fastapi import Request
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from pwdlib import PasswordHash
from pwdlib.hashers.bcrypt import BcryptHasher

from app.core.metrics import SecurityMetrics
from app.domain.user import CSRFValidationError, InvalidCredentialsError
Expand All @@ -20,17 +21,15 @@ def __init__(self, settings: Settings, security_metrics: SecurityMetrics) -> Non
self.settings = settings
self._security_metrics = security_metrics
# --8<-- [start:password_hashing]
self.pwd_context = CryptContext(
schemes=["bcrypt"],
deprecated="auto",
bcrypt__rounds=self.settings.BCRYPT_ROUNDS,
)
self._password_hash = PasswordHash((
BcryptHasher(rounds=self.settings.BCRYPT_ROUNDS),
))

def verify_password(self, plain_password: str, hashed_password: str) -> bool:
return self.pwd_context.verify(plain_password, hashed_password) # type: ignore
return self._password_hash.verify(plain_password, hashed_password)

def get_password_hash(self, password: str) -> str:
return self.pwd_context.hash(password) # type: ignore
return self._password_hash.hash(password)
# --8<-- [end:password_hashing]

# --8<-- [start:create_access_token]
Expand Down
2 changes: 2 additions & 0 deletions backend/app/services/k8s_worker/pod_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ def _build_pod_spec(
containers=[container],
restart_policy="Never",
active_deadline_seconds=timeout,
runtime_class_name=self._settings.K8S_POD_RUNTIME_CLASS_NAME,
host_users=False, # User namespace isolation — remaps container UIDs to unprivileged host UIDs
volumes=[
k8s_client.V1Volume(
name="script-volume",
Expand Down
99 changes: 99 additions & 0 deletions backend/app/services/k8s_worker/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def __init__(
# Kubernetes clients created from ApiClient
self.v1 = k8s_client.CoreV1Api(api_client)
self.apps_v1 = k8s_client.AppsV1Api(api_client)
self.networking_v1 = k8s_client.NetworkingV1Api(api_client)

# Components
self.pod_builder = PodBuilder(settings=settings)
Expand Down Expand Up @@ -252,6 +253,104 @@ async def _publish_pod_creation_failed(self, command: CreatePodCommandEvent, err
)
await self.producer.produce(event_to_produce=event, key=command.execution_id)

async def ensure_namespace_security(self) -> None:
"""Apply security controls to the executor namespace at startup.

Creates:
- Default-deny NetworkPolicy for executor pods (blocks lateral movement and exfiltration)
- ResourceQuota to cap aggregate pod/resource consumption
- Pod Security Admission labels (Restricted profile)
"""
namespace = self._settings.K8S_NAMESPACE
await self._ensure_executor_network_policy(namespace)
await self._ensure_executor_resource_quota(namespace)
await self._apply_psa_labels(namespace)

async def _ensure_executor_network_policy(self, namespace: str) -> None:
"""Create default-deny NetworkPolicy for executor pods."""
policy_name = "executor-deny-all"

policy = k8s_client.V1NetworkPolicy(
api_version="networking.k8s.io/v1",
kind="NetworkPolicy",
metadata=k8s_client.V1ObjectMeta(
name=policy_name,
namespace=namespace,
labels={"app": "integr8s", "component": "security"},
),
spec=k8s_client.V1NetworkPolicySpec(
pod_selector=k8s_client.V1LabelSelector(
match_labels={"component": "executor"},
),
policy_types=["Ingress", "Egress"],
ingress=[],
egress=[],
),
)

try:
await self.networking_v1.read_namespaced_network_policy(name=policy_name, namespace=namespace)
await self.networking_v1.replace_namespaced_network_policy(
name=policy_name, namespace=namespace, body=policy,
)
Comment thread
HardMax71 marked this conversation as resolved.
Outdated
Comment thread
HardMax71 marked this conversation as resolved.
Outdated
self.logger.info(f"NetworkPolicy '{policy_name}' updated in namespace {namespace}")
except ApiException as e:
if e.status == 404:
await self.networking_v1.create_namespaced_network_policy(namespace=namespace, body=policy)
self.logger.info(f"NetworkPolicy '{policy_name}' created in namespace {namespace}")
Comment thread
HardMax71 marked this conversation as resolved.
Outdated
else:
self.logger.error(f"Failed to apply NetworkPolicy '{policy_name}': {e.reason}")
Comment thread
HardMax71 marked this conversation as resolved.
Outdated
Comment thread
HardMax71 marked this conversation as resolved.
Outdated

Comment thread
HardMax71 marked this conversation as resolved.
Outdated
async def _ensure_executor_resource_quota(self, namespace: str) -> None:
"""Create ResourceQuota to cap aggregate executor pod consumption."""
quota_name = "executor-quota"

quota = k8s_client.V1ResourceQuota(
api_version="v1",
kind="ResourceQuota",
metadata=k8s_client.V1ObjectMeta(
name=quota_name,
namespace=namespace,
labels={"app": "integr8s", "component": "security"},
),
spec=k8s_client.V1ResourceQuotaSpec(
hard={
"pods": str(self._settings.K8S_MAX_CONCURRENT_PODS),
"requests.cpu": f"{self._settings.K8S_MAX_CONCURRENT_PODS}",
"requests.memory": f"{self._settings.K8S_MAX_CONCURRENT_PODS * 128}Mi",
"limits.cpu": f"{self._settings.K8S_MAX_CONCURRENT_PODS}",
"limits.memory": f"{self._settings.K8S_MAX_CONCURRENT_PODS * 128}Mi",
Comment thread
HardMax71 marked this conversation as resolved.
Outdated
},
Comment thread
HardMax71 marked this conversation as resolved.
Outdated
),
)

try:
await self.v1.read_namespaced_resource_quota(name=quota_name, namespace=namespace)
await self.v1.replace_namespaced_resource_quota(name=quota_name, namespace=namespace, body=quota)
Comment thread
HardMax71 marked this conversation as resolved.
Outdated
self.logger.info(f"ResourceQuota '{quota_name}' updated in namespace {namespace}")
except ApiException as e:
if e.status == 404:
await self.v1.create_namespaced_resource_quota(namespace=namespace, body=quota)
self.logger.info(f"ResourceQuota '{quota_name}' created in namespace {namespace}")
Comment thread
HardMax71 marked this conversation as resolved.
Outdated
else:
self.logger.error(f"Failed to apply ResourceQuota '{quota_name}': {e.reason}")

Comment thread
HardMax71 marked this conversation as resolved.
async def _apply_psa_labels(self, namespace: str) -> None:
"""Apply Pod Security Admission labels to the executor namespace."""
psa_labels = {
"pod-security.kubernetes.io/enforce": "restricted",
"pod-security.kubernetes.io/enforce-version": "latest",
Comment thread
HardMax71 marked this conversation as resolved.
"pod-security.kubernetes.io/warn": "restricted",
"pod-security.kubernetes.io/audit": "restricted",
}

patch_body = {"metadata": {"labels": psa_labels}}
try:
await self.v1.patch_namespace(name=namespace, body=patch_body)
self.logger.info(f"Pod Security Admission labels applied to namespace {namespace}")
except ApiException as e:
self.logger.error(f"Failed to apply PSA labels to namespace {namespace}: {e.reason}")
Comment thread
HardMax71 marked this conversation as resolved.
Outdated

async def ensure_image_pre_puller_daemonset(self) -> None:
"""Ensure the runtime image pre-puller DaemonSet exists."""
daemonset_name = "runtime-image-pre-puller"
Expand Down
1 change: 1 addition & 0 deletions backend/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(
K8S_POD_MEMORY_REQUEST: str = "128Mi"
K8S_POD_EXECUTION_TIMEOUT: int = 300 # in seconds
K8S_POD_PRIORITY_CLASS_NAME: str | None = None
K8S_POD_RUNTIME_CLASS_NAME: str | None = None # e.g. "gvisor" for sandboxed execution

SUPPORTED_RUNTIMES: dict[str, LanguageInfoDomain] = Field(default_factory=lambda: RUNTIME_MATRIX)

Expand Down
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dependencies = [
"opentelemetry-util-http==0.60b1",
"packaging==24.1",
"beanie==2.0.1",
"passlib==1.7.4",
"pwdlib[bcrypt]==0.2.1",
"pathspec==0.12.1",
"prometheus-fastapi-instrumentator==7.0.0",
"prometheus_client==0.21.0",
Expand Down
9 changes: 5 additions & 4 deletions backend/scripts/seed_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

from app.settings import Settings
from bson import ObjectId
from passlib.context import CryptContext
from pwdlib import PasswordHash
from pwdlib.hashers.bcrypt import BcryptHasher
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.mongo_client import AsyncMongoClient

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
pwd_hasher = PasswordHash((BcryptHasher(rounds=12),))
Comment thread
HardMax71 marked this conversation as resolved.
Outdated


async def upsert_user(
Expand All @@ -42,7 +43,7 @@ async def upsert_user(
{"username": username},
{
"$set": {
"hashed_password": pwd_context.hash(password),
"hashed_password": pwd_hasher.hash(password),
"role": role,
"is_superuser": is_superuser,
"is_active": True,
Expand All @@ -58,7 +59,7 @@ async def upsert_user(
"user_id": str(ObjectId()),
"username": username,
"email": email,
"hashed_password": pwd_context.hash(password),
"hashed_password": pwd_hasher.hash(password),
"role": role,
"is_active": True,
"is_superuser": is_superuser,
Expand Down
1 change: 1 addition & 0 deletions backend/secrets.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@

SECRET_KEY = "CHANGE_ME_min_32_chars_long_!!!!"
MONGODB_URL = "mongodb://root:rootpassword@mongo:27017/integr8scode?authSource=admin"
REDIS_PASSWORD = "redispassword"
Comment thread
HardMax71 marked this conversation as resolved.
Comment thread
HardMax71 marked this conversation as resolved.
Loading
Loading