이제 도구 네 개가 각 에이전트마다 파이썬 함수를 하나씩 배선하는 대신 하나의 AgentCore Gateway 뒤에 놓인다. cloudwatch-read, logs-read, cost-read는 스포크 계정에서 읽기 전용 역할을 어썸하며 무엇도 변경할 수 없고, 플랫폼의 유일한 변경 도구인 ssm-execute조차 스포크에 직접 닿지 못한다. 사람의 Slack 승인을 기다리며 멈추는 Step Functions 실행을 시작할 수 있을 뿐이다. 그 멈춤은 UI를 위한 장식이 아니다. 스포크 계정에서 무언가를 바꿀 수 있는 AWS 자격 증명이 발급되는 유일한 지점이며, 사람이 승인을 클릭한 뒤에만 발급된다.

1부는 시나리오를 정하고 AgentCore와 Strands를 선택했다. 2부는 계정 경계를 구축했다. 이번 편이 그대로 존재한다고 가정하고 수정 없이 재사용하는 ops-readonlyops-mutate 스포크 역할이다. 3부는 도구 두 개를 직접 배선한 첫 에이전트를 내놓았다. 트리아지 에이전트가 인프로세스로 호출하는, @tool이 붙은 평범한 파이썬 함수인 cloudwatch_read와 logs_read였고, 그때는 Gateway가 아직 라우팅할 대상도 없는 간접 계층에 불과했을 것이라고 분명히 말했다. 이번 편은 그 간접 계층이 제 몫을 하기 시작하는 지점이다. 두 번째 읽기 도구(cost-read)와 플랫폼의 첫 변경 도구(ssm-execute)가 이제 둘 다 존재하며, 각 에이전트가 독립적으로 호출할 수 있는 도구가 네 개라는 것은 공유되고 중앙에서 범위가 지정된 도구 평면이 더 이상 시기상조가 아니게 되는 딱 그 숫자다. 동반 코드는 github.com/flightlesstux/agents-on-callterraform/20-gateway-tools/agents/tools/에 있으며, 아래의 모든 스니펫은 글을 위해 단순화한 것이 아니라 그 파일들에서 그대로 발췌했다.

인라인 도구가 두 번째 에이전트에서 확장을 멈추는 이유

3부의 도구 두 개는 agents/triage/agent.py 안에 있었다. 평범한 함수, 각각 크로스 계정 어썸-롤 호출 하나씩, 그것들을 필요로 하는 단 하나의 에이전트가 직접 호출했다. 에이전트가 하나뿐일 때는 아무 문제가 없다. 문제는 두 번째 에이전트에서 시작된다. 5부의 비용 에이전트는 cloudwatch-read와 cost-read가 필요하고, 런북 에이전트는 cloudwatch-read, logs-read, 그리고 결국 ssm-execute가 필요하다. 3부가 했던 방식대로 인프로세스로 배선하면, 그것은 같은 파이썬 함수를 세 개의 에이전트 코드베이스에 붙여넣는 것이고, 세 개의 IAM 역할이 각각 독립적으로 같은 sts:AssumeRole 권한을 부여하는 것이며, 크로스 계정 세션이 만들어지는 방식의 버그를 고쳐야 할 곳이 세 군데가 되는 것이다. 한 사본의 외부 ID 처리에 있는 오타는 그 하나의 에이전트의 호출이 다른 둘과 다른 방식으로 실패하기 시작할 때까지 아무도 알아차리지 못한다.

AgentCore Gateway는 도구를 더 똑똑하게 만들어서가 아니라 정확히 한 번만 존재하게 만들어서 복사-붙여넣기를 없앤다. 도구 하나당 Lambda 하나, Lambda 하나당 실행 역할 하나, 모든 에이전트의 도구 호출을 올바른 대상으로 라우팅하는 Gateway 하나다. 에이전트 자신의 IAM 역할은 더 이상 어떤 스포크로든 sts:AssumeRole이 전혀 필요 없다(3부의 트리아지 런타임 역할은 그 에이전트가 이번 편보다 먼저 만들어졌으므로 여전히 그 권한을 갖고 있다). 앞으로 나올 에이전트는 Gateway의 MCP 엔드포인트를 호출할 권한만 있으면 되며, 플랫폼이 발급하는 모든 크로스 계정 읽기 자격 증명은 이 모듈이 소유하는 정확히 네 개의 Lambda 실행 역할에서 나온다. 시리즈가 끝날 때까지 에이전트가 몇 개가 되든 상관없이 그렇다.

