3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Auth0+Amazon Bedrock AgentCore Identityの認証をTerraformで構築する

Posted at

はじめに

Amazon Bedrock AgentCore考察記事第5弾。

前回・前々回の記事では、Amazon Bedrock AgentCore GatewayとAuth0の認証を組み合わせた。

今回は、Amazon Bedrock AgentCore Gatewayを使用せずに独立した認証を行う場合の方法を考察する。
Gatewayを使用する方が簡単であるため、あまりこれを単独で行う意味はないかもしれない、という点にご留意いただきたい。

Workload Identityについて

今回の記事で重要になるのが、Workload Identityというリソースだ。

Workload identities represent the digital identity of your agents within the AWS ecosystem. They serve as a stable anchor point that persists across different deployment environments and authentication schemes, allowing agents to maintain consistent identity whether they're using IAM roles for AWS resource access, OAuth2 tokens for external service integration, or API keys for third-party tool access. The identity system abstracts the complexity of managing multiple credential types while providing a unified interface for authentication and authorization operations.

Workload identities integrate seamlessly with the broader AgentCore Identity ecosystem, including the token vault for secure credential storage (see Secure credential storage), Resource credential providers for external service access (see Configure credential provider), and the AgentCore Identity directory for centralized management

要は、「認証タイプに関係なく、統一されたインタフェースで安全に認証に必要な情報を管理するよ」ということらしい。ここで言う認証タイプとは、OAuth2認証や、API Keyによる認証を指す。
なお、「安全に認証な必要な情報を管理」の詳細はこちら。

Secure credential storage

The token vault provides security for storing OAuth 2.0 tokens, OAuth client credentials, and API keys with comprehensive encryption at rest and in transit. All credentials are encrypted using either customer-managed or service-managed AWS KMS keys and access-controlled to prevent unauthorized retrieval. The vault implements strict access controls, ensuring that credentials can only be accessed by authorized agents for specific purposes and only when they present verifiable proof of workload identity.

Building on OAuth 2.0's scope-based security model, the token vault implements additional security measures where every access request is validated independently, even from callers within the same trust domain. This extra security mechanism is necessary to protect end-user data from malicious or misbehaving agent code. The vault securely stores OAuth 2.0 tokens, reducing security risks while improving your overall security posture.

今回、Amazon Bedrock AgentCore Identityのキー情報としては、Auth0のUser Federation認証で払い出したJWTを用い、その後にWorkload Identity経由で別の認証リソースに対してMachine to Machine認証を行う、というモデルにする。

Auth0

上記の通り、User Federation認証のためのResource Server/クライアントと、Machine to Machine認証用のリソース/クライアントを作成する。

User Federation認証用Resource Server/クライアント

ここは、前回と比べても特筆すべき部分はない。通常のユーザ認証によるJWT払い出しができればOKだ。

resource "auth0_client" "amazon_bedrock_agentcore_identity_uf" {
  name        = "Amazon Bedrock AgentCore Identity(User Federation)"
  description = "API for Amazon Bedrock AgentCore Idneity(User Federation) authorization"
  app_type    = "native"

  custom_login_page_on                = false
  is_first_party                      = true
  is_token_endpoint_ip_header_trusted = false
  oidc_conformant                     = true
  require_proof_of_possession         = false

  grant_types = [
    "urn:ietf:params:oauth:grant-type:device_code",
  ]

  jwt_configuration {
    alg                 = "RS256"
    lifetime_in_seconds = 3600
    secret_encoded      = false
  }

  refresh_token {
    leeway          = 0
    token_lifetime  = 31557600
    rotation_type   = "non-rotating"
    expiration_type = "non-expiring"
  }
}

