DNS RobotDNS Propagation Checker
홈DNS 조회WHOISIP 조회SSL
DNS RobotDNS Propagation Checker

차세대 DNS 검사 도구

개인정보 보호정책이용약관소개블로그문의

DNS 도구

DNS 조회DNS 속도 테스트도메인에서 IP로NS 조회MX 조회모두 보기

이메일 도구

SPF 레코드 확인DMARC 확인DKIM 확인SMTP 테스트 도구이메일 헤더 분석모두 보기

웹사이트 도구

WHOIS 조회호스팅 확인 도구도메인 가용성 확인서브도메인 검색CMS 감지기모두 보기

네트워크 도구

Ping 도구트레이스라우트포트 확인HTTP 헤더 확인SSL 인증서 확인모두 보기

IP 도구

IP 조회내 IP 주소 확인IP 블랙리스트 확인IP에서 호스트명으로ASN 조회모두 보기

유틸리티 도구

QR 코드 스캐너QR 코드 생성기UPI QR Code GeneratorWiFi QR Code Generator모스 부호 변환기모두 보기
© 2026 DNS Robot. 개발: ❤ Shaik Brothers
모든 시스템 정상 운영 중
Made with
홈/블로그/웹 보안 모범 사례: 6개 계층별 실전 가이드 (2026)

웹 보안 모범 사례: 6개 계층별 실전 가이드 (2026)

Shaik Vahid2026년 9월 29일30 분 소요
웹 보안 모범 사례를 도메인·DNS, TLS, HTTP 헤더, 애플리케이션, 의존성, 서버의 6개 계층과 계층별 검증 방법으로 정리한 다이어그램
웹 보안 모범 사례를 도메인·DNS, TLS, HTTP 헤더, 애플리케이션, 의존성, 서버의 6개 계층과 계층별 검증 방법으로 정리한 다이어그램

핵심 요약

웹 보안은 여러 계층을 겹겹이 쌓을 때 비로소 제대로 작동합니다. 도메인과 DNS를 잠그고(레지스트라 락, DNSSEC, CAA), HSTS와 함께 최신 TLS를 강제하고, 엄격한 HTTP 보안 헤더를 보내고, 비밀번호는 Argon2id로 해싱한 뒤 MFA와 속도 제한으로 보호하고, 입력 검증과 접근 제어는 서버에서 강제하고, 의존성은 버전을 고정해 감사하고, 서비스하지 않는 포트는 모두 닫고, 침해를 알아챌 수 있을 만큼 로그를 남기세요. 아래의 모든 항목에는 정확한 설정과 실제로 적용되었는지 확인하는 무료 방법이 함께 있습니다. 테스트하지 않은 보안 통제는 없는 것과 마찬가지이기 때문입니다.

Advertisement

웹 보안 모범 사례란 무엇인가요?

웹 보안 모범 사례는 웹사이트나 웹 애플리케이션이 탈취되거나, 변조(디페이스)되거나, 방문자를 공격하는 데 악용되거나, 데이터가 조용히 빠져나가는 일을 막는 보안 통제의 집합입니다. 제품 하나나 설정 하나로 끝나는 문제가 아닙니다. 도메인 레지스트라에서 시작해 DNS, TLS, 서버가 보내는 HTTP 헤더, 로그인과 입력을 처리하는 코드, 기반으로 삼는 서드파티 패키지, 서버 자체를 거쳐, 마지막으로 문제가 생겼을 때 알려 주는 로그까지 이어지는 일련의 결정입니다.

계층별로 생각해야 하는 이유는 공격자가 그렇게 생각하기 때문입니다. Verizon의 2026 데이터 침해 조사 보고서(DBIR)에 따르면 이제 침해 사고의 31%가 소프트웨어 취약점 악용에서 시작되며, 처음으로 탈취된 크리덴셜을 제치고 가장 흔한 침투 경로가 되었습니다. 또한 전체 침해 사고의 48%에 랜섬웨어가 관련되어 있습니다. 패치 하나를 빠뜨리거나, API 키 하나가 유출되거나, 속도 제한이 없는 관리자 페이지 하나만 있어도 충분합니다. 이 가이드에 나오는 보안 통제 중 특별히 까다로운 것은 없습니다. 침해당하는 사이트는 대개 한 계층을 다듬느라 다른 계층의 기본을 건너뛴 곳입니다.

이 가이드는 바깥쪽에서 안쪽 순서로 구성되어 있습니다. 각 항목은 왜 중요한지, 정확히 무엇을 설정해야 하는지, 명령어나 무료 도구로 어떻게 검증하는지를 설명합니다. HTTP 헤더 확인과 SSL 인증서 확인 도구에서 가장 흔히 발견되는 실패는 잘못된 결정이 아니라, 누군가 켜져 있다고 믿고 한 번도 확인하지 않은 설정이기 때문입니다.

참고

딱 한 시간만 낼 수 있다면: 레지스트라에서 레지스트라 락과 MFA를 켜고, HSTS로 HTTPS를 강제하고, 3계층 표에 나온 보안 헤더를 추가하고, Argon2id로 비밀번호를 해싱하고, 알려진 CVE가 있는 의존성을 모두 업데이트하고, 80번과 443번을 제외한 모든 포트를 닫으세요. 이것만으로도 실제 웹 침해 사고 대부분의 침투 경로를 막을 수 있습니다.

웹 보안 스택: 보호해야 할 6개 계층

웹사이트에 대한 모든 공격은 6개 계층 중 하나를 노립니다. 아래 표는 이 가이드 전체의 지도입니다. 각 계층에 무엇이 있는지, 보통 어떻게 공격받는지, 방어가 실제로 적용되었는지 어떤 무료 검사로 확인하는지를 정리했습니다.

계층공격자가 노리는 것핵심 모범 사례검증 방법
1. 도메인과 DNS도메인 탈취, 네임서버 변경, 방치된 서브도메인 탈취, 부정 인증서 발급레지스트라 락, MFA, DNSSEC, CAA, 오래된 레코드 점검WHOIS 조회, DNS 조회, 서브도메인 검색
2. 전송 계층(TLS)HTTP로 다운그레이드, 암호화 제거, 오래된 암호 스위트 악용, 만료된 인증서TLS 1.2 이상, preload를 적용한 HSTS, 자동 갱신SSL 인증서 확인
3. HTTP 헤더스크립트 삽입(XSS), 클릭재킹용 프레임 삽입, 콘텐츠 유형 스니핑, 리퍼러 유출CSP, frame-ancestors, nosniff, Referrer-Policy, Permissions-PolicyHTTP 헤더 확인
4. 애플리케이션크리덴셜 스터핑, 인젝션, 접근 제어 취약점, CSRF, 안전하지 않은 파일 업로드Argon2id + MFA + 속도 제한, 매개변수화된 쿼리, 서버 측 권한 검사, SameSite 쿠키코드 리뷰, OWASP ZAP, 비밀번호 강도 테스트
5. 의존성오염된 패키지, 라이브러리의 알려진 CVE, 침해된 CI 파이프라인락파일, 감사, 버전 고정, SBOM, 최소 권한 토큰npm audit, Dependabot, Trivy
6. 서버와 운영열린 포트, 기본 크리덴셜, 누락된 패치, 백업 부재, 로그 부재방화벽, 키 전용 SSH, 패치, 3-2-1 백업, 중앙 로깅포트 확인, IP 블랙리스트 확인, 도메인 상태 검사기

팁

위에서 아래 순서로 진행하세요. 1~3계층은 오후 한나절이면 끝낼 수 있는 설정 작업이며, 모든 페이지를 한 번에 보호합니다. 4~6계층은 코드 리뷰와 배포 프로세스에 녹아 있어야 하는 지속적인 습관입니다.

Advertisement

1계층: 도메인과 DNS 잠그기

이 가이드의 나머지 내용은 모두 여러분이 여전히 도메인을 통제하고 있다는 전제에서 출발합니다. 공격자가 레지스트라에 로그인하거나 네임서버를 바꿀 수 있다면 트래픽을 원하는 곳으로 보내고, 여러분의 도메인 이름으로 유효한 인증서를 발급받고, 여러분에게 오는 모든 이메일을 읽을 수 있습니다. 그동안 여러분의 애플리케이션 코드는 단 한 줄도 실행되지 않습니다. 그래서 도메인과 DNS 보안이 첫 번째 웹 보안 모범 사례이며, 그 내용은 거의 전부 설정 작업입니다.

먼저 자신의 도메인으로 WHOIS 조회를 실행해 세 가지를 확인하세요. 상태 코드에 clientTransferProhibited가 포함되어 있는지, 만료일이 1년 이상 남았는지, 레지스트라가 실제로 여러분이 계정을 가진 곳인지입니다. 회사 도메인이 예전 외주 업체의 레지스트라 계정에 그대로 남아 있는 경우가 의외로 흔합니다. WHOIS 조회 가이드에서 조회 결과에 나오는 모든 상태 코드를 설명합니다.

