瀏覽代碼

feat: add internal service auth

Jax Docker 1 月之前
父節點
當前提交
beda8aec0d

+ 2 - 0
deployments/docker/.env.example

@@ -26,6 +26,8 @@ AGENT_PLATFORM_EMBEDDING_MODEL=local-hash-v1
 AGENT_PLATFORM_MAX_TIMEOUT_SECONDS=30
 AGENT_PLATFORM_AUTH_REQUIRED=false
 AGENT_PLATFORM_AUTHZ_REQUIRED=false
+AGENT_PLATFORM_INTERNAL_SERVICE_AUTH_REQUIRED=false
+AGENT_PLATFORM_INTERNAL_SERVICE_TOKEN=replace-with-shared-internal-token
 AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS=1
 AGENT_PLATFORM_WORKER_LEASE_SECONDS=300
 AGENT_PLATFORM_SCHEDULER_WORKER_CLAIM_LIMIT=20

+ 24 - 0
deployments/docker/docker-compose.yml

@@ -1,3 +1,7 @@
+x-agent-platform-common-env: &agent-platform-common-env
+  AGENT_PLATFORM_INTERNAL_SERVICE_AUTH_REQUIRED: ${AGENT_PLATFORM_INTERNAL_SERVICE_AUTH_REQUIRED:-false}
+  AGENT_PLATFORM_INTERNAL_SERVICE_TOKEN: ${AGENT_PLATFORM_INTERNAL_SERVICE_TOKEN:-}
+
 services:
   postgres:
     image: pgvector/pgvector:pg16
@@ -85,6 +89,7 @@ services:
     container_name: agent-platform-workflow-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8002"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_WORKFLOW_DATABASE_URL:-sqlite:////data/workflow_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
@@ -106,6 +111,7 @@ services:
     container_name: agent-platform-session-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SESSION_DATABASE_URL:-sqlite:////data/session_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_RUNTIME_SERVICE_URL: http://runtime-service:8003
@@ -131,6 +137,7 @@ services:
     container_name: agent-platform-tool-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8004"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TOOL_DATABASE_URL:-sqlite:////data/tool_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
@@ -152,6 +159,7 @@ services:
     container_name: agent-platform-model-gateway-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8005"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_PROVIDER_BASE_URL: ${AGENT_PLATFORM_PROVIDER_BASE_URL:-http://host.docker.internal:11434/v1}
       AGENT_PLATFORM_PROVIDER_API_KEY: ${AGENT_PLATFORM_PROVIDER_API_KEY:-}
       AGENT_PLATFORM_DEFAULT_MODEL: ${AGENT_PLATFORM_DEFAULT_MODEL:-}
@@ -172,6 +180,7 @@ services:
     container_name: agent-platform-code-runner-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8006"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_PYTHON_BIN: python
       AGENT_PLATFORM_MAX_TIMEOUT_SECONDS: ${AGENT_PLATFORM_MAX_TIMEOUT_SECONDS:-30}
     ports:
@@ -191,6 +200,7 @@ services:
     container_name: agent-platform-agent-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8007"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AGENT_DATABASE_URL:-sqlite:////data/agent_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_MODEL_GATEWAY_SERVICE_URL: http://model-gateway-service:8005
