티스토리 뷰

Back-End/개인 공부

Nginx + SSL + DNS

poopooreum 2026. 4. 30. 21:24
반응형

✏️ 1. DNS 기본 개념

DNS란?

DNS(Domain Name System)는 도메인 이름을 IP 주소로 변환해주는 인터넷의 전화번호부로 사람이 기억하기 쉬운 도메인을 컴퓨터가 이해하는 IP로 바꿔주는 역할을 한다.

eat-ssu.tech → 43.200.49.228

 

DNS 조회 흐름(eat-ssu를 예시로)

브라우저에서 eat-ssu.tech 입력
        ↓
1. 브라우저 캐시 확인
        ↓
2. OS 캐시 확인 (/etc/hosts)
        ↓
3. ISP DNS 서버 (SKT, KT 등) 조회
        ↓
4. 루트 DNS 서버 (.)
        ↓
5. TLD DNS 서버 (.tech, .com 등)
        ↓
6. 권한 있는 DNS 서버 (get.tech, Route53 등)
        ↓
IP 주소 반환 → TTL 동안 캐싱

DNS 레코드 종류

레코드 용도 예시
A 도메인 → IPv4 eat-ssu.tech → 43.200.49.228
AAAA 도메인 → IPv6 eat-ssu.tech → 2001:db8::1
CNAME 도메인 → 도메인 www → eat-ssu.tech
MX 메일 서버 eat-ssu.tech → mail.eat-ssu.tech
TXT 텍스트 정보 도메인 소유권 인증 등

A 레코드에서 @는 루트 도메인을 의미한다.

@   → 43.200.49.228   (eat-ssu.tech)
dev → 3.37.229.16     (dev.eat-ssu.tech)
www → 43.200.49.228   (www.eat-ssu.tech)

TTL(Time To Live)은 DNS 응답을 캐시에 얼마나 보관할지 결정하는 값으로 초 단위를 사용한다.

DNS 레코드를 변경해도 TTL이 만료되기 전까지는 기존 캐시를 사용한다. 즉 TTL이 86400이면 변경 후 최대 24시간까지 기존 IP로 접속될 수 있다

TTL 300   = 5분
TTL 3600  = 1시간
TTL 7200  = 2시간
TTL 86400 = 24시간
작업 24시간 전: TTL을 300(5분)으로 낮추기
        ↓
기존 TTL이 만료될 때까지 대기 (최대 기존 TTL 시간)
        ↓
서버 이전 및 DNS 변경
        ↓
변경 후 5분이면 전 세계 전파 완료
        ↓
작업 완료 후 TTL 원래 값으로 복구

DNS 확인 명령어

# 기본 조회
dig eat-ssu.tech

# IP만 출력
dig eat-ssu.tech +short
# 43.200.49.228

# 특정 DNS 서버로 조회 (구글 DNS)
dig @8.8.8.8 eat-ssu.tech +short

# TTL 확인
dig @8.8.8.8 eat-ssu.tech
# eat-ssu.tech. 7200 IN A 43.200.49.228
#               ^^^^
#               남은 TTL(초)

# nslookup으로 조회 (Windows/Mac)
nslookup eat-ssu.tech

✏️ 2. SSL과 HTTPS

HTTP vs HTTPS

HTTP는 데이터를 평문으로 전송하기 때문에 중간에서 누군가 패킷을 가로채면 내용을 그대로 볼 수 있다. HTTPS는 SSL/TLS로 암호화하기 때문에 가로채도 내용을 알 수 없다.

  HTTP HTTPS
포트 80 443
암호화 X 0
인증서 불필요 필요
보안 취약 강함

SSL 인증서란?

SSL 인증서는 두 가지 역할을 한다.

  1. 신원 확인: 이 서버가 진짜 eat-ssu.tech 서버임을 증명
  2. 암호화 키 교환: 클라이언트와 서버 간 암호화 통신을 위한 공개키 포함

인증서는 CA(Certificate Authority, 인증 기관)가 발급하며 Let's Encrypt는 무료 CA로 certbot을 통해 쉽게 발급받을 수 있고,

