4 분 소요

지난 편에서 Nginx와 Certbot으로 HTTPS를 붙였을 때는 작업이 거의 끝났다고 생각했습니다. https://www.git-ranker.com 접속이 정상이고, 브라우저 경고도 사라졌기 때문입니다.

그런데 운영을 하던 중, 하나의 큰 문제가 발견되었습니다.

  • http://www.git-ranker.com은 정상 리다이렉트
  • http://git-ranker.com404

같은 서비스를 가리키는 URL인데 www 유무에 따라 동작이 갈리는 상태였습니다.


www/non-www 접속 불일치가 만든 첫 장애 신호

이번에 문제로 본 지점은 단순히 “HTTPS가 된다”가 아니었습니다. 실제 운영에서는 아래 경로가 일관된 정책으로 동작해야 합니다.

  • http://git-ranker.com
  • http://www.git-ranker.com
  • https://git-ranker.com
  • https://www.git-ranker.com

이 중 일부가 실패하면 사용자 입장에서는 서비스가 불안정하게 보입니다. 검색 유입, 공유 링크, 브라우저 자동완성이 섞이는 환경에서는 이런 불일치가 바로 이탈로 이어집니다.


요청이 갈린 원인 추적: Host 매칭과 리다이렉트 조건

Certbot이 생성한 Nginx 로직의 허점

원인을 파악하기 위해 설정 파일을 확인했더니, HTTPS 적용 당시 Certbot이 만든 리다이렉트 블록이 아래처럼 들어가 있었습니다.

server {
    # Certbot이 생성한 방어적 리다이렉트 로직
    if ($host = www.git-ranker.com) {
        return 308 https://$host$request_uri;
    }

    listen 80;
    server_name www.git-ranker.com;
    return 404;
}

문제는 if ($host = www.git-ranker.com) 조건이었습니다. www 없이 들어온 요청은 조건을 통과하지 못하고 return 404로 종료됩니다.

Nginx는 요청의 Host 헤더와 server_name으로 서버 블록을 선택합니다.

즉, http://git-ranker.com이 실패한 건 애플리케이션 문제가 아니라 Host 매칭 범위가 www로 고정되어 있었기 때문이었습니다.

왜 Certbot은 이런 방어적 코드를 만들었을까

당시 초기 인증서 발급 대상이 www.git-ranker.com만 포함되어 있었기 때문에, 자동 생성된 리다이렉트도 그 호스트만 안전하게 HTTPS로 넘기도록 제한된 상태였습니다.

이 상태에서 모든 호스트를 무조건 HTTPS로 넘기면, 인증서 범위에 없는 도메인에서 TLS 이름 검증 오류가 날 수 있습니다. 그래서 Certbot이 보수적으로 www만 리다이렉트하고 나머지는 404로 정리한 흐름으로 해석할 수 있었습니다.

루트 도메인까지 HTTPS로 확장하려다 확인한 DNS 이슈

저는 여기서 멈추지 않고 git-ranker.com도 같은 정책으로 HTTPS에 포함하기로 했습니다. 그래서 Nginx server_name에 루트 도메인을 추가하고, Certbot으로 git-ranker.com을 포함한 인증서 발급을 시도했습니다.

바로 그때 Detail: no valid A records found for git-ranker.com 에러가 발생했습니다.

Certbot failed to authenticate some domains ...

Detail: no valid A records found for git-ranker.com; no valid AAAA records found for git-ranker.com

이 에러는 Nginx 내부 에러가 아니라 Let’s Encrypt 검증 단계에서 루트 도메인의 A/AAAA 조회가 유효하지 않다는 뜻입니다.

핵심은, Let’s Encrypt가 인증서에 포함된 도메인을 각각 DNS로 검증한다는 점이었습니다. 이번에는 git-ranker.com을 인증서 대상에 추가했기 때문에 루트 도메인의 A/AAAA 조회가 먼저 맞아야 했습니다.

당시에는 www.git-ranker.com만 CNAME으로 DDNS를 따라가도록 두고 , git-ranker.com에는 A 레코드를 아직 매핑하지 않은 상태였습니다.

그래서 www 경로는 살아 있어도 루트 도메인 검증은 바로 실패했고, 이번 no valid A records가 그 상태를 보여준 에러였습니다.

해결 전에 정리한 세 가지 선택지

첫 번째 고민, 대표 도메인(canonical) 방향

가장 먼저 정한 건 “어떤 주소를 공식 주소로 볼 것인가”였습니다. www.git-ranker.com을 대표 주소로 유지하면 현재 인증서, 리버스 프록시, 앱 설정과의 충돌이 거의 없고 변경 범위도 작아서 장애 리스크를 낮출 수 있습니다. 반면 URL이 길어진다는 단점이 있습니다.

반대로 git-ranker.com(루트 도메인)으로 통일하면 URL은 더 직관적이지만, 기존 리다이렉트 규칙, 인증서 SAN 범위, 내부 링크 정책까지 함께 점검해야 해서 검증 포인트가 확 늘어납니다. 이번 작업 목표가 구조 개편이 아니라 빠른 안정화였기 때문에, 저는 www 대표 주소를 유지하는 방향을 선택했습니다.

두 번째 고민, Cloudflare 이전을 지금 할 것인가