resource "auth0_resource_server" "amazon_bedrock_agentcore_identity_uf" {
  name        = "Amazon Bedrock AgentCore Idneity(User Federation) Resource Server"
  identifier  = "urn:${local.auth0_resource_server_uf}:api"
  signing_alg = "RS256"

  allow_offline_access                            = false
  token_lifetime                                  = 3600
  skip_consent_for_verifiable_first_party_clients = true
  enforce_policies                                = true
  token_dialect                                   = "access_token"
}

Machine to Machine認証用Resource Server/クライアント

今回、scopeはテキトーで、access:exampleとする。
※この後作成するクライアントのエージェントアプリと合致していればなんでも良い

resource "auth0_client" "amazon_bedrock_agentcore_identity_m2m" {
  name        = "Amazon Bedrock AgentCore Identity(Machine to Machine)"
  description = "API for Amazon Bedrock AgentCore Idneity(Machine to Machine) authorization"
  app_type    = "non_interactive"

  custom_login_page_on                = false
  is_first_party                      = true
  is_token_endpoint_ip_header_trusted = false
  oidc_conformant                     = true
  require_proof_of_possession         = false

  grant_types = [
    "client_credentials",
  ]

  callbacks = [
    "https://bedrock-agentcore.${data.aws_region.current.region}.amazonaws.com/identities/oauth2/callback"
  ]

  jwt_configuration {
    alg                 = "RS256"
    lifetime_in_seconds = 3600
    secret_encoded      = false
  }

  refresh_token {
    leeway          = 0
    token_lifetime  = 31557600
    rotation_type   = "non-rotating"
    expiration_type = "non-expiring"
  }
}

resource "auth0_resource_server" "amazon_bedrock_agentcore_identity_m2m" {
  name        = "Amazon Bedrock AgentCore Idneity(Machine to Machine) Resource Server"
  identifier  = "urn:${local.auth0_resource_server_m2m}:api"
  signing_alg = "RS256"

  allow_offline_access                            = false
  token_lifetime                                  = 300
  skip_consent_for_verifiable_first_party_clients = true
}

resource "auth0_client_credentials" "amazon_bedrock_agentcore_identity_m2m" {
  client_id             = auth0_client.amazon_bedrock_agentcore_identity_m2m.id
  authentication_method = "client_secret_post"
}

resource "auth0_resource_server_scopes" "amazon_bedrock_agentcore_identity_m2m" {
  resource_server_identifier = auth0_resource_server.amazon_bedrock_agentcore_identity_m2m.identifier

  scopes {
    name = "access:example"
  }
}

resource "auth0_client_grant" "amazon_bedrock_agentcore_identity_m2m" {
  client_id = auth0_client.amazon_bedrock_agentcore_identity_m2m.id
  audience  = auth0_resource_server.amazon_bedrock_agentcore_identity_m2m.identifier
  scopes = [
    "access:example",
  ]
}

IAM

前回までの記事では、Amazon Bedrock AgentCoreのプリンシパルに対する権限設定が必要であったが、今回はローカルで動作させるため、ロールを新規に作成する必要はない。必要に応じて、既存のロールに以下のポリシーをアタッチしよう。

なお、今回は、この後のソースでJWTを使った認証を行うためにactionsはbedrock-agentcore:GetWorkloadAccessTokenForJWTを設定しているが、呼び出すAPIによって権限も変えよう。
対象となるAPIは以下の通りだ。

  • bedrock-agentcore:GetWorkloadAccessToken
  • bedrock-agentcore:GetWorkloadAccessTokenForUserId
data "aws_iam_policy_document" "bac_identity_custom" {
  statement {
    effect = "Allow"

    actions = [
      "bedrock-agentcore:GetWorkloadAccessTokenForJWT",
    ]

    resources = [
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:workload-identity-directory/default",
      data.external.bedrock_agentcore_workload_identity.result.workload_identity_arn,
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "bedrock-agentcore:GetResourceOauth2Token",
    ]

    resources = [
      "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:token-vault/default",
      data.external.bedrock_agentcore_oauth2_credential_provider.result.credential_provider_arn,
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "secretsmanager:GetSecretValue",
    ]

    resources = [
      data.external.bedrock_agentcore_oauth2_credential_provider.result.client_secret_arn,
    ]
  }
}

