이름 해석과 TLS 신뢰
50분 안팎
학습 목표
DNS 응답과 TLS 검증의 역할을 설명합니다.
개념
주소 발견과 상대 신뢰를 분리하기
앱 주소에 연결되지 않는다는 신고를 받으면 먼저 실패한 단계를 정합니다. 이름을 주소로 바꾸지 못했는지, 주소를 얻었지만 연결하지 못했는지, 연결 뒤 인증서를 거절했는지에 따라 확인 대상이 다릅니다. 이번에는 vault.example의 합성 관찰 JSON을 분류합니다. 실제 DNS 질의나 TLS 통신을 수행하는 검사기가 아니라 주어진 기록에서 다음 조사 위치를 결정하는 연습입니다.
DNS는 이름에 대한 레코드 정보를 찾는 체계입니다. 주소를 얻었다는 사실은 앱의 정상 작동이나 소유자의 신원을 입증하지 않습니다. 한 이름에 여러 주소가 나올 수 있고 DNS 응답을 얻은 위치와 시점도 중요합니다. HTTPS 새 연결의 논리 흐름은 이름 해석, 전송 연결, TLS 검증, HTTP 처리로 설명할 수 있지만 연결 재사용 등으로 매 요청에 모든 단계가 새로 나타나는 것은 아닙니다.
DNS 결과를 HTTP 숫자와 혼동하지 않기
관찰의 rcode는 DNS 응답 상태입니다. NOERROR는 DNS 수준에서 오류 응답이 아니었다는 뜻이며 원하는 종류의 주소가 늘 존재한다는 뜻은 아닙니다. NXDOMAIN은 질의 이름이 존재하지 않는다는 응답이고 SERVFAIL은 서버가 질의를 처리하지 못했다는 결과입니다. 이 둘을 HTTP 404와 같은 사건으로 합치면 앱 처리 전 실패와 앱 응답을 구분할 수 없습니다.
이번 입력의 addresses는 관찰한 주소 목록입니다. NOERROR인데 목록이 비어 있으면 NO_ADDRESS로 분류합니다. 목록이 비어 있다는 이유만으로 이름 자체가 없다고 바꾸지 않습니다. 실제 기록이라면 질의 종류, CNAME 관계, 해석기 위치를 더 확인해야 하지만 이 문제는 해당 정보가 주어지지 않았습니다. 제공된 필드 이상의 원인을 확정하지 않는 습관을 익힙니다.
192.0.2.10과 2001:db8::1은 예제 관찰용 주소입니다. 실습에서는 해당 주소로 접속하지 않습니다. addresses의 각 문자열을 ipaddress.ip_address로 파싱하여 주소 형식인지 확인합니다. 문자열 bad는 이름일 수도 있지만 이 필드는 IP 주소 계약이므로 INVALID입니다. 443이라는 포트 번호를 주소 뒤에 붙이는 형식도 이 목록의 계약에 없습니다.
TLS가 확인하는 이름
인증서 기반 TLS 서버 인증에서는 접속할 때 기대한 서비스 이름과 인증서의 식별 정보가 맞는지 확인해야 합니다. 신뢰 가능한 발급 경로와 유효 기간 검사도 함께 필요합니다. 이 레슨의 name은 기대 이름이고 certNames는 합성 인증서의 정확한 DNS 이름 목록입니다. IP 주소가 같다는 이유로 다른 이름의 인증서를 허용하면 의도한 상대의 이름 확인이 빠집니다.
실제 인증서 검증은 라이브러리에 맡깁니다. 아래 분류기는 와일드카드, IDNA, 인증서 확장, 폐기 상태를 처리하지 않습니다. 단순한 ASCII 이름의 대소문자를 맞춘 뒤 정확한 문자열 일치만 다룹니다. certNames에 *.example을 넣어도 vault.example의 일치로 계산하지 않는 계약입니다. 이 한계를 설명하고 문자열 비교 함수를 운영 TLS 검증 함수로 사용하지 않습니다.
chainOK와 timeOK는 관찰자가 이미 얻었다고 가정한 불리언 결과입니다. 분류기가 인증서 체인을 새로 검증하거나 현재 시간을 측정한 것은 아닙니다. 둘 중 하나가 False면 TLS_REJECT로 표시합니다. 이름과 주소 조건이 맞아도 신뢰 경로나 기간 문제가 남을 수 있습니다. 실제 조사에서는 오류 문구와 클라이언트 시간·신뢰 저장소를 더 확인합니다.
분류 우선순위가 의미하는 것
먼저 필드 형식을 검사하고 형식 오류면 INVALID로 끝냅니다. 유효한 입력에서 DNS 오류를 먼저, 그 다음 주소 없음, 이름 불일치, 체인·기간 거절 순서로 분류합니다. 여러 조건이 나빠도 하나의 우선 결과만 출력합니다. NAME_MISMATCH가 나온 기록의 chainOK가 False일 수도 있으므로 이 값이 체인 검증 통과를 보장하는 것은 아닙니다.
모든 조건을 만족한 결과의 이름은 OBSERVATION_OK입니다. HTTPS 안전이라는 표현 대신 관찰 필드가 계약을 만족했다고 말합니다. 합성 결과에 TCP 연결 성공, HTTP 응답 내용, 앱의 접근 통제는 없습니다. 기록의 name이나 bool 값이 조작되었을 가능성도 이 함수로 확인하지 않습니다. 보안 담당자는 관찰 데이터의 신뢰성과 그 데이터에 대한 분류를 구분해야 합니다.
전체 입력은 JSON 배열이고 각 원소가 한 관찰 객체입니다. 출력은 입력 순서를 유지하는 JSON 문자열 배열입니다. 빈 입력 배열은 빈 출력 배열입니다. 문법 자체가 깨진 JSON은 json.load의 예외로 드러나며 행 단위 INVALID와 다릅니다. 유효한 JSON 배열 안의 빈 객체, 숫자 원소, 잘못된 주소는 해당 원소만 INVALID로 분류합니다.
판단 함수를 완성하는 순서
브라우저 starter는 필드·형식 검사와 입력 처리를 제공합니다. 유효한 관찰에는 아직 TODO를 반환합니다. classify의 마지막 반환을 다섯 조건 분기로 바꿉니다. rcode 검사 다음에 addresses의 빈 목록을 보고, 그 뒤 기대 이름을 비교합니다. 인증서 조건을 먼저 보면 DNS가 실패한 기록에서 그 단계까지 수행했다고 잘못 읽을 수 있습니다.
bool과 문자열은 구분합니다. JSON의 true는 Python의 True가 되지만 문자열 "true"는 그대로 문자열입니다. bool("false")도 True이므로 강제 bool 변환으로 입력을 고치지 않습니다. type(value) is bool 검사를 통해 제공된 관찰 형식을 유지합니다. 형식 오류를 신뢰 실패와 섞지 않아야 수집 파이프라인 오류를 서버 인증서 문제로 오진하지 않습니다.
테스트는 정상, 주소 없음, NXDOMAIN, 다른 인증서 이름, 체인 실패, 기간 실패, 대소문자 이름, IPv6 주소와 결측 필드를 포함합니다. 정상 사례 하나를 통과했다고 끝내지 않고 각 실패가 예상한 단계 이름으로 분류되는지 봅니다. 빈 배열과 잘못된 주소는 최소 입력 및 경계 입력입니다. 입력별 답을 하드코딩하지 말고 같은 규칙이 새 행에도 적용되게 만듭니다.
오류 메시지가 알려 주는 범위
JSONDecodeError는 JSON 문법부터 확인하라는 뜻입니다. 대괄호 대신 중괄호로 전체 입력을 만들었거나 쉼표와 따옴표가 틀렸는지 봅니다. NAME_MISMATCH 예상이 OBSERVATION_OK로 나오면 주소만 확인하고 이름 검사를 빠뜨렸는지 찾습니다. NO_ADDRESS 예상이 DNS_ERROR로 나오면 NOERROR와 빈 답변을 한 조건으로 묶었는지 확인합니다.
실무 TLS 오류에 검증을 끄는 옵션을 적용하면 연결이 진행될 수 있지만 신뢰 조건이 복구된 증거가 되지는 않습니다. 기대 이름, 서버 인증서, 발급 체인, 유효 기간, 클라이언트 신뢰 설정을 조사한 뒤 필요한 항목을 고칩니다. 이름 검증의 역할은 RFC 9525에 근거하며 이 분류기의 단순 모델과 표준 전체를 구분합니다.
분류를 보고서 문장으로 바꾸기
“합성 관찰에서 이름 불일치로 분류했습니다”라는 문장에는 입력 name, certNames, 판정 순서를 연결합니다. “운영 인증서가 틀립니다”라는 결론은 실제 인증서를 조사하지 않았으므로 쓰지 않습니다. DNS_ERROR도 해석기의 전체 장애를 의미하지 않습니다. 주어진 rcode를 실패로 분류했다는 사실과 추가로 확인할 항목을 나누어 적습니다.
미션에는 이 판단 함수를 고정 DNS fixture에 적용한 E03이 생성됩니다. 루프백 HTTP 결과와 합성 DNS·TLS 결과는 서로 다른 관찰 종류입니다. 한 파일에 모으더라도 kind가 synthetic-fixture임을 남기고 실제 TLS 미검증을 표시합니다. DNS 캐시와 인증서 경로의 깊은 설명은 더 읽기로 연결하며 이번 목표는 각 오류의 단계와 다음 확인을 설명하는 것입니다.
따라하기
지원한 관찰의 전체 판정
정상과 이름 불일치 결과를 합성 입력으로 직접 계산합니다.
import sys, json, ipaddress
def classify(row):
if not isinstance(row, dict):
return "INVALID"
keys = ("name", "rcode", "addresses", "certNames", "chainOK", "timeOK")
if not all(k in row for k in keys):
return "INVALID"
if not isinstance(row["name"], str) or not row["name"]:
return "INVALID"
if row["rcode"] not in ("NOERROR", "NXDOMAIN", "SERVFAIL"):
return "INVALID"
if not isinstance(row["addresses"], list) or not isinstance(row["certNames"], list):
return "INVALID"
if not all(isinstance(n, str) and n for n in row["certNames"]):
return "INVALID"
if type(row["chainOK"]) is not bool or type(row["timeOK"]) is not bool:
return "INVALID"
try:
for address in row["addresses"]:
if not isinstance(address, str):
return "INVALID"
ipaddress.ip_address(address)
except ValueError:
return "INVALID"
if row["rcode"] != "NOERROR":
return "DNS_ERROR"
if not row["addresses"]:
return "NO_ADDRESS"
if row["name"].lower() not in [n.lower() for n in row["certNames"]]:
return "NAME_MISMATCH"
if not row["chainOK"] or not row["timeOK"]:
return "TLS_REJECT"
return "OBSERVATION_OK"
rows = [{'name': 'vault.example', 'rcode': 'NOERROR', 'addresses': ['192.0.2.10'], 'certNames': ['vault.example'], 'chainOK': True, 'timeOK': True}, {'name': 'vault.example', 'rcode': 'NOERROR', 'addresses': ['192.0.2.10'], 'certNames': ['other.example'], 'chainOK': True, 'timeOK': True}]
print(json.dumps([classify(row) for row in rows], ensure_ascii=False))
실행 결과
["OBSERVATION_OK", "NAME_MISMATCH"]
주소 부재와 DNS 오류 구분
서로 다른 단계의 실패를 분리합니다.
import sys, json, ipaddress
def classify(row):
if not isinstance(row, dict):
return "INVALID"
keys = ("name", "rcode", "addresses", "certNames", "chainOK", "timeOK")
if not all(k in row for k in keys):
return "INVALID"
if not isinstance(row["name"], str) or not row["name"]:
return "INVALID"
if row["rcode"] not in ("NOERROR", "NXDOMAIN", "SERVFAIL"):
return "INVALID"
if not isinstance(row["addresses"], list) or not isinstance(row["certNames"], list):
return "INVALID"
if not all(isinstance(n, str) and n for n in row["certNames"]):
return "INVALID"
if type(row["chainOK"]) is not bool or type(row["timeOK"]) is not bool:
return "INVALID"
try:
for address in row["addresses"]:
if not isinstance(address, str):
return "INVALID"
ipaddress.ip_address(address)
except ValueError:
return "INVALID"
if row["rcode"] != "NOERROR":
return "DNS_ERROR"
if not row["addresses"]:
return "NO_ADDRESS"
if row["name"].lower() not in [n.lower() for n in row["certNames"]]:
return "NAME_MISMATCH"
if not row["chainOK"] or not row["timeOK"]:
return "TLS_REJECT"
return "OBSERVATION_OK"
rows = [{'name': 'vault.example', 'rcode': 'NOERROR', 'addresses': [], 'certNames': ['vault.example'], 'chainOK': True, 'timeOK': True}, {'name': 'vault.example', 'rcode': 'NXDOMAIN', 'addresses': ['192.0.2.10'], 'certNames': ['vault.example'], 'chainOK': True, 'timeOK': True}]
print(json.dumps([classify(row) for row in rows], ensure_ascii=False))
실행 결과
["NO_ADDRESS", "DNS_ERROR"]
형식 오류와 신뢰 거절 대조
문자열 불리언과 실제 False를 구분합니다.
import sys, json, ipaddress
def classify(row):
if not isinstance(row, dict):
return "INVALID"
keys = ("name", "rcode", "addresses", "certNames", "chainOK", "timeOK")
if not all(k in row for k in keys):
return "INVALID"
if not isinstance(row["name"], str) or not row["name"]:
return "INVALID"
if row["rcode"] not in ("NOERROR", "NXDOMAIN", "SERVFAIL"):
return "INVALID"
if not isinstance(row["addresses"], list) or not isinstance(row["certNames"], list):
return "INVALID"
if not all(isinstance(n, str) and n for n in row["certNames"]):
return "INVALID"
if type(row["chainOK"]) is not bool or type(row["timeOK"]) is not bool:
return "INVALID"
try:
for address in row["addresses"]:
if not isinstance(address, str):
return "INVALID"
ipaddress.ip_address(address)
except ValueError:
return "INVALID"
if row["rcode"] != "NOERROR":
return "DNS_ERROR"
if not row["addresses"]:
return "NO_ADDRESS"
if row["name"].lower() not in [n.lower() for n in row["certNames"]]:
return "NAME_MISMATCH"
if not row["chainOK"] or not row["timeOK"]:
return "TLS_REJECT"
return "OBSERVATION_OK"
rows = [{'name': 'vault.example', 'rcode': 'NOERROR', 'addresses': ['192.0.2.10'], 'certNames': ['vault.example'], 'chainOK': 'true', 'timeOK': True}, {'name': 'vault.example', 'rcode': 'NOERROR', 'addresses': ['192.0.2.10'], 'certNames': ['vault.example'], 'chainOK': False, 'timeOK': True}, {}]
print(json.dumps([classify(row) for row in rows], ensure_ascii=False))
실행 결과
["INVALID", "TLS_REJECT", "INVALID"]
확인 문제
실습
JSON 배열의 관찰 객체를 classify로 분류하여 같은 순서의 문자열 배열을 출력합니다. 필드는 name, rcode, addresses, certNames, chainOK, timeOK입니다. 형식 검사 후 DNS_ERROR → NO_ADDRESS → NAME_MISMATCH → TLS_REJECT → OBSERVATION_OK 순서로 판정합니다. DNS_ERROR는 NXDOMAIN 또는 SERVFAIL, NO_ADDRESS는 NOERROR의 빈 주소 목록입니다. 이름은 ASCII 대소문자 무시 정확 일치만 지원합니다. 실제 DNS·TLS 검증은 하지 않습니다. JSON 문법과 전체 배열 형식은 올바르게 제공됩니다.
모범 답안
import sys, json, ipaddress
def classify(row):
if not isinstance(row, dict):
return "INVALID"
keys = ("name", "rcode", "addresses", "certNames", "chainOK", "timeOK")
if not all(k in row for k in keys):
return "INVALID"
if not isinstance(row["name"], str) or not row["name"]:
return "INVALID"
if row["rcode"] not in ("NOERROR", "NXDOMAIN", "SERVFAIL"):
return "INVALID"
if not isinstance(row["addresses"], list) or not isinstance(row["certNames"], list):
return "INVALID"
if not all(isinstance(n, str) and n for n in row["certNames"]):
return "INVALID"
if type(row["chainOK"]) is not bool or type(row["timeOK"]) is not bool:
return "INVALID"
try:
for address in row["addresses"]:
if not isinstance(address, str):
return "INVALID"
ipaddress.ip_address(address)
except ValueError:
return "INVALID"
if row["rcode"] != "NOERROR":
return "DNS_ERROR"
if not row["addresses"]:
return "NO_ADDRESS"
if row["name"].lower() not in [n.lower() for n in row["certNames"]]:
return "NAME_MISMATCH"
if not row["chainOK"] or not row["timeOK"]:
return "TLS_REJECT"
return "OBSERVATION_OK"
rows = json.load(sys.stdin)
print(json.dumps([classify(row) for row in rows], ensure_ascii=False))
더 읽기
면접 질문
- DNS 응답 성공과 TLS 이름 검증 성공을 구분하여 설명합니다.