레지스트라 락, MFA, 만료 알림

레지스트라 락(전송 잠금이라고도 함)을 활성화해 여러분이 명시적으로 잠금을 해제하지 않는 한 도메인을 다른 레지스트라로 옮길 수 없게 하고, 레지스트라 계정 자체에도 다단계 인증(MFA)을 켜세요. 로그인 하나가 그 아래의 모든 것을 통제하기 때문에 레지스트라 계정은 피싱 공격자가 가장 좋아하는 표적입니다. 레지스트라가 지원한다면 SMS 대신 하드웨어 키나 인증 앱을 사용하세요.

만료되지 않을 결제 수단으로 도메인 자동 갱신을 설정하고, 그래도 만료일 60일 전에 캘린더 알림을 따로 추가해 두세요. 만료된 도메인은 몇 시간 안에 드롭캐처(drop-catcher)에게 넘어가며, 새 소유자는 여러분의 이메일, 트래픽, 그리고 해당 도메인을 신뢰하는 모든 OAuth 로그인을 그대로 물려받습니다.

  • 레지스트리 락(레지스트리 수준에서 별도 채널을 통해 수동으로 처리하는 잠금)은 비즈니스가 의존하는 도메인이라면 추가 비용을 들일 가치가 있습니다. 레지스트라 계정이 탈취되더라도 네임서버를 바꿀 수 없게 막아 줍니다.

  • 역할을 분리하세요. 도메인 비용을 내는 사람, 도메인을 소유한 계정, DNS 제공업체를 각각 문서화하고, 특정 직원 한 명의 메일함에 묶이지 않게 하세요.

  • WHOIS 개인정보 보호를 켜서 연락처 정보가 피싱 공격의 지도가 되지 않게 하고, 계정에는 domains@ 같은 모니터링되는 역할 주소를 등록해 두세요.

DNSSEC로 영역 서명하기

DNSSEC는 DNS 영역(zone)에 서명해 리졸버가 위조된 응답을 탐지할 수 있게 합니다. DNSSEC가 없으면 리졸버의 캐시를 오염시킬 수 있는 공격자가 방문자를 가짜 서버로 보내도, 브라우저에는 여전히 여러분의 도메인 이름이 표시됩니다. 대부분의 관리형 DNS 제공업체(Cloudflare, Route 53, Google Cloud DNS, 그리고 많은 레지스트라)는 스위치 하나로 DNSSEC를 켤 수 있으며, 수동으로 해야 할 단계는 레지스트라에 DS 레코드를 게시하는 것뿐입니다. dig 명령이나 도메인 상태 검사기의 DNSSEC 검사로 확인하세요:

bash
dig +dnssec +short yourdomain.com A
# A signed zone returns the record AND an RRSIG line:
# 203.0.113.10
# A 13 2 3600 20261015000000 20260924000000 34505 yourdomain.com. Kx3f...==

dig +short yourdomain.com DS
# A DS record at the parent zone proves the chain of trust is complete
# 34505 13 2 6A1B...C9

CAA로 인증서 발급 제한하기

CAA 레코드(RFC 8659)는 어떤 인증 기관(CA)이 여러분의 도메인에 인증서를 발급할 수 있는지 지정합니다. 모든 공개 CA는 발급 전에 이 레코드를 반드시 확인해야 하므로, 다른 CA의 도메인 검증을 속여 넘긴 공격자도 CAA 레코드로 막을 수 있습니다. 일반적인 경우는 레코드 세 개로 충분합니다. 누가 발급할 수 있는지, 누가 와일드카드를 발급할 수 있는지, 거부된 요청을 어디로 보고할지입니다:

dns
yourdomain.com.  CAA 0 issue "letsencrypt.org"
yourdomain.com.  CAA 0 issuewild ";"
yourdomain.com.  CAA 0 iodef "mailto:security@yourdomain.com"

경고