Amazon Bedrock AgentCore Identity

Amazon Bedrock AgentCore OAuth2 Credential Provider

まずは簡単な方から作成する。呼び出すPythonスクリプトは前回までのものと同じで問題ないし。設定もTerraformのリソース識別子以外は変更しなくて問題ない。

data "external" "bedrock_agentcore_oauth2_credential_provider" {
  program = ["python3", "./bedrock_agentcore_oauth2_credential_provider.py"]

  query = {
    aws_region    = data.aws_region.current.region
    provider_name = local.bac_oauth2_credential_provider_name
    discovery_url = "https://${var.auth0_domain}/.well-known/openid-configuration"
    client_id     = auth0_client.amazon_bedrock_agentcore_identity_m2m.client_id
    client_secret = auth0_client_credentials.amazon_bedrock_agentcore_identity_m2m.client_secret
  }
}

Amazon Bedrock AgentCore Workload Identity

こちらも作り自体は難しくないが、一点だけ特殊な点がある。Pythonスクリプトのendpoint_url=f"https://bedrock-agentcore.{aws_region}.amazonaws.com"のステートメントだ。
通常、boto3でbedrock-agentcore-controlのクライアントを作成する場合は、裏ではAmazon Bedrock AgentCoreのコントロールプレーンhttps://bedrock-agentcore-control.{aws_region}.amazonaws.comを呼び出しているのだが、コントロールプレーン経由で作ったWorkload Identityは、ユーザトランザクション中に任意にトークン発行のAPIを実行することができない(Validationエラーになる)
そのため、今回はendpoint_urlで無理矢理データプレーンに捻じ曲げて利用する。

無理矢理捻じ曲げると言っても、実はBedrock AgentCore SDKの中でも同じような捻じ曲げをしているので、公式の実施方法である。なぜこういう作りになっているかは非常に謎だが、コントロールプレーンで作ったリソースは、コントロールプレーンの他のリソースからしか触れないようになっているようだ。

上述の通り、コントロールプレーンでもWorkload Identityは使用している。RuntimeもGatewayも、リソース作成時に裏でWorkload Identityを同時作成しているのだ(aws bedrock-agentcore-control list-workload-identitiesしてみると分かる)。

しかも、Runtimeについては、作るときは同時に作るのに、削除時は同時に消してくれない。試行錯誤でリソースを作ったり消したりしていると、知らないうちにリソースが大量に増えているので気を付けよう。

data "external" "bedrock_agentcore_workload_identity" {
  program = ["python3", "./bedrock_agentcore_workload_identity.py"]

  query = {
    aws_region    = data.aws_region.current.region
    identity_name = local.bac_workload_identity_name
  }
}
bedrock_agentcore_workload_identity.py
"""
Create Amazon Bedrock AgentCore Workload Identity if it doesn't exist yet.
"""

import boto3
import botocore
import json
import sys


def main():
    params = json.load(sys.stdin)
    aws_region = params["aws_region"]
    identity_name = params["identity_name"]

    bac = boto3.client(
        "bedrock-agentcore-control",
        region_name=aws_region,
        endpoint_url=f"https://bedrock-agentcore.{aws_region}.amazonaws.com",
    )

    try:
        get_result = bac.get_workload_identity(name=identity_name)
        print(
            json.dumps(
                {
                    "workload_identity_arn": get_result["workloadIdentityArn"],
                }
            )
        )
        exit(0)
    except botocore.exceptions.ClientError as e:
        code = e.response.get("Error", {}).get("Code")
        if code == "ResourceNotFoundException":
            pass
        else:
            sys.stderr.write(
                json.dumps({"error": f"get_workload_identity() failed {str(e)} {code}"})
            )
            sys.exit(1)

    try:
        create_result = bac.create_workload_identity(name=identity_name)
    except botocore.exceptions.ClientError as e:
        sys.stderr.write(
            json.dumps({"error": f"create_workload_identity() failed {str(e)}"})
        )
        sys.exit(1)

    print(
        json.dumps(
            {
                "workload_identity_arn": create_result["workloadIdentityArn"],
            }
        )
    )


