initial commit
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
||||
"""Security validation for container creation requests."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.config import settings
|
||||
from app.models import ContainerCreate
|
||||
|
||||
|
||||
class SecurityValidationError(HTTPException):
|
||||
"""Exception raised when security validation fails."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Security validation failed: {message}",
|
||||
)
|
||||
|
||||
|
||||
def validate_image(image: str) -> None:
|
||||
"""Validate that the image is allowed."""
|
||||
if not settings.allowed_images:
|
||||
return # All images allowed if list is empty
|
||||
|
||||
# Check if image matches any allowed pattern
|
||||
for allowed in settings.allowed_images:
|
||||
if image == allowed or image.startswith(f"{allowed}:"):
|
||||
return
|
||||
|
||||
raise SecurityValidationError(
|
||||
f"Image '{image}' is not in the allowed images list"
|
||||
)
|
||||
|
||||
|
||||
def validate_volume_path(host_path: str) -> None:
|
||||
"""Validate that a volume mount path is safe."""
|
||||
# Normalize the path
|
||||
try:
|
||||
normalized = Path(host_path).resolve()
|
||||
except Exception:
|
||||
raise SecurityValidationError(f"Invalid volume path: {host_path}")
|
||||
|
||||
# Check against blocked paths
|
||||
normalized_str = str(normalized)
|
||||
for blocked in settings.blocked_volume_paths:
|
||||
blocked_path = Path(blocked).resolve()
|
||||
# Block exact matches and subdirectories of blocked paths
|
||||
if normalized == blocked_path:
|
||||
raise SecurityValidationError(
|
||||
f"Volume mount to '{host_path}' is not allowed"
|
||||
)
|
||||
# Also block if it's trying to mount a parent of a sensitive path
|
||||
try:
|
||||
normalized.relative_to(blocked_path)
|
||||
raise SecurityValidationError(
|
||||
f"Volume mount to '{host_path}' is not allowed (overlaps with protected path)"
|
||||
)
|
||||
except ValueError:
|
||||
pass # Not a subdirectory, which is fine
|
||||
|
||||
# Block if the blocked path is a subdirectory of the mount
|
||||
try:
|
||||
blocked_path.relative_to(normalized)
|
||||
raise SecurityValidationError(
|
||||
f"Volume mount to '{host_path}' is not allowed (would expose protected paths)"
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def validate_volumes(volumes: Optional[Dict[str, Any]]) -> None:
|
||||
"""Validate all volume mounts."""
|
||||
if not volumes:
|
||||
return
|
||||
|
||||
for host_path in volumes.keys():
|
||||
validate_volume_path(host_path)
|
||||
|
||||
|
||||
def validate_capabilities(cap_add: Optional[List[str]]) -> None:
|
||||
"""Validate requested capabilities."""
|
||||
if not cap_add:
|
||||
return
|
||||
|
||||
if not settings.allowed_capabilities:
|
||||
raise SecurityValidationError(
|
||||
"Adding capabilities is not allowed"
|
||||
)
|
||||
|
||||
for cap in cap_add:
|
||||
if cap.upper() not in settings.allowed_capabilities:
|
||||
raise SecurityValidationError(
|
||||
f"Capability '{cap}' is not allowed"
|
||||
)
|
||||
|
||||
|
||||
def check_dangerous_options(raw_data: Dict[str, Any]) -> None:
|
||||
"""Check for dangerous options that should never be allowed."""
|
||||
dangerous_options = {
|
||||
"privileged": "Privileged mode is not allowed",
|
||||
"network_mode": None, # Special handling
|
||||
"pid_mode": "PID mode 'host' is not allowed",
|
||||
"ipc_mode": "IPC mode 'host' is not allowed",
|
||||
"cap_add": None, # Handled separately
|
||||
"devices": "Device mappings are not allowed",
|
||||
"security_opt": "Security options are not allowed",
|
||||
"sysctls": "Sysctl settings are not allowed",
|
||||
}
|
||||
|
||||
for option, message in dangerous_options.items():
|
||||
if option not in raw_data:
|
||||
continue
|
||||
|
||||
value = raw_data[option]
|
||||
|
||||
if option == "privileged" and value is True:
|
||||
raise SecurityValidationError(message)
|
||||
|
||||
if option == "network_mode" and value == "host":
|
||||
raise SecurityValidationError("Network mode 'host' is not allowed")
|
||||
|
||||
if option == "pid_mode" and value == "host":
|
||||
raise SecurityValidationError(message)
|
||||
|
||||
if option == "ipc_mode" and value == "host":
|
||||
raise SecurityValidationError(message)
|
||||
|
||||
if option == "devices" and value:
|
||||
raise SecurityValidationError(message)
|
||||
|
||||
if option == "security_opt" and value:
|
||||
raise SecurityValidationError(message)
|
||||
|
||||
if option == "sysctls" and value:
|
||||
raise SecurityValidationError(message)
|
||||
|
||||
if option == "cap_add" and value:
|
||||
validate_capabilities(value)
|
||||
|
||||
|
||||
def validate_container_request(
|
||||
request: ContainerCreate, raw_data: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Validate a container creation request for security issues.
|
||||
|
||||
Args:
|
||||
request: The parsed ContainerCreate model
|
||||
raw_data: The raw request data (to check for extra fields)
|
||||
|
||||
Raises:
|
||||
SecurityValidationError: If validation fails
|
||||
"""
|
||||
# Validate image
|
||||
validate_image(request.image)
|
||||
|
||||
# Validate volumes
|
||||
validate_volumes(
|
||||
{k: v.model_dump() for k, v in request.volumes.items()}
|
||||
if request.volumes
|
||||
else None
|
||||
)
|
||||
|
||||
# Check for dangerous options in raw data
|
||||
if raw_data:
|
||||
check_dangerous_options(raw_data)
|
||||
Reference in New Issue
Block a user