CAA를 추가하기 전에 현재 여러분의 인증서를 발급하는 CA를 모두 목록으로 만드세요. CDN이나 호스팅 패널 뒤에서 쓰이는 CA도 포함해야 합니다(Cloudflare는 여러 CA를 번갈아 사용하며, 많은 호스팅 업체가 Sectigo나 Let's Encrypt를 씁니다). 실제 CA를 빠뜨린 CAA 레코드는 다음 갱신을 아무 경고 없이 실패하게 만듭니다. 먼저 SSL 인증서 확인으로 현재 발급자를 확인하세요.

오래된 레코드 점검: 서브도메인 탈취

서브도메인 탈취(subdomain takeover)는 DNS 레코드가 더 이상 쓰지 않는 서비스를 여전히 가리키고 있을 때 일어납니다. 삭제된 GitHub Pages 사이트, Azure 앱, S3 버킷, SaaS 업체의 호스트 이름을 가리키는 CNAME 같은 경우입니다. 누구든 버려진 대상을 등록하면 old-app.yourdomain.com에서 여러분의 이름으로, 그것도 유효한 인증서까지 갖춘 채 콘텐츠를 제공할 수 있습니다. 아무도 그 레코드가 있다는 사실을 기억하지 못하기 때문에 버그 바운티 프로그램에서 가장 흔한 발견 사항 중 하나입니다.

인증서 투명성(CT) 로그를 읽는 서브도메인 검색으로 도메인을 검사하면 지금까지 인증서가 발급된 모든 호스트 이름을 볼 수 있습니다. 그런 다음 DNS 조회로 하나씩 확인하고, 대상이 NXDOMAIN을 반환하거나 제공업체의 "no such app" 페이지를 보여 주는 레코드는 삭제하세요. 이 점검을 분기마다 반복하고, 서비스를 폐기할 때는 DNS 레코드 삭제를 절차에 포함하세요.

팁

인증서 투명성은 조기 경보 시스템 역할도 합니다. Cert Spotter와 Cloudflare의 CT 모니터링은 어떤 CA든 여러분의 도메인에 인증서를 발급할 때마다 이메일로 알려 줄 수 있습니다. 예상치 못한 인증서는 DNS나 레지스트라 침해를 보여 주는 첫 번째 신호인 경우가 많습니다.

2계층: 제대로 구성한 HTTPS와 TLS

HTTPS는 더 이상 모범 사례가 아니라 기본입니다. 브라우저는 일반 HTTP 페이지를 "주의 요함"으로 표시합니다. 안전한 사이트와 단지 암호화만 된 사이트를 가르는 기준은 허용하는 TLS 버전, HTTP로 접속할 여지가 조금이라도 남아 있는지, 그리고 2026년 3월에 시작된 인증서 유효 기간 단축을 견딜 만큼 갱신이 자동화되어 있는지입니다.

Advertisement

TLS 1.2는 최소, TLS 1.3은 권장

TLS 1.0과 1.1은 2021년 RFC 8996으로 공식 폐기되었으며, 현재 어떤 브라우저도 이 버전으로 협상하지 않습니다. 서버에서 이 버전들을 비활성화하고, TLS 1.3(알려진 취약한 암호 스위트를 모두 제거하고 한 번의 왕복으로 핸드셰이크를 완료)을 우선 사용하며, TLS 1.2는 ECDHE-ECDSA-AES128-GCM-SHA256이나 ECDHE-RSA-CHACHA20-POLY1305 같은 AEAD 암호 스위트와 함께만 유지하세요.

외부에서 테스트할 때 중요한 줄은 두 가지입니다. 협상된 프로토콜, 그리고 전체 인증서 체인이 검증되었다는 뜻인 Verify return code: 0입니다. 중간 인증서 누락은 "Chrome에서는 되는데 curl과 Android에서는 실패한다"는 문의의 가장 흔한 원인입니다. SSL 인증서 체인 가이드에서 해결 방법을 확인할 수 있고, SSL 인증서 확인 도구는 불완전한 체인을 바로 표시합니다. 다음은 dnsrobot.net을 대상으로 실행한 검사 결과입니다:

bash
echo | openssl s_client -connect dnsrobot.net:443 -servername dnsrobot.net 2>/dev/null \
  | grep -E 'Protocol|Cipher|Verify return'

# Protocol  : TLSv1.3
# Cipher    : TLS_AES_256_GCM_SHA384
# Verify return code: 0 (ok)

# Confirm the old versions are refused:
openssl s_client -connect dnsrobot.net:443 -tls1_1 2>&1 | grep -iE 'alert|no protocols'

참고

암호 스위트 목록은 복사해 붙여 넣은 설정이 가장 쉽게 낡는 부분입니다. 직접 작성하는 대신 Mozilla SSL Configuration Generator로 nginx, Apache, Caddy, HAProxy용 최신 권장 서버 블록을 생성하고, 아주 오래된 클라이언트를 지원해야 하는 경우가 아니라면 "Intermediate" 프로필을 선택하세요.

HSTS: 브라우저가 다시는 HTTP를 쓰지 않게 하기

HTTP를 HTTPS로 리디렉션하는 것만으로는 충분하지 않습니다. 방문자가 http://yourdomain.com으로 보내는 첫 요청은 여전히 평문으로 전송되며, 같은 네트워크에 있는 공격자가 리디렉션보다 먼저 응답할 수 있습니다. HTTP Strict Transport Security(RFC 6797)가 이 문제를 해결합니다. 브라우저가 이 헤더를 한 번 보고 나면, 이후 여러분의 도메인으로 향하는 모든 http:// URL을 요청을 보내기 전에 https://로 바꿔 씁니다.

max-age는 최소 1년(31536000초)으로 설정하고, 모든 서브도메인이 HTTPS를 제공하게 되면 includeSubDomains를 추가한 다음, hstspreload.org에 도메인을 제출해 Chrome, Firefox, Safari, Edge에 하드코딩된 상태로 배포되게 하세요. 프리로드는 첫 방문의 공백을 완전히 없애 줍니다. HTTP 헤더 확인 도구로 헤더를 검증하세요. 이 도구는 A~F 보안 점수의 일부로 HSTS를 평가합니다.

http
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

경고

HSTS는 되돌리기 어려운 일방통행 문입니다. includeSubDomains가 프리로드되고 나면, 유효한 HTTPS를 제공할 수 없는 서브도메인은 내부 도구든 잊힌 dev. 호스트든 브라우저에서 접속할 수 없게 됩니다. 단계적으로 적용하세요. 일주일 동안 max-age=300으로 운영한 뒤 한 달, 그다음 1년으로 늘리고, 그 후에야 preload를 추가하세요.

47일 인증서: 지금 갱신을 자동화하세요

CA/Browser Forum의 투표안 SC-081은 모든 공개 TLS 인증서의 최대 유효 기간을 단계적으로 줄이고 있습니다. 첫 번째 단축이 2026년 3월 15일에 시행되었기 때문에 오늘 구매하거나 갱신하는 인증서는 이미 200일로 제한되며, 2029년이 되면 인증서를 대략 6주마다 교체해야 합니다:

시행일인증서 최대 유효 기간도메인 검증 재사용 기간
2026년 3월 15일 이전398일398일
2026년 3월 15일200일200일
2027년 3월 15일100일100일
2029년 3월 15일47일10일

수동 갱신으로는 이 일정을 버틸 수 없습니다. 모범 사례는 ACME를 통한 완전 자동 발급입니다. certbot, acme.sh, Caddy로 Let's Encrypt나 Google Trust Services 인증서를 발급받거나, Cloudflare, Vercel, Netlify와 대부분의 호스팅 패널에 내장된 자동 인증서를 사용하세요. 필요해지기 전에 갱신 경로를 테스트하세요(certbot이라면 certbot renew --dry-run). 찾아야 할 실패 원인은 마지막 갱신 이후 추가된 CAA 레코드나 방화벽 규칙이 이제 검증을 막고 있는 경우입니다. 무엇을 사용하든 외부에서도 만료일을 모니터링하세요. SSL 인증서 확인 도구는 남은 일수를 보여 주며, 인증서가 만료되면 모든 방문이 전체 화면 브라우저 경고로 바뀝니다.

3계층: HTTP 보안 헤더

보안 헤더는 서버가 브라우저에게 페이지로 무엇을 해도 되고 무엇을 하면 안 되는지 알려 주는 지시입니다. 어떤 스크립트를 실행할 수 있는지, 페이지를 프레임 안에 넣을 수 있는지, 브라우저가 콘텐츠 유형을 추측해도 되는지, 방문자가 어디에서 왔는지를 다른 사이트에 얼마나 알려 줄지를 정합니다. 비용이 전혀 들지 않고, 모든 페이지에 한 번에 적용되며, 공격 유형 전체를 차단합니다. 동시에 대부분의 사이트가 일부를 잘못 설정하는 계층이기도 해서 HTTP 헤더 확인 도구가 이를 등급으로 평가합니다. 다음은 dnsrobot.net이 보내는 헤더 중 보안 헤더만 추린 것입니다:

bash
curl -sI https://dnsrobot.net/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'

strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self' ... https://*.googletagmanager.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(self), microphone=(), geolocation=(), payment=(), usb=()

모든 사이트가 보내야 할 7가지 보안 헤더

일반적인 사이트가 목표로 삼아야 할 값입니다. 처음 다섯 개가 등급을 좌우하고, 나머지 두 개는 다른 헤더를 적용한 뒤 부담 없이 추가할 수 있는 항목입니다.

헤더권장 값막아 주는 공격
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preload프로토콜 다운그레이드, HTTP를 통한 쿠키 탈취
Content-Security-Policy논스 기반 script-src('strict-dynamic' 포함); object-src 'none'; base-uri 'none'; frame-ancestors 'none'크로스 사이트 스크립팅(XSS), 데이터 삽입, 클릭재킹
X-Content-Type-Optionsnosniff업로드된 파일을 실행 가능한 스크립트로 바꾸는 MIME 스니핑
Referrer-Policystrict-origin-when-cross-origin전체 URL(토큰, 검색어)이 서드파티에 유출되는 것
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=()서드파티 스크립트가 강력한 브라우저 API를 몰래 사용하는 것
X-Frame-OptionsDENY(frame-ancestors의 레거시 대체 수단)CSP Level 2를 지원하지 않는 브라우저에서의 클릭재킹
Cross-Origin-Opener-Policysame-originwindow.opener를 통한 교차 창 공격

사이트를 깨뜨리지 않고 Content Security Policy 적용하기

Content Security Policy는 XSS를 막는 가장 효과적인 단일 방어 수단입니다. 인젝션 버그가 있더라도 삽입된 스크립트가 실행되지 못하게 막기 때문입니다. 문제는 엄격한 정책이 여러분이 잊고 있던 인라인 스크립트나 서드파티 태그를 모두 깨뜨린다는 점입니다. Google이 권장하고 MDN에 문서화된 최신 방식은 논스(nonce) 기반 정책입니다. 서버가 응답마다 무작위 논스를 생성해 의도적으로 렌더링하는 모든 script 태그에 붙이고, 브라우저는 그 외의 스크립트를 모두 거부합니다.

정책을 실용적으로 만드는 것은 'strict-dynamic'입니다. 논스가 붙은 스크립트는 추가 스크립트(애널리틱스, 태그 관리자, 위젯)를 하나하나 허용 목록에 넣지 않아도 불러올 수 있으며, https:와 'unsafe-inline' 토큰은 최신 브라우저에서는 무시되고 오래된 브라우저를 위한 대체 수단으로만 쓰입니다. 먼저 Content-Security-Policy-Report-Only로 배포해 일주일 동안 위반 보고서를 지켜본 뒤 강제 모드로 전환하세요. Next.js, Rails, Django 같은 프레임워크는 논스를 자체 지원하며, 정적 사이트는 대신 해시 기반 script-src를 사용할 수 있습니다.

http
Content-Security-Policy:
  script-src 'nonce-r4nd0m1z3dV4lu3' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none';
  form-action 'self';
  report-to csp-endpoint

<!-- Only scripts carrying the nonce run; strict-dynamic lets them load their own dependencies -->
<script nonce="r4nd0m1z3dV4lu3" src="/app.js"></script>

참고

script-src 'self' https://cdn.example.com 같은 허용 목록 방식의 정책도 없는 것보다는 낫지만, 허용된 호스트에 있는 JSONP 엔드포인트나 오래된 라이브러리를 이용하면 우회할 수 있습니다. 아직 논스를 쓸 수 없다면 최소한 object-src 'none'과 base-uri 'none'을 설정하세요. 흔한 우회 경로 두 가지를 즉시 막아 줍니다.

클릭재킹 방어: frame-ancestors와 X-Frame-Options

클릭재킹은 여러분의 사이트를 공격자의 페이지 안에 보이지 않게 불러온 뒤, 방문자가 볼 수 없는 버튼을 클릭하도록 속이는 공격입니다. CSP의 frame-ancestors 'none'(자체 페이지를 임베드한다면 'self')이 모든 최신 브라우저에서 이를 막고, X-Frame-Options: DENY가 오래된 브라우저를 커버합니다. 의도적으로 임베드 가능한 위젯을 제공한다면 해당 경로만, 그리고 필요한 출처(origin)에 대해서만 예외를 두세요. X-Frame-Options 가이드에서 정확한 서버 규칙과 테스트 중에 마주칠 "refused to connect" 오류를 자세히 설명합니다.

nosniff, Referrer-Policy, Permissions-Policy, COOP

X-Content-Type-Options: nosniff는 브라우저가 Content-Type을 임의로 추측하지 못하게 합니다. 바로 이 추측 때문에 HTML이 담긴 사용자 업로드 "이미지"가 실행 가능한 페이지로 바뀝니다. Referrer-Policy: strict-origin-when-cross-origin(최신 브라우저의 기본값이지만 명시적으로 설정하세요)은 다른 사이트에 여러분의 출처만 보내므로, URL에 포함된 비밀번호 재설정 토큰과 검색어가 노출되지 않습니다. Permissions-Policy는 사용하지 않는 브라우저 기능을 비활성화해, 서드파티 스크립트가 침해되더라도 카메라를 켜거나 위치를 읽을 수 없게 합니다. Cross-Origin-Opener-Policy: same-origin은 여러분이 연 페이지와의 window.opener 연결을 끊어 교차 창 공격의 한 유형을 차단합니다.

제거해야 할 헤더도 중요합니다. Server와 X-Powered-By는 정확한 소프트웨어 버전을 취약점 스캐너에 광고하는 셈이고, X-XSS-Protection은 폐기되었으며 오래된 브라우저에서 버그를 일으킬 수 있으므로 0으로 설정하거나 아예 제거하세요. CMS 감지기를 사용하면 스캐너가 헤더만으로 여러분의 기술 스택에 대해 무엇을 알아낼 수 있는지 볼 수 있습니다.

팁

헤더는 페이지마다 설정하지 말고 엣지(CDN, 리버스 프록시, 프레임워크 미들웨어)에서 한 번에 설정하세요. Next.js라면 proxy 또는 middleware 파일, nginx라면 server 컨텍스트의 add_header 블록, Apache라면 Header always set입니다. 그리고 배포할 때마다 HTTP 헤더 확인을 다시 실행하세요. 새 프레임워크 버전이나 CDN 설정 때문에 헤더 하나가 조용히 빠질 수 있습니다.

4계층: 크리덴셜 스터핑을 견디는 인증

공격자가 비밀번호를 하나씩 추측하는 경우는 드뭅니다. 다른 사이트에서 유출된 수십억 개의 이메일·비밀번호 쌍을 그대로 대입하고(크리덴셜 스터핑), 나머지는 피싱으로 얻습니다. 따라서 인증 모범 사례는 세 가지로 이루어집니다. 데이터베이스가 유출되어도 비밀번호까지 유출되지 않도록 저장하고, 탈취된 비밀번호 하나만으로는 쓸모없게 만들고, 무차별 대입 공격이 의미 없을 만큼 느리게 만드는 것입니다. OWASP는 OWASP Top 10:2025에서 Authentication Failures(인증 실패)를 A07로 분류합니다.

Advertisement

Argon2id 또는 bcrypt로 비밀번호 해싱하기

비밀번호를 그대로 저장해서는 안 되며, 빠른 해시로 저장해서도 안 됩니다. MD5, SHA-1은 물론 SHA-256도 빠르게 동작하도록 설계되었기 때문에, 유출된 해시 테이블은 GPU 한 장으로 초당 수십억 번 대입해 볼 수 있습니다. 비밀번호마다 고유한 솔트를 사용하는 느리고 메모리 집약적인 비밀번호 해싱 함수를 사용하세요. OWASP Password Storage Cheat Sheet의 권장 순서는 다음과 같습니다:

  • Argon2id: 최소 19 MiB 메모리, 반복 2회, 병렬도 1(또는 46 MiB 메모리에 반복 1회).

  • scrypt: Argon2를 사용할 수 없는 경우 N = 2^17, r = 8, p = 1.

  • bcrypt: 작업 계수(work factor) 10 이상(72바이트 입력 제한에 주의하세요. 더 긴 비밀번호를 미리 해싱할 때는 신중해야 합니다).

  • PBKDF2-HMAC-SHA256: FIPS 규정 준수 때문에 불가피한 경우에만 600,000회 반복으로 사용.

javascript
// Node.js with the argon2 package
import argon2 from "argon2";

const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 19456, // KiB = 19 MiB
  timeCost: 2,
  parallelism: 1,
});

