Secure AI agents with Amazon Bedrock AgentCore Identity на Amazon ECS — ИИ для бизнеса

Secure AI agents with Amazon Bedrock AgentCore Identity на Amazon ECS

Прослушать статью

AI-агенты в продакшене требуют защищенного доступа к внешним сервисам. Amazon Bedrock AgentCore Identity, доступный как отдельный сервис, защищает доступ AI-агентов к внешним сервисам независимо от того, работают ли они на таких вычислительных платформах, как Amazon ECS, Amazon EKS, AWS Lambda или вне AWS.

Ранее в отдельной публикации уже рассматривалось управление учетными данными AgentCore Identity для AI-агентов. При запуске агентов в средах вроде ECS возникают два вопроса: как построить принадлежащую приложению конечную точку Session Binding и как управлять жизненным циклом workload access token.

В этой статье реализован Authorization Code Grant (3-legged OAuth) на Amazon ECS с безопасной привязкой сессии и токенами с ограниченными правами. Показана рабочая реализация со следующими свойствами:

  • Безопасная привязка сессии, которая предотвращает CSRF-атаки и browser-swapping-атаки
  • Токены доступа, ограниченные каждой пользовательской сессией, в соответствии с принципом наименьших привилегий
  • Разделение ответственности между рабочей нагрузкой агента и сервисом привязки сессии
  • Аутентификация и авторизация с использованием OAuth 2.0 и OIDC

Аутентификация и авторизация с OAuth 2.0 и OIDC

Это решение использует OAuth 2.0 (RFC 6749) и OpenID Connect (OIDC). OIDC аутентифицирует пользователей — кто они, а OAuth 2.0 авторизует их действия — что они могут делать.

Мы сосредоточены на Authorization Code Grant для доступа, делегированного пользователем. Пользователь проходит аутентификацию у поставщика идентификации и дает согласие. Затем приложение обменивает authorization code на access token, что создает аудируемый след. В этом потоке пользователь аутентифицируется у провайдера идентификации и дает согласие на доступ агента к конкретным ресурсам от своего имени. Приложение обменивает полученный authorization code на scoped access token, который Amazon Bedrock AgentCore Identity хранит в token vault. Поскольку каждый токен привязан к конкретной пользовательской идентичности и явному согласию, решение сохраняет проверяемую цепочку от аутентификации пользователя до действия агента.

Authorization Code Grant хорошо подходит для agentic workloads, которые действуют от имени пользователей, потому что он обеспечивает согласие пользователя до того, как агент сможет действовать, session binding, которая проверяет, что пользователь, инициировавший запрос на авторизацию, и пользователь, давший согласие, — это одно и то же лицо, а также scoped delegation, ограничивающую агента только теми правами, которые одобрил пользователь.

Callback URL против session binding URL

В этом контексте поток Authorization Code Grant использует два URL, которые часто путают:

  • Callback URL: автоматически создается при создании OAuth client в AgentCore Identity. Он указывает на AgentCore Identity и должен быть зарегистрирован в Authorization Server как адрес перенаправления, куда отправляется authorization code после аутентификации пользователя.
  • Session Binding URL: URL, указывающий на сервис под управлением клиента, который завершает привязку сессии между аутентифицированным пользователем и OAuth-потоком. Эта конечная точка реализуется и размещается заказчиком.

Обзор решения

Эта архитектурная схема показывает, как AgentCore Identity защищает self-hosted AI-агента на Amazon ECS. В инструкции используется Microsoft Entra ID как поставщик идентификации, но поддерживаются и другие OIDC-совместимые провайдеры. Полный исходный код и предварительные требования для этого walkthrough доступны в сопутствующем GitHub repository.

