infra
Platform

모듈 맵

[Infra Ops] 계정 권한 관리, 보안 헤더, TLS 강화 실무

0 / 52 완료

펼치기
0 / 52 완료0%

인프라 운영 & SRE · 47 / 52

[Infra Ops] 계정 권한 관리, 보안 헤더, TLS 강화 실무

관리자 경로 제한, CORS/Cookie 보안 설정, 보안 헤더, TLS hardening까지 — 서버를 안전하게 유지하는 인프라 보안 운영 실무

🚨INCIDENT ALERT
HIGH

서비스를 배포하고 보안팀으로부터 메일을 받습니다. "취약점 스캔 결과 — TLS 1.0 활성화, 보안 헤더 미설정, 관리자 페이지 외부 접근 허용." 이게 얼마나 위험한 것인지, 어떻게 고쳐야 하는지 막막합니다.

보안은 한 번에 완벽하게 만드는 것이 아닙니다. 실무에서는 "지금 당장 꼭 해야 하는 것"부터 순서대로 적용합니다. 이 모듈은 인프라 엔지니어가 직접 Nginx 설정으로 적용할 수 있는 보안 강화 항목들을 다룹니다.

이번 챕터에서 배울 것
  • 1Nginx에서 관리자 경로를 IP 기반으로 제한하고 숨김 파일 접근을 차단할 수 있다
  • 2보안 헤더(X-Frame-Options, HSTS, CSP 등) 5개를 Nginx에 설정할 수 있다
  • 3CORS 설정에서 와일드카드 대신 특정 출처만 허용하는 방법을 적용할 수 있다
  • 4쿠키에 Secure, HttpOnly, SameSite 속성을 설정하는 방법을 설명할 수 있다
  • 5TLS 1.0/1.1을 비활성화하고 강화된 cipher suite를 적용할 수 있다

계정 권한 관리

💡개념

root 직접 접속 차단과 불필요 계정 관리

보안 감사 결과서를 받았습니다. 첫 번째 항목이 "root 직접 로그인 허용"입니다. 서버를 처음 세팅했을 때 편의를 위해 놔뒀던 설정인데, 공격자가 SSH 브루트포스를 시도할 때 root 계정이 존재하면 하나의 계정만 크래킹하면 시스템 전체를 장악할 수 있습니다. 감사에서 지적받기 전에, 서버 세팅 시 기본적으로 처리해야 하는 계정 보안 항목들이 있습니다.

서버 보안의 기본은 공격 표면을 줄이는 것입니다. 필요 없는 계정을 없애고, root 직접 접속을 막고, 필요한 권한만 부여하는 것이 출발점입니다.

서버 보안 계층 — 공격 표면 축소가 기본: 불필요 계정·서비스 제거, root 직접 SSH 차단(PermitRootLogin no)·sudo 경유, 최소 권한 부여, 포트 최소 개방. 각 계층이 독립적으로 방어해 한 곳이 뚫려도 다음 계층이 막음확대

로컬 터미널
# root 직접 SSH 로그인 차단 (/etc/ssh/sshd_config)
grep "PermitRootLogin" /etc/ssh/sshd_config
# 기존 항목을 중복 생성하지 말고 편집기로 값을 검토·수정합니다.
sudo cp -a /etc/ssh/sshd_config "/etc/ssh/sshd_config.bak.$(date +%Y%m%d%H%M%S)"
sudoedit /etc/ssh/sshd_config
# 문법 검증에 실패하면 reload하지 않습니다.
sudo sshd -t && sudo systemctl reload sshd

# 활성 계정 목록 확인
awk -F: '$3 >= 1000 {print $1, $3, $7}' /etc/passwd
# UID 1000 이상 = 일반 사용자 계정

# 불필요 계정 잠금 (삭제보다 잠금 권장 — 파일 소유자 관계 유지)
sudo usermod -L testuser        # 계정 잠금
sudo passwd -S testuser         # 상태 확인 (L = Locked)