// Later, on login:
const ok = await argon2.verify(hash, submittedPassword);

참고

로그인 서버에서 해시 한 번에 약 100~250ms가 걸리도록 비용을 조정하세요. 실제 사용자는 전혀 느끼지 못하지만, 공격자는 코어당 초당 수십억 번이 아니라 수천 번밖에 대입하지 못합니다. 매개변수를 올릴 때마다 로그인에 성공하는 시점에 다시 해싱하면 오래된 해시가 자동으로 업그레이드됩니다.

실제로 도움이 되는 비밀번호 규칙 (NIST SP 800-63B)

대부분의 사이트가 여전히 강제하는 조합 규칙(대문자 하나, 특수문자 하나, 90일마다 변경)은 Summer2026! 같은 예측 가능한 비밀번호를 만들고 재사용을 부추깁니다. 최신 NIST SP 800-63B 지침은 이를 실제로 중요한 요소를 측정하는 규칙으로 대체합니다:

  • MFA를 함께 요구하는 경우 최소 8자, 비밀번호만 단독으로 쓰는 경우 최소 15자로 하고, 최대 길이는 64자 이상 허용하세요.

  • 조합 규칙 없음, 주기적 강제 변경 없음. 침해 증거가 있을 때만 변경을 요구하세요.

  • 모든 새 비밀번호를 유출 목록과 대조하고(Have I Been Pwned range API를 사용하면 비밀번호를 전송하지 않고도 확인할 수 있습니다), 이미 유출된 것으로 알려진 비밀번호는 거부하세요.

  • 붙여넣기와 비밀번호 관리자를 허용하고, 공백과 유니코드를 받아들이며, 규칙이 아니라 엔트로피를 기준으로 한 강도 표시기를 보여 주세요.

비밀번호 강도 테스트를 사용하면 이런 기준이 옛 규칙과 어떻게 다른지 확인할 수 있습니다. 엔트로피와 패턴 탐지로 크래킹 시간을 추정하고 비밀번호를 유출 데이터와 대조하므로, 빨간색 "특수문자가 필요합니다" 메시지보다 훨씬 유용한 피드백을 줍니다.

MFA와 패스키

다단계 인증은 탈취된 비밀번호를 막다른 길로 만듭니다. 모든 사용자에게 제공하고, 관리자·재무·고객지원 역할에는 필수로 적용하세요. 선택지는 피싱 저항성을 기준으로 고르세요. 패스키와 FIDO2 보안 키는 크리덴셜이 실제 출처에 묶여 있어 피싱할 수 없습니다. 인증 앱(TOTP)은 좋은 선택입니다. SMS 코드는 없는 것보다는 낫지만 SIM 스와핑으로 가로챌 수 있습니다. 패스키는 모든 최신 브라우저와 운영체제에서 지원되며 비밀번호 자체를 없애므로, 크리덴셜 스터핑이라는 공격 유형도 함께 사라집니다.

복구 경로도 로그인만큼 신중하게 보호하세요. 복구 코드는 해시로 저장하고, 이메일 재설정에는 15분 안에 만료되는 일회용 토큰을 사용하며, 보안 질문은 쓰지 마세요.

속도 제한, 계정 잠금, 봇 방어

로그인, 회원가입, 비밀번호 재설정, MFA 엔드포인트에는 IP별 그리고 계정별 속도 제한을 적용하고, 한도에 도달하면 429 Too Many Requests를 Retry-After 헤더와 함께 반환하세요(HTTP 오류 429 가이드에서 클라이언트가 어떻게 대응해야 하는지 설명합니다). 한 계정에서 실패가 반복되면 점점 길어지는 지연이나 임시 잠금을 함께 적용하고, 분산 공격을 받는 엔드포인트에는 CAPTCHA나 작업 증명(proof-of-work) 챌린지를 추가하세요. 실패한 로그인은 모두 출발지 IP와 함께 기록해, 크리덴셜 스터핑 공격의 패턴을 몇 달이 아니라 몇 분 안에 파악할 수 있게 하세요.

nginx
# nginx: 5 login attempts per minute per client IP
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

location = /api/login {
    limit_req zone=login burst=4 nodelay;
    limit_req_status 429;
    proxy_pass http://app;
}

경고

"존재하지 않는 사용자"와 "잘못된 비밀번호"의 오류 메시지를 똑같이 유지하고, 두 경우의 응답 시간도 같게 만드세요(사용자가 존재하지 않을 때는 더미 비밀번호를 해싱). 그렇지 않으면 로그인 폼이 사용자 열거(user enumeration) API 역할까지 하게 됩니다.