SSL 핸드셰이크 흐름은 아래의 표와 같다.
클라이언트                          서버
    |                                |
    |---- ClientHello -------------->|  (지원하는 암호화 방식 목록)
    |                                |
    |<--- ServerHello + 인증서 ------|  (선택한 암호화 방식 + 공개키)
    |                                |
    |  인증서 검증                    |
    |  (CA 서명 확인, 도메인 일치 확인)|
    |                                |
    |---- 대칭키 교환 --------------->|
    |                                |
    |<===== 암호화 통신 시작 ========>|

인증서 구성 파일

certbot으로 발급받으면 다음 파일들이 생성되며 nginx에서는 fullchain.pem과 privkey.pem 두 개만 사용한다.

/etc/letsencrypt/live/eat-ssu.tech/
├── fullchain.pem   # 서버 인증서 + 중간 인증서 (nginx에서 사용)
├── privkey.pem     # 개인키 (절대 외부에 노출 금지)
├── cert.pem        # 서버 인증서만
└── chain.pem       # 중간 인증서만

 

CN과 SAN
  • CN(Common Name): 인증서의 주 도메인
  • SAN(Subject Alternative Name): 인증서가 커버하는 모든 도메인 목록

현대 브라우저는 CN을 무시하고 SAN만 검사한다. 인증서의 SAN에 접속하려는 도메인이 없으면 인증서 오류가 발생한다.

# 인증서 CN 확인
openssl x509 -noout -subject -in /etc/letsencrypt/live/eat-ssu.tech/fullchain.pem
# subject=CN = eat-ssu.tech

# 인증서 만료일 확인
openssl x509 -noout -dates -in /etc/letsencrypt/live/eat-ssu.tech/fullchain.pem
# notBefore=Apr 24 09:18:28 2026 GMT
# notAfter=Jul 23 09:18:27 2026 GMT

# 실제 서버가 내려주는 인증서 확인
echo | openssl s_client -connect eat-ssu.tech:443 -servername eat-ssu.tech 2>/dev/null \
| openssl x509 -noout -subject
# subject=CN = eat-ssu.tech

# SAN 확인
echo | openssl s_client -connect eat-ssu.tech:443 -servername eat-ssu.tech 2>/dev/null \
| openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
# DNS:eat-ssu.tech
certbot 사용법
# nginx 플러그인으로 인증서 발급 (nginx 설정 자동 수정)
sudo certbot --nginx -d eat-ssu.tech

# 인증서만 발급 (nginx 설정 수동)
sudo certbot certonly --nginx -d eat-ssu.tech

# 여러 도메인 한 번에 발급
sudo certbot certonly --nginx -d eat-ssu.tech -d www.eat-ssu.tech

# 인증서 갱신 (Let's Encrypt 인증서는 90일 유효)
sudo certbot renew

# 갱신 테스트 (실제 갱신 X)
sudo certbot renew --dry-run

# 발급된 인증서 목록 확인
sudo certbot certificates

주의: certbot 발급 시 해당 도메인이 반드시 이 서버 IP를 가리키고 있어야 한다. Let's Encrypt가 인증 과정에서 도메인으로 직접 접속해 검증하기 때문이다.

✏️ 3. Nginx 기본 개념

Nginx란?

Nginx는 웹 서버이자 리버스 프록시다.

  • 웹 서버: 정적 파일(HTML, CSS, 이미지 등) 직접 서빙
  • 리버스 프록시: 클라이언트 요청을 받아 내부 서버(Spring Boot 등)로 전달
왜 Nginx를 앞에 두는가
클라이언트 → Nginx(80/443) → Spring Boot(9000)

 

Spring Boot만 단독으로 운영하면 안 되는가? 이론상 가능하지만 다음 이유로 Nginx를 앞에 둔다.

  • SSL 처리: HTTPS 인증서 관리를 Nginx가 담당, Spring은 HTTP만 처리
  • 포트 관리: 80/443 포트 바인딩 (1024 이하 포트는 root 권한 필요)
  • 정적 파일 서빙: 이미지, CSS 등 Nginx가 직접 처리해서 Spring 부하 감소
  • 로드 밸런싱: 여러 Spring 인스턴스로 요청 분산
  • 보안: Spring 포트(9000) 직접 노출 방지