# 로그인 실패 기록 확인 (배포판에 따라 btmp 권한·도구가 다를 수 있음)
sudo lastb -F | head -20

관리자 경로 제한

💡개념

Nginx에서 IP 기반 접근 제어

관리자 경로(/admin, /management, /actuator 등)를 인터넷에 공개하면 브루트포스 공격의 대상이 됩니다. 내부 IP에서만 접근하도록 제한하는 것이 기본 보안 조치입니다.

Nginx
# /etc/nginx/conf.d/app.conf

server {
    listen 80;
    server_name example.com;

    # 관리자 경로 — 내부 IP만 허용
    location /admin {
        allow 10.0.0.0/8;      # 내부 사설 IP 대역
        allow 192.168.0.0/16;  # 사무실 IP 대역
        deny all;              # 나머지 전부 차단
    }

    # Spring Boot Actuator — 내부만 허용 (민감 정보 노출 방지)
    location /actuator {
        allow 127.0.0.1;
        allow 10.0.0.0/8;
        deny all;
    }

    # 숨김 파일/디렉터리 접근 차단 (.git, .env 등)
    location ~ /\. {
        deny all;
        return 404;
    }

    # 특정 파일 확장자 차단
    location ~* \.(sql|bak|log|conf|env)$ {
        deny all;
        return 404;
    }

    # 일반 요청
    location / {
        proxy_pass http://localhost:8080;
    }
}

deny all만 쓰면 403을 반환해 경로 존재 여부가 노출됩니다. return 404를 함께 쓰면 경로가 아예 없는 것처럼 보입니다.

보안 헤더 설정

💡개념

응답 헤더로 브라우저 보안 정책 강제하기

보안 점검 결과 "X-Frame-Options 헤더 미설정"이 취약점으로 나왔습니다. 개발팀에 전달했더니 "그게 뭔데요?"라는 답이 왔습니다. 이 헤더들은 코드 레벨 취약점이 아니라 브라우저에게 보내는 정책 지시입니다. 설정하지 않으면 클릭재킹, XSS, MIME 스니핑 같은 공격에 그대로 노출됩니다. Nginx 설정에 몇 줄만 추가하면 애플리케이션 코드를 건드리지 않고도 이 취약점들을 한번에 처리할 수 있습니다.

보안 헤더는 서버가 브라우저에게 "이 사이트에서는 이런 보안 정책을 따르라"고 지시하는 방법입니다. 코드 변경 없이 Nginx 설정만으로 주요 클라이언트 사이드 공격을 방어할 수 있습니다.

브라우저 보안 응답 헤더 5종 — Content-Security-Policy(XSS 방어, 허용 출처만 실행)·Strict-Transport-Security(HTTPS 강제)·X-Frame-Options(클릭재킹 방어, iframe 차단)·X-Content-Type-Options nosniff(MIME 스니핑 차단)·Referrer-Policy(리퍼러 유출 제어). 서버가 Nginx add_header로 응답에 심으면 브라우저가 정책을 강제하며, curl -I나 개발도구로 확인한다. CSP는 처음엔 Report-Only로 영향 없이 위반만 수집한다확대

Nginx
# /etc/nginx/conf.d/security-headers.conf
# 또는 server 블록 내부에 추가

# Clickjacking 방지 — iframe에서 이 페이지를 로드 차단
add_header X-Frame-Options "SAMEORIGIN" always;

# MIME 스니핑 방지 — 브라우저가 Content-Type을 변경하지 못하게 함
add_header X-Content-Type-Options "nosniff" always;

# 구형 브라우저용 XSS 필터 (최신 브라우저는 CSP로 대체)
add_header X-XSS-Protection "1; mode=block" always;

# HSTS — HTTPS 연결 강제 (1년)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Content Security Policy — 리소스 출처 제한
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" always;

# Referrer Policy — 외부 링크 클릭 시 Referer 헤더 제어
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Permissions Policy — 브라우저 기능 접근 제한 (카메라, 마이크 등)
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

