Devin.KR

주소 범위와 통신 규칙

65분 안팎

학습 목표

CIDR과 허용 규칙을 연결합니다.

개념

허용 판단은 요청의 끝점을 좁히는 일입니다

안내판 API가 실행되어도 누가 어떤 포트로 접근하는지 정하지 않으면 서비스의 공개 범위가 우연히 결정됩니다. 접속 오류를 고치려고 모든 주소와 모든 포트를 열면 정상 요청 하나는 살릴 수 있지만 불필요한 접근도 허용하게 됩니다. 먼저 출발지 IP, 목적지 포트, 프로토콜을 한 요청의 조건으로 적습니다. 이번 레슨은 IPv4와 TCP만 다루며 목적지 서비스는 이미 선택된 것으로 가정합니다. 실제 방화벽을 바꾸기 전에 계산 가능한 작은 정책으로 허용 범위를 확인합니다.

모듈 환경과 입력 약속

브라우저 실습은 Python 3의 표준 라이브러리 ipaddress와 json을 사용합니다. 표준 입력은 JSON 한 객체이며 ip, port, protocol, rules가 필요합니다. 로컬 zip은 압축 해제한 루트에서 Bash와 Python 3으로 검사합니다. IaC 실행에는 개인 Docker daemon과 OpenTofu 1.8 이상을 사용하고 클라우드 계정은 요구하지 않습니다. 실행 불가 환경에서는 external 대기로 남깁니다. 실습의 입력과 출력은 공개 교육 자료이고 회사 IP나 운영 설정을 붙여 넣지 않습니다.

CIDR은 주소 묶음의 길이를 표시합니다

10.40.2.0/24에서 24는 앞부분 24비트가 네트워크를 식별한다는 뜻입니다. IPv4는 32비트이므로 남은 8비트로 256개 주소를 표현합니다. 이 계산은 주소 묶음의 크기이며 특정 클라우드에서 실제 할당할 수 있는 호스트 수와 같다고 단정하지 않습니다. /32는 한 주소만 포함하고 /0은 IPv4 전체를 포함합니다. 숫자가 크면 주소 범위가 좁아집니다. 문자열이 10.40.2로 시작하는지 보는 대신 비트 경계를 해석하는 라이브러리를 사용합니다.

양 끝의 포함 여부를 확인합니다

주소 범위 10.40.2.0/24는 10.40.2.0부터 10.40.2.255까지 포함합니다. 이 실습은 주소가 집합에 속하는지 판단하므로 네트워크 주소와 마지막 주소도 포함으로 취급합니다. 실제 인터페이스에 그 주소를 배정해도 된다는 뜻은 아닙니다. 10.40.3.0은 바로 다음 구간이므로 제외됩니다. 가운데 주소 하나만 검사하면 마지막 바이트의 경계 처리가 잘못되어도 발견하지 못합니다. 네트워크 구성 검사와 호스트 할당 검사를 분리하여 테스트 이름을 붙입니다.

주소와 포트는 함께 맞아야 합니다

허용 규칙 하나는 cidr, port, protocol 세 필드로 구성합니다. 요청의 IP가 범위에 포함되고 목적지 port가 같으며 protocol이 tcp일 때 그 규칙이 맞습니다. 범위에 속한다는 이유만으로 5432 저장소 포트까지 허용하지 않습니다. 반대로 허용된 8080 포트라도 다른 출발지라면 거부합니다. 여러 규칙은 어느 하나라도 맞으면 ALLOW를 출력하는 합집합입니다. 일치하는 규칙이 없거나 목록이 비어 있으면 DENY를 출력합니다. 순서가 우선순위를 만들지는 않습니다.

기본 거부와 잘못된 입력을 나눕니다

DENY는 유효한 요청이 정책에 맞지 않았다는 결과입니다. ERROR는 정책이나 요청을 해석할 수 없다는 결과입니다. 999.1.1.1, 포트 0, 문자열 포트, 잘못된 CIDR은 정상적인 거부 요청으로 감추지 않습니다. JSON 파싱 예외도 ERROR 한 줄로 변환합니다. 담당자는 오류가 입력 생산자의 문제인지 허용 범위 밖의 요청인지 구분할 수 있어야 합니다. 교육 채점에서는 둘 다 연결을 허용하지 않지만 조사할 책임과 수정할 파일은 다릅니다.

정규화 정책을 명시합니다

ip_network에 strict=True를 사용하면 10.40.2.7/24처럼 호스트 비트가 켜진 CIDR을 오류로 읽습니다. 라이브러리가 이를 자동으로 10.40.2.0/24로 넓혀 주면 작성자가 의도한 한 주소 제한을 놓칠 수 있기 때문입니다. 단일 주소를 허용하려면 10.40.2.7/32로 적습니다. 운영 도구마다 정규화 정책이 다를 수 있어 문서를 확인해야 하지만 이 실습의 계약은 모호한 범위를 거부하는 것입니다. ERROR가 나면 strict 옵션을 끄기 전에 입력의 의도를 확인합니다.

규칙 전부를 먼저 검사합니다

첫 규칙이 맞았다고 즉시 종료하면 뒤에 있는 잘못된 CIDR을 검증하지 못합니다. solution은 요청과 규칙을 전부 파싱하고 조건을 검사한 다음 any로 허용 여부를 계산합니다. 목록 중 하나라도 잘못되면 ERROR를 냅니다. 규칙 일부만 읽은 성공은 정책 전체가 정상이라는 증거가 아닙니다. JSON에서 true는 Python의 bool로 바뀌며 bool은 int의 하위 타입이라 isinstance만 사용하면 포트 true가 숫자처럼 통과할 수 있습니다. type(port) is int로 이 경계를 막습니다.