Решение разворачивает два сервиса в Amazon ECS за Application Load Balancer. Agentic Workload запускает AI-агента и обрабатывает пользовательские запросы. Session Binding Service обрабатывает OAuth callbacks, связывая пользовательские сессии со сторонними access token. Оба сервиса используют Amazon Bedrock AgentCore Identity для входящей аутентификации через OIDC и исходящей авторизации действий от имени пользователя. Пронумерованные пометки на схеме соответствуют следующим описаниям.

  1. Входящая аутентификация и маршрутизация трафика: запросы приходят в Amazon Application Load Balancer (ALB), который аутентифицирует пользователя через встроенный OIDC-flow ALB. Трафик шифруется по HTTPS с использованием сертификата из AWS Certificate Manager, а alias A record в публичной hosted zone Amazon Route 53 направляет трафик на балансировщик. После аутентификации пользователя через OIDC ALB пересылает запрос в кластер Amazon ECS. ALB добавляет заголовок x-amzn-oidc-data с claims пользователя в формате JWT, где поле sub уникально идентифицирует пользователя.
  2. Рабочая нагрузка агента: Agentic Workload поднимает сервер FastAPI с конечной точкой /invocations, которая принимает sessionId и message. Сервер FastAPI передает их агенту, построенному с помощью Strands Agents. Также можно использовать LangChain или другие agent SDK, поскольку сервер обрабатывает запросы независимо от фреймворка агента. Агент вызывает large language model (LLM) в Amazon Bedrock, хотя подойдут и другие поставщики моделей. Состояние сессии агент хранит в бакете Amazon S3 и использует claim sub пользователя как префикс ключа, чтобы изолировать сессии между пользователями. У агента также есть инструменты для действий от имени пользователя в GitHub, что требует OAuth access token пользователя.
  3. Исходящая аутентификация с AgentCore Identity: когда агенту нужно действовать от имени пользователя в стороннем сервисе вроде GitHub, он запрашивает OAuth access token через AgentCore Identity. Если действительного токена нет, AgentCore Identity инициирует Authorization Code Grant flow и предлагает пользователю разрешить доступ.
  4. Обработка OAuth callback: после того как пользователь разрешает доступ, Session Binding Service завершает OAuth-flow, связывая авторизацию с правильной пользовательской сессией через AgentCore Identity.
  5. Пользовательский интерфейс: сервер FastAPI, на котором размещена agentic workload, открывает конечную точку /docs, которая рендерит OpenAPI specification как интерактивную HTML-страницу. Конечный пользователь взаимодействует с агентом через эту страницу, которая служит минимальным интерфейсом для демонстрации.

Amazon CloudWatch собирает логи, а отдельный бакет S3 хранит access logs как для load balancer, так и для data bucket. ECS загружает контейнерные образы из Amazon ECR. Набор базовых правил AWS WAF прикреплен к load balancer для базовой защиты от распространенных веб-эксплойтов. Customer managed key (CMK) в Amazon KMS шифрует данные, за исключением бакета access logs, для которого требуется управляемое Amazon S3 шифрование (SSE-S3).

Amazon Bedrock AgentCore Identity: Authorization Code Grant

В этой инструкции адаптирован общий session binding flow AgentCore Identity для self-hosted-архитектуры, использующей ALB для аутентификации, выделенный Session Binding Service и прямые API-вызовы вместо AgentCore SDK и Runtime.

Диаграмма последовательности показывает, как workload identity AgentCore Identity, workload access tokens и OAuth 2.0 credential provider работают вместе, чтобы безопасно предоставлять OAuth token агенту от имени пользователя. Этот поток предполагает, что аутентифицированный пользователь еще не авторизовал агенту доступ к своим ресурсам, то есть в AgentCore Identity Token Vault нет действительного токена.

  1. Аутентифицированный пользователь отправляет запрос в agentic workload. Agentic Workload извлекает идентификатор пользователя из claim sub в подписанном ALB JWT из заголовка x-amzn-oidc-data, чтобы идентифицировать пользователя.
  2. Agentic Workload вызывает API GetWorkloadAccessTokenForUserId, передавая userId и workloadName, чтобы получить workload access token, представляющий идентичность агента в рамках этого пользователя.
  3. AgentCore Identity возвращает workload access token в agentic workload.
  4. Agentic Workload вызывает API GetResourceOauth2Token, передавая workload access token, имя настроенного OAuth 2.0 credential provider, session binding URL (см. параметр callbackUrl parameter) и необходимые scopes, например scope read:user в GitHub. Это запрашивает OAuth token для стороннего сервиса (в данном случае GitHub).
  5. Поскольку для этого пользователя действительного токена нет, AgentCore Identity создает sessionURI, который отслеживает состояние процесса авторизации в ходе последующих запросов и ответов во время OAuth 2.0-аутентификации.
  6. AgentCore Identity возвращает authorization URL и session URI в workload.
  7. Agentic Workload возвращает authorization URL пользователю, предлагая ему разрешить доступ.
  8. Пользователь открывает authorization URL и предоставляет агенту разрешение на экране согласия стороннего провайдера.
  9. Authorization Server отправляет authorization code в AgentCore Identity.
  10. AgentCore Identity перенаправляет пользователя на Session Binding URL с добавленным session URI, маршрутизируя его в Session Binding Service.
  11. Браузер пользователя следует редиректу к Session Binding Service через Session Binding URL. ALB добавляет JWT в заголовок x-amzn-oidc-data.
  12. Session Binding Service вызывает API CompleteResourceTokenAuth с session URI и user ID, извлеченным из JWT, привязывая завершенную авторизацию к правильной пользовательской сессии. При успехе сервис возвращает статическую HTML-страницу, принадлежащую приложению, подтверждающую успешную авторизацию.
  13. AgentCore Identity обменивает authorization code у Authorization Server на OAuth2 access token.
  14. Authorization Server возвращает OAuth2 access token.
  15. AgentCore Identity сохраняет токен в Token Vault.
  16. AgentCore Identity возвращает успешный результат в Session Binding Service.
  17. Session Binding Service показывает пользователю сообщение «Authorization complete».