Nginx 설정 파일 위치
/etc/nginx/
├── nginx.conf                  # 메인 설정 (worker 수, 로그 경로 등)
├── sites-available/            # 가상 호스트 설정 파일 보관
│   └── default                 # 실제 설정 파일
├── sites-enabled/              # 활성화된 설정 심볼릭 링크
│   └── default -> ../sites-available/default
└── conf.d/                     # 추가 설정 파일

 

sites-available에 설정을 작성하고 sites-enabled에 심볼릭 링크를 걸어 활성화한다.

# 심볼릭 링크 생성 (활성화)
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

# 심볼릭 링크 제거 (비활성화)
sudo rm /etc/nginx/sites-enabled/myapp

✏️ 4. Nginx 설정 파일 

server 블록

하나의 설정 파일에 여러 server 블록을 둘 수 있다. 각 블록은 listen 포트와 server_name의 조합으로 구분되며 default_server는 매칭되는 server 블록이 없을 때 기본으로 처리하는 블록이다.

# 443 포트 + eat-ssu.tech 도메인 처리
server {
    listen 443 ssl;
    server_name eat-ssu.tech;
    ...
}

# 443 포트 + dev.eat-ssu.tech 도메인 처리
server {
    listen 443 ssl;
    server_name dev.eat-ssu.tech;
    ...
}

# 80 포트 + 모든 도메인 처리
server {
    listen 80 default_server;
    server_name _;   # _는 모든 도메인을 의미
    ...
}

listen 지시어

listen 80;                  # IPv4, 80포트
listen [::]:80;             # IPv6, 80포트
listen 80 default_server;   # 기본 서버로 지정
listen 443 ssl;             # SSL 활성화
listen [::]:443 ssl ipv6only=on;  # IPv6 전용

location 블록

요청 경로에 따라 다르게 처리할 수 있다.

server {
    server_name eat-ssu.tech;

    # 모든 요청 → Spring Boot로 전달
    location / {
        proxy_pass http://localhost:9000;
    }

    # /static/ 경로 → 파일 직접 서빙
    location /static/ {
        root /var/www/html;
        expires 30d;   # 브라우저 캐시 30일
    }

    # 정확히 /health 경로만
    location = /health {
        return 200 "ok";
        add_header Content-Type text/plain;
    }

    # /api/ 로 시작하는 경로
    location /api/ {
        proxy_pass http://localhost:9000;
    }
}

location 매칭 우선순위

= (정확히 일치)     최우선
^~ (앞부분 일치)    두 번째
~ (정규식, 대소문자 구분)
~* (정규식, 대소문자 무시)
/ (일반 매칭)       최후순위

proxy_pass와 proxy_set_header

location / {
    # Spring Boot(9000포트)로 요청 전달
    proxy_pass http://localhost:9000;

    # HTTP/1.1 사용 (Keep-Alive 지원, WebSocket 등)
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    # 원본 Host 헤더 전달
    # 없으면 Spring이 localhost를 Host로 인식
    proxy_set_header Host $host;

    # 클라이언트 실제 IP 전달
    # Spring에서 request.getRemoteAddr() 시 실제 IP 반환
    proxy_set_header X-Real-IP $remote_addr;

    # 프록시 체인 IP 목록
    # 여러 프록시를 거친 경우 전체 IP 체인
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # 원본 프로토콜 전달 (http 또는 https)
    # Spring Security에서 HTTPS 여부 판단에 사용
    proxy_set_header X-Forwarded-Proto $scheme;
}

spring boot에서의 헤더 활용

이 설정을 하면 Spring이 X-Forwarded-* 헤더를 읽어 실제 클라이언트 정보를 사용하여 서버 에러 알림 등에 사용 가능하다

# application.yml
server:
  forward-headers-strategy: framework

SSL 설정

server {
    listen 443 ssl;
    server_name eat-ssu.tech;

    # 인증서 경로
    ssl_certificate /etc/letsencrypt/live/eat-ssu.tech/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/eat-ssu.tech/privkey.pem;

    # certbot이 생성한 보안 설정
    # TLS 버전(1.2, 1.3만 허용), 암호화 방식 등
    include /etc/letsencrypt/options-ssl-nginx.conf;

    # DH 파라미터 (키 교환 보안 강화)
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    ...
}

HTTP -> HTTPS 리다이렉트