MCP 도구 평면으로서의 AgentCore Gateway

AgentCore Gateway는 2025년 10월 13일 AgentCore의 정식 출시 이후로 API, Lambda 함수, 기존 서비스를 MCP 호환 도구로 변환해왔으며, IAM 기반 인가도 같은 정식 출시 시점에 함께 제공됐다. 모든 Gateway 대상이 사용하는 프로토콜인 MCP 자체는 Anthropic이 2024년 11월에 AI 애플리케이션을 데이터와 도구 소스에 연결하기 위해 공개한 개방형 표준이다. Gateway의 기여는 에이전트의 코드가 각 Lambda의 ARN과 호출 형태를 직접 알아야 하는 대신, 네 개의 개별 Lambda 함수를 에이전트의 클라이언트 라이브러리가 도구를 발견할 수 있는 하나의 MCP 서버로 바꿔주는 데 있다. 가격은 AgentCore의 나머지 부분과 같은 형태를 따른다. API 호출(ListTools, InvokeTool, Ping) 1,000건당 0.005달러, 매달 인덱싱된 도구 100개당 0.02달러로, 도구 네 개는 모델 호출 한 번의 토큰 비용에 비하면 사실상 무료나 다름없다.

Gateway 리소스 자체는 짧다. authorizer_type = "AWS_IAM"은 이 시리즈 전체가 기대온 것과 같은 IAM 경계가 애초에 누가 Gateway를 호출할 수 있는지를 결정한다는 뜻이다. 이미 자체 신원 시스템을 갖춘 플랫폼을 위해 별도의 JWT 발급자를 세울 필요가 없다:

resource "aws_bedrockagentcore_gateway" "tools" {
  name            = "${var.platform_name}-tools"
  description     = "MCP tool plane: cloudwatch-read, logs-read, cost-read, ssm-execute. AWS_IAM authorizer, IAM does the access control both for who can call the Gateway and what each tool Lambda can touch."
  role_arn        = aws_iam_role.gateway.arn
  protocol_type   = "MCP"
  authorizer_type = "AWS_IAM"

  # Optional Cedar policy layer, see variables.tf's cedar_policy_engine_arn
  # comment for why the engine itself is a variable, not a resource, at this
  # part's date.
  dynamic "policy_engine_configuration" {
    for_each = var.cedar_policy_engine_arn == null ? [] : [var.cedar_policy_engine_arn]
    content {
      arn  = policy_engine_configuration.value
      mode = var.cedar_policy_engine_mode
    }
  }

  tags = merge(var.tags, { Component = "gateway" })
}

policy_engine_configuration 블록은 이 시리즈에서 지금까지 나온 테라폼 중 가장 최신 것이므로 날짜를 확인해둘 가치가 있다. Gateway용 Cedar 기반 정책 평가는 2026년 5월 27일 aws 프로바이더에 목록 전용 리소스로 먼저 실렸고, 이번 편 자체 날짜보다 8일 앞선 6월 10일, 프로바이더 버전 6.50.0에서 이 쓰기 경로 블록을 갖췄다. 시리즈의 백데이팅 규칙 안에서는 안전하지만 아슬아슬하게 안전할 뿐이며, Cedar 정책 엔진 자체를 만드는 테라폼 리소스는 아직 없고 이미 존재하는 것을 연결하는 것만 가능하다. var.cedar_policy_engine_arn은 기본값이 null이고 이 블록은 dynamic으로 감싸져 있다. 2부가 Bedrock 모델 접근에 썼던 것과 같은 패턴으로, 테라폼이 참조할 수는 있지만 아직 완전히 프로비저닝할 수는 없는 실제 AWS 기능이다.

Lambda 하나, 임무 하나: 도구 패턴

모든 도구는 같은 형태를 따른다. Lambda 함수 하나, 그 도구 하나에 필요한 것만으로 범위가 지정된 실행 역할, 그리고 에이전트의 MCP 클라이언트가 Lambda의 소스를 읽지 않고도 호출할 수 있도록 도구의 입력 스키마를 기술하는 Gateway 대상이다. cloudwatch-read가 가장 명확한 예다. 3부가 이미 내놓은 것과 같은 로직을 그저 옮겼을 뿐이기 때문이다. 실행 역할은 Lambda 서비스만 신뢰하며 두 가지만 부여한다. 설정된 스포크들에서 ops-readonly를 어썸하는 것과, 자신의 CloudWatch Logs에 쓰는 것이다:

data "aws_iam_policy_document" "cloudwatch_read_permissions" {
  statement {
    sid       = "AssumeOpsReadonlyInSpokes"
    effect    = "Allow"
    actions   = ["sts:AssumeRole"]
    resources = local.ops_readonly_role_arns
  }

  statement {
    sid    = "WriteOwnLogs"
    effect = "Allow"
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]
    resources = [local.lambda_log_arn]
  }
}

resource "aws_lambda_function" "cloudwatch_read" {
  function_name    = "${var.platform_name}-cloudwatch-read"
  description      = "AgentCore Gateway MCP tool: reads a CloudWatch metric from a spoke account via ops-readonly. Read-only."
  role             = aws_iam_role.cloudwatch_read.arn
  handler          = "cloudwatch_read.handler"
  runtime          = "python3.12"
  timeout          = 30
  memory_size      = 256
  filename         = data.archive_file.cloudwatch_read_zip.output_path
  source_code_hash = data.archive_file.cloudwatch_read_zip.output_base64sha256
  ...
}

그 정책 문서 어디에도 와일드카드 리소스가 없다. local.ops_readonly_role_arns는 설정된 스포크 계정 ID마다 ARN을 하나씩 만들고, 이 역할이 건드릴 수 있는 것은 그 목록과 자신의 로그 그룹 말고는 아무것도 없다. logs-read와 cost-read는 동일한 신뢰 정책과 권한 문서를 재사용한다. 다른 것은 핸들러 코드와 Gateway에 노출되는 도구 스키마뿐이다. 핸들러 자체는 3부의 로직을 거의 그대로이며, 파이썬 키워드 인자 대신 Gateway가 제공하는 이벤트에서 인자를 읽어온다:

def handler(event: dict, context) -> dict:
    """Lambda entrypoint for the cloudwatch-read Gateway target.
    ...
    """
    spoke_account_id = event["spoke_account_id"]
    namespace = event["namespace"]
    metric_name = event["metric_name"]
    dimensions = event.get("dimensions", {})
    lookback_minutes = int(event.get("lookback_minutes", 60))
    stat = event.get("stat", "Average")
    period_seconds = int(event.get("period_seconds", 60))

    session = assume_role(spoke_account_id, OPS_READONLY_ROLE_NAME, "cloudwatch-read")
    cloudwatch = session.client("cloudwatch", config=_boto_config)

Gateway 대상은 그 Lambda를 에이전트의 MCP 클라이언트가 발견할 수 있는 무언가로 바꿔주는 것이다. 이름, 설명, 그리고 JSON 스키마 형태의 입력 계약이다. 전체 블록은 파라미터 일곱 개까지 이어지며, 아래의 형태는 모든 도구에 공통이다:

resource "aws_bedrockagentcore_gateway_target" "cloudwatch_read" {
  gateway_identifier = aws_bedrockagentcore_gateway.tools.gateway_id
  name               = "cloudwatch-read"
  description        = "Reads a CloudWatch metric's recent datapoints from a spoke account."

  credential_provider_configuration {
    gateway_iam_role {}
  }

  target_configuration {
    mcp {
      lambda {
        lambda_arn = aws_lambda_function.cloudwatch_read.arn

        tool_schema {
          inline_payload {
            name        = "cloudwatch_read"
            description = "Read a CloudWatch metric's recent datapoints from a spoke account. An empty datapoint list is a real finding, not an error."

            input_schema {
              type = "object"

              property {
                name        = "spoke_account_id"
                type        = "string"
                description = "12-digit account ID of the spoke to query."
                required    = true
              }
              ...
            }
          }
        }
      }
    }
  }
}

credential_provider_configuration { gateway_iam_role {} }는 잠시 멈춰볼 가치가 있는 세부 사항이다. Gateway가 OAuth 흐름이나 크리덴셜 프로바이더에서 가져온 API 키가 아니라 순수 IAM을 통해 Gateway 자신의 실행 역할로 대상 Lambda를 호출하라고 지시하는 것이다. 같은 계정, 같은 플랫폼의 Lambda에는 그것이 옳은 선택이다. Gateway의 role_arn은 정확히 이 네 개의 Lambda ARN에 대해서만 lambda:InvokeFunction을 부여받으며 그 이상은 아니다. 이 시리즈의 모든 역할이 지금까지 지켜온 것과 같은, 목적 하나에 권한 하나라는 원칙이다.