При последующих запросах необходимость повторной авторизации зависит от учетных данных, выданных authorization server. AgentCore Identity хранит в Token Vault и access token, и refresh token, если он доступен. Когда refresh token присутствует — как в GitHub при включенном User-to-server token expiration — AgentCore Identity автоматически использует его для получения нового access token после истечения срока действия исходного токена, без нового запроса согласия пользователя. Если refresh token не был выдан и access token истек, пользователя попросят пройти авторизацию снова. Обратите внимание, что токены могут быть также отозваны на стороне провайдера; в таком случае параметр forceAuthentication: true принудительно запускает новый поток аутентификации.

Привязка сессии:

Session binding защищает от двух угроз безопасности:

Cross-Site Request Forgery (CSRF): атакующий пытается привязать собственный OAuth token к идентичности жертвы, из-за чего агент жертвы незаметно получает доступ к ресурсам атакующего, что позволяет выполнять утечку и внедрение данных.

Browser Swapping Attack: атакующий обманом заставляет жертву дать согласие от своего имени, привязывая OAuth token жертвы к идентичности атакующего и давая атакующему прямой доступ к ресурсам жертвы.

Session binding предотвращает обе атаки, гарантируя, что user ID в agent workload совпадает с user ID в Session Binding Service, при этом обе идентичности криптографически подтверждаются через цепочку аутентификации.

AgentCore Identity также поддерживает необязательный параметр customState в API GetResourceOauth2Token, который можно использовать для передачи криптографически случайного nonce, чтобы защитить конечную точку callback от CSRF-атак, как рекомендует спецификация OAuth 2.0.

Почему мы используем GetWorkloadAccessTokenForUserId с AWS ALB и Microsoft Entra ID

Рекомендуемое API для получения workload access token — GetWorkloadAccessTokenForJWT. В этом решении вместо него используется GetWorkloadAccessTokenForUserId.

GetWorkloadAccessTokenForJWT требует динамически валидируемый JWT, подпись которого можно проверить во время выполнения по опубликованным ключам подписи issuer, а claim aud которого совпадает с вашим приложением. Чтобы получить такой токен от Microsoft Entra ID, необходимо включить Application ID в scope OIDC-запроса аутентификации; подробности см. в AgentCore Microsoft Inbound documentation.

Однако это несовместимо с потоком AWS ALB OIDC.