세션, 쿠키, CSRF

로그인한 뒤에는 세션 쿠키가 곧 사용자이므로, 비밀번호와 같은 수준으로 보호해야 합니다. 쿠키 속성 세 가지와 이름 접두사 하나가 대부분의 일을 해냅니다:

  • Secure 속성이 있으면 쿠키가 평문 HTTP로는 절대 전송되지 않으므로, 공용 Wi-Fi에서 스니핑될 수 없습니다.

  • HttpOnly 속성은 쿠키를 JavaScript에서 보이지 않게 하므로, XSS 버그가 있어도 쿠키를 읽을 수 없습니다.

  • SameSite=Lax(관리자 패널이라면 Strict)는 크로스 사이트 POST 요청에 쿠키가 실리지 않게 해 대부분의 CSRF 공격을 무력화합니다.

  • __Host- 접두사를 붙이면 브라우저는 쿠키가 Secure이고 Path=/이며 Domain 속성이 없을 때만 쿠키를 받아들이므로, 서브도메인이 쿠키를 덮어쓸 수 없습니다.

http
Set-Cookie: __Host-session=9f1c7e2a...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=28800

팁

상태를 변경하는 작업은 절대 GET으로 호출할 수 있어서는 안 됩니다. /account/delete?id=42 같은 링크는 쿠키 플래그를 아무리 잘 설정해도, 사용자가 방문하는 어떤 페이지의 <img> 태그로든 실행될 수 있습니다.

CSRF: SameSite 뒤의 두 번째 방어선

크로스 사이트 요청 위조(CSRF)는 로그인된 브라우저가 다른 페이지에서 여러분의 사이트로 상태 변경 요청을 보내도록 속이는 공격입니다. SameSite 쿠키가 일반적인 경우를 막아 주지만, GET이 아닌 모든 요청에는 두 번째 방어를 유지하세요. 숨겨진 필드나 사용자 정의 헤더에 담은 세션별 anti-CSRF 토큰, 또는 Origin(또는 Sec-Fetch-Site) 헤더가 여러분의 출처와 일치하는지 확인하는 검사입니다. 대부분의 프레임워크(Django, Rails, Laravel, Next.js 서버 액션)는 기본으로 이를 처리합니다. 흔한 실수는 API 엔드포인트에서 이 기능을 끈 뒤 그 엔드포인트를 브라우저에서 호출하는 것입니다.

세션 ID, ID 교체, JWT

세션 ID는 최소 128비트의 무작위성으로 생성하고, 세션 고정 공격을 막기 위해 로그인할 때와 권한이 바뀔 때마다 ID를 교체하고, 일정 시간 활동이 없으면 세션을 만료시키고, 서버 측에서 세션을 무효화하는 "모든 기기에서 로그아웃" 버튼을 제공하세요. JWT는 수명을 짧게 유지하고(몇 분 단위로, 폐기할 수 있는 리프레시 토큰과 함께), 어떤 스크립트든 읽을 수 있는 localStorage에는 절대 저장하지 마세요. 서명 키가 유출되면 그 키로 서명한 모든 토큰을 위조할 수 있게 되므로 완전한 침해로 간주해야 합니다.

인젝션과 XSS: 입력은 검증하고 출력은 인코딩하기

인젝션(A05:2025)과 크로스 사이트 스크립팅은 장소만 다를 뿐 같은 실수입니다. 사용자에게서 받은 데이터를 마치 코드인 것처럼 인터프리터(SQL, 셸, LDAP 쿼리, HTML 페이지)에 넘기는 것입니다. 해결책도 어디서나 같습니다. 데이터와 코드를 분리하고, 문자열을 이어 붙여 명령을 만들지 마세요.

Advertisement

문자열 조합 대신 매개변수화된 쿼리

SQL에는 프리페어드 스테이트먼트(prepared statement)나 이를 생성해 주는 ORM을 사용하세요. 그러면 데이터베이스는 입력을 쿼리 구조를 절대 바꿀 수 없는 값으로 취급합니다. 같은 규칙이 셸 명령(문자열이 아니라 인자 배열로 전달), NoSQL 필터(사용자 입력에 포함된 {"$gt": ""} 같은 연산자 객체는 거부), 파일 경로(경로를 해석한 뒤 의도한 디렉터리 안에 머무는지 확인)에도 적용됩니다.

javascript
// Vulnerable: the input becomes part of the query text
const rows = await db.query(`SELECT * FROM users WHERE email = '${email}'`);

// Safe: the driver sends the value separately from the query
const rows = await db.query("SELECT * FROM users WHERE email = $1", [email]);

// Python / psycopg: same idea
cur.execute("SELECT * FROM users WHERE email = %s", (email,))

출력 인코딩으로 XSS 막기

크로스 사이트 스크립팅은 사용자가 제어하는 텍스트가 들어가는 위치(컨텍스트)에 맞게 인코딩되지 않은 채 페이지에 쓰일 때 발생합니다. 최신 템플릿 엔진(React와 JSX, Vue, Jinja2, Blade, ERB)은 기본적으로 인코딩합니다. 오늘날 XSS는 대개 이 보호를 우회하는 탈출구에서 생깁니다. dangerouslySetInnerHTML, v-html, |safe, innerHTML, 그리고 사용자 데이터로 URL이나 인라인 이벤트 핸들러를 만드는 경우입니다. 정확한 컨텍스트에 맞춰 인코딩하고, 리치 HTML은 정규식이 아니라 DOMPurify 같은 전용 라이브러리로 새니타이즈하세요.

데이터가 들어가는 위치인코딩 방식예시
HTML 본문HTML 엔티티<는 &lt;로, "는 &quot;로 바뀜
HTML 속성속성용 엔티티(따옴표로 감싼 값)속성 값은 항상 따옴표로 감싸고 "와 '를 인코딩
JavaScript스크립트 본문에 직접 삽입하지 말고 data- 속성이나 JSON <script type="application/json"> 블록으로 데이터를 전달문자열 안의 </script>는 \u003c/script\u003e로 바뀜
URL 매개변수퍼센트 인코딩encodeURIComponent(value)
CSS 값아예 사용하지 말고 허용 목록에서 클래스 이름을 선택사용자 입력을 style에 절대 보간하지 않기

SSRF, 파일 업로드, 역직렬화

서버 측 요청 위조(SSRF): 서버가 사용자가 입력한 URL을 가져온다면(웹훅, 이미지 프록시, 링크 미리보기), 공격자는 그 요청을 내부 서비스나 클라우드 메타데이터 엔드포인트 169.254.169.254로 향하게 해 크리덴셜을 읽어 낼 수 있습니다. 먼저 호스트 이름을 해석하고, 사설 및 링크 로컬 대역은 거부하고, 리디렉션을 비활성화하고, URL을 가져오는 컴포넌트는 내부 어디에도 접근할 수 없는 네트워크 세그먼트에 두세요.

파일 업로드: 파일 유형은 확장자나 클라이언트가 보낸 Content-Type이 아니라 내용을 검사해 검증하세요. 크기 제한을 두고, 파일은 웹 루트 밖이나 오브젝트 스토리지에 직접 생성한 무작위 이름으로 저장하며, 가능하면 nosniff와 Content-Disposition: attachment를 적용한 별도 출처에서 제공하세요. 업로드된 파일이 웹 서버가 실행할 수 있는 위치에 놓이게 해서는 절대 안 됩니다.

역직렬화와 대량 할당(mass assignment): 임의의 클래스를 인스턴스화할 수 있는 형식(Java 네이티브 직렬화, Python pickle, PHP unserialize, 사용자 정의 태그를 쓰는 YAML)으로 신뢰할 수 없는 데이터를 역직렬화하지 마세요. 스키마를 갖춘 JSON을 사용하세요. 요청 본문은 명시적인 필드 허용 목록에만 바인딩해, 우연히 해당 컬럼이 있는 모델에 사용자가 "role": "admin"을 POST로 밀어 넣을 수 없게 하세요.

경고

검증은 형태(이메일인가, 양의 정수인가, 이 다섯 값 중 하나인가?)를 확인하기 위한 것이지 보안 수단이 아닙니다. <script>나 따옴표 문자를 제거하는 차단 목록은 언제나 우회할 수 있습니다. 예상하는 값만 받아들이고, 출력할 때는 인코딩하고, 저장할 때는 매개변수화하세요.

Broken Access Control: 가장 큰 위험

