https://www.ssllabs.com/ssltest/에서 자기 사이트를 테스트해 보면 SSL 자체는 A이지만 헤더 설정 부족으로 등급이 깎이는 경우가 흔합니다. 헤더는 응답 한 줄을 추가하는 것이 전부인데, XSS, 클릭재킹, MITM 다운그레이드 공격을 한 번에 차단해줍니다.


한눈에 보는 핵심 헤더

헤더 차단하는 공격 우선순위
Strict-Transport-Security HTTP 다운그레이드 / 중간자 필수
Content-Security-Policy XSS / 데이터 인젝션 필수
X-Content-Type-Options MIME 스니핑 필수
X-Frame-Options 클릭재킹 필수
Referrer-Policy 레퍼러 정보 누출 권장
Permissions-Policy 브라우저 기능 무단 사용 권장

가장 먼저 추가해야 하는 것은 위 4개의 “필수” 헤더입니다.


1. HSTS (Strict-Transport-Security)

브라우저가 이 도메인은 절대 HTTP로 접근하지 말라고 기억하게 만듭니다. SSL Strip 같은 다운그레이드 공격을 무력화합니다.

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
  • max-age=31536000 — 1년간 유지
  • includeSubDomains — 모든 서브도메인에 동일 적용
  • preload — 브라우저 빌트인 목록(HSTS Preload)에 등재 신청용

주의: HSTS는 신중히 적용해야 합니다. 한 번 적용되면 만료 전까지 HTTP로 절대 접근할 수 없습니다. 인증서 갱신 실패나 서브도메인 중 HTTP만 쓰는 게 있다면 사이트 전체가 차단됩니다.

도입 절차:

  1. max-age=600 (10분)로 시작 → 며칠 운영
  2. max-age=86400 (1일) → 1주 관찰
  3. max-age=31536000 (1년)으로 강화
  4. (선택) hstspreload.org에 등록

2. Content-Security-Policy (CSP)

페이지에서 로드 가능한 리소스의 출처를 제한합니다. XSS 공격이 성공해도 외부로 데이터를 빼돌리거나 외부 스크립트를 실행하기 어렵게 만듭니다.

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
디렉티브 의미
default-src 'self' 기본은 같은 출처만 허용
script-src 스크립트 출처 제한
style-src 스타일시트 출처
img-src 이미지 출처
connect-src XHR, fetch, WebSocket
frame-ancestors 'none' iframe 임베딩 차단 (X-Frame-Options 상위 호환)
form-action 'self' 폼 전송 대상 제한

CSP 도입 절차

기존 사이트에 갑자기 적용하면 정상 리소스까지 차단됩니다. 다음 순서로 도입하세요.

1단계 — Report-Only 모드 (차단 없이 위반 사항만 수집)

add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;

서버에서 /csp-report 엔드포인트를 만들어 위반 보고서를 수집·분석합니다.

2단계 — 위반 사항을 모두 정상화한 뒤 Report-Only를 떼고 일반 CSP로 전환합니다.

3단계unsafe-inline, unsafe-eval을 제거할 수 있도록 인라인 스크립트를 nonce/hash 방식으로 바꿉니다.

<script nonce="random123">/* 인라인 스크립트 */</script>
add_header Content-Security-Policy "script-src 'self' 'nonce-random123'" always;

3. X-Content-Type-Options

브라우저가 응답의 Content-Type을 멋대로 추측(MIME sniffing)하지 못하게 합니다.

add_header X-Content-Type-Options "nosniff" always;

이 한 줄만 추가하면 끝입니다. 부작용 없이 무조건 추가하세요.


4. X-Frame-Options

다른 사이트가 우리 페이지를 iframe으로 감싸지 못하게 합니다(클릭재킹 방어).

add_header X-Frame-Options "SAMEORIGIN" always;
의미
DENY 모든 iframe 거부
SAMEORIGIN 같은 출처만 허용
ALLOW-FROM <uri> 특정 사이트만 (현재는 비표준, CSP frame-ancestors 사용 권장)

CSP frame-ancestors를 이미 설정했다면 X-Frame-Options는 옛 브라우저 호환용으로만 의미가 있습니다.


5. Referrer-Policy

다른 사이트로 이동할 때 어떤 레퍼러 정보를 노출할지 제어합니다.

add_header Referrer-Policy "strict-origin-when-cross-origin" always;
동작
no-referrer 어떤 경우에도 레퍼러 안 보냄
same-origin 같은 출처만
strict-origin-when-cross-origin 권장 — 같은 출처는 전체, 외부는 도메인만, HTTPS→HTTP 다운그레이드 시 안 보냄
unsafe-url 항상 전체 URL (비권장)

특별한 이유가 없다면 strict-origin-when-cross-origin이 정답입니다.


6. Permissions-Policy

페이지가 카메라, 마이크, 위치, 결제 API 등을 사용할 권한을 명시합니다.

add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(self)" always;
  • () 빈 괄호 — 모든 출처 차단
  • (self) — 같은 출처만 허용
  • (self "https://trusted.com") — 특정 출처도 허용

XSS가 성공해도 카메라·마이크 자동 켜기, 결제 API 무단 호출 등을 차단할 수 있습니다.


7. 한 번에 적용하는 Nginx 설정

/etc/nginx/conf.d/security-headers.conf로 분리해 모든 사이트에 include하면 관리가 편합니다.

# /etc/nginx/conf.d/security-headers.conf
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

# CSP는 사이트마다 다르므로 server 블록에서 개별 적용

서버 블록에서 사이트별로 CSP 추가:

server {
    listen 443 ssl http2;
    server_name example.com;

    include /etc/nginx/conf.d/security-headers.conf;

    add_header Content-Security-Policy "default-src 'self'; ..." always;
}

8. 검증

설정을 적용한 뒤 다음 도구로 확인합니다.

# 명령행에서 헤더만 확인
curl -I https://example.com

흔한 실수

always를 빼먹는다

  • Nginx의 add_header는 기본적으로 2xx, 3xx 응답에만 적용됩니다. 404, 500 같은 에러 페이지에도 헤더가 붙으려면 always가 필수입니다.

add_header가 server 블록과 location 블록에 섞여 있을 때

  • location 블록에 add_header가 하나라도 있으면 server 블록의 add_header덮어쓰여 사라집니다(상속 안 됨). location에서도 모든 헤더를 다시 명시하거나, location에서는 add_header를 쓰지 않는 것이 안전합니다.

CSP의 unsafe-inline을 영원히 두는 경우

  • 처음에는 어쩔 수 없지만, nonce/hash로 옮기면 XSS 방어 효과가 비교할 수 없이 커집니다. 우선순위를 두고 단계적으로 제거하세요.

HSTS를 처음부터 1년으로 적용

  • 인증서 갱신 실패 시 사이트가 1년간 접근 불가가 됩니다. 짧게 시작 → 점진적으로 늘리세요.

마무리

보안 헤더는 추가 비용도, 성능 저하도 거의 없는 보안 강화 수단입니다. SSL 인증서를 발급한 다음 단계로 반드시 적용해야 합니다.

TCP-80.NET의 VPS / 전용서버에서 Nginx 보안 설정 중 막히는 부분이 있으면 @tcp80net으로 한국어 문의 가능합니다.