В рамках OIDC-handshake, как описано в ALB OIDC documentation, ALB отправляет access token в UserInfo endpoint Entra, чтобы получить claims аутентифицированного пользователя — это обязательный шаг в потоке аутентификации ALB. Этот UserInfo endpoint размещен в Microsoft Graph (https://graph.microsoft.com/oidc/userinfo) и принимает только токены, scoped для Microsoft Graph. Если включить Application ID в scope, полученный access token будет иметь ваше приложение в качестве audience, UserInfo endpoint отклонит его с 401, а ALB вернет 561.

Если убрать Application ID из scope, Entra по умолчанию назначает audience access token как Microsoft Graph (00000003-0000-0000-c000-000000000000). Handshake ALB проходит, но полученный JWT нельзя динамически валидировать AgentCore. Он непригоден для использования с GetWorkloadAccessTokenForJWT.

Это решение: ALB завершает handshake с использованием Graph-scoped token. Затем ALB пересылает в заголовке x-amzn-oidc-data подписанный ALB JWT с claims пользователя из UserInfo endpoint, включая claim sub, который уникально идентифицирует аутентифицированного пользователя. Мы валидируем этот подписанный ALB JWT с помощью опубликованных AWS ключей подписи, извлекаем sub и передаем его в GetWorkloadAccessTokenForUserId.

Реализация

Полный код доступен в GitHub repository.

Получение workload access token

Сервер извлекает идентификатор пользователя из claim sub в JWT и запрашивает workload access token у AgentCore Identity. Затем сервер использует этот токен, session ID и сообщение, чтобы вызвать агента от имени пользователя. Обратите внимание, что session ID здесь означает conversation session агента, а не OAuth session URI из потока авторизации.

Формулы и расчет
@router.post("/invocations")
async def invoke_agent(
    request: InvocationRequest,
    user_id: str = Depends(get_current_user),
    settings: Settings = Depends(get_settings),
    agent_service: AgentService = Depends(get_agent_service),
) -> StreamingResponse:
 """Invoke agent with streaming response."""
try:
        agentcore = boto3.client("bedrock-agentcore", region_name=settings.identity_aws_region)
        response = agentcore.get_workload_access_token_for_user_id(
            workloadName=settings.workload_identity_name, userId=user_id
        )
        workload_access_token = response["workloadAccessToken"]        
return StreamingResponse(
            content=agent_service.stream_response(
                user_message=request.user_message,
                session_id=request.session_id,
                user_id=user_id,
                workload_access_token=workload_access_token,
            ),
            media_type="text/event-stream",
        )

Запрос access token

Сервер использует декоратор require_access_token из AgentCore SDK для получения OAuth 2.0 access token, см. Obtain OAuth 2.0 access token. Мы модифицируем декоратор так, чтобы он принимал workload access token как явный параметр, а не определял его внутри, что дает прямой контроль над жизненным циклом токена и при этом сохраняет логику SDK для получения токена и обработки ошибок.

Формулы и расчет
def requires_access_token(
    *,
    provider_name: str,
    scopes: list[str],
    auth_flow: Literal["M2M", "USER_FEDERATION"],
    workload_access_token: str | None = None,
    session_binding_url: str | None = None,
    on_auth_url: Callable[[str], Any] | None = None,
    force_authentication: bool = False,
    token_poller: TokenPoller | None = None,
    custom_state: str | None = None,
    custom_parameters: dict[str, str] | None = None,
    into: str = "access_token",
    region: str | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
 """Fetch OAuth2 access token with explicit workload token.
    Args:
        provider_name: The credential provider name
        scopes: OAuth2 scopes to request
        auth_flow: Authentication flow type ("M2M" or "USER_FEDERATION")
        workload_access_token: The workload access token (explicit, not from context)
        session_binding_url: Session Binding URL pointing to the customer-managed service that completes the session binding
        on_auth_url: Handler invoked with the authorization URL when user authorization is required
        force_authentication: Force re-authentication
        token_poller: Custom token poller implementation
        custom_state: State for callback verification
        custom_parameters: Additional OAuth parameters
        into: Parameter name to inject the token into
        region: AWS region
    Returns:
        Decorator function
    """
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
    client = IdentityClient(region)

    @wraps(func)
    async def wrapper(*args: Any, **kwargs: Any) -> Any:
        try:
            if not workload_access_token:
                raise ValueError("workload_access_token is required")
            token = await client.get_token(
                provider_name=provider_name,
                agent_identity_token=workload_access_token,
                scopes=scopes,
                auth_flow=auth_flow,
                callback_url=session_binding_url,
                on_auth_url=on_auth_url,
                force_authentication=force_authentication,
                token_poller=token_poller,
                custom_state=custom_state,
                custom_parameters=custom_parameters,
            )
            kwargs[into] = token
            return await func(*args, **kwargs)
        except Exception:
            logger.exception("Error in requires_access_token decorator")
            raise
return wrapper

return decorator

Наш класс инструментов использует этот декоратор, чтобы передавать access token при вызове GitHub API.

Формулы и расчет
class GitHubTools:
 """Tools for interacting with GitHub using OAuth authentication."""
def _on_auth_url(self, url: str) -> None:
 """Handle authorization URL by raising AuthorizationRequiredError.
    This URL must be presented to the user to grant access.
    """
raise AuthorizationRequiredError(provider="GitHub", auth_url=url)

async def _call_github_api(
    self, endpoint: str, scopes: list[str], params: dict | None = None
) -> Any:
 """Make authenticated GitHub API call.
    Raises:
        ApiError: When API call fails
    """
@requires_access_token(
        provider_name=self.config.provider_name,
        scopes=scopes,
        auth_flow="USER_FEDERATION",
        workload_access_token=self.config.workload_access_token,
        session_binding_url=self.config.session_binding_url,
        on_auth_url=self._on_auth_url,
        region=self.config.aws_region,
    )
    async def make_request(*, access_token: str) -> Any:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{self.config.github_api_base}{endpoint}",
                headers={
                    "Authorization": f"Bearer {access_token}",
                    "Accept": "application/vnd.github+json",
                    "X-GitHub-Api-Version": "2022-11-28",
                },
                params=params or {},
                timeout=10.0,
            )
            response.raise_for_status()
            return response.json()

    try:
        return await make_request()

Каждый инструмент в классе использует этот метод, как показано ниже:

Формулы и расчет
from strands import tool
class GitHubTools:
    @tool
async def get_github_user(self) -> GitHubUser:
 """Get the authenticated GitHub user's profile information.
 
        Use this tool when the user wants to:
        - See their GitHub profile
        - Check who they are authenticated as
        - View their GitHub account details
        Returns:
            GitHub user profile
        Raises:
            ApiError: When API call fails
        """
        result: dict[str, Any] = await self._call_github_api(
            "/user", scopes=["read:user"]
        )
        return GitHubUser.model_validate(result)

Три ключевых проектных решения:

  • Pydantic BaseModel в качестве типов возврата: GitHubUser и GitHubProject — подклассы BaseModel. Strands автоматически выводит описания инструментов из их схемы и docstring, давая LLM структурированный контекст о типе возврата каждого инструмента.
  • Обработка ошибок с сохранением типов: когда токен отсутствует и AgentCore Identity возвращает authorization URL, callback on_auth_url выбрасывает AuthorizationRequiredError, а не возвращает строку — инструмент, объявляющий GitHubUser как тип возврата, не может вернуть URL. Потоковая прослойка агента перехватывает исключение и показывает URL пользователю.
  • Scopes на уровне инструмента: каждый инструмент объявляет только те OAuth scopes, которые ему действительно нужны, поддерживая согласие в рамках принципа наименьших привилегий.

Завершение OAuth-потока привязки сессии

Далее рассматривается сервис привязки сессии. Когда пользователь разрешает доступ в GitHub, GitHub перенаправляет его на {session_binding_url}?session_id={session_id}, где session_id соответствует session URI, который AgentCore Identity включил в исходный authorization URL. Это связывает запрос на привязку сессии с конкретным OAuth-потоком, инициированным агентом.

Формулы и расчет
@router.get("/session-binding", response_class=HTMLResponse)
async def oauth_session_binding(
    session_id: str = Query(..., description="Session URI from AgentCore Identity"),
    user_id: str = Depends(get_current_user),
    settings: Settings = Depends(get_settings),
) -> HTMLResponse:
 """Handle OAuth2 session binding from external providers.""" 
    client = boto3.client("bedrock-agentcore", region_name=settings.identity_region)
 
    try:
        client.complete_resource_token_auth(
            sessionUri=session_id,
            userIdentifier={"userId": user_id},
        )

Сервис извлекает идентификатор пользователя из claim sub в заголовке x-amzn-oidc-data, обеспечивая единообразную идентичность на всем протяжении потока. Затем он вызывает complete_resource_token_auth с session URI и user ID, что привязывает полученный access token к правильной пользовательской сессии.

Очистка

Чтобы не нести будущие расходы, удалите ресурсы, созданные этим решением, когда они больше не нужны. Следуйте инструкциям по очистке ресурсов.

Заключение

В этой статье вы узнали, как защитить AI-агентов на Amazon ECS с помощью Amazon Bedrock AgentCore Identity. Вы увидели, как входящая аутентификация проверяет идентичность пользователя через OIDC, как исходящая аутентификация реализует OAuth 2.0 с привязкой сессии, и как разделение session binding от рабочей нагрузки агента позволяет независимо масштабировать компоненты и защищает от атак. Этот подход работает на разных вычислительных платформах — независимо от того, запускаете ли вы агентов в ECS, EKS, Lambda или вообще вне AWS. Он также подходит не только для GitHub, но и для других сервисов с OAuth 2.0, таких как Jira, Salesforce или Google Calendar. Дальнейшие шаги:

  1. Изучите полный код в GitHub, чтобы увидеть реализацию
  2. Адаптируйте подход под своего OAuth-провайдера, заменив GitHub на ваш сервис
  3. Изучите дополнительные паттерны в AgentCore Identity Samples repository
  4. Прочитайте материал про AgentCore Runtime для управляемого хостинга агентов
  5. Изучите документацию AgentCore Identity

Материал — перевод статьи с английского.

Оригинал: Secure AI agents with Amazon Bedrock AgentCore Identity on Amazon ECS