제한된 모델의 범위를 설명합니다

이 판정기는 AWS 보안그룹 전체를 흉내 내지 않습니다. 송신 규칙, 상태 추적, 보안그룹 ID 출발지, IPv6, 네트워크 ACL은 구현하지 않습니다. 여기서 ALLOW는 한 규칙 집합이 허용한다는 의미이며 실제 라우팅과 프로세스 listen 여부까지 보장하지 않습니다. 모델의 출력과 실제 요청 성공을 같은 문장으로 쓰면 사고 원인을 잘못 좁힙니다. 다음 레슨에서 주소·경로·접근 정책을 각각 다른 층으로 배치합니다.

따라하기에서 라이브러리 경계를 읽습니다

첫 실행에서는 범위의 첫 주소, 마지막 주소, 다음 구간의 첫 주소를 확인합니다. 이어 /32로 바꾸어 두 인접 주소를 비교합니다. 브라우저 starter는 입력을 읽지만 항상 DENY를 출력하므로 허용 사례가 실패합니다. 정책을 계산하는 함수를 작성하고 오류 처리 뒤 결과를 한 줄 출력합니다. 디버깅용 객체 출력은 채점 출력에 섞지 않습니다. ValueError가 발생한 위치를 찾을 때 ip_address와 ip_network를 따로 호출하면 어느 필드가 잘못됐는지 좁힐 수 있습니다.

검사 결과로 좁은 정책을 유지합니다

과제를 끝낼 때 정상 허용, 잘못된 포트, 범위 바깥, 빈 목록, 잘못된 입력을 모두 통과시킵니다. 허용 규칙을 0.0.0.0/0으로 바꿔 테스트 하나를 살리는 수정은 주소 경계 학습을 포기하는 것입니다. 차단된 요청에서 ip와 port를 바꾸어 한 조건씩 비교하면 원인을 설명할 수 있습니다. 제출 설명에는 주소 포함 검사와 규칙 조건의 결합을 적습니다. 계층 통신 설명은 더 읽기로 이어 가고 이 레슨에서는 실제 판정 코드를 완성합니다.

따라하기

CIDR 경계 관찰

주소가 범위에 포함되는지 세 위치를 비교합니다.

import ipaddress
n=ipaddress.ip_network('10.40.2.0/24')
for s in ['10.40.2.0','10.40.2.255','10.40.3.0']:
    print(s, ipaddress.ip_address(s) in n)

실행 결과

10.40.2.0 True
10.40.2.255 True
10.40.3.0 False

단일 주소 정책

/32에서 인접 주소는 허용 집합 밖입니다.

import ipaddress
n=ipaddress.ip_network('10.40.2.7/32')
for s in ['10.40.2.7','10.40.2.8']:
    print(s, ipaddress.ip_address(s) in n)

실행 결과

10.40.2.7 True
10.40.2.8 False

판정 코드 실행

파싱과 전체 규칙 검증을 거친 모범 답안의 출력입니다.

import sys, json, ipaddress
def decide(data):
    if not isinstance(data, dict): raise ValueError('object required')
    ip=ipaddress.IPv4Address(data['ip'])
    port=data['port']
    if type(port) is not int or not 1 <= port <= 65535: raise ValueError('port')
    if data['protocol'] != 'tcp': raise ValueError('only tcp')
    rules=data['rules']
    if not isinstance(rules,list): raise ValueError('rules')
    parsed=[]
    for row in rules:
        net=ipaddress.IPv4Network(row['cidr'],strict=True)
        rp=row['port']
        if type(rp) is not int or not 1 <= rp <= 65535 or row['protocol'] != 'tcp': raise ValueError('rule')
        parsed.append((net,rp))
    return 'ALLOW' if any(ip in net and port==rp for net,rp in parsed) else 'DENY'
try:
    print(decide(json.load(sys.stdin)))
except (ValueError, KeyError, TypeError, AttributeError):
    print('ERROR')

실행 결과

ALLOW

결함 수정과 경계 확인

브라우저 starter의 고정 DENY를 판정 함수로 바꾸고 제공된 모든 테스트를 실행합니다. ERROR 사례는 예외가 그대로 노출되지 않도록 처리합니다.

확인 문제

실습

JSON의 ip·port·protocol·rules를 읽습니다. IPv4 TCP만 받으며 유효한 포트는 정수 1~65535입니다. rules의 cidr·port·protocol을 전부 검증한 뒤 하나라도 맞으면 ALLOW, 유효하지만 불일치하면 DENY, 빈 입력·형식 오류면 ERROR 한 줄을 출력합니다. 호스트 비트가 켜진 CIDR과 IPv6는 오류입니다.

모범 답안
import sys, json, ipaddress
def decide(data):
    if not isinstance(data, dict): raise ValueError('object required')
    ip=ipaddress.IPv4Address(data['ip'])
    port=data['port']
    if type(port) is not int or not 1 <= port <= 65535: raise ValueError('port')
    if data['protocol'] != 'tcp': raise ValueError('only tcp')
    rules=data['rules']
    if not isinstance(rules,list): raise ValueError('rules')
    parsed=[]
    for row in rules:
        net=ipaddress.IPv4Network(row['cidr'],strict=True)
        rp=row['port']
        if type(rp) is not int or not 1 <= rp <= 65535 or row['protocol'] != 'tcp': raise ValueError('rule')
        parsed.append((net,rp))
    return 'ALLOW' if any(ip in net and port==rp for net,rp in parsed) else 'DENY'
try:
    print(decide(json.load(sys.stdin)))
except (ValueError, KeyError, TypeError, AttributeError):
    print('ERROR')

더 읽기

면접 질문

  • VPC·서브넷·보안그룹의 역할을 설명합니다.