Broken Access Control(접근 제어 취약점)은 2021년부터 OWASP Top 10의 1위를 지켜 왔고, 2025년판에서도 A01:2025로 그 자리를 유지했습니다. 단일 버그가 아니라 습관의 문제입니다. 요청마다 상대가 누구인지(인증)는 확인하면서 무엇에 접근해도 되는지(권한 부여)는 확인하지 않는 것입니다. 전형적인 형태는 안전하지 않은 직접 객체 참조(IDOR)입니다. GET /api/invoices/1042는 청구서 소유자에게도 작동하지만, 숫자만 바꾼 다른 누구에게나 작동합니다.

  • 기본값은 거부. 모든 라우트에는 접근을 허용하는 명시적인 규칙이 있어야 하며, 규칙이 없으면 200이 아니라 403을 반환해야 합니다.

  • 서버에서, 객체 단위로 강제하세요. UI에서 버튼을 숨기는 것은 접근 제어가 아닙니다. 모든 읽기와 쓰기에서 현재 사용자가 해당 레코드를 소유하거나 접근 권한이 있는지 확인해야 하며, 일괄 처리 엔드포인트, 내보내기, 백그라운드 작업도 예외가 아닙니다.

  • 도움이 된다면 추측할 수 없는 ID(UUID)를 사용하되, 그것에 의존하지는 마세요. 숨기는 것은 권한 부여가 아닙니다.

  • CORS를 잠그세요. 크리덴셜과 함께 Access-Control-Allow-Origin: *를 쓰거나, 요청이 보내는 Origin을 그대로 반사하면 사용자가 방문하는 모든 웹사이트에 여러분의 API를 넘겨주는 것과 같습니다.

  • 디렉터리 목록을 비활성화하고, 웹 서버 수준에서 .git, .env, 백업 및 설정 파일을 차단하고, 관리자 패널은 공개 인터넷에서 분리하거나 MFA와 IP 허용 목록 뒤에 두세요.

  • 권한 부여 실패에 속도 제한을 걸고 기록하세요. 한 세션에서 403이 연달아 발생한다면 열거 공격이 진행 중이라는 뜻입니다.

javascript
// Express: ownership check on every object access
app.get("/api/invoices/:id", requireAuth, async (req, res) => {
  const invoice = await Invoice.findById(req.params.id);
  if (!invoice || invoice.ownerId !== req.user.id) {
    return res.status(404).end(); // 404, not 403: don't confirm the record exists
  }
  res.json(invoice);
});

참고

공격자처럼 접근 제어를 테스트하세요. 권한이 낮은 사용자로 로그인해 모든 요청을 캡처한 다음, 각 요청을 다른 사용자의 ID로 바꿔서, 그리고 Authorization 헤더를 제거한 채로 재전송해 보세요. 자동화 스캐너는 인젝션은 찾아내지만 권한 부여 버그는 거의 찾지 못합니다. 그래서 이런 버그가 몇 년씩 살아남습니다.

5계층: 의존성과 소프트웨어 공급망

일반적인 웹 애플리케이션에서 직접 작성한 코드는 5% 정도이고 나머지 95%는 내려받은 코드입니다. 그래서 2025년 OWASP Top 10에 Software Supply Chain Failures(소프트웨어 공급망 실패)가 A03으로 처음 등장했고, 2026 DBIR에서는 취약점 악용이 탈취된 크리덴셜을 앞질렀습니다. 2025년 9월에는 피싱에 당한 메인테이너 한 명 때문에, 합쳐서 주간 다운로드가 20억 회를 넘는 chalk, debug 외 16개 npm 패키지에 암호화폐 탈취 악성 코드가 들어갔습니다. 해당 기간에 버전을 고정하지 않고 설치한 모든 프로젝트가 이를 자동으로 받아 갔습니다.

  • 락파일을 커밋하고(package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, go.sum) CI에서는 npm ci로 설치해 빌드를 재현 가능하게 만들고, 새 업스트림 릴리스가 검토 없이 끼어들지 못하게 하세요.

  • 지속적으로 스캔하세요: npm audit, pip-audit, bundler-audit, 업데이트용 GitHub Dependabot이나 Renovate, 그리고 OS 계층용 Trivy나 Grype 같은 컨테이너 스캐너를 사용합니다.

  • 보안과 무관한 업그레이드는 며칠 늦추세요(Renovate의 minimumReleaseAge). 오염된 릴리스는 대개 여러분이 도입하기 전에 회수됩니다. 단, 보안 패치는 즉시 적용하세요.

  • CI에서 서드파티 액션과 스크립트를 커밋 SHA로 고정하고, 공개 CDN에서 불러오는 모든 스크립트에는 Subresource Integrity(SRI, integrity="sha384-...")를 적용하세요.

  • CI에는 최소 권한만 부여하세요: 가능한 곳에서는 읽기 전용 토큰을 쓰고, 오래 유지되는 시크릿 대신 수명이 짧은 OIDC 크리덴셜을 사용하며, 게시 권한은 릴리스 작업에만 부여하세요.

  • 빌드 시점에 SBOM을 생성하세요(CycloneDX 또는 SPDX). 다음번 Log4Shell급 CVE가 터졌을 때 "우리도 영향을 받는가?"라는 질문에 몇 분 안에 답할 수 있습니다.

bash
# Reproducible install + audit in CI
npm ci --ignore-scripts
npm audit --audit-level=high

# Pin a GitHub Action to a commit, not a floating tag
# uses: actions/checkout@v4                 <- can change underneath you
# uses: actions/checkout@<full-commit-sha>  # v4.2.2

# Subresource Integrity for a CDN script (hash from: openssl dgst -sha384 -binary lib.min.js | openssl base64 -A)
<script src="https://cdn.example.com/lib.min.js"
        integrity="sha384-<base64-hash>"
        crossorigin="anonymous"></script>

팁

npm ci --ignore-scripts는 대부분의 npm 악성 코드가 실행에 이용하는 설치 시점 스크립트를 차단합니다. 그런 다음 빌드 단계가 정말 필요한 소수의 패키지에만 스크립트를 다시 허용하세요.

WordPress나 다른 CMS를 운영한다면 플러그인과 테마에도 같은 규칙이 적용되며, CMS 침해는 대부분 여기서 시작됩니다. 코어와 모든 플러그인을 최신 상태로 유지하고, 쓰지 않는 것은 삭제하고, CMS 감지기로 스캐너가 여러분의 버전에 대해 무엇을 알아낼 수 있는지 확인하세요.

6계층: 서버 강화(포트, 패치, 시크릿, 백업)

그 아래의 서버가 SSH 비밀번호 로그인을 허용하거나, 데이터베이스를 공개 포트에서 실행하거나, 구축 이후 한 번도 패치되지 않았다면 애플리케이션 계층의 보안은 아무 의미가 없습니다. 서버 모범 사례는 지루하지만, 랜섬웨어가 들어오는 곳이 바로 여기입니다.

서비스하지 않는 포트는 모두 닫기

공개 웹 서버에 열려 있어야 할 것은 80번과 443번 포트, 그리고 자신의 IP 대역에서 접속하는 SSH뿐입니다. 데이터베이스(3306, 5432, 27017), Redis(6379), Elasticsearch(9200), 관리자 패널, 메트릭 엔드포인트는 localhost나 사설 네트워크에만 바인딩해야 합니다. 변경할 때마다 포트 확인 도구로 외부에서 점검하세요. 그것이 공격자가 보는 모습이며, 클라우드 보안 그룹 설정은 잘못 읽기 쉽습니다.

bash
# Ubuntu: default-deny firewall, allow only web + SSH from your office range
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp
sudo ufw enable

# SSH: keys only, no root password login
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
sudo systemctl reload ssh

정기적인 패치, 시크릿은 코드 밖에

운영체제의 무인 보안 업데이트를 활성화하고, 사용하는 런타임과 프레임워크의 보안 메일링 리스트를 구독하고, 인터넷에 노출된 구성 요소의 치명적인 CVE는 당일에 처리할 작업으로 다루세요. 서비스는 권한 없는 사용자로, 컨테이너 안에서 또는 systemd 샌드박싱을 적용해 실행하세요. 그래야 프로세스 하나가 침해되어도 시스템 전체를 읽을 수 없습니다.

시크릿(데이터베이스 비밀번호, API 키, 서명 키)은 저장소, Docker 이미지 레이어, 클라이언트 측 JavaScript 어디에도 있어서는 안 됩니다. 배포 시점에 주입되는 환경 변수나 시크릿 관리자에서 불러오고, 접근 권한이 있던 사람이 떠나면 교체하고, gitleaks 같은 도구로 저장소 기록을 스캔하세요. 한 번이라도 커밋된 키는 커밋을 삭제한 뒤에도 영원히 노출된 것으로 봐야 하기 때문입니다.

경고

NEXT_PUBLIC_, VITE_, REACT_APP_ 접두사가 붙은 값은 모두 브라우저 번들에 포함되므로 정의상 공개된 정보입니다. 서드파티 API 키는 대신 자체 서버 라우트 뒤에 두세요. 공개 API 엔드포인트 테스트 가이드에서 프런트엔드가 실제로 무엇을 노출하는지 확인하는 방법을 설명합니다.

