AWS OpenSearch Access Policy Principal

정의 (Definition)

AWS OpenSearch의 Access Policy Principal은 도메인에 대한 IAM 리소스 기반 정책(Resource-based Policy) 의 핵심 요소로, 네트워크 도달 가능성(VPC/Public)이나 내부 인증(Basic Auth)과 독립적으로 “누가(Which IAM Entity)” 요청을 보낼 자격이 있는지를 결정하는 1차 보안 경계입니다.

문제가 되는 배경 (Problem Context)

OpenSearch 도메인이 VPC 내부에 존재하거나 Master User/Basic Auth가 설정되어 있다는 이유로, 편의상 Access Policy의 PrincipalAWS: "*" 또는 AWS: "...:root"로 넓게 설정하는 경우가 많습니다. 이는 “네트워크가 격리되었으니 안전하다”는 착각에서 비롯되지만, 실제로는 공격 면적(Attack Surface)을 계정 전체 혹은 전 세계로 확장하는 결과를 초래합니다.

핵심 메커니즘 (How it Works)

OpenSearch 접근 제어는 두 가지 축으로 작동하며, Principal은 그중 첫 번째 관문입니다.

  1. IAM Access Policy (Authorization): 요청이 도메인에 도달했을 때, “이 주체가 API를 호출할 권한이 있는가?”를 검사합니다.
  2. Internal Auth (Authentication): “이 사용자가 누구인가?”(Basic Auth, FGAC)를 검사합니다.

설정 패턴 비교

패턴설정 예시 (Principal)의미위험도비고
Wildcard{ "AWS": "*" }모든 AWS 계정의 주체 허용🚨 CriticalPublic Access와 동급. 조건(Condition) 필수.
Account Root{ "AWS": "arn:aws:iam::111:root" }해당 계정 내 모든 IAM 주체 허용⚠️ High계정 내 침해 사고 시 방어력 상실. 편의성은 높음.
Specific Roles{ "AWS": ["arn:..:role/A", "arn:..:role/B"] }명시된 특정 역할만 허용✅ Safe최소 권한 원칙 준수. 운영 오버헤드 있음.

❌ 위험한 설정 (Wildcard Principal)

access_policies = jsonencode({
  Version = "2012-10-17"
  Statement = [{
    Effect    = "Allow"
    Principal = { AWS = "*" } # 위험: 타 계정 포함 모든 주체 허용
    Action    = "es:*"
    Resource  = "arn:aws:es:${region}:${account}:domain/${domain_name}/*"
  }]
})

✅ 권장 설정 (Specific Roles)

접근이 필요한 Workload IRSABastion Role만 명시하여 권한 경계를 명확히 합니다.

access_policies = jsonencode({
  Version = "2012-10-17"
  Statement = [{
    Effect    = "Allow"
    Principal = {
      AWS = concat(
        values(module.workload_irsa_roles)[*].arn, # 워크로드 Role 자동 수집
        [aws_iam_role.bastion.arn]                 # 운영자(Bastion) Role
      )
    }
    Action    = "es:*"
    Resource  = "arn:aws:es:${region}:${account}:domain/${domain_name}/*"
  }]
})

위험 시나리오: Principal: "*" + Action: "es:*" 상태라면, 공격자가 유효한 AWS 자격 증명(아무 계정)으로 SigV4 서명을 해 요청을 보내면, Basic Auth(비밀번호) 단계를 우회하고 통과될 수 있습니다.

불변 조건과 보장 범위 (Invariants & Guarantees)

  • 보장: Principal에 명시되지 않은 IAM Entity는 (설령 올바른 Basic Auth 정보를 가졌더라도) IAM 레벨에서 거부됩니다(Explicit Deny 우선).
  • 비보장: “VPC 내부에 있다”는 사실이 “IAM 권한이 있다”는 것을 의미하지 않습니다. VPC는 네트워크 경로일 뿐, IAM 인증 인가가 아닙니다.
  • 비보장: “엔드포인트 주소를 모른다”는 보안 경계가 아닙니다(Security through obscurity is not security).

비유 (Analogy)

  • VPC: “폐쇄형 아파트 단지” (물리적/네트워크 격리)
  • Access Policy (Principal): “현관문 도어락의 등록된 지문”
  1. *: 아파트 단지에 들어온 아무나(배달원, 이웃, 침입자) 문을 열 수 있음.
  2. root: 같은 가족(계정) 이면 문을 열 수 있음. (철없는 동생이나 도둑맞은 가족의 지문도 포함)
  3. Specific Role: 가장(Admin)과 허락된 자녀(Workload) 만 문을 열 수 있음.

실무적 함의 (Operational Implications)

  • 계정 Root(:root)의 함정: :root를 쓰면 운영은 편하지만(Role 추가 시 정책 수정 불필요), 계정 내 불필요한 Role(CI, 테스트용 등)이나 탈취된 내부 자격증명이 공격 경로가 됩니다.
  • Role 목록 관리 자동화: Specific Roles 방식은 Role 추가 시마다 정책을 업데이트해야 하는 오버헤드가 있습니다. 이를 줄이기 위해 Terraform 등에서 concat(), values() 등을 활용해 Role 목록을 동적으로 수집하는 패턴을 권장합니다.
  • 예외적 허용: 샌드박스 환경 등에서 부득이하게 범위를 넓힐 때는 Principal을 넓히더라도 반드시 Condition: { "StringEquals": { "aws:SourceVpce": "..." } }를 병행해야 합니다.

주의사항 / 오해 (Pitfalls & Misconceptions)

  • “비밀번호(Basic Auth)가 있으니 괜찮다?”: IAM 정책에서 허용되면 SigV4 서명으로 비밀번호 없이 통과될 수 있는 경로가 존재합니다.
  • “Private VPC니까 외부에서 못 들어온다?”: VPN, TGW, VPC Peering, 혹은 내부의 탈취된 컨테이너(SSRF 등)를 통해 네트워크 장벽은 우회될 수 있습니다. 이때 IAM 정책이 최후의 방어선입니다.

References