읽기 전용은 슬로건이 아니라 세 가지 별개의 IAM 사실이다

"기본값은 읽기 전용"이라는 말은 버그와 부딪혀도 살아남을 때만 의미가 있다. 이 배선은 구체적인 버그 세 가지를 견딘다. 블라스트 반경: 세 개의 읽기 역할은 스포크의 상태를 바꿀 수 있는 권한을 전혀 갖고 있지 않다. ops-readonly로의 sts:AssumeRole뿐이며, 그 정책(2부)은 읽기 전용 관리형 정책 위에 명시적 거부를 한 겹 더 쌓아둔다. 그래서 에이전트를 설득해 읽기 도구에게 무언가를 삭제하라고 시키는 프롬프트 인젝션은 갈 곳이 없다. 감사: 모든 읽기는 여전히 sts:AssumeRole을 거쳐 계정 경계를 넘으므로, 보안 계정의 CloudTrail은 어떤 도구 Lambda가 언제 어느 스포크를 건드렸는지 정확히 본다. 사람 팀과의 신뢰: 승인 게이트 뒤에 놓인 ssm-execute를 보고, 어디에도 변경 액션을 갖지 않은 역할을 가진 이름이 read인 도구 세 개를 보는 온콜 엔지니어는 "읽기만 한다"는 주장을 그냥 믿는 게 아니라 믿을 근거를 갖는다. aws iam get-role-policy로 5분이면 확인할 수 있는 것이지, 모델이 따랐다고 누군가 신뢰해야 하는 시스템 프롬프트 속 한 문장이 아니다.

유일한 변경 도구, 그리고 그 뒤의 게이트

ssm-execute는 IAM 역할에 스포크로의 sts:AssumeRole 권한이 아예 포함되지 않은, 플랫폼에서 유일한 도구다. 전체 권한 집합은 정확히 하나의 상태 머신 ARN으로 범위가 지정된 states:StartExecution뿐이다:

data "aws_iam_policy_document" "ssm_execute_permissions" {
  statement {
    sid       = "StartApprovalStateMachineOnly"
    effect    = "Allow"
    actions   = ["states:StartExecution"]
    resources = [aws_sfn_state_machine.mutation_approval.arn]
  }
  ...
}

ssm-execute를 호출한다고 해서 무언가가 실행되는 것은 아니다. 실행을 제안할 뿐이다. 핸들러는 Step Functions 실행을 만들기도 전에 제안된 문서를 로컬 허용 목록과 대조해 확인한다. 진짜 검사, 즉 2부의 ops-mutate 자체 IAM 정책 위에 얹힌 중복된 조기 실패 검사이며, 이 Lambda의 코드에 버그가 있을 때 실제로 버텨주는 것은 그 진짜 검사 쪽이다:

if ssm_document_arn not in ALLOWED_SSM_DOCUMENT_ARNS:
    return {
        "status": "rejected",
        "reason": f"{ssm_document_arn} is not on the allowlist; no approval request was created.",
    }

execution = _sfn.start_execution(
    stateMachineArn=APPROVAL_STATE_MACHINE_ARN,
    input=json.dumps({
        "spoke_account_id": spoke_account_id,
        "ssm_document_arn": ssm_document_arn,
        "ssm_parameters": ssm_parameters,
        "diagnosis": diagnosis,
        "blast_radius": blast_radius,
    }),
)

상태 머신은 .waitForTaskToken이 그 이름값을 하는 지점이다. 이 패턴에 대한 Step Functions 자체 튜토리얼은 Task 상태가 무기한 멈출 수 있고, 토큰을 하나 생성하며, 상태 머신 바깥의 무언가가 정확히 그 토큰으로 SendTaskSuccessSendTaskFailure를 호출해야만 재개된다는 것, 그리고 대기 상태와 전체 실행 모두 각자의 TimeoutSeconds가 있어야만 멈춘 실행이 영원히 끝나지 않는 일을 막을 수 있다는 것을 명시하고 있다. NotifySlackAndWaitForApproval은 태스크 토큰을 페이로드에 담아 slack-post를 호출하고, 그러면 멈추는 것은 Lambda가 아니라 Step Functions 자신이다:

definition = jsonencode({
  Comment        = "Human approval gate for ops-mutate SSM Automation execution."
  StartAt        = "NotifySlackAndWaitForApproval"
  TimeoutSeconds = var.state_machine_timeout_seconds
  States = {
    NotifySlackAndWaitForApproval = {
      Type     = "Task"
      Resource = "arn:aws:states:::lambda:invoke.waitForTaskToken"
      Parameters = {
        FunctionName = aws_lambda_function.slack_post.arn
        Payload = {
          "TaskToken.$"      = "$$.Task.Token"
          "Diagnosis.$"      = "$.diagnosis"
          "BlastRadius.$"    = "$.blast_radius"
          "SsmDocument.$"    = "$.ssm_document_arn"
          "SsmParameters.$"  = "$.ssm_parameters"
          "SpokeAccountId.$" = "$.spoke_account_id"
        }
      }
      TimeoutSeconds = var.approval_wait_timeout_seconds
      Next           = "RunApprovedAutomation"
      Catch = [
        {
          ErrorEquals = ["States.ALL"]
          Next        = "ApprovalDeniedOrTimedOut"
        }
      ]
    }
    ...
  }
})

위에는 나오지 않은 두 상태가 뒤따른다. RunApprovedAutomation은 대기 상태 자신의 출력을 페이로드로 삼아 executor.py를 호출하고, ApprovalDeniedOrTimedOut은 위의 Catch가 라우팅해 들어가는 평범한 Fail 상태다. slack-post는 진단, 블라스트 반경, 정확한 SSM 문서, 파라미터를 담은 카드와 함께 태스크 토큰을 실어나르는 승인/거부 링크를 게시하며, 각 링크는 approval_callback.py가 뒷받침하는 API Gateway 경로(/succeed, /fail)를 가리킨다. 그 Lambda의 임무는 클릭된 링크의 토큰에 대해 send_task_successsend_task_failure를 한 번 호출하는 것 전부이며, 그 IAM 정책이 둘 다를 Resource = "*"에 부여하는 데는 실제 이유가 있다. 어느 API도 ARN으로 특정 실행을 지정하지 않고, 오직 불투명한 토큰만으로 지정한다. 2부의 InspectAndStopOwnExecutions 문이 SSM 자체 검사 API에 대해 부딪혔던 것과 같은 형태다. 분명히 짚어둘 가치가 있다. 그 토큰을 GET 링크의 쿼리 문자열에 인코딩하는 것은 실제 약점이며, 전달되거나 로깅된 링크는 곧 전달되거나 로깅된 승인이다. 이는 숨기지 않고 slack_post.py에 문서화되어 있으며, 이것이 중요한 무언가를 상대로 돌아가기 전에 서명되고 일회용이며 데이터스토어에 기반한 토큰으로 교체되어야 한다. 이 경로들에는 인가자가 전혀 없다. 토큰 소지 자체가 접근 제어의 전부이므로, 의도된 Slack 승인자뿐 아니라 그 링크를 손에 넣은 누구든 발동시킬 수 있다.

승인이 나야만 스포크 계정을 변경할 권한을 가진 무언가가 만들어진다. executor.py는 실행 역할이 ops-mutate를 어썸할 수 있는 유일한 Lambda다. 2부의 신뢰 정책은 정확히 이 Lambda의 역할 ARN만을 유일한 프린시펄로 지정하며, 그 자신의 자격 증명 수명은 의도적으로 짧다:

session = assume_role(
    spoke_account_id,
    OPS_MUTATE_ROLE_NAME,
    "post-approval-executor",
    duration_seconds=300,
)
ssm = session.client("ssm", config=_boto_config)

response = ssm.start_automation_execution(
    DocumentName=ssm_document_arn,
    Parameters={k: [v] if not isinstance(v, list) else v for k, v in ssm_parameters.items()},
)

5분, 읽기 도구의 기본값인 15분보다 짧다. 이 자격 증명은 정확히 하나의 API 호출을 하기 위해 존재하며, 이 함수가 의도하지 않은 후속 작업을 위해 재사용 가능한 상태로 남아 있으라고 있는 것이 아니다. Lambda 두 개, IAM 역할 두 개, 상태 머신 실행을 통한 좁은 인계 하나, 이것이 "유일한 변경 도구는 스포크에 직접 닿을 수 없다"를 이 코드가 boto3의 STS 클라이언트를 임포트하지 않기로 선택해서 지키는 약속이 아니라 배선 자체의 속성으로 만든다.

도구가 거부됐을 때 에이전트가 보는 것