@@ -227,6 +237,7 @@ services:
         SERVICE_PATH: services/agent-service
     command: ["python", "-m", "app.worker"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AGENT_DATABASE_URL:-sqlite:////data/agent_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_MODEL_GATEWAY_SERVICE_URL: http://model-gateway-service:8005
@@ -260,6 +271,7 @@ services:
     container_name: agent-platform-memory-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8008"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_MEMORY_DATABASE_URL:-sqlite:////data/memory_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
@@ -281,6 +293,7 @@ services:
     container_name: agent-platform-team-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8009"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TEAM_DATABASE_URL:-sqlite:////data/team_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_AGENT_SERVICE_URL: http://agent-service:8007
@@ -303,6 +316,7 @@ services:
         SERVICE_PATH: services/team-service
     command: ["python", "-m", "app.worker"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TEAM_DATABASE_URL:-sqlite:////data/team_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_AGENT_SERVICE_URL: http://agent-service:8007
@@ -327,6 +341,7 @@ services:
     container_name: agent-platform-skill-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8010"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SKILL_DATABASE_URL:-sqlite:////data/skill_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
@@ -348,6 +363,7 @@ services:
     container_name: agent-platform-human-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8011"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_HUMAN_DATABASE_URL:-sqlite:////data/human_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
@@ -369,6 +385,7 @@ services:
     container_name: agent-platform-knowledge-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8012"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_KNOWLEDGE_DATABASE_URL:-sqlite:////data/knowledge_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_EMBEDDING_PROVIDER: ${AGENT_PLATFORM_EMBEDDING_PROVIDER:-local}
@@ -394,6 +411,7 @@ services:
     container_name: agent-platform-event-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8013"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_EVENT_DATABASE_URL:-sqlite:////data/event_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
@@ -415,6 +433,7 @@ services:
     container_name: agent-platform-auth-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8014"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AUTH_DATABASE_URL:-sqlite:////data/auth_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
@@ -436,6 +455,7 @@ services:
     container_name: agent-platform-scheduler-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8015"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SCHEDULER_DATABASE_URL:-sqlite:////data/scheduler_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
@@ -456,6 +476,7 @@ services:
         SERVICE_PATH: services/scheduler-service
     command: ["python", "-m", "app.worker"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SCHEDULER_DATABASE_URL:-sqlite:////data/scheduler_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_EVENT_SERVICE_URL: http://event-service:8013
@@ -479,6 +500,7 @@ services:
     container_name: agent-platform-runtime-service
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8003"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_RUNTIME_DATABASE_URL:-sqlite:////data/runtime_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_WORKFLOW_SERVICE_URL: http://workflow-service:8002
@@ -536,6 +558,7 @@ services:
         SERVICE_PATH: services/runtime-service
     command: ["python", "-m", "app.worker"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_RUNTIME_DATABASE_URL:-sqlite:////data/runtime_service.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_WORKFLOW_SERVICE_URL: http://workflow-service:8002
@@ -574,6 +597,7 @@ services:
     container_name: agent-platform-api-gateway
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
     environment:
+      <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_API_GATEWAY_DATABASE_URL:-sqlite:////data/api_gateway.db}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
       AGENT_PLATFORM_WORKFLOW_SERVICE_URL: http://workflow-service:8002

+ 4 - 0
libs/core-shared/src/core_shared/config.py

@@ -11,6 +11,10 @@ class ServiceSettings(BaseSettings):
     database_url: str = Field(default="sqlite:///./service.db")
     redis_url: str = Field(default="redis://127.0.0.1:6379/0")
     echo_sql: bool = Field(default=False)
+    internal_service_auth_required: bool = Field(default=False)
+    internal_service_token: str | None = Field(default=None)
+    internal_service_token_header_name: str = Field(default="x-internal-service-token")
+    internal_service_name_header_name: str = Field(default="x-internal-service-name")
 
     model_config = SettingsConfigDict(
         env_prefix="AGENT_PLATFORM_",

+ 129 - 0
libs/core-shared/src/core_shared/security.py

@@ -0,0 +1,129 @@
+from __future__ import annotations
+
+import hmac
+from collections.abc import Mapping, MutableMapping
+from typing import Any, Awaitable, Callable, Protocol
+
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.requests import Request
+from starlette.responses import JSONResponse, Response
+from starlette.types import ASGIApp
+
+
+SENSITIVE_KEYWORDS = (
+    "api_key",
+    "apikey",
+    "authorization",
+    "bearer",
+    "client_secret",
+    "credential",
+    "password",
+    "private_key",
+    "secret",
+    "token",
+)
+
+
+class InternalServiceSettings(Protocol):
+    service_name: str
+    internal_service_auth_required: bool
+    internal_service_token: str | None
+    internal_service_token_header_name: str
+    internal_service_name_header_name: str
+
+
+class InternalServiceAuthMiddleware(BaseHTTPMiddleware):
+    def __init__(self, app: ASGIApp, settings: InternalServiceSettings) -> None:
+        super().__init__(app)
+        self._settings = settings
+
+    async def dispatch(
+        self,
+        request: Request,
+        call_next: Callable[[Request], Awaitable[Response]],
+    ) -> Response:
+        if not self._settings.internal_service_auth_required:
+            return await call_next(request)
+        if _is_public_internal_auth_path(request.url.path):
+            return await call_next(request)
+
+        configured_token = self._settings.internal_service_token
+        provided_token = request.headers.get(self._settings.internal_service_token_header_name)
+        if not configured_token:
+            return JSONResponse(
+                status_code=503,
+                content={"detail": "internal service auth token is not configured"},
+            )
+        if provided_token is None or not hmac.compare_digest(provided_token, configured_token):
+            return JSONResponse(
+                status_code=401,
+                content={"detail": "invalid internal service token"},
+            )
+        return await call_next(request)
+
+
+def add_internal_service_auth(app: Any, settings: InternalServiceSettings) -> None:
+    app.add_middleware(InternalServiceAuthMiddleware, settings=settings)
+
+
+def build_internal_service_headers(
+    settings: InternalServiceSettings,
+    *,
+    source_service: str | None = None,
+) -> dict[str, str]:
+    headers: dict[str, str] = {}
+    if settings.internal_service_token:
+        headers[settings.internal_service_token_header_name] = settings.internal_service_token
+    headers[settings.internal_service_name_header_name] = source_service or settings.service_name
+    return headers
+
+
+def merge_internal_service_headers(
+    headers: Mapping[str, str] | None,
+    settings: InternalServiceSettings,
+    *,
+    source_service: str | None = None,
+) -> dict[str, str]:
+    merged = dict(headers or {})
+    merged.update(build_internal_service_headers(settings, source_service=source_service))
+    return merged
+
+
+def mask_sensitive_value(value: Any) -> Any:
+    if value is None:
+        return None
+    text = str(value)
+    if len(text) <= 8:
+        return "***"
+    return f"{text[:4]}...{text[-4:]}"
+
+
+def mask_sensitive_mapping(payload: Mapping[str, Any]) -> dict[str, Any]:
+    return {str(key): _mask_value(str(key), value) for key, value in payload.items()}
+
+
+def mask_sensitive_mutable_mapping(payload: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
+    for key, value in list(payload.items()):
+        payload[key] = _mask_value(str(key), value)
+    return payload
+
+
+def _mask_value(key: str, value: Any) -> Any:
+    if _is_sensitive_key(key):
+        return mask_sensitive_value(value)
+    if isinstance(value, Mapping):
+        return mask_sensitive_mapping(value)
+    if isinstance(value, list):
+        return [_mask_value(key, item) for item in value]
+    return value
+
+
+def _is_sensitive_key(key: str) -> bool:
+    normalized = key.lower().replace("-", "_")
+    return any(keyword in normalized for keyword in SENSITIVE_KEYWORDS)
+
+
+def _is_public_internal_auth_path(path: str) -> bool:
+    if path == "/metrics" or path.endswith("/health"):
+        return True
+    return path in {"/docs", "/redoc", "/openapi.json"}

+ 2 - 0
services/agent-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import AgentServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/agents", tags=["agents"])
     return app

+ 5 - 2
services/api-gateway/app/api/routes.py

@@ -135,7 +135,7 @@ def get_gateway_settings() -> ApiGatewaySettings:
 
 
 def get_service_proxy(settings: ApiGatewaySettings = Depends(get_gateway_settings)) -> ServiceProxy:
-    return ServiceProxy(timeout_seconds=settings.proxy_timeout_seconds)
+    return ServiceProxy(settings=settings, timeout_seconds=settings.proxy_timeout_seconds)
 
 
 def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName, ProxyTarget]:
@@ -238,7 +238,10 @@ async def downstream_health_check(
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
 ) -> GatewayServicesHealthResponse:
     targets = build_proxy_targets(settings)
-    health_proxy = ServiceProxy(timeout_seconds=settings.downstream_health_timeout_seconds)
+    health_proxy = ServiceProxy(
+        settings=settings,
+        timeout_seconds=settings.downstream_health_timeout_seconds,
+    )
     downstream_services = await asyncio.gather(
         *[health_proxy.check_health(target) for target in targets.values()]
     )

+ 18 - 3
services/api-gateway/app/infrastructure/proxy.py

@@ -3,8 +3,10 @@ from typing import Literal
 
 import httpx
 from fastapi import Request, Response
+from core_shared.security import build_internal_service_headers
 
 from app.infrastructure.audit import mark_gateway_target
+from app.bootstrap.settings import ApiGatewaySettings
 from app.infrastructure.request_context import (
     REQUEST_ID_HEADER,
     TENANT_ID_HEADER,
@@ -40,7 +42,8 @@ class ProxyTarget:
 
 
 class ServiceProxy:
-    def __init__(self, *, timeout_seconds: float) -> None:
+    def __init__(self, *, settings: ApiGatewaySettings, timeout_seconds: float) -> None:
+        self.settings = settings
         self.timeout_seconds = timeout_seconds
 
     async def forward(
@@ -60,6 +63,7 @@ class ServiceProxy:
         request_context = get_gateway_request_context(request)
         headers[REQUEST_ID_HEADER] = request_context.request_id
         headers[TENANT_ID_HEADER] = request_context.tenant_id
+        headers.update(build_internal_service_headers(self.settings))
         body = await request.body()
 
         async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
@@ -82,7 +86,10 @@ class ServiceProxy:
         health_url = f"{target.base_url.rstrip('/')}{target.health_path}"
         try:
             async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
-                response = await client.get(health_url)
+                response = await client.get(
+                    health_url,
+                    headers=build_internal_service_headers(self.settings),
+                )
         except httpx.HTTPError as exc:
             return DownstreamServiceHealth(
                 service=target.service_name,
@@ -108,7 +115,15 @@ def build_target_url(*, target: ProxyTarget, path: str) -> str:
 
 
 def build_forward_headers(request: Request) -> dict[str, str]:
-    skipped_headers = {"host", "content-length", "connection", REQUEST_ID_HEADER, TENANT_ID_HEADER}
+    skipped_headers = {
+        "host",
+        "content-length",
+        "connection",
+        REQUEST_ID_HEADER,
+        TENANT_ID_HEADER,
+        "x-internal-service-token",
+        "x-internal-service-name",
+    }
     return {
         key: value
         for key, value in request.headers.items()

+ 2 - 0
services/api-gateway/app/infrastructure/request_context.py

@@ -7,6 +7,7 @@ import httpx
 from fastapi import Request, Response
 from starlette.responses import JSONResponse
 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
+from core_shared.security import build_internal_service_headers
 
 from app.bootstrap.settings import ApiGatewaySettings
 from app.domain.repositories import ApiKeyRepository
@@ -229,6 +230,7 @@ def check_auth_service_permission(
         with httpx.Client(timeout=settings.authz_timeout_seconds) as client:
             response = client.post(
                 f"{settings.auth_service_url.rstrip('/')}/auth/permissions/check",
+                headers=build_internal_service_headers(settings),
                 json={
                     "tenant_id": tenant_id,
                     "user_id": user_id,

+ 2 - 0
services/auth-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import AuthServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/auth", tags=["auth"])
     return app

+ 2 - 0
services/code-runner-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import CodeRunnerServiceSettings
@@ -13,5 +14,6 @@ def create_app() -> FastAPI:
     )
     app.state.settings = settings
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/code", tags=["code"])
     return app

+ 2 - 0
services/event-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import EventServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/events", tags=["events"])
     return app

+ 2 - 0
services/human-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import HumanServiceSettings
@@ -12,5 +13,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/human", tags=["human"])
     return app

+ 2 - 0
services/knowledge-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import KnowledgeServiceSettings
@@ -12,5 +13,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/knowledge", tags=["knowledge"])
     return app

+ 2 - 0
services/memory-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import MemoryServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/memories", tags=["memories"])
     return app

+ 2 - 0
services/model-gateway-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import ModelGatewayServiceSettings
@@ -13,5 +14,6 @@ def create_app() -> FastAPI:
     )
     app.state.settings = settings
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/models", tags=["models"])
     return app

+ 2 - 0
services/runtime-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import RuntimeServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/runtime", tags=["runtime"])
     return app

+ 2 - 0
services/scheduler-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import SchedulerServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/scheduler", tags=["scheduler"])
     return app

+ 2 - 0
services/session-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import SessionServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/sessions", tags=["sessions"])
     return app

+ 2 - 0
services/skill-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import SkillServiceSettings
@@ -12,5 +13,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/skills", tags=["skills"])
     return app

+ 2 - 0
services/team-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import TeamServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/teams", tags=["teams"])
     return app

+ 2 - 0
services/tool-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import ToolServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/tools", tags=["tools"])
     return app

+ 2 - 0
services/workflow-service/app/bootstrap/app.py

@@ -1,5 +1,6 @@
 from fastapi import FastAPI
 from core_shared.observability import add_observability
+from core_shared.security import add_internal_service_auth
 
 from app.api.routes import router
 from app.bootstrap.settings import WorkflowServiceSettings
@@ -15,5 +16,6 @@ def create_app() -> FastAPI:
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)
+    add_internal_service_auth(app, settings)
     app.include_router(router, prefix="/workflows", tags=["workflows"])
     return app

+ 56 - 0
tests/test_security.py

@@ -0,0 +1,56 @@
+import asyncio
+
+import httpx
+from fastapi import FastAPI
+
+from core_shared.config import ServiceSettings
+from core_shared.security import (
+    add_internal_service_auth,
+    build_internal_service_headers,
+    mask_sensitive_mapping,
+)
+
+
+def test_mask_sensitive_mapping_masks_nested_secrets() -> None:
+    payload = {
+        "api_key": "agp_super_secret_value",
+        "nested": {"authorization": "Bearer token-value"},
+        "safe": "visible",
+    }
+
+    masked = mask_sensitive_mapping(payload)
+
+    assert masked["api_key"] != payload["api_key"]
+    assert masked["nested"]["authorization"] != payload["nested"]["authorization"]
+    assert masked["safe"] == "visible"
+
+
+def test_internal_service_auth_middleware_requires_token() -> None:
+    asyncio.run(_run_internal_service_auth_smoke())
+
+
+async def _run_internal_service_auth_smoke() -> None:
+    settings = ServiceSettings(
+        service_name="test-service",
+        internal_service_auth_required=True,
+        internal_service_token="secret-token",
+    )
+    app = FastAPI()
+    add_internal_service_auth(app, settings)
+
+    @app.get("/private")
+    async def private() -> dict[str, str]:
+        return {"status": "ok"}
+
+    transport = httpx.ASGITransport(app=app)
+    async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
+        denied_response = await client.get("/private")
+        allowed_response = await client.get(
+            "/private",
+            headers=build_internal_service_headers(settings, source_service="caller"),
+        )
+        health_response = await client.get("/private/health")
+
+    assert denied_response.status_code == 401
+    assert allowed_response.status_code == 200
+    assert health_response.status_code == 404