server {
    listen 80;
    listen [::]:80;
    server_name eat-ssu.tech;

    # 301: 영구 리다이렉트
    # $host: 요청한 도메인 유지
    # $request_uri: 경로 + 쿼리스트링 유지
    return 301 https://$host$request_uri;
}

✏️ 5. 완성된 nginx default 파일 예시

dev/prod 두 개의 서버를 운영할 때 설정 예시이다.

# =============================================
# 기본 HTTP 서버 (매칭 안 되는 요청 처리)
# =============================================
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;

    location / {
        return 444;  # 연결 끊기 (보안)
    }
}

# =============================================
# prod 서버: eat-ssu.tech
# =============================================

# HTTP → HTTPS 리다이렉트
server {
    listen 80;
    listen [::]:80;
    server_name eat-ssu.tech;
    return 301 https://$host$request_uri;
}

# HTTPS 메인 서버
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name eat-ssu.tech;

    # SSL 인증서
    ssl_certificate /etc/letsencrypt/live/eat-ssu.tech/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/eat-ssu.tech/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Spring Boot로 프록시
    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# =============================================
# dev 서버: dev.eat-ssu.tech
# =============================================

# HTTP → HTTPS 리다이렉트
server {
    listen 80;
    listen [::]:80;
    server_name dev.eat-ssu.tech;

    if ($host = dev.eat-ssu.tech) {
        return 301 https://$host$request_uri;
    }
    return 404;
}

