diff --git a/.env.example b/.env.example index 637a9301aa..83dd4412a5 100644 --- a/.env.example +++ b/.env.example @@ -121,6 +121,9 @@ TRACECAT__BLOB_STORAGE_BUCKET_REGISTRY=tracecat-registry TRACECAT__BLOB_STORAGE_BUCKET_AGENT=tracecat-agent # Total request attempts (initial try + retries) for transient S3/MinIO failures. TRACECAT__BLOB_STORAGE_MAX_ATTEMPTS=5 +# Verify TLS certificates when connecting to blob storage (S3/MinIO). +# Set to false for self-hosted S3-compatible storage with self-signed/absent certs. +TRACECAT__BLOB_STORAGE_SSL_VERIFY=true # --- MinIO --- MINIO_ROOT_USER=minio diff --git a/tests/unit/test_storage_blob.py b/tests/unit/test_storage_blob.py index ddabce32f0..f8bbf2464a 100644 --- a/tests/unit/test_storage_blob.py +++ b/tests/unit/test_storage_blob.py @@ -71,6 +71,12 @@ async def test_get_storage_client_minio_uses_endpoint_and_env(self, monkeypatch) "http://localhost:9002", raising=False, ) + monkeypatch.setattr( + blob_module.config, + "TRACECAT__BLOB_STORAGE_SSL_VERIFY", + True, + raising=False, + ) monkeypatch.setenv("AWS_ACCESS_KEY_ID", "minioadmin") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "minioadmin") @@ -85,6 +91,7 @@ async def test_get_storage_client_minio_uses_endpoint_and_env(self, monkeypatch) "s3", endpoint_url="http://localhost:9002", config=blob_module._STORAGE_CLIENT_CONFIG, + verify=None, aws_access_key_id="minioadmin", aws_secret_access_key="minioadmin", ) @@ -98,6 +105,12 @@ async def test_get_storage_client_s3_defaults(self, monkeypatch): None, raising=False, ) + monkeypatch.setattr( + blob_module.config, + "TRACECAT__BLOB_STORAGE_SSL_VERIFY", + True, + raising=False, + ) with patch("tracecat.storage.blob.aioboto3.Session") as mock_session_cls: mock_session = mock_session_cls.return_value @@ -107,9 +120,61 @@ async def test_get_storage_client_s3_defaults(self, monkeypatch): async with get_storage_client() as client: assert client is mock_client mock_session.client.assert_called_once_with( - "s3", config=blob_module._STORAGE_CLIENT_CONFIG + "s3", config=blob_module._STORAGE_CLIENT_CONFIG, verify=None ) + @pytest.mark.anyio + async def test_get_storage_client_minio_ssl_verify_disabled(self, monkeypatch): + """SSL verification can be disabled for self-signed MinIO/S3 endpoints.""" + monkeypatch.setattr( + blob_module.config, + "TRACECAT__BLOB_STORAGE_ENDPOINT", + "https://minio.internal:9000", + raising=False, + ) + monkeypatch.setattr( + blob_module.config, + "TRACECAT__BLOB_STORAGE_SSL_VERIFY", + False, + raising=False, + ) + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "minioadmin") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "minioadmin") + + with patch("tracecat.storage.blob.aioboto3.Session") as mock_session_cls: + mock_session = mock_session_cls.return_value + mock_client = AsyncMock() + mock_session.client.return_value.__aenter__.return_value = mock_client + + async with get_storage_client() as client: + assert client is mock_client + assert mock_session.client.call_args.kwargs["verify"] is False + + @pytest.mark.anyio + async def test_get_storage_client_s3_ssl_verify_disabled(self, monkeypatch): + """SSL verification flag is honored on the default AWS S3 path too.""" + monkeypatch.setattr( + blob_module.config, + "TRACECAT__BLOB_STORAGE_ENDPOINT", + None, + raising=False, + ) + monkeypatch.setattr( + blob_module.config, + "TRACECAT__BLOB_STORAGE_SSL_VERIFY", + False, + raising=False, + ) + + with patch("tracecat.storage.blob.aioboto3.Session") as mock_session_cls: + mock_session = mock_session_cls.return_value + mock_client = AsyncMock() + mock_session.client.return_value.__aenter__.return_value = mock_client + + async with get_storage_client() as client: + assert client is mock_client + assert mock_session.client.call_args.kwargs["verify"] is False + @pytest.mark.anyio @patch("tracecat.storage.blob.get_storage_client") async def test_upload_file(self, mock_get_client): diff --git a/tracecat/config.py b/tracecat/config.py index f13a3fb12a..8b962f35e4 100644 --- a/tracecat/config.py +++ b/tracecat/config.py @@ -456,6 +456,14 @@ def _parse_auth_types() -> set[AuthType]: TRACECAT__BLOB_STORAGE_ENDPOINT = os.environ.get("TRACECAT__BLOB_STORAGE_ENDPOINT", "") """Endpoint URL for blob storage.""" +TRACECAT__BLOB_STORAGE_SSL_VERIFY = ( + os.environ.get("TRACECAT__BLOB_STORAGE_SSL_VERIFY", "true").lower() == "true" +) +"""Verify TLS certificates when connecting to blob storage (S3/MinIO). + +Set to false for self-hosted S3-compatible storage that terminates TLS with a +self-signed or otherwise unverifiable certificate. Defaults to true.""" + TRACECAT__BLOB_STORAGE_MAX_ATTEMPTS = int( os.environ.get("TRACECAT__BLOB_STORAGE_MAX_ATTEMPTS") or 5 ) diff --git a/tracecat/storage/blob.py b/tracecat/storage/blob.py index f8b10bdcee..e1e8ae1586 100644 --- a/tracecat/storage/blob.py +++ b/tracecat/storage/blob.py @@ -53,6 +53,11 @@ async def get_storage_client() -> AsyncIterator[S3Client]: Configured aioboto3 S3 client """ session = aioboto3.Session() + # Only override TLS verification when it is explicitly disabled. Passing + # verify=True would bypass botocore's default CA-bundle resolution + # (AWS_CA_BUNDLE / the ca_bundle config), so leave it as None in the default + # case to preserve that behavior. + verify = None if config.TRACECAT__BLOB_STORAGE_SSL_VERIFY else False # Configure client based on protocol if config.TRACECAT__BLOB_STORAGE_ENDPOINT: # MinIO configuration - use AWS_* or MINIO_ROOT_* credentials @@ -60,6 +65,7 @@ async def get_storage_client() -> AsyncIterator[S3Client]: "s3", endpoint_url=config.TRACECAT__BLOB_STORAGE_ENDPOINT, config=_STORAGE_CLIENT_CONFIG, + verify=verify, # Defaults to minio default credentials. MUST REPLACE WITH PRODUCTION CREDENTIALS. aws_access_key_id=os.environ.get( "AWS_ACCESS_KEY_ID", @@ -73,7 +79,11 @@ async def get_storage_client() -> AsyncIterator[S3Client]: yield client else: # AWS S3 configuration - use AWS credentials from environment or default credential chain - async with session.client("s3", config=_STORAGE_CLIENT_CONFIG) as client: + async with session.client( + "s3", + config=_STORAGE_CLIENT_CONFIG, + verify=verify, + ) as client: yield client