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만 쓰는 게 있다면 사이트 전체가 차단됩니다.
도입 절차:
max-age=600(10분)로 시작 → 며칠 운영max-age=86400(1일) → 1주 관찰max-age=31536000(1년)으로 강화- (선택) 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. 검증
설정을 적용한 뒤 다음 도구로 확인합니다.
- https://securityheaders.com — A 등급이 목표
- https://www.ssllabs.com/ssltest/ — A+ 등급이 목표
- https://csp-evaluator.withgoogle.com — CSP 정책 약점 검증
# 명령행에서 헤더만 확인
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으로 한국어 문의 가능합니다.