# HTTPS dev 서버
server {
    listen 443 ssl;
    listen [::]:443 ssl ipv6only=on;
    server_name dev.eat-ssu.tech;

    # dev 인증서
    ssl_certificate /etc/letsencrypt/live/dev.eat-ssu.tech/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/dev.eat-ssu.tech/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://localhost:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

✏️ 6. 클라이언트 요청 전체 흐름

앱에서 GET https://eat-ssu.tech/meals 요청을 보낼 때 전체 흐름이다.

1. DNS 조회
   앱 → OS DNS 캐시 확인
       → ISP DNS 서버 (캐시 없으면 권한 있는 DNS 서버까지 조회)
       → eat-ssu.tech = 43.200.49.228 반환
       → TTL 동안 캐싱

2. TCP 연결
   앱 → 43.200.49.228:443 TCP SYN
   서버 → SYN-ACK
   앱 → ACK (3-way handshake 완료)

3. SSL 핸드셰이크
   앱 → ClientHello (지원 암호화 방식)
   서버 → ServerHello + eat-ssu.tech 인증서
   앱 → 인증서 검증 (CA 서명, 도메인, 만료일)
   앱 → 대칭키 생성 후 공개키로 암호화해서 전송
   이후 대칭키로 암호화 통신

4. HTTP 요청 (암호화됨)
   앱 → GET /meals HTTP/1.1
        Host: eat-ssu.tech
        Authorization: Bearer {token}
        ...

5. Nginx 처리
   443 포트 수신
   → server_name eat-ssu.tech 매칭
   → SSL 복호화
   → location / 매칭
   → proxy_pass http://127.0.0.1:9000/meals

6. Spring Boot 처리
   ControllerLogAspect:
   REQUEST GET /meals args=userId=4748, ...

   JwtAuthenticationFilter:
   → Authorization 헤더에서 토큰 추출
   → 토큰 검증
   → SecurityContext에 인증 정보 저장

   MealsController:
   → MealsService 호출
   → JPA로 DB 조회
   → 응답 반환

   ControllerLogAspect:
   RESPONSE GET /meals (14ms) result={...}

7. 응답 반환
   Spring → Nginx → 암호화 → 앱

✏️ 7. 주의할 점들

 DNS 관련

  • 서버 이전 전 TTL을 반드시 낮추기 => 작업 전날 TTL을 300으로 낮추고 기존 TTL이 만료될 때까지 기다려야 한다. 이미 캐싱된 TTL이 만료되어야 새 TTL이 적용되기 때문이다.
  • ISP마다 TTL 준수 여부가 다르다 =>  일부 ISP는 TTL을 무시하고 자체적으로 더 길게 캐싱하는 경우가 있다. 특히 SKT가 이런 경향이 있다. DNS 변경 후 전파가 늦어지면 dnschecker.org에서 어느 ISP/지역이 문제인지 확인할 수 있다.
  • DNS 변경은 되돌리기 어렵다 =>  변경 즉시 전 세계로 전파가 시작되기 때문에 실수하면 되돌리더라도 TTL만큼 시간이 걸린다.

SSL  관련

  • 인증서 도메인과 접속 도메인이 반드시 일치해야 한다 => dev.eat-ssu.tech 인증서를 eat-ssu.tech 도메인에 사용하면 SSL 오류가 발생한다. 앱에서는 연결 자체를 거부한다.
  • 개인키(privkey.pem)는 절대 외부에 노출하면 안 된다 => 개인키가 유출되면 서버를 사칭할 수 있다. git에 올리거나 로그에 남기면 절대 안 된다.
  • certbot 발급 시 해당 도메인이 이 서버를 가리켜야 한다 => DNS가 다른 서버를 가리키고 있으면 Let's Encrypt 인증에 실패한다.
  • Let's Encrypt 인증서는 90일마다 갱신해야 한다.
# crontab에 자동 갱신 등록
sudo crontab -e
0 12 * * * /usr/bin/certbot renew --quiet

 

Nginx  관련

설정 변경 후 반드시 문법 검사 하기 => 문법 오류가 있으면 reload 시 nginx가 다운될 가능성

sudo nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

 

proxy_pass에서 HTTP/HTTPS 구분을 정확히 하기 => HTTP로 HTTPS 포트(443)에 요청하면 400 Bad Request가 발생

proxy_pass http://localhost:9000;   # 내부 통신 (HTTP)
proxy_pass https://43.200.49.228;   # 외부 서버로 포워딩 (HTTPS)

 

리다이렉트 루프 주의하기

# 잘못된 설정 예시 - 무한 루프 발생
server {
    listen 80;
    server_name eat-ssu.tech;
    return 301 https://eat-ssu.tech$request_uri;
}

server {
    listen 443 ssl;
    server_name eat-ssu.tech;
    location / {
        # prod가 다시 http로 리다이렉트하면 루프
        proxy_pass http://43.200.49.228:80;
    }
}

try_files와 proxy_pass는 함께 쓸 수 없다

# 잘못된 설정 - proxy_pass가 무시됨
location / {
    try_files $uri $uri/ =404;
    proxy_pass http://localhost:9000;  # 동작 안 함
}

# 올바른 설정
location / {
    proxy_pass http://localhost:9000;
}

✏️ 8. 자주 쓰는 명령어 모음

# ==================
# Nginx 명령어
# ==================

# 설정 문법 검사 (변경 후 항상 먼저 실행)
sudo nginx -t

# 설정 재로드 (무중단, 권장)
sudo nginx -s reload

# nginx 재시작 (중단 후 시작)
sudo systemctl restart nginx

# nginx 상태 확인
sudo systemctl status nginx

# nginx 시작/중지
sudo systemctl start nginx
sudo systemctl stop nginx

# 접속 로그 실시간 확인
sudo tail -f /var/log/nginx/access.log

# 에러 로그 실시간 확인
sudo tail -f /var/log/nginx/error.log

# ==================
# SSL/certbot 명령어
# ==================

# 인증서 발급
sudo certbot certonly --nginx -d eat-ssu.tech

# 발급된 인증서 목록 확인
sudo certbot certificates

# 인증서 갱신
sudo certbot renew

# 갱신 테스트
sudo certbot renew --dry-run

# 인증서 정보 확인
openssl x509 -noout -subject -dates -in /etc/letsencrypt/live/eat-ssu.tech/fullchain.pem

# 서버 인증서 확인
echo | openssl s_client -connect eat-ssu.tech:443 -servername eat-ssu.tech 2>/dev/null \
| openssl x509 -noout -subject

# ==================
# DNS 명령어
# ==================

# 기본 조회
dig eat-ssu.tech +short

# 특정 DNS 서버로 조회
dig @8.8.8.8 eat-ssu.tech +short

# TTL 포함 조회
dig @8.8.8.8 eat-ssu.tech

# SSL 연결 확인
curl -v https://eat-ssu.tech 2>&1 | grep -E "subject|SSL|issuer"
 
 
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함