if __name__ == "__main__":
    main()

Destroy用の削除スクリプトの作成

externalデータソース方式だと、terraform destroy時にリソースが消せないため、terraform_dataを削除時にのみ発動するようにして、明示的に削除をしておく。
Destroy時はcommand内で直接他リソースへの参照ができないため、inputで予め値を作っておくことでこの問題に対処が可能だ。

resource "terraform_data" "destroy_bedrock_agentcore" {
  input = {
    aws_region                      = data.aws_region.current.region
    workload_identity_name          = local.bac_workload_identity_name
    oauth2_credential_provider_name = local.bac_oauth2_credential_provider_name
  }

  provisioner "local-exec" {
    when    = destroy
    command = <<-EOT
      aws bedrock-agentcore-control delete-workload-identity \
        --region ${self.input.aws_region} \
        --name ${self.input.workload_identity_name} &&
      aws bedrock-agentcore-control delete-oauth2-credential-provider \
        --region ${self.input.aws_region} \
        --name ${self.input.oauth2_credential_provider_name}
    EOT
  }
}

いざ、動かす!

……の前に、実際に動かすAIエージェントの認証部分のコードを作ろう。

なお、スクリプトでは、Bedrock AgentCore SDKを使用していない。
Bedrock AgentCore SDKでは、OAuth2認証をする際にcustomParametersが設定できないため、M2M認証におけるaudienceをIdPに渡せないのだ。

    device_code_response = requests.post(
        f"https://{os.environ.get("AUTH0_DOMAIN")}/oauth/device/code",
        data={
            "client_id": os.environ.get("AUTH0_CLIENT_ID"),
            "scope": "access:example",
            "audience": os.environ.get("AUTH0_AUDIENCE_UF"),
        },
    ).json()

まずは、上記でUser Federation認証を起動し、JWTを発行する。
※前回までの記事と同様で、scopeだけ変えてある。

次に、Workload Identityからアクセストークンを取得する。

    access_token_result = bac.get_workload_access_token_for_jwt(
        workloadName=os.environ.get("AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_NAME"),
        userToken=access_token,
    )
    workload_access_token = access_token_result["workloadAccessToken"]

さらにここで取れたworkload_access_tokenを使って、M2MのResource Serverに対してOAuth2認証をすればよい。

    oauth2_token_result = bac.get_resource_oauth2_token(
        workloadIdentityToken=workload_access_token,
        resourceCredentialProviderName=os.environ.get(
            "AWS_BEDROCK_AGENTCORE_CREDENTIAL_PROVIDER_NAME"
        ),
        scopes=["access:example"],
        oauth2Flow="M2M",
        forceAuthentication=False,
        customParameters={"audience": os.environ.get("AUTH0_AUDIENCE_M2M")},
    )

これで払い出しをしていけば、前回同様の画面でGatewayを使わずとも認証ができる。

さて、これでエージェントを実行すれば、OutboundなGatewayがなくても認証できることが分かるだろう。

なお、この方式の場合、最初に払い出しているAuth0のJWTトークンがexpiredになっていなくても、都度認証してトークンを払い出している。サポート発行したが、現時点では都度認証にいくのが仕様らしいので、APIコール数によって課金されるモデルのIdPを利用している場合は注意が必要だ。
この件は、サポートケース内でもフィードバックしているので、GAまでに改善されることを期待したい。

というわけで、仕様上の懸念が多少残るものの、これで、Gatewayを使わないAmazon Bedrock AgentCore Identity単独でのユーザ認証ができるようになった!

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?