새 서브도메인 하나를 열자고 DNS 레코드를 만들고, 인증서를 받고, 서버 블록을 추가하고 재시작한다. 프로젝트가 수백 개면 이 일을 수백 번 한다. 그런데 이 중 대부분은 한 번만 해두면 되는 일이다. 와일드카드 DNS와 호스트 라우팅을 미리 깔아두면, 새 서브도메인을 여는 일은 데이터베이스에 행 하나 추가하는 것으로 끝난다.
여기까지는 깔끔하다. 진짜 이야기는 그다음이다. "와일드카드 DNS"와 "와일드카드 인증서(TLS)"는 이름만 같을 뿐 적용 규칙이 다르다. 이 둘을 같은 규칙이라고 믿는 순간, "DNS는 분명 응답하는데 인증서 때문에 안 열리는" 사고가 난다. 그래서 이 글의 중심 질문은 "몇 단계까지 되느냐"가 아니라 "언제 안 되느냐"다.
건물 주소(도메인)는 하나지만, 그 안에 방(서브도메인)은 얼마든지 만들 수 있다. 방을 하나 틀 때마다 외벽을 뚫지는 않는다. 방문객은 모두 정문 하나로 들어오고, 로비의 안내데스크가 "어느 방 찾으세요?"를 듣고 호실을 알려준다. 이 그림만 머리에 깔아두면 뒤의 내용이 전부 여기에 들어온다.
방을 하나 더 여는 일은 배관 공사가 아니라, 명부에 이름 한 줄 적는 것이다.
DNS에 *.ontle.kr 형태의 와일드카드 레코드를 등록해둔다. 여기서 존(zone)은 한 도메인의 DNS 레코드를 담는 묶음이고, 레이블(label)은 도메인을 점으로 끊은 각 토막(report-2026·ontle·kr)이다. 존에 따로 정의되지 않은 이름으로 질의가 들어오면, 이 와일드카드가 대신 응답을 만들어 보낸다. report-2026.ontle.kr이든 dashboard-abc.ontle.kr이든, 미리 등록하지 않아도 같은 목적지로 나간다.
; 와일드카드 A 레코드 (직접 IP로)
*.ontle.kr. 3600 IN A 203.0.113.10
; 또는 CDN/터널을 경유하면 CNAME
*.ontle.kr. 3600 IN CNAME edge.example-cdn.net.
흔히 "와일드카드는 바로 아래 한 단계만 커버한다"고들 하는데, 그건 DNS의 규칙이 아니다. 어느 계층의 규칙인지는 뒤에서 따로 짚는다. 여기서는 바탕을 한 번 깔았다는 것까지만 기억하면 된다.
모든 요청은 하나의 진입점으로 들어온다. 이 진입점은 리버스 프록시(들어오는 요청을 제일 앞에서 받아 뒤 서버로 넘기는 중계소)나 CDN 엣지, 또는 아웃바운드 터널이다. 진입점은 HTTP 요청에 실린 Host 헤더 — 브라우저가 주소창에 넣은 도메인 이름 — 를 읽는다. TLS를 쓰면 SNI(접속하려는 이름을 미리 알려주는 값)가 같은 역할을 한다. 어느 문으로 들어왔는지는 IP가 아니라 이름표로 갈린다.
GET / HTTP/1.1
Host: report-2026.ontle.kr
진입점 뒤의 애플리케이션은 Host 값을 받아 "이 이름이 등록돼 있나?"를 조회한다. 있으면 연결된 콘텐츠를, 없으면 404나 안내 페이지를 보여준다. 명부는 개념적으로 이렇게 생겼다.
| 서브도메인 | 콘텐츠 유형 | 소유자 | 상태 |
|---|---|---|---|
| report-2026 | 분석 대시보드 | 프로젝트 A | 발행됨 |
| jihoon-blog | 개인 블로그 | 사용자 B | 발행됨 |
| team-x | 팀 랜딩 | 팀 X | 임시중지 |
새 서브도메인을 여는 일이 "DNS 추가 → 인증서 발급 → 서버 설정 → 재시작"에서 이 표에 행 하나 추가로 줄어든다. DNS와 인증서는 이미 준비돼 있으니, 남는 건 데이터베이스 트랜잭션 하나뿐이다. DNS 전파를 기다릴 이유도 없다. 와일드카드 레코드는 오래전에 퍼져 끝난 상태고, 바뀌는 건 애플리케이션 안의 명부뿐이라 보통 수 초 안에 반영된다.
고객에게 납품할 분석 대시보드를 report-2026이라는 이름으로 발행했다고 하자. report-2026.ontle.kr에 접속하면 온틀의 헤더나 내비게이션이 감싸는 게 아니라, 그 대시보드의 원본 HTML이 최상위 문서로 그대로 응답된다.
차이는 체감으로 드러난다. 이 주소를 카카오톡에 보내면 온틀 로고가 아니라 그 대시보드의 제목·설명·썸네일이 뜬다. 구글 검색에도 "온틀"이 아니라 그 사이트가 직접 색인된다. 명함처럼 건네도, 검색엔진에 잡혀도 온전히 고객 것으로 보인다. 이것이 온틀에서 산출물을 발행했을 때 이름.ontle.kr이 뷰어에 갇히지 않고 독립 웹사이트로 보이는 이유다.
여기서부터가 내가 실무에서 사고를 가장 많이 본 지점이다. 두 "와일드카드"는 이름만 같고 적용 범위 규칙이 다르다. 뭉개는 순간 "DNS는 응답하는데 인증서 오류로 안 열리는" 상황을 만난다.
*.ontle.kr 레코드): 레이블 개수로 제한되지 않는다. 가로막는 노드만 없으면 a.b.ontle.kr처럼 여러 단계 아래 이름에도 응답을 만들어낸다. 단, 이미 존재하는 이름에는 적용되지 않는다(근거 RFC 4592).*.ontle.kr): 가장 왼쪽 레이블 한 개만 매칭한다. report-2026.ontle.kr은 덮지만 a.b.ontle.kr이나 ontle.kr(맨 위 이름 그 자체)은 덮지 못한다. 이 "한 단계" 제한이 RFC 6125 §6.4.3과 CA/Browser Forum Baseline Requirements가 정한 규칙이고, 후속 표준 RFC 9525도 그대로 유지한다.그래서 진짜 질문은 "언제 안 되느냐"다. 이름.ontle.kr처럼 한 단계짜리만 발행하는 한, 와일드카드 인증서 한 장이면 충분하다 — 온틀의 기본 발행 형태다. 하지만 팀명.고객사.ontle.kr처럼 두 단계 깊이로 내려가는 이름을 열고 싶다면, DNS는 (중간 노드가 없으면) 응답하더라도 TLS 인증서는 *.ontle.kr 한 장으로 안 덮인다. 그 깊이를 쓰려면 *.고객사.ontle.kr 인증서를 따로 발급하거나, 인증서의 SAN(한 인증서가 커버하는 이름 목록)에 그 이름을 직접 넣어야 한다. "DNS가 응답한다"와 "TLS가 신뢰된다"는 별개의 층위다.
[심화] 와일드카드가 안 통하는 한 곳 — empty non-terminal
안내데스크 비유를 다시 가져오자. 명부에 이름이 없어도, 그 아래 세부 호실에 이미 입주자가 있으면 안내데스크는 그 구역을 "빈 이름"으로 보지 않는다. DNS도 똑같다. 이름이 존재하는 경로는 둘이다 — (1) 그 이름에 직접 레코드가 붙어 있거나, (2) 자기 레코드는 없어도 그 아래에 레코드를 가진 하위 이름이 있는 경우. 두 번째가 empty non-terminal(빈 비단말, ENT)이다. 와일드카드는 "정의되지 않은 이름"만 받아주므로, 이렇게 존재하는 이름은 건드리지 않는다.
문제없는 경우 — DNS는
*.ontle.kr그대로 두고 발행은 DB 명부에만 한다. 새 DNS 레코드가 안 생기니 ENT도 안 생긴다.x.team.ontle.kr을 발행해도team.ontle.kr은 와일드카드 응답을 그대로 유지한다.함정 —
x.team.ontle.kr에 실제 DNS 레코드를 직접 박고 상위team.ontle.kr엔 아무것도 두지 않으면,team.ontle.kr이 ENT가 된다. 이제 와일드카드가team.ontle.kr을 대신 받지 않는다. 조회하면 NXDOMAIN("이름 자체가 없음")이 아니라 NODATA("이름은 있는데 그 타입의 레코드가 없음")가 돌아오고, 결국 안 열린다.요약하면, DB에만 행을 추가하는 한 ENT는 안 생긴다. 깊은 이름에 개별 DNS 레코드를 박기 시작할 때만 조심하면 된다(RFC 4592).
구조가 단순할수록 운영 규칙이 느슨하면 사고가 커진다. 서브도메인을 대량으로 발급한다면 아래는 기본으로 깔고 가는 게 좋다.
www, api, admin, mail, blog, app, cdn, status 같은 이름을 사용자가 임의로 가져가면, 사용자 콘텐츠가 시스템 관리 페이지처럼 보이거나 반대로 실제 시스템 서비스가 밀려난다. 처음부터 예약 목록에 넣어 막고, 입력값은 대소문자·공백을 정규화해서 비교한다.
RESERVED_SUBDOMAINS = frozenset({
"www", "api", "admin", "mail", "ftp", "blog",
"app", "cdn", "status", "docs", "help", "support",
})
def is_reserved(name: str) -> bool:
normalized = name.strip().lower()
return normalized in RESERVED_SUBDOMAINS
이름 배정은 애플리케이션 로직의 "있는지 확인 후 생성"에 맡기면 안 된다. 같은 이름을 동시에 요청한 두 트랜잭션이 둘 다 "없다"고 판단해 둘 다 끼워 넣으면, 한 이름에 두 산출물이 걸린다. 정규화한 이름 컬럼에 데이터베이스 UNIQUE 제약을 걸고, 중복 삽입 시 DB가 내는 오류를 애플리케이션에서 "이미 사용 중"으로 매핑하는 게 안전하다.
제한 없이 발급하면 스팸성 대량 생성과 자원 고갈에 그대로 노출된다. 요금제나 계정 등급별로 "동시 발행 가능한 서브도메인 수"에 상한을 두고, 초과 시 명확히 안내하며 막는다.
CDN이나 엣지 캐시를 쓰면, 새로 발행한 이름이 아직 캐시에 없어 첫 요청이 느리거나, 반대로 회수한 이름이 캐시에 남아 옛 콘텐츠를 계속 보여준다. 발행·회수 이벤트가 나면 그 이름의 캐시 무효화(purge)를 라우팅 테이블 갱신과 한 트랜잭션처럼 묶어두는 게 좋다.
로그엔 성공이 찍혔는데 고객은 404를 본다. 라우팅 테이블에 행이 들어갔다고 그 주소가 실제로 열린다는 보장은 없다. 흔한 원인은 이렇다 — 캐시가 안 갱신됐거나, 라우팅 조건에 오타가 있거나, (개별 DNS 레코드를 박았다면) 앞서 본 ENT 때문에 DNS 단계에서 이미 막혔거나, 인증서가 그 깊이를 못 덮거나.
그래서 서브도메인 발행은 "DB에 기록 성공"에서 끝나면 안 된다. 실제 URL에 HTTP 200이 뜨는지, 브라우저로 열어 화면이 진짜 나오는지 눈으로 확인하는 데까지가 한 세트다. 로그와 DB 카운트는 "시도했다"는 증거이지 "됐다"는 증거가 아니다.
"라우팅 테이블에
report-2026.ontle.kr을 추가했습니다." — 여기까지는 시도다. 그 주소에 들어갔을 때 화면이 뜨는지는 아직 아무도 모른다."라우팅 테이블에 추가하고,
curl -sI https://report-2026.ontle.kr로 HTTP 200과 TLS 정상을 확인했고, 브라우저로 열어 대시보드가 렌더링되는 것까지 봤습니다." — 이제야 됐다고 말할 수 있다.
산출물을 만들 때마다 DNS·인증서·배포 서버를 다시 만질 필요는 없다. 온틀은 이 구조 위에서, 만든 결과물을 이름.ontle.kr이라는 독립 웹사이트로 그 자리에서 발행한다. 직접 해보고 싶다면 https://ontle.kr 에서 시작할 수 있다.
`*.ontle.kr` 인증서 한 장이 1단계 이름 전부를 덮는 건 편의의 핵심이자 대가입니다. 모든 서브도메인이 같은 인증서와 개인키에 묶이므로, 그 키가 유출되면 영향 범위가 전체로 번집니다. 그래서 키 보관(접근 권한 최소화)과 주기적 교체를 운영 기본으로 챙겨야 합니다. 특정 서브도메인을 더 강하게 보호하고 싶다면 그 이름에 전용 인증서를 추가로 발급해, 그 서버에서는 전용 인증서만 제시하도록 할 수 있습니다. 다만 전용 인증서를 발급한다고 기존 `*.ontle.kr` 와일드카드가 그 이름에 대해 유효성을 잃는 것은 아닙니다 — 와일드카드 인증서는 여전히 그 이름에도 유효하므로, 와일드카드 개인키가 유출되면 그 이름을 사칭하는 위험은 그대로 남습니다. 즉 전용 인증서 발급만으로 그 이름이 와일드카드의 적용 범위에서 빠지거나 "완전히 격리"되지는 않습니다(참고: RFC 9525 §7.1).
DNS가 응답한다고 콘텐츠가 있는 건 아닙니다. 등록되지 않은 이름으로 들어오면 애플리케이션이 명부에 없다고 판단해 404나 안내 페이지로 돌려보냅니다. 여기에 예약어 목록과 계정당 발행 수 상한을 더하면, "아무 이름이나 받아주는" 입구가 실제 남용으로 이어지는 걸 앞단에서 막을 수 있습니다.
DNS는 이미 `*.ontle.kr` 와일드카드가 받고 있으니 새 이름은 명부(DB)에 추가하는 것으로 흡수됩니다. 다만 과거에 개별 DNS 레코드를 직접 박아둔 이름이 있다면, 그걸 정리할 때 상위 이름이 ENT가 되지 않는지부터 확인해야 합니다. 그 이름 자체를 계속 서빙해야 한다면 명시적 레코드를 유지하거나, 라우팅을 명부로 옮기면서 불필요한 개별 레코드를 걷어내는 순서로 진행하세요.
글 · 박지훈