always 키워드를 붙여야 4xx, 5xx 에러 응답에도 헤더가 포함됩니다.

헤더별 목적 요약:

헤더방어 대상
X-Frame-OptionsClickjacking (악성 iframe으로 클릭 유도)
X-Content-Type-OptionsMIME 스니핑 기반 공격
Strict-Transport-SecuritySSL Stripping (HTTP 다운그레이드)
Content-Security-PolicyXSS (인라인 스크립트 실행)
Referrer-Policy민감한 URL 정보 외부 노출
로컬 또는 서버
# 보안 헤더 적용 확인
curl -I https://example.com \
  | grep -E "X-Frame|X-Content|Strict|Content-Security|Referrer"

서버 보안 점검 체크리스트 — 최소 보안 기준확대

CORS 설정

💡개념

안전한 CORS 정책 구성

CORS(Cross-Origin Resource Sharing)는 API를 어떤 도메인에서 호출할 수 있는지를 제어합니다. 개발 편의를 위해 *로 열어두고 운영에 그대로 배포하는 경우가 흔합니다.

Nginx
# 특정 출처만 허용하는 CORS 설정
map $http_origin $cors_origin {
    default "";
    "https://www.example.com"     $http_origin;
    "https://app.example.com"     $http_origin;
    "https://admin.example.com"   $http_origin;
}

server {
    location /api/ {
        if ($cors_origin) {
            add_header "Access-Control-Allow-Origin" $cors_origin always;
            add_header "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE, OPTIONS" always;
            add_header "Access-Control-Allow-Headers" "Authorization, Content-Type" always;
            add_header "Access-Control-Allow-Credentials" "true" always;
        }

        # Preflight 요청 처리
        if ($request_method = "OPTIONS") {
            add_header "Access-Control-Max-Age" 1728000;
            add_header "Content-Length" 0;
            return 204;
        }

        proxy_pass http://localhost:8080;
    }
}

map 지시어로 허용 출처 목록을 관리하면, 새 도메인 추가가 목록에 한 줄 추가로 끝납니다.

