IAM 정책에서 aws:SourceIp 조건을 사용해 API 호출을 특정 IP 대역으로 제한하는 것은 가장 흔한 네트워크 기반 접근 제어 패턴 중 하나입니다. 사무실 IP나 VPN 대역에서만 AWS 콘솔 접근을 허용하거나, 특정 CIDR에서만 S3 버킷 조작을 가능하게 하는 정책은 거의 모든 조직에서 사용하고 있을 겁니다.
그런데 이 정책이 VPC 엔드포인트(AWS PrivateLink)를 통한 호출에서는 의도대로 동작하지 않을 수 있다는 사실을 알고 계셨나요?
aws:SourceIp는 API 요청의 원본 소스 IP 주소를 담고 있는 글로벌 조건 키입니다. 인터넷을 통해(퍼블릭 엔드포인트로) AWS API를 호출하면, 이 값에는 호출자의 퍼블릭 IP가 정상적으로 들어갑니다.
문제는 VPC 엔드포인트(인터페이스 엔드포인트, Gateway 엔드포인트 모두)를 통해 AWS 서비스에 접근할 때 발생합니다.
VPC 엔드포인트를 경유하는 요청에서는 aws:SourceIp 조건 키가 채워지지 않거나, 예상과 다른 값이 들어갑니다. 이는 요청이 퍼블릭 인터넷을 거치지 않고 AWS 내부 네트워크를 통해 라우팅되기 때문입니다.
다음과 같은 S3 버킷 정책을 생각해 봅시다:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictBySourceIp",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": "203.0.113.0/24"
}
}
}
]
}
이 정책의 의도는 203.0.113.0/24 대역 외부에서의 모든 S3 접근을 차단하는 것입니다.
하지만 VPC 엔드포인트를 통해 접근하는 EC2 인스턴스나 Lambda 함수에서는 aws:SourceIp가 평가되지 않기 때문에, 이 Deny 조건이 매칭되지 않아 접근이 허용될 수 있습니다.
반대 케이스도 문제입니다. Allow 정책에서 aws:SourceIp를 조건으로 사용하면, VPC 엔드포인트 경유 시 조건이 매칭되지 않아 정당한 접근까지 차단되는 상황도 발생합니다.
요약하면:
| 정책 유형 | 의도 | VPC 엔드포인트 경유 시 실제 동작 |
|---|---|---|
Deny + NotIpAddress + aws:SourceIp | 허용 IP 외 차단 | 조건 미매칭 → Deny 미적용 → 의도치 않은 허용 |
Allow + IpAddress + aws:SourceIp | 허용 IP만 접근 | 조건 미매칭 → Allow 미적용 → 정당한 접근 차단 |
AWS는 이 문제를 해결하기 위해 aws:VpcSourceIp 조건 키를 제공합니다.
aws:SourceIp: 퍼블릭 엔드포인트를 통한 요청의 소스 IPaws:VpcSourceIp: VPC 엔드포인트를 통한 요청의 원본 프라이빗 IPaws:VpcSourceIp는 요청이 VPC 엔드포인트를 경유할 때만 채워지고, 퍼블릭 엔드포인트를 통한 요청에서는 채워지지 않습니다. 정확히 aws:SourceIp와 상호 보완적인 관계입니다.
VPC 엔드포인트 환경과 퍼블릭 접근을 모두 커버하려면, 두 조건을 조합해야 합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAccessFromUnauthorizedNetworks",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": "203.0.113.0/24"
},
"NotIpAddress": {
"aws:VpcSourceIp": "10.0.0.0/16"
},
"StringNotEquals": {
"aws:SourceVpce": "vpce-0123456789abcdef0"
}
}
}
]
}
주의: 위 정책에서 동일한 연산자(
NotIpAddress)를 두 번 사용하면 JSON 키 중복으로 마지막 값만 적용됩니다. 실제 구현에서는 아래와 같이 하나의 연산자 블록 안에 두 키를 넣어야 합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnlessFromCorporateOrVpc",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": "203.0.113.0/24",
"aws:VpcSourceIp": "10.0.0.0/16"
},
"StringNotEquals": {
"aws:SourceVpce": "vpce-0123456789abcdef0"
}
}
}
]
}
이 정책은 다음 세 가지 경우를 모두 벗어나는 요청만 Deny합니다:
203.0.113.0/24가 아니고10.0.0.0/16이 아니고핵심:
Condition블록 내에서 같은 레벨의 조건들은 AND로 평가되지만, 같은 연산자 내의 서로 다른 키는 각각 독립적으로 평가되어 하나라도 매칭되면 해당 연산자 조건을 충족합니다.aws:SourceIp와aws:VpcSourceIp는 동시에 채워지지 않으므로 이 구조가 정확히 의도대로 동작합니다.
네트워크 경계를 더 엄격하게 통제하려면 aws:SourceVpc나 aws:SourceVpce를 추가로 활용합니다:
{
"Condition": {
"StringNotEquals": {
"aws:SourceVpc": "vpc-0abcdef1234567890"
},
"NotIpAddress": {
"aws:SourceIp": "203.0.113.0/24"
}
}
}
기존 IAM 정책과 리소스 정책에서 이 이슈에 해당하는지 빠르게 점검하려면:
aws:SourceIp를 사용하는 모든 정책을 검색한다. IAM 정책, S3 버킷 정책, KMS 키 정책, SQS 큐 정책 등 리소스 정책 전체를 대상으로 합니다.aws:VpcSourceIp 또는 aws:SourceVpce/aws:SourceVpc 조건이 함께 사용되고 있는지 확인한다. 하나라도 누락되어 있다면 잠재적 위험이 존재합니다.aws:SourceIp는 직관적이고 편리한 조건 키이지만, VPC 엔드포인트가 보편화된 현재 환경에서는 이것만으로 네트워크 기반 접근 제어가 완성되지 않습니다. 특히 하이브리드 환경(퍼블릭 + VPC 내부 접근이 혼재)에서는 aws:VpcSourceIp와의 조합이 필수입니다.
보안 아키텍처의 핵심은 하나의 제어가 실패했을 때 다른 제어가 보완하는 다층 방어(Defense in Depth)입니다. IP 기반 접근 제어에서도 동일한 원칙이 적용됩니다.
기존 정책을 지금 바로 검토해 보시기 바랍니다.
참고 자료