랜섬웨어에도 살아남는 백업, 그리고 앞단의 CDN

3-2-1 규칙을 따르세요. 사본 3개를 서로 다른 매체 2종에 보관하고 그중 1개는 외부에 두며, 서버에 도달한 랜섬웨어가 백업까지 암호화하지 못하도록 최소 1개는 변경 불가능(immutable)하거나 오프라인 상태로 두세요. 백업은 암호화하고, 데이터베이스와 업로드 디렉터리를 포함하며, 무엇보다 정기적으로 복원을 테스트하세요. 아무도 복원해 보지 않은 백업은 계획이 아니라 희망일 뿐입니다. 복구 시간이 사고를 단순한 서비스 중단으로 끝낼지, 회사의 존립을 위협하는 사태로 키울지를 결정합니다.

오리진 앞단에 CDN이나 WAF(Cloudflare, Fastly, WAF를 적용한 AWS CloudFront, 또는 호스팅 업체의 동등한 서비스)를 두세요. 대용량 DDoS를 흡수하고, 흔한 인젝션 페이로드와 악성 봇에 대한 관리형 규칙을 적용하고, 오리진 IP를 숨기며, 애플리케이션을 건드리지 않고 속도 제한과 지역 규칙을 적용할 수 있는 지점이 되어 줍니다. 그런 다음 오리진 방화벽을 제한해 CDN의 IP 대역만 443번 포트에 직접 접근할 수 있게 하세요. 그렇지 않으면 공격자는 CDN을 그냥 우회합니다. 호스팅 확인 도구는 사이트의 실제 오리진이 CDN 뒤에서 노출되어 있는지 보여 줍니다.

이메일 인증: SPF, DKIM, DMARC

웹 보안에는 비밀번호 재설정, 청구서, 고객지원 답장을 전달하는 이메일도 포함됩니다. SPF, DKIM, DMARC가 없으면 누구나 billing@yourdomain.com 명의로 메일을 보낼 수 있으며, 2024년 2월부터 Google과 Yahoo는 대량 발송자에게 세 가지를 모두 요구하고 있습니다. 전체 구성은 DNS TXT 레코드 세 개입니다:

dns
yourdomain.com.                    TXT  "v=spf1 include:_spf.google.com -all"
google._domainkey.yourdomain.com.  TXT  "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
_dmarc.yourdomain.com.             TXT  "v=DMARC1; p=reject; rua=mailto:dmarc@yourdomain.com; adkim=s; aspf=s"

참고

DMARC p=reject는 브랜드 보호 수단이기도 합니다. 피싱 캠페인이 여러분의 도메인을 발신자(From) 줄에 그대로 넣는 것을 막을 수 있는 유일한 방법입니다. 집계 보고서(rua=)를 보면 누가 시도하고 있는지 알 수 있습니다.

DMARC는 p=none으로 시작해 보고서를 수집하고, 모든 정상 발신자(마케팅 플랫폼, 헬프데스크, CRM)가 정렬(alignment)을 통과하면 p=quarantine, 그다음 p=reject로 올리세요. MTA-STS와 TLS-RPT 레코드를 추가해 메일 서버로의 암호화 전송을 강제하고, 메일을 보내지 않는 도메인에는 null MX(MX 0 .)를 게시해 아예 스푸핑할 수 없게 하세요. 각 레코드는 SPF 레코드 확인, DKIM 확인, DMARC 확인으로 검증하고, 전달률이 떨어지면 IP 블랙리스트 확인으로 발송 IP가 차단 목록에 올라 있는지 확인하세요.

로그, 모니터링, 그리고 침해 대비

OWASP 2025의 10개 카테고리 중 두 개는 실수가 일어난 뒤의 일에 관한 것입니다. Security Logging and Alerting Failures(보안 로깅 및 경보 실패, A09)와 새로 추가된 Mishandling of Exceptional Conditions(예외 상황 처리 미흡, A10)입니다. 전형적인 침해는 증거가 기록되지 않았거나, 기록되었지만 아무도 보지 않았기 때문에 여전히 몇 달 동안 발견되지 않습니다.

  • 맥락과 함께 보안 이벤트를 기록하세요: 모든 로그인 성공과 실패, MFA 변경, 비밀번호 재설정, 권한 변경, 접근 제어 거부, 입력 검증 실패, 관리자 작업을 타임스탬프, 사용자 ID, 출발지 IP, 사용자 에이전트와 함께 남기세요.

  • 시크릿은 절대 로그에 남기지 마세요: 비밀번호, 세션 토큰, 전체 카드 번호, API 키가 여기에 해당합니다. 호출하는 곳마다가 아니라 로깅 계층에서 마스킹하세요.

  • 로그를 서버 밖으로 보내세요: 최소 90일 보존 기간을 둔 중앙 저장소(CloudWatch, Loki, Elastic, SIEM)로 전송하면, root 권한을 얻은 공격자도 흔적을 지울 수 없습니다.

  • 단일 이벤트가 아니라 패턴에 경보를 거세요: 1분 동안 50회의 로그인 실패, 한 세션에서 급증한 403, 업무 시간 외에 생성된 새 관리자 계정, 요청하지 않았는데 여러분의 도메인으로 발급된 인증서 같은 패턴입니다.

  • 실패 시 차단(fail closed)하세요: 하위 검사(인증 서비스, 속도 제한기, WAF)에서 오류가 나면 요청을 거부하세요. A10이 존재하는 이유는 차단했어야 할 검사가 예외를 던질 때 접근을 허용해 버리는 시스템이 그만큼 많기 때문입니다.

  • 외부에서 모니터링하세요: 가동 시간, 인증서 만료, DNS 변경, 차단 목록 등재, 헤더 회귀를 확인합니다. 도메인 상태 검사기는 DNS, SSL, 이메일 인증, 헤더 검사를 하나의 점수로 묶어 주므로 배포할 때마다 다시 실행할 수 있습니다.

필요해지기 전에 사고 대응 계획 작성하기

누가 비상 대기하는지, 모든 크리덴셜을 어떻게 교체하는지, 사이트를 어떻게 읽기 전용으로 전환하는지, 깨끗한 백업이 어디에 있는지, 어느 규제 기관이나 고객에게 몇 시간 안에 통지해야 하는지(GDPR은 72시간)를 미리 정해 두세요. 1년에 한 번은 모의 훈련(테이블톱 연습)을 실시하세요. 계획을 연습해 둔 팀은 며칠 만에 복구하지만, 계획이 없는 팀은 몇 주 동안 즉흥적으로 대응합니다.

팁

연락처 주소와 PGP 키를 담은 security.txt 파일을 /.well-known/security.txt 경로에 추가하세요(RFC 9116). 여러분의 사이트에서 버그를 발견한 연구자는 이 파일을 이용합니다. 이 파일이 없으면 연구자가 버그를 공개적으로 게시해 버릴 수도 있습니다.

테스트: 스캐너, 모의 해킹, 지속적인 점검

위의 모든 보안 통제는 검증할 수 있으며, 시간이 지나도 무너지지 않도록 가능한 한 검증을 자동화해야 합니다. 무료로 즉시 할 수 있는 것부터 주기적인 유료 점검까지, 합리적인 단계는 다음과 같습니다:

점검 항목도구주기
TLS 설정, 체인, 만료SSL 인증서 확인, SSL Labs, testssl.sh인증서나 서버를 변경할 때마다, 만료는 매일
보안 헤더와 CSPHTTP 헤더 확인, MDN HTTP Observatory배포할 때마다(CI에 포함)
열린 포트포트 확인, nmap방화벽이나 클라우드 설정을 변경할 때마다
DNS, DNSSEC, 이메일 인증도메인 상태 검사기, DMARC 확인매월, 그리고 DNS를 수정할 때마다
방치된 서브도메인서브도메인 검색분기마다, 그리고 서비스를 폐기할 때마다
알려진 취약점이 있는 의존성npm audit, Dependabot, Trivy빌드할 때마다
코드 수준 결함(SAST)Semgrep, CodeQL풀 리퀘스트마다
런타임 웹 취약점(DAST)OWASP ZAP, Nuclei매주 스테이징 환경 대상
권한 부여와 비즈니스 로직수동 모의 해킹 또는 버그 바운티 프로그램매년, 그리고 주요 기능 출시 후

스캐너는 인젝션, 헤더, 오래된 구성 요소는 잘 찾지만 권한 부여와 비즈니스 로직에는 약합니다. 따라서 금전이나 개인정보를 다루는 서비스라면 1년에 최소 한 번은 사람이 직접 수행하는 테스트 예산을 확보하세요. 수정 순서는 노출 정도로 정하세요. 인증되지 않은 사용자가 인터넷에서 악용할 수 있는 문제가 가장 먼저입니다.