Cloudflare 이전은 분명 매력적인 선택지였습니다. DNS 운영 기능, 보안 옵션, 관리 편의성 측면에서 얻는 이점이 분명했기 때문입니다.

다만 최근들어 Cloudflare 장애 공지가 연속적으로 올라오는 시기이기도 했습니다. 2025-07-14 1.1.1.1 장애, 2025-11-18 장애, 2025-12-05 장애 같은 이슈를 보면서 “지금 이 타이밍에 외부 플랫폼 전환까지 한 번에 가져가는 게 맞나”를 다시 생각하게 됐습니다.

그리고 저는 홈서버를 개인 PC로 구축한 이유가 핵심 인프라를 내가 직접 통제하고 운영 경험을 쌓고 싶어서였습니다. 그래서 플랫폼 이전을 섞지 않고, 현재 환경에서 문제를 해결하는 것에 집중하기로 했습니다.

세 번째 고민, 유동 IP 환경에서 루트 도메인 A 레코드 운영

Cloudflare 이전을 보류한 뒤에는, 현재 홈서버 환경에서 루트 도메인을 어떻게 안정적으로 운영할지가 핵심 과제가 됐습니다. git-ranker.com까지 HTTPS 인증서를 발급하려면, 먼저 루트 도메인이 DNS에서 서버를 정확히 가리켜야 했고, 그래서 A 레코드에 공인 IPv4를 직접 매핑해야 했습니다.

문제는 이 공인 IPv4가 고정값이 아니라는 점이었습니다. 운영 환경이 미니PC + 가정용 ISP + 포트포워딩이라 공유기 재부팅, WAN 재연결, DHCP 임대 갱신이 발생하면 IP가 바뀔 수 있습니다. 이때 DNS가 이전 IP를 계속 가리키면 서비스 프로세스가 살아 있어도 외부 접속은 실패합니다. 루트 도메인 @를 A 레코드로 운영하는 이상, IP 변경 시 A 값을 직접 갱신해야 하고 TTL 전파가 끝날 때까지 일부 구간은 접속이 불안정할 수 있습니다.

그럼에도 이번에는 A 레코드에 당시 공인 IP를 직접 넣는 방식을 선택했습니다. 실제 IP 변경 빈도가 높지 않았고, 이번 작업의 목표가 구조 변경보다 루트 도메인 HTTPS를 빠르게 안정화하는 데 있었기 때문입니다.


역할 분리로 정리한 DNS와 Nginx 정책

DNS 레코드를 먼저 고정하기

record

현재 레코드는 아래처럼 유지했습니다.

  • @ A -> 118.xxx.xxx.xx
  • www CNAME -> hyoserver.iptime.org

이 단계에서 먼저 해결하고 싶었던 건 단 하나였습니다. 사용자가 어떤 URL로 들어오더라도 요청이 홈서버까지 도달할 수 있게 DNS 경로를 안정화하는 것이었습니다.

git-ranker.com@ A 레코드로 서버 IP를 직접 가리키고, www.git-ranker.com은 DDNS 호스트를 CNAME으로 따라가게 해 도달 경로를 먼저 고정했습니다.

Nginx 리다이렉트 정책을 코드로 고정하기

최종 설정에서는 git-ranker.comwww.git-ranker.com 요청을 모두 https://www.git-ranker.com으로 308 리다이렉트되도록 통일했습니다.

server {
    listen 80;
    listen [::]:80;
    server_name www.git-ranker.com git-ranker.com;

    return 308 https://www.git-ranker.com$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name git-ranker.com;

    return 308 https://www.git-ranker.com$request_uri;
}

요청 흐름

실제 요청 흐름

예를 들어 사용자가 http://git-ranker.com으로 접속하면, 요청은 아래 순서로 진행됩니다.

  1. 클라이언트가 먼저 nginx 80으로 요청합니다.
  2. nginx 80308 Location: https://www.git-ranker.com...을 응답합니다.
  3. 브라우저(클라이언트)는 이 응답을 보고 https://www.git-ranker.com으로 새 요청을 보냅니다.
  4. 그 다음부터는 nginx 443 www domain -> reverse proxy -> application 경로로 처리됩니다.

즉, 비대표 URL(http://git-ranker.com, https://git-ranker.com)로 들어온 요청은 리다이렉트 응답을 한 번 거친 뒤, 최종적으로 대표 URL 경로에서 애플리케이션으로 전달됩니다.

A 레코드 반영 후 DNS 확인

Nginx 설정만 고쳐서는 끝나지 않았기 때문에, 루트 도메인 A 레코드가 실제로 반영됐는지도 함께 확인했습니다.

dig +short A git-ranker.com
> 118.xxx.xxx.xx

dig +short CNAME www.git-ranker.com
> hyoserver.iptime.org.

Certbot 재발급 성공 확인

DNS와 Nginx를 맞춘 뒤, 루트 도메인을 포함해 Certbot을 다시 실행했고 아래와 같은 성공 메시지를 확인했습니다.

sudo certbot --nginx -d git-ranker.com -d www.git-ranker.com
Successfully received certificate.
Successfully deployed certificate for git-ranker.com
Successfully deployed certificate for www.git-ranker.com

댓글남기기