CORS 동작 흐름과 안전한 구성 — CORS는 API를 어떤 도메인에서 호출할 수 있는지 제어한다. 브라우저(https://app.example.com)가 실제 요청 전에 ① OPTIONS 프리플라이트를 보내면 ② 서버가 허용 출처 목록(map)에 있는지 확인하고 ③ 목록에 있으면 Access-Control-Allow-Origin 등 헤더를 붙여 실제 요청을 허용한다(목록 밖 출처엔 헤더가 없어 차단). 서버가 붙이는 헤더: Access-Control-Allow-Origin(* 대신 map으로 특정 출처만)·Allow-Methods·Allow-Headers·Allow-Credentials, 프리플라이트는 Max-Age·return 204. 개발 편의로 Allow-Origin: * 를 열고 운영에 그대로 배포하는 사고가 흔한데, 특히 Allow-Credentials: true와 함께면 인증 쿠키까지 실려 위험하다. CORS는 브라우저 보호 장치일 뿐 서버 자체 인증·인가를 대체하지 않는다확대

Cookie 보안 설정

💡개념

세션 쿠키 보안 강화

쿠키에 보안 속성을 설정하지 않으면 세션 하이재킹, XSS를 통한 쿠키 탈취 위험이 있습니다. 세 가지 속성을 반드시 설정해야 합니다.

Nginx
# Nginx에서 Proxy를 통해 쿠키에 보안 속성 추가
# (애플리케이션이 Set-Cookie를 발급할 때 속성이 없으면 Nginx에서 추가)
proxy_cookie_flags ~ secure httponly samesite=strict;

애플리케이션 레벨(Spring Boot 예시):

YAML
# application.yml
server:
  servlet:
    session:
      cookie:
        secure: true       # HTTPS에서만 전송
        http-only: true    # JavaScript에서 접근 불가 (XSS 방어)
        same-site: strict  # 외부 사이트 요청에 쿠키 미포함 (CSRF 방어)

쿠키 보안 속성 의미:

속성방어 대상효과
Secure평문 전송HTTP 요청에서 쿠키 미전송
HttpOnlyXSS 탈취JavaScript document.cookie 접근 차단
SameSite=StrictCSRF외부 사이트에서 발생한 요청에 쿠키 미포함

세션 쿠키 보안 3속성 — Secure·HttpOnly·SameSite. Secure는 평문 전송 가로채기를 막는다 — HTTPS 연결에서만 쿠키를 전송해(HTTP 요청엔 미전송) 공용 Wi-Fi 등에서 평문으로 오가는 세션 쿠키 스니핑을 방어한다. HttpOnly는 XSS 쿠키 탈취를 막는다 — JavaScript의 document.cookie 접근을 차단해 XSS로 삽입된 스크립트가 세션을 훔치는 것을 방어한다. SameSite=Strict는 CSRF를 막는다 — 외부 사이트에서 발생한 요청에는 쿠키를 미포함해 악성 사이트가 사용자의 로그인 쿠키를 실어 위조 요청을 보내는 것을 방어한다. 세 속성은 각각 다른 공격(가로채기·XSS·CSRF)을 막으므로 하나만 걸어선 부족하고 셋을 함께 건다. 애플리케이션(Spring Boot server.servlet.session.cookie) 또는 Nginx proxy_cookie_flags로 설정한다확대

TLS Hardening

💡개념

TLS 버전과 암호화 스위트 강화

TLS 설정이 취약하면 구버전 프로토콜과 약한 암호화 알고리즘을 이용한 공격에 노출됩니다. 현재 권장 설정은 TLS 1.2 + 1.3만 허용하고 구버전과 약한 cipher를 제거하는 것입니다.

Nginx
# /etc/nginx/conf.d/ssl.conf (또는 server 블록 내)

# TLS 버전 — 1.2와 1.3만 허용 (1.0, 1.1 제거)
ssl_protocols TLSv1.2 TLSv1.3;

# 강화된 Cipher Suite (ECDHE 기반, 순방향 암호화)
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';

# 서버가 cipher 선택권을 가짐 (클라이언트가 약한 것 선택하지 못하도록)
ssl_prefer_server_ciphers on;

# ECDH 파라미터 최적화
ssl_ecdh_curve secp384r1;

# OCSP Stapling (인증서 유효성 검증 빠르게)
ssl_stapling on;
ssl_stapling_verify on;

# DH 파라미터 (Diffie-Hellman 키 교환)
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
로컬 터미널
# DH 파라미터 생성 (최초 한 번만)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

# 설정 적용 후 TLS 버전 확인
openssl s_client -connect example.com:443 -tls1   2>&1 | grep "handshake failure"
# TLS 1.0이 비활성화됐으면 handshake failure 출력됨

openssl s_client -connect example.com:443 -tls1_2 2>&1 | grep "Cipher"
# TLS 1.2는 정상 접속됨

TLS 하드닝 — 취약 vs 강화 설정 비교. 취약한 설정은 TLS 1.0/1.1을 허용(구버전 프로토콜, POODLE·BEAST 같은 알려진 취약점)하고 약한 cipher(RC4·3DES)를 두며 클라이언트가 cipher를 선택하게 두어(공격자가 약한 cipher로 협상 유도) 구버전·약한 알고리즘 공격에 노출된다. 강화된 설정은 ssl_protocols로 TLS 1.2/1.3만 허용하고, ECDHE 기반 강한 cipher로 순방향 암호화(Forward Secrecy)를 제공하며, ssl_prefer_server_ciphers on으로 서버가 cipher 선택권을 갖고, OCSP Stapling과 DH params로 인증서 검증·키 교환을 강화한다. 순방향 암호화는 세션마다 임시 키를 써 서버 개인키가 나중에 유출돼도 과거 세션을 복호화할 수 없게 한다. 검증은 openssl s_client -tls1로 handshake failure가 나오는지(TLS 1.0 비활성) 확인한다확대

실습

1현재 보안 헤더 설정 상태 확인

운영 서버 또는 테스트 서버에서 보안 헤더 적용 여부를 확인합니다. 헤더가 없으면 출력이 비어있습니다. 적용 후 다시 실행해 각 헤더가 응답에 포함됐는지 확인합니다.

curl -s -I https://example.com | grep -E 'X-Frame-Options|Strict-Transport|Content-Security|X-Content-Type'
🔍실행 후 확인할 것
  • Strict-Transport-Security 헤더를 먼저 확인 — max-age가 31536000(1년) 이상이면 정상. 0이거나 없으면 HSTS 미적용으로 SSL 다운그레이드 공격에 취약
  • Server 헤더에 "nginx/1.18.0" 같이 버전이 노출되면 즉각 server_tokens off 적용 — 버전 정보는 CVE 공격자의 첫 번째 타겟. 헤더가 없거나 "nginx"만 표시되면 정상
  • X-Frame-Options 없고 Content-Security-Policy도 없는 조합이면 — 클릭재킹 공격에 완전 무방비 상태. 두 헤더가 모두 누락된 서버는 보안 등급 F
2TLS 버전 및 인증서 확인

OpenSSL 클라이언트로 실제 TLS 연결을 맺어 사용 중인 프로토콜 버전과 Cipher Suite를 확인합니다. TLSv1.3 또는 TLSv1.2가 출력되어야 정상이며, TLSv1이 출력되면 구버전이 활성화된 것입니다.

openssl s_client -connect example.com:443 -showcerts 2>/dev/null | grep -E 'Protocol|Cipher|subject'
🔍실행 후 확인할 것
  • Protocol 줄을 먼저 확인 — TLSv1.3 또는 TLSv1.2면 정상. TLSv1.1 이하가 나오면 nginx ssl_protocols 설정에 구버전이 포함된 것으로 즉시 제거
  • Cipher Suite에 ECDHE가 있으면 순방향 암호화(PFS) 지원 — ECDHE가 없고 RSA만 있으면 서버 키 유출 시 과거 트래픽 복호화 가능. ECDHE 포함 cipher suite로 우선순위 설정 필요
  • openssl s_client -tls1로 접속 시도 시 "handshake failure"가 나야 정상 — CONNECTED가 나오면 TLS 1.0이 활성화된 것으로 PCI DSS 위반. RC4 또는 3DES가 협상되면 CVSS 7.5 이상 취약점

트러블슈팅

원인: includeSubDomains와 긴 max-age를 한 번에 설정하면, 브라우저 캐시에 저장된 HSTS 정책 때문에 HTTP로 접속을 시도하는 모든 서브도메인이 즉시 차단됩니다. HSTS는 한번 브라우저에 저장되면 만료까지 수정이 어렵습니다.

로컬 또는 서버
# 현재 HSTS 설정 확인
curl -I https://example.com | grep Strict

# 안전한 HSTS 적용 순서:
# 1단계: 짧은 max-age로 테스트 시작 (300초 = 5분)
add_header Strict-Transport-Security "max-age=300" always;

# 2단계: 모든 서브도메인이 HTTPS로 전환됐는지 확인
# 내부 서비스 포함 전수 점검

# 3단계: 문제 없으면 max-age 늘리기 (86400 = 1일)
add_header Strict-Transport-Security "max-age=86400" always;

# 4단계: 최종 적용 (31536000 = 1년, includeSubDomains 추가)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# 브라우저에서 HSTS 캐시 삭제 방법 (Chrome):
# chrome://net-internals/#hsts → Delete domain security policies

원인: CSP의 default-src 'self' 또는 script-src 'self' 정책은 인라인 스크립트(<script> 태그 내 코드)와 eval()을 기본으로 차단합니다.

로컬 터미널
# 브라우저 개발자 도구에서 CSP 위반 확인
# Console 탭에서:
# "Refused to execute inline script because it violates the following Content Security Policy directive: 'script-src 'self''"

# 해결 방법 1: unsafe-inline 일시 허용 (보안 수준 낮음, 임시 방편)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'";

# 해결 방법 2: nonce 방식 (권장)
# Nginx에서 매 요청마다 랜덤 nonce 생성 (lua-nginx-module 필요)
# 또는 애플리케이션에서 nonce를 생성해 헤더와 스크립트 태그에 동일하게 삽입

# 해결 방법 3: 인라인 스크립트를 외부 파일로 분리 (가장 안전)
# <script>...</script> → <script src="/js/app.js"></script>

# CSP Report-Only 모드로 먼저 테스트 (차단 없이 위반 로그만)
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'" always;
# 위반 사항이 콘솔에만 기록되고 실제로 차단하지 않음 — 프로덕션 영향 없이 테스트 가능

심화 — 헤더가 조용히 사라지고, 침해가 노이즈에 묻히는 곳

💡개념

심화: add_header는 상속되지 않는다 — 보안 헤더가 특정 경로에서 사라지는 이유

앞에서 보안 헤더를 server 레벨에 넣고, CORS는 location /apiadd_header로 붙였습니다. 그런데 이 둘을 함께 쓰면 정작 /api 응답에서 보안 헤더가 통째로 사라집니다 — 배포 후 스캐너가 "일부 경로만 헤더 누락"으로 잡아내는 흔한 사고입니다.

  • Nginx add_header는 병합이 아니라 치환 상속이다: 안쪽 블록(location)에 add_header하나라도 정의돼 있으면, 바깥 블록(server·http)의 add_header들은 그 location에서 전부 무효가 됩니다. 상속이 '더하기'가 아니라 '현재 레벨에 정의가 있으면 상위를 통째로 버림'으로 동작하기 때문입니다.
  • 그래서 CORS 한 줄이 보안 헤더 전부를 지운다: /apiAccess-Control-Allow-Origin 하나를 add_header로 붙이는 순간, server 레벨의 X-Frame-Options·Strict-Transport-Security·Content-Security-Policy/api 응답에서 사라집니다. curl -I /엔 보이는데 curl -I /api/...엔 없습니다.
  • always도 이 문제를 못 고친다: always 플래그는 4xx·5xx 에러 응답에도 헤더를 붙일지 여부일 뿐, 블록 간 상속 규칙과는 무관합니다. expires, proxy_set_header 같은 다른 배열형 지시어도 같은 치환 상속을 따릅니다.
  • 다음 단계 — 재선언하고 검증한다: 보안 헤더를 한 파일로 만들어(include /etc/nginx/snippets/security-headers.conf;) add_header를 재정의하는 모든 location에서 다시 include하거나, 상속 규칙이 다른 headers-more 모듈의 more_set_headers를 씁니다. 무엇보다 배포 후 루트뿐 아니라 /api·에러 페이지 같은 주요 경로마다 curl -I로 헤더 실재를 직접 확인해야 합니다.
로컬 또는 서버
# 루트엔 보안 헤더가 보이는데
curl -sI https://example.com/ | grep -E 'X-Frame|Strict-Transport|Content-Security'
# add_header(CORS)를 붙인 /api 응답엔 사라졌는지 확인
curl -sI https://example.com/api/health | grep -E 'X-Frame|Strict-Transport|Content-Security'

'설정했다'와 '모든 응답에 실제로 붙는다'는 다릅니다 — 보안 헤더는 반드시 경로별로 검증하는 것이 운영의 기본입니다.

상황: auth.log에 SSH 로그인 실패가 하루 수천 건씩 찍혀 팀은 '늘 있는 배경 소음'으로 무시해 왔습니다. 그런데 한 서버에서 크론 시간이 아닌 새벽에 대량 데이터가 외부로 나갔습니다. 돌아보니 로그인 실패도, 접근 로그도, sudo 기록도 각각 떼어 보면 정상 범위처럼 보였습니다.

원인: 단일 로그 관점의 한계입니다. 실패 로그 하나하나는 무해하지만, '한 IP에서 여러 계정으로 실패를 뿌리는 스프레이 → 그중 한 계정에서 Accepted(성공) → 곧바로 sudo로 권한 상승 → 평소와 다른 시각의 대량 아웃바운드'라는 시퀀스를 시각순으로 이어 보면 침해가 드러납니다. 개별 임계치만 감시하면 이 시퀀스가 노이즈에 묻힙니다. 특히 스프레이 뒤의 단 한 번의 '성공'이 진짜 위험 신호인데, 다들 '실패'만 세고 있었습니다.

진단: 실패·성공·권한상승을 IP·계정별로 상관지어 하나의 타임라인에 올립니다.

로컬 터미널
# 1) 한 IP가 여러 계정에 실패를 뿌렸는지(스프레이) — IP별 실패 집계
grep "Failed password" /var/log/auth.log | grep -oE "from [0-9.]+" | sort | uniq -c | sort -rn | head
# 2) 그 IP가 결국 성공했는지 — 같은 IP의 Accepted 확인
grep "Accepted" /var/log/auth.log | grep "198.51.100.7"
# 3) 성공 직후 sudo 권한 상승과 비정상 시각 활동을 같은 타임라인으로 대조
grep -E "sudo:.*COMMAND=" /var/log/auth.log | grep "deploy"

해결: 개별 임계치 알림에 더해 '실패 스프레이 → 성공 → 권한 상승 → 비정상 아웃바운드'로 이어지는 시퀀스를 하나의 상관 규칙으로 감시합니다. auth·sudo·nginx·방화벽 로그를 중앙으로 모아(Filebeat/rsyslog 기반 로그 수집 파이프라인 구성) 시각축으로 상관분석(SIEM)하는 것이 핵심입니다. 이 상관이 가능하려면 이 모듈에서 다룬 계정 보안 — PermitRootLogin no + 개인 계정 + sudo — 로 '누가 무엇을 했는지'가 계정별로 남아 있어야 합니다. 즉 이 모듈의 계정 보안은 예방일 뿐 아니라 탐지의 전제입니다. 그리고 성공 로그인은 실패보다 오히려 더 주의 깊게 봐야 합니다.

💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

보안팀의 취약점 스캔 결과를 받았을 때, 또는 PCI-DSS/ISO27001 감사를 준비할 때 이 설정들이 체크리스트에 들어갑니다. 코드 변경 없이 Nginx 설정 몇 줄로 보안 점수를 크게 올릴 수 있습니다.

보안 설정 우선순위 (긴급→일반):

즉시 적용 (P0):
□ TLS 1.0/1.1 비활성화
□ 관리자 경로 IP 제한
□ 불필요한 HTTP 메서드(TRACE, DELETE) 차단

빠른 시일 내 (P1):
□ HSTS 설정 (짧은 max-age로 테스트 후 확장)
□ X-Frame-Options, X-Content-Type-Options
□ HttpOnly, Secure 쿠키 속성

품질 개선 (P2):
□ Content-Security-Policy (Report-Only로 시작)
□ CORS 정책 구체화
□ Nginx server_tokens off (버전 정보 숨기기)
로컬 또는 서버
# Nginx 버전 정보 숨기기 (http 블록에 추가)
# server_tokens off;

# 설정 후 확인
curl -I https://example.com | grep Server
# "Server: nginx" 만 나오고 버전 번호 없어야 함

명령어·단축키 빠른 참조

계정·보안 헤더·TLS를 점검하고 검증할 때 쓰는 명령을 모았습니다.

명령어/단축키용도자주 쓰는 예
grep PermitRootLogin + sed -iroot 직접 로그인 차단sshd_configno로 후 systemctl reload sshd
awk -F: '$3>=1000'일반 사용자 계정 목록awk -F: '$3>=1000{print $1,$3,$7}' /etc/passwd
usermod -L / passwd -S계정 잠금·상태 확인passwd -S user (L=Locked)
lastb / last -F로그인 실패·성공 이력lastb | head -20
curl -I응답 보안 헤더 확인curl -sI https://host | grep -E 'X-Frame|Strict-Transport|Content-Security'
curl -sI /api경로별 헤더 실재 검증add_header 치환 상속으로 /api엔 사라졌는지
openssl s_client -connect … -tls1구버전 TLS 비활성 확인handshake failure면 정상(TLS 1.0 차단)
openssl s_client -tls1_2 | grep Cipher협상 프로토콜·cipher 확인ECDHE 있으면 순방향 암호화(PFS)
openssl dhparamDH 파라미터 생성openssl dhparam -out dhparam.pem 2048
grep "Failed password" | grep -oE "from …" | sort | uniq -cIP별 실패 스프레이 집계무차별 대입 IP 탐지
grep "Accepted"스프레이 뒤 성공 로그인 확인grep Accepted auth.log | grep <IP> — 진짜 위험 신호
grep -E "sudo:.*COMMAND="성공 직후 권한 상승 추적계정별 감사 추적
server_tokens offnginx 버전 정보 숨김curl -I | grep ServerServer: nginx만 확인

관련 모듈로 더 깊이:

다음 모듈에서는 WAF/WAAP로 웹 공격을 차단하고, CDN 캐시를 관리하는 실무를 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

서비스에 Nginx에서 HTTP → HTTPS 리다이렉트를 설정했습니다. 하지만 보안팀에서 'SSL stripping 공격에 여전히 취약하다'고 지적했습니다. Nginx 리다이렉트 외에 추가로 필요한 보안 설정은?

Q2

프론트엔드(React)와 백엔드 API가 도메인이 달라 CORS 오류가 발생합니다. 개발 편의를 위해 백엔드에서 Access-Control-Allow-Origin: * 로 설정하면 어떤 문제가 생기는가?

Q3

서비스에 HTTPS가 적용됐는데 보안팀에서 '세션 쿠키가 HTTP 요청에도 전송되고 있다'고 지적했습니다. 이 문제를 해결하려면 세션 쿠키에 어떤 속성을 추가해야 하는가?

Q4

TLS 1.0/1.1을 비활성화해야 하는 이유는?

Q5

운영 서버에 root로 직접 SSH 접속하던 관행을 막고, 일반 계정 로그인 후 sudo를 쓰게 바꾸는 이유는?

Q6

HSTS·Secure 쿠키 외에, 클릭재킹(iframe 삽입)과 MIME 스니핑 같은 브라우저 측 공격을 막으려고 응답 헤더로 강제하는 정책은?

Q7

[심화] server 블록에 X-Frame-Options·HSTS 등 보안 헤더를 넣었다. curl -I로 / 는 헤더가 보이는데, add_header로 CORS를 붙인 location /api 응답에는 보안 헤더가 하나도 없다. 원인은?

Q8

[심화] SSH 로그인 실패가 매일 수천 건이라 노이즈로 무시해 왔는데 한 서버에서 침해가 일어났다. 개별 로그로는 못 잡던 이 침해를 탐지하는 가장 효과적인 방법은?

0 / 8 답변

🧪 실습으로 확인하기

서버 침해 의심 대응 — 격리·조사·증거 보존

고급

새벽에 의심스러운 프로세스와 아웃바운드 트래픽이 발견됐다. 침해 대응의 원칙은 "끄지 말고 격리, 지우지 말고 보존"이다. 의심 프로세스·네트워크 연결·지속성(크론/서비스/SSH 키)을 조사하고, 네트워크를 격리하면서 휘발성 증거를 보존하고, 침투 경로 단서를 로그에서 찾는다. 성급한 재부팅·삭제가 왜 최악인지 체득한다.

50📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요