여기에는 서로 구분할 가치가 있는 세 가지 다른 종류의 "거부"가 있다. 애플리케이션 레벨 거부: ssm-execute의 허용 목록 검사는 {"status": "rejected", "reason": ...}를 평범한 도구 결과로 반환한다. 예외도, 만들어진 실행도 없다. 에이전트는 이를 있는 그대로 보고한다. IAM 레벨 거부: lambda:InvokeFunction 부여나 어썸-롤 부여가 빠져 있으면 호출은 도구 결과가 아니라 도구 오류로 드러나는 AWS AccessDeniedException으로 실패한다. 3부의 시스템 프롬프트가 트리아지 에이전트에게 존중하도록 이미 훈련시킨, 강제 정지에 해당하는 구분이다. 그리고 cedar_policy_engine_arnmode = "ENFORCE"로 설정된 뒤에는 정책 레벨 거부가 있다. Cedar는 어떤 도구 자신의 코드 바깥에 있는 규칙을 근거로 호출이 Lambda에 닿기도 전에 거부한다. LOG_ONLY에서는 같은 호출이 기록만 되고 통과되는데, 이것이 ENFORCE로 전환하기 전에 실제 트래픽을 상대로 새 정책을 그곳에서 먼저 시험해봐야 하는 이유다. ENFORCE에서는 지나치게 광범위한 규칙이, 원래 아무 문제도 없었던 호출을 실패시키면서도 왜 그런지 설명해줄 도구 레벨 로그를 전혀 남기지 않는다.

지켜봐야 할 실패 모드

이 파이프라인이 실제 제안을 상대로 돌아가기 전에 알아둘 가치가 있는 것이 다섯 가지 있다. 두 개의 SSM 문서 허용 목록, ssm-execute의 로컬 사본과 ops-mutate 자체 IAM 정책은 독립적으로 적용되는 두 개의 테라폼 모듈(이 모듈과 00-foundation)에 있으며, 둘이 동일하게 유지되도록 강제하는 것은 아무것도 없다. 로컬 목록에 빠진 문서는 승인 요청이 생기기도 전에 거부되지만, IAM 목록에 빠진 문서는 요청은 만들어지고 나서 ops-mutate가 실행을 거부하는데, 후자가 더 나쁜 이유는 사람이 그 사실을 발견하기 전에 이미 승인을 클릭했기 때문이다. 대기 상태든 실행 자체든 어느 TimeoutSeconds 값 하나라도 빠뜨리면, 응답 없는 Slack 제안이 요란하게 ApprovalDeniedOrTimedOut으로 실패하는 대신 영원히 나이만 먹는 멈춘 실행이 된다. Step Functions의 사람 승인 튜토리얼이 이 패턴이 깨지는 가장 흔한 방식으로 정확히 지목하는 것이다. 그리고 승인 링크 자체의 약점은 서명되고 일회용이며 데이터스토어에 기반한 토큰으로 교체되기 전까지는 그대로 남는다. 이 게시물이 올라가는 모든 Slack 채널을, 메시지를 전달하는 것이 승인을 전달하는 것과 같은 곳으로 취급해야 한다. 상태를 변경하는 GET 요청은 우발적인 승인도 초대한다. Slack 자체의 링크 언펄러, 회사의 링크 보안 스캐너, 브라우저 프리페치 모두 사람이 클릭하기도 전에 그 URL을 가져올 수 있으며, 여기 어떤 코드 경로도 그 페치를 의도적인 승인과 구분하지 않는다. 그리고 approval_callback.py는 클릭된 링크의 쿼리 문자열에서 제안 JSON을 send_task_success로 그대로 넘기며, Step Functions가 그 토큰에 대해 실제로 기록한 것과 대조해 검사하지 않는다. 그래서 변조된 링크는 사람이 Slack에서 본 것과 다른 ssm_document_arn이나 ssm_parameters를 바꿔치기할 수 있다. ops-mutate 자체의 IAM 허용 목록이 피해를 이미 허용된 범위로 제한하기는 하지만, 사람이 승인한 정확한 문서-파라미터 조합이 실제로 실행되는 것과 배선되어 있는 것은 아니다.

다음으로 읽을 글

위 스니펫에서 생략한 IAM 정책 문서와 나머지 세 개의 Gateway 대상을 포함한 전체 terraform/20-gateway-tools/ 모듈과 agents/tools/는 동반 저장소인 github.com/flightlesstux/agents-on-call에 있다. 이런 플랫폼을 대규모로 운영할 때의 데이터베이스와 인프라 쪽 이야기는 ercan.cloud에, 허브는 ercanermis.com에 있다.