웹 보안 체크리스트

이 표를 프로젝트의 README나 티켓 트래커에 복사해 두세요. 각 줄은 위에서 다룬 모범 사례 하나이며, 완료 여부를 표시할 수 있도록 작성했습니다. 마지막 열이 완료의 증거입니다.

계층모범 사례완료 기준
도메인레지스트라 락과 레지스트라 계정 MFA 적용, 자동 갱신 켜짐WHOIS에 clientTransferProhibited 표시, 만료일까지 12개월 이상
DNSDNSSEC 서명, 사용하는 CA만 나열한 CAA 레코드dig +dnssec 결과에 RRSIG 반환, 상위 영역에 DS 레코드 존재
DNS방치된 CNAME 또는 A 레코드 없음서브도메인 검색에서 찾은 모든 호스트 이름이 직접 통제하는 대상으로 해석됨
TLSTLS 1.2 이상만 허용, TLS 1.3 우선, 전체 체인 제공SSL 인증서 확인 결과: 체인 완전, TLS 1.0/1.1 거부
TLSmax-age 1년, includeSubDomains, 프리로드를 적용한 HSTShstspreload.org에 등재됨
TLSACME로 갱신 자동화, 만료 모니터링certbot renew --dry-run 통과, 만료 14일 전 알림 설정
헤더CSP(논스 기반), frame-ancestors, nosniff, Referrer-Policy, Permissions-PolicyHTTP 헤더 확인 등급 A
인증Argon2id 또는 bcrypt 해싱, NIST 비밀번호 규칙, 유출 목록 대조해시 1회에 100~250ms 소요, 조합 규칙 없음
인증모든 사용자에게 MFA 제공, 관리자는 필수, 패스키 지원두 번째 인증 요소 없이는 관리자 로그인 불가
인증로그인, 재설정, MFA 엔드포인트에 IP별·계정별 속도 제한1분 안의 여섯 번째 시도에 429 반환
세션Secure, HttpOnly, SameSite를 적용한 __Host- 쿠키, 로그인 시 ID 교체개발자 도구에서 쿠키 속성 확인 가능, 로그인 후 이전 세션 무효화
입력모든 곳에 매개변수화된 쿼리, 컨텍스트별 출력 인코딩, 내용 기반 업로드 검증코드 검색에서 문자열로 조합한 쿼리 없음, XSS 페이로드가 텍스트로 렌더링됨
접근 제어기본 거부 라우팅, 객체별 소유권 검사, 엄격한 CORS다른 사용자의 ID로 재전송하면 404 또는 403 반환
의존성락파일 커밋, npm ci, CI에서 감사, 액션을 SHA로 고정, CDN 스크립트에 SRI 적용심각도 high인 CVE가 있으면 빌드 실패
서버80번과 443번만 공개, SSH는 키 인증만, 무인 보안 업데이트, 시크릿은 환경 변수나 볼트에 보관포트 확인 결과 인터넷에서 22, 3306, 6379번 포트가 닫혀 있음
백업변경 불가능한 사본 1개를 포함한 3-2-1 백업, 복원 테스트 완료마지막 복원 테스트 성공일이 90일 이내
이메일SPF -all, DKIM, DMARC p=reject, MTA-STSDMARC 확인 통과, 집계 보고서 수신 중
모니터링인증 및 권한 부여 이벤트 중앙 기록, 패턴 기반 경보, 외부 가동 시간·인증서·DNS 모니터링테스트 경보 발송 및 수신 확인
대응사고 대응 계획 문서화, security.txt 게시, 모의 훈련 실시최근 12개월 안에 계획 검토

OWASP Top 10:2025와의 대응 관계

OWASP Top 10은 웹 애플리케이션 위험 목록 중 가장 많이 인용되는 목록이며, 2025년판에서는 순위가 재편되고 두 개의 새 카테고리가 추가되었습니다. 고객, 감사인, 컴플라이언스 프레임워크가 이에 어떻게 대응하는지 묻는다면, 아래 표가 이 가이드의 모범 사례와의 대응표입니다:

OWASP Top 10:2025이 가이드의 모범 사례
A01 Broken Access Control(접근 제어 취약점)기본 거부, 객체별 검사, 엄격한 CORS, 관리자 페이지 잠금
A02 Security Misconfiguration(보안 설정 오류)보안 헤더, HSTS, 닫힌 포트, 버전 헤더 제거, 디렉터리 목록 비활성화
A03 Software Supply Chain Failures(소프트웨어 공급망 실패, 신규)락파일, 감사, 고정된 액션, SRI, SBOM, 최소 권한 CI
A04 Cryptographic Failures(암호화 실패)TLS 1.2 이상 및 1.3, HSTS, Argon2id, 시크릿 관리, 암호화된 백업
A05 Injection(인젝션)매개변수화된 쿼리, 출력 인코딩, CSP, 업로드 검증
A06 Insecure Design(안전하지 않은 설계)계층별 위협 모델, 실패 시 차단 기본값, 설계 단계부터 반영한 속도 제한
A07 Authentication Failures(인증 실패)NIST 비밀번호 규칙, 유출 대조, MFA와 패스키, 세션 ID 교체
A08 Software or Data Integrity Failures(소프트웨어 및 데이터 무결성 실패)SRI, 서명 및 버전 고정된 의존성, 안전한 역직렬화
A09 Security Logging and Alerting Failures(보안 로깅 및 경보 실패)중앙 로깅, 패턴 기반 경보, 외부 모니터링
A10 Mishandling of Exceptional Conditions(예외 상황 처리 미흡, 신규)실패 시 차단, 일관된 오류 응답, 사용자에게 스택 트레이스 노출 금지

참고

OWASP의 ASVS(Application Security Verification Standard, 애플리케이션 보안 검증 표준)는 같은 내용을 감사용 번호 체크리스트로 정리한 것입니다. 공개 사이트라면 Level 1이 현실적인 목표이고, 개인정보나 금융 데이터를 다룬다면 Level 2를 목표로 하세요.

사이트의 보안 헤더를 몇 초 만에 확인하세요

DNS Robot의 무료 HTTP 헤더 확인 도구는 어떤 URL이든 가져와 HSTS, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy 등 보안 헤더를 A부터 F까지 등급으로 평가하고, 실제로 발견한 값과 누락된 항목을 정확히 보여 줍니다. 회원가입이 필요 없습니다.

사용해보기 HTTP 헤더 확인

Advertisement

웹 보안 모범 사례 FAQ

최신 TLS와 HSTS로 HTTPS를 강제하고, 엄격한 보안 헤더(특히 Content Security Policy)를 보내고, 비밀번호는 Argon2id로 해싱한 뒤 MFA와 속도 제한으로 보호하고, 매개변수화된 쿼리와 출력 인코딩을 사용하고, 모든 객체에 대해 서버에서 접근 제어를 강제하고, 의존성은 패치를 적용하고 버전을 고정하고, 사용하지 않는 포트를 닫고, 보안 이벤트를 중앙에서 기록하세요. 다른 모든 것이 도메인 이름을 계속 통제하고 있다는 전제 위에 있으므로, 도메인과 DNS부터 잠가야 합니다.

관련 도구

HTTP Headers CheckSSL Certificate CheckPort CheckerDomain Health CheckerDMARC CheckerSubdomain FinderPassword Strength Tester

관련 기사

SSL 인증서 체인이란? 작동 방식 설명X-Frame-Options Explained: Fix “Refused to Connect” in an iframeHTTP Error 429 Too Many Requests: 원인 분석과 해결 방법WHOIS 조회 가이드: 도메인 소유자 확인과 레코드 읽는 법

목차

  • 웹 보안 모범 사례란 무엇인가요?
  • 웹 보안 스택: 보호해야 할 6개 계층
  • 1계층: 도메인과 DNS 잠그기
  • 2계층: 제대로 구성한 HTTPS와 TLS
  • 3계층: HTTP 보안 헤더
  • 4계층: 크리덴셜 스터핑을 견디는 인증
  • 세션, 쿠키, CSRF
  • 인젝션과 XSS: 입력은 검증하고 출력은 인코딩하기
  • Broken Access Control: 가장 큰 위험
  • 5계층: 의존성과 소프트웨어 공급망
  • 6계층: 서버 강화(포트, 패치, 시크릿, 백업)
  • 이메일 인증: SPF, DKIM, DMARC
  • 로그, 모니터링, 그리고 침해 대비
  • 테스트: 스캐너, 모의 해킹, 지속적인 점검
  • 웹 보안 체크리스트
  • OWASP Top 10:2025와의 대응 관계
  • 자주 묻는 질문