도메인과 HTTPS: saip.nexus 연결하기
IP 주소로만 열리던 블로그에 도메인을 붙이고 HTTPS로 운영하기까지의 기록이다. 인증서는 Caddy가 자동으로 받고 갱신한다. Caddy와 인증서 발급 원리는 "Docker로 포장하기" 노트에 있다.
도메인과 DNS
sequenceDiagram
participant B as 브라우저
participant D as DNS
participant S as 서버
B->>D: saip.nexus 의 IP는?
D-->>B: [고정 IP] (A 레코드)
B->>S: https://saip.nexus 요청
- 도메인: IP 주소에 붙이는 이름. 서버를 옮겨도 DNS만 바꾸면 주소가 유지된다.
- A 레코드: "이 이름은 이 IPv4 주소"라는 한 줄.
@는 도메인 자체를 뜻한다. - TTL: DNS 응답을 얼마나 기억할지. 처음 설정은 몇 분 만에 퍼졌다.
도메인 고르기
비어 있는지 먼저 조회: RDAP
WHOIS의 후속인 RDAP로 등록 여부를 조회할 수 있다. 응답이 404면 등록 기록이 없다(프리미엄이나 예약어일 수 있으니 최종 확인은 구매 화면에서).
curl -s -o /dev/null -w '%{http_code}\n' -L https://rdap.org/domain/saip.blog # 404 = 미등록
# 새 gTLD는 레지스트리 RDAP 서버를 IANA 목록에서 찾아 직접 묻는다
curl -s https://data.iana.org/rdap/dns.json
너무 자주 물으면 429(요청 과다)로 막힌다.
무엇을 보고 고르나
| 기준 | 내용 |
|---|---|
| 갱신 가격 | 새 도메인 종류(gTLD)는 첫해만 싸고 2년차부터 비싼 경우가 많다. 블로그 주소는 몇 년씩 쓰므로 갱신가가 실제 비용이다 |
| 평판 | 한때 스팸에 많이 쓰인 도메인 종류는 회사 방화벽이나 메신저가 의심할 수 있다. 회사에서 열어 볼 학습 노트라면 중요하다 |
| 확장성 | 개인 이름 도메인 하나면 blog., todo. 같은 서브도메인을 무료로 늘릴 수 있다 |
| 등록처 | Cloudflare Registrar는 원가에 팔고 갱신가가 같으며 DNS가 무료. 가비아는 한국어·국내 결제. Route 53은 DNS 월 요금이 따로 붙는다 |
saip.dev는 이미 등록되어 있어서 saip.nexus를 Cloudflare에서 샀다.
Cloudflare DNS 설정
| 항목 | 값 |
|---|---|
| Type | A |
| Name | @ |
| IPv4 | [고정 IP] |
| Proxy status | DNS only (회색 구름) |
| TTL | Auto |
Warning
주황 구름(Proxied)을 켜면 Cloudflare가 중간에서 HTTPS를 처리한다. 서버의 Caddy가 직접 인증서를 받는 구성과 충돌하므로 DNS only로 둔다.
반영 확인은 여러 DNS에 물어본다.
curl -s "https://cloudflare-dns.com/dns-query?name=saip.nexus&type=A" -H 'accept: application/dns-json'
curl -s "https://dns.google/resolve?name=saip.nexus&type=A"
Note
Windows nslookup은 결과를 한글로 출력해서 grep "Address" 같은 필터가 아무것도 못 찾았다. DNS가 안 된 줄 알았는데 필터 문제였다. DNS over HTTPS(JSON)로 물으면 언어와 상관없이 확인할 수 있다.
HTTPS 켜기: 한 줄 바꾸기
서버 .env의 DOMAIN=:80(IP로 http만)을 DOMAIN=saip.nexus로 바꾸고 다시 띄운다.
sed -i "s/^DOMAIN=.*/DOMAIN=saip.nexus/" ~/saip/.env
docker compose up -d
docker compose logs caddy | grep -o '"msg":"[^"]*certificate[^"]*"'
# "obtaining certificate" → "trying to solve challenge" → "certificate obtained successfully"
확인 결과:
| 확인 | 결과 |
|---|---|
http://saip.nexus |
308 → https://saip.nexus/ |
https://saip.nexus |
200 |
| 인증서 | Let's Encrypt, 90일. Caddy가 만료 전에 자동 갱신 |
Google 로그인 redirect_uri |
https://saip.nexus/login/oauth2/code/google |
echo | openssl s_client -connect saip.nexus:443 -servername saip.nexus 2>/dev/null \
| openssl x509 -noout -issuer -subject -enddate
Google 로그인 운영 설정
- OAuth 클라이언트의 승인된 리디렉션 URI에
https://saip.nexus/login/oauth2/code/google추가. 로컬용http://localhost:8080/...은 남겨 둔다. - 동의 화면을 테스트 → 프로덕션으로 게시. 테스트 상태에서는 테스트 사용자로 등록한 계정만 로그인된다. 댓글 다는 방문자도 로그인하려면 게시해야 한다. 이메일·이름·프로필만 쓰므로 심사는 없다.
앱 코드는 바꾸지 않았다. 앱이 Caddy가 붙여 주는 X-Forwarded-Proto: https를 읽어 자기 주소를 https://saip.nexus로 알기 때문이다(server.forward-headers-strategy: framework).
발견한 것: 세션 쿠키에 Secure가 없었다
set-cookie: JSESSIONID=...; Path=/; HttpOnly ← 고치기 전
set-cookie: JSESSIONID=...; Path=/; Secure; HttpOnly ← 고친 뒤
HTTPS 여부는 Spring이 전달 헤더로 알아채지만, 세션 쿠키는 Tomcat이 만든다. Tomcat은 Caddy에게서 http로 받으니 Secure를 붙이지 않았다. Secure가 없으면 누군가 처음 http://로 접속하는 순간 브라우저가 쿠키를 평문으로 보낼 수 있다. 운영 compose에만 설정을 넣었다(로컬 http://localhost 개발에는 영향 없음).
environment:
SERVER_SERVLET_SESSION_COOKIE_SECURE: "true"
Tip
배포하면 응답 헤더를 직접 보자. curl -sI https://도메인/...으로 Set-Cookie, Strict-Transport-Security, Content-Security-Policy를 확인하면 프록시 뒤에서만 생기는 문제를 잡을 수 있다.
HSTS(Strict-Transport-Security: max-age=31536000 ; includeSubDomains)는 Spring Security가 HTTPS 요청에 자동으로 붙인다. 브라우저가 1년 동안 이 도메인을 https로만 접속한다.
/blog를 운영 서버로
PC의 사용자 환경변수 두 개만 바꾼다. 토큰은 환경변수끼리 복사하고 출력하지 않는다.
| 변수 | 값 |
|---|---|
BLOG_URL |
https://saip.nexus |
BLOG_TOKEN |
서버 .env의 BLOG_API_TOKEN과 같은 값(로컬 개발용과 다름) |
로컬 개발 서버에 올려 보고 싶을 때는 그 창에서만 바꾼다.
$env:BLOG_URL = "http://localhost:8080"
$env:BLOG_TOKEN = [Environment]::GetEnvironmentVariable("BLOG_API_TOKEN", "User")
blog.py는 토큰이 평문으로 나가지 않게 localhost가 아니면 https만 허용한다. 그래서 IP(http)만 있던 동안에는 운영 서버로 노트를 올리지 못했고, 이 노트가 운영 서버에 바로 올라간 첫 노트다.
댓글 0
로그인하고 댓글 달기