diff --git a/docker-compose.mlflow-security.yml b/docker-compose.mlflow-security.yml index bad4912c..d5bf922c 100644 --- a/docker-compose.mlflow-security.yml +++ b/docker-compose.mlflow-security.yml @@ -1,8 +1,9 @@ services: mlflow: - image: ${MLFLOW_IMAGE:-ghcr.io/mlflow/mlflow:v3.14.0} + image: ${MLFLOW_IMAGE:-ghcr.io/mlflow/mlflow:v3.15.1} environment: MLFLOW_SERVER_ENABLE_JOB_EXECUTION: "false" + MLFLOW_SERVER_ALLOWED_HOSTS: "mlflow:5000,hurc_mlflow:5000,localhost:*,127.0.0.1:*" PYTHONDONTWRITEBYTECODE: "1" HOME: /tmp/mlflow-home XDG_CACHE_HOME: /tmp/.cache diff --git a/docs/technical/MLFLOW_SECURITY_BASELINE.md b/docs/technical/MLFLOW_SECURITY_BASELINE.md index d20035a8..e6590f43 100644 --- a/docs/technical/MLFLOW_SECURITY_BASELINE.md +++ b/docs/technical/MLFLOW_SECURITY_BASELINE.md @@ -12,8 +12,8 @@ The Python client and server image must use the same MLflow version and must not The reviewed baseline in this repository is: -- Python client: `mlflow==3.14.0`; -- Server image: `ghcr.io/mlflow/mlflow:v3.14.0`. +- Python client: `mlflow==3.15.1`; +- Server image: `ghcr.io/mlflow/mlflow:v3.15.1`. This version is newer than the fixes referenced for archive path traversal, model-serving command injection and FastAPI authentication bypass issues. @@ -25,7 +25,13 @@ The MLflow host port must remain bound to loopback: 127.0.0.1:${MLFLOW_HOST_PORT:-5000}:5000 ``` -MLflow must not be published on `0.0.0.0` at the host boundary. Containers that require tracking access communicate through the private `backend-net` network. +The MLflow Host allowlist must remain restricted to the Compose service name, the fixed container name and loopback health-check hosts: + +```yaml +MLFLOW_SERVER_ALLOWED_HOSTS: "mlflow:5000,hurc_mlflow:5000,localhost:*,127.0.0.1:*" +``` + +Do not replace this allowlist with `*`. MLflow must not be published on `0.0.0.0` at the host boundary. Containers that require tracking access communicate through the private `backend-net` network. For remote access, place MLflow behind an approved reverse proxy or identity-aware gateway. Do not expose the container port directly to an external network. diff --git a/infra/vision-trainer/main.py b/infra/vision-trainer/main.py index f96962af..e0d16d47 100644 --- a/infra/vision-trainer/main.py +++ b/infra/vision-trainer/main.py @@ -1,5 +1,6 @@ import json import os +import re import threading import uuid from datetime import datetime, timezone @@ -49,6 +50,11 @@ def now_iso(): return datetime.now(timezone.utc).isoformat() +def normalize_mlflow_metric_name(value: str): + normalized = re.sub(r"[^A-Za-z0-9_. /:-]", "_", value) + return normalized or "metric" + + def default_state(): return {"datasets": {}, "samples": {}, "jobs": {}} @@ -177,7 +183,7 @@ def run_training(job_id: str): ) best_path = Path(result.save_dir) / "weights" / "best.pt" mlflow.log_artifact(str(best_path), artifact_path="model") - metrics = {key: float(value) for key, value in getattr(result, "results_dict", {}).items() if isinstance(value, (int, float))} + metrics = {normalize_mlflow_metric_name(key): float(value) for key, value in getattr(result, "results_dict", {}).items() if isinstance(value, (int, float))} if metrics: mlflow.log_metrics(metrics) with LOCK: diff --git a/infra/vision-trainer/requirements.txt b/infra/vision-trainer/requirements.txt index ec7a8834..5c986bc6 100644 --- a/infra/vision-trainer/requirements.txt +++ b/infra/vision-trainer/requirements.txt @@ -1,7 +1,7 @@ -fastapi==0.116.1 -uvicorn[standard]==0.35.0 -python-multipart==0.0.31 +fastapi==0.141.1 +uvicorn[standard]==0.52.3 +python-multipart==0.0.32 Pillow==12.3.0 -PyYAML==6.0.2 -ultralytics==8.3.203 -mlflow==3.14.0 +PyYAML==6.0.3 +ultralytics==8.4.121 +mlflow==3.15.1 diff --git a/scripts/check-mlflow-security.mjs b/scripts/check-mlflow-security.mjs index 45be0777..c84ca1c1 100644 --- a/scripts/check-mlflow-security.mjs +++ b/scripts/check-mlflow-security.mjs @@ -53,6 +53,7 @@ if (packageVersion && imageVersion && compareVersion(packageVersion, imageVersio requireText(platformRunner, "'docker-compose.mlflow-security.yml'", 'Platform compose must load the MLflow security override.'); requireText(securityOverride, 'MLFLOW_SERVER_ENABLE_JOB_EXECUTION: "false"', 'MLflow job execution must be explicitly disabled.'); +requireText(securityOverride, 'MLFLOW_SERVER_ALLOWED_HOSTS: "mlflow:5000,hurc_mlflow:5000,localhost:*,127.0.0.1:*"', 'MLflow must allow only the expected internal and loopback Host headers.'); requireText(securityOverride, 'read_only: true', 'MLflow container root filesystem must be read-only.'); requireText(securityOverride, 'no-new-privileges:true', 'MLflow container must enable no-new-privileges.'); requireText(securityOverride, 'cap_drop:', 'MLflow container must drop Linux capabilities.');