본문으로 건너뛰기

"NGINX" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

모든 태그 보기

NGINX Rift 힙 오버플로우

· 약 6분

먼저, 무엇이 터지나

rewrite 한 줄에 이름 없는 PCRE 캡처($1, $2)와 대체 문자열의 물음표(?)가 함께 들어가면, 비인증 공격자가 HTTP 요청 한 번으로 워커의 힙을 부숴요. CVSS v4.0 9.2 Critical, 2008년 NGINX 0.6.27부터 18년간 조용히 있었던 결함이에요.

F5와 depthfirst가 2026-05-13에 공동 공개했고, 별명은 NGINX Rift입니다. 사흘 뒤인 5월 16일부터 VulnCheck canary가 실 익스플로이트 시도를 잡기 시작했습니다. 운영자는 즉시 패치 또는 임시 완화 둘 중 하나는 골라야 합니다.

18년 묵은 결함이라는 의미

src/http/ngx_http_script.c에 있는 결함입니다. 2008년 NGINX 0.6.27에 처음 들어간 후 지금까지 모든 빌드가 영향권입니다. 단일 함수 한 줄짜리 회귀가 아니라, 스크립트 엔진의 설계 단계에서 두 단계 호출 사이의 상태 누출이 그대로 굳어 버린 형태입니다.

지금까지 익숙했던 NGINX 취약점은 보통 모듈 단위였고 영향 버전 범위도 1~2년 정도였습니다. Rift는 0.6.27 ~ 1.30.0, NGINX Plus는 R32 ~ R36까지 한 줄로 묶입니다. 같은 코드 경로를 그대로 가져다 쓴 fork와 다운스트림도 똑같이 영향을 받습니다.

영향 받는 버전과 해결 버전

제품영향 받는 버전해결 버전
NGINX PlusR36 P4 미만R36 P4 이상
NGINX PlusR35 / R34 / R33지원 라인업으로 마이그레이션
NGINX PlusR32 P6 미만R32 P6 이상
NGINX Open Source1.30.1 미만 (≥ 0.6.27)1.30.1 (stable) / 1.31.0 (mainline)
NGINX Open Source0.9.7 이하지원 라인업으로 마이그레이션

R35, R34, R33은 별도 패치 빌드가 없습니다. 지원 중인 라인(R36 P4 또는 R32 P6)으로 마이그레이션이 답입니다. F5 advisory는 K000161019, CVE 메타데이터는 NVD 페이지에 있습니다.

발현 조건

세 가지가 한 자리에서 만나야 합니다.

  1. rewrite 지시문이 있습니다.
  2. 그 뒤에 rewrite, if, set 중 하나가 따라옵니다.
  3. 대체 문자열에 이름 없는 PCRE 캡처($1, $2 …)와 물음표(?)가 같이 들어갑니다.

예를 들어 다음 같은 흔한 패턴이 그대로 함정입니다.

location /old/ {
# 취약: 이름 없는 $1 + 물음표 ?
rewrite ^/old/(.*)$ /new/$1?source=legacy break;
if ($http_user_agent ~ "bot") {
return 403;
}
}

이 자리에서 비인증 공격자가 특정 형태의 HTTP 요청을 한 번 보내면 워커의 힙에 경계 너머 쓰기가 일어납니다. ASLR이 꺼져 있으면 임의 코드 실행이 가능해집니다 — 워커 권한이므로 권한 상승은 그다음 단계지만, 컨테이너/구형 임베디드 환경에서는 그것만으로도 충분히 치명적입니다.

두 단계 스크립트 엔진에서 새는 상태 플래그

NGINX의 스크립트 엔진은 같은 rewrite를 두 번 돕니다. 길이 계산 패스(length pass)에서 출력 버퍼 크기를 정하고, 복사 패스(copy pass)에서 실제 값을 씁니다.

문제는 두 패스 사이에서 is_args 상태 플래그가 그대로 남는다는 점입니다. 길이 계산 단계에서 물음표를 만나 is_args = 1로 켜진 플래그가 복사 단계의 다음 캡처 처리에 누출됩니다. 복사 단계의 ngx_escape_uri()는 켜진 플래그를 보고 인자(query string)용 이스케이프 규칙으로 더 많은 바이트를 써넣습니다 — 길이 계산 단계에서는 다른 규칙으로 짧게 잡아 둔 그 버퍼에 말입니다.

그 결과 heap chunk 경계 너머에 의도된 길이만큼 정확히 흘러넘칩니다. 우연이 아니라 결정론적으로 재현되는 경로입니다.

패치(1.30.1 / 1.31.0)는 두 패스 진입 시 is_args를 명시적으로 reset하는 한 줄이 골자입니다. 18년 동안 묻혀 있던 이유는 발현 조건이 좁기 때문입니다. if / set이 뒤따르는 rewrite 체인에서만 두 패스가 같은 캡처를 두 번 보고, 거기에 이름 없는 캡처와 물음표가 동시에 있어야 합니다. 운영 환경에서 충분히 흔한 패턴인데 fuzz 코퍼스가 거기에 닿지 않았습니다.

임시 완화는 named capture로 교체

패치를 당장 못 올리는 환경에서는 F5와 depthfirst 모두 같은 권고를 합니다. 모든 rewrite에서 이름 없는 캡처를 이름 있는 캡처로 바꿉니다.

# Before (취약)
rewrite ^/old/(.*)$ /new/$1?source=legacy break;

# After (안전)
rewrite ^/old/(?<path>.*)$ /new/$path?source=legacy break;

이름 있는 캡처는 NGINX 스크립트 엔진의 다른 코드 경로를 타고, 거기에는 같은 플래그 누출이 없습니다. 동일한 동작을 유지하면서 결함 경로만 회피합니다.

운영 중인 모든 config 파일에서 이름 없는 캡처 패턴을 검색해 둘 자리만 골라냅니다.

grep -rnE 'rewrite[^;]*\$[0-9]' /etc/nginx/

지금 해야 할 일

  1. 버전을 확인합니다. nginx -v로 정확한 빌드를 봅니다. NGINX Plus는 nginx-plus -V에서 R 라인업이 나옵니다.
  2. 노출도를 평가합니다. 위의 grep 한 줄로 이름 없는 캡처 + 물음표 패턴을 가진 rewrite가 있는지 봅니다. 없으면 시간을 벌 수 있습니다.
  3. 패치를 적용합니다. Open Source는 1.30.1 / 1.31.0, NGINX Plus는 R36 P4 / R32 P6입니다. R33~R35는 지원 라인으로 마이그레이션합니다.
  4. 즉시 패치할 수 없으면 named capture로 우회합니다. 임시 패치 검증은 두 단계 — 정상 트래픽 회귀 테스트와 PoC 변형 요청 차단 확인으로 진행합니다.
  5. ASLR을 확인합니다. cat /proc/sys/kernel/randomize_va_space2인지 봅니다. 0이면 임의 코드 실행 위험이 같이 살아 있습니다.
  6. WAF/CDN 룰을 확인합니다. 앞단에 Cloudflare나 F5 BIG-IP 등이 있으면 벤더의 임시 시그니처가 이미 배포돼 있습니다. Cloudflare는 5월 13일 당일에 managed rule을 풀었습니다.
  7. 로그를 회수합니다. error.log에서 워커 재시작 패턴(worker process ... exited on signal 11)을 5월 13일 이후 기간으로 잡고 봅니다. 평소보다 빈도가 높으면 정찰 시도 흔적일 수 있습니다.

실 익스플로이트 현황

PoC는 DepthFirstDisclosures/Nginx-Rift repo에 공개돼 있습니다. VulnCheck의 canary 시스템이 5월 16일부터 인터넷 노출 NGINX를 향한 실 익스플로이트 시도를 잡기 시작했다. 공개에서 실 공격까지 사흘입니다. Ingress NGINX(K8s)로도 같은 결함이 그대로 전파됩니다. 클러스터 운영자 관점에서는 HeroDevs의 분석이 가장 친절합니다.

정리

항목
CVECVE-2026-42945 (NGINX Rift)
분류CWE-122 Heap-based Buffer Overflow
CVSSv4.0 9.2 Critical / v3.1 8.1 High
공개일2026-05-13
발견자depthfirst (with F5)
도입 시점2008년 NGINX 0.6.27
패치NGINX 1.30.1 / 1.31.0/NGINX Plus R36 P4 / R32 P6
임시 완화이름 없는 PCRE 캡처를 이름 있는 캡처로 교체
실 익스플로이트2026-05-16부터 관측 (VulnCheck)

if/set이 따라붙는 rewrite 체인은 어디서나 자주 써요. 운영하는 NGINX가 한 대라도 외부에 노출돼 있고, rewrite config에 $1 같은 표현이 한 줄이라도 들어 있다면, 지금이 1.30.1 패치를 올릴 가장 좋은 순간이에요.

참고

커스텀 도메인과 SSL

· 약 4분

이 글은 Hugo + PaperMod 블로그 세팅 시리즈의 마지막 글이에요.

  1. Hugo + PaperMod로 기술 블로그 만들기
  2. NCP 서버에 Hugo 블로그 배포하기
  3. 커스텀 도메인 연결과 Let's Encrypt SSL 설정 ← 현재 글

진행 순서

도메인과 SSL 설정에는 순서가 있습니다. 순서가 바뀌면 인증서 발급이 실패합니다.

도메인 구매 → DNS A 레코드 설정 → Nginx server_name 변경
→ hugo.toml baseURL 변경 → certbot SSL 발급 → 재배포

1. 도메인 구매

도메인 구매처는 여러 곳이 있습니다.

업체특징
Cloudflare원가 판매, DNS 관리 편리
가비아국내 최대, 한글 지원
NCP서버와 같은 곳에서 관리

저는 Cloudflare를 선택했어요. DNS 관리 UI가 깔끔하고 추가 비용이 없어요.

.dev 도메인 참고사항

.dev는 Google이 관리하는 TLD로, HSTS preload가 적용되어 있습니다.

  • 모든 .dev 도메인은 브라우저에서 HTTPS만 허용됩니다.
  • HTTP로는 접속 자체가 불가능합니다.
  • SSL 설정이 필수입니다.

기술 블로그에 어울리는 도메인이지만, SSL 없이는 쓸 수 없다는 점을 알고 있어야 합니다.

2. DNS 설정

Cloudflare DNS에서 A 레코드를 추가합니다.

TypeNameContentProxy
A@서버IPDNS only

중요: Proxy를 반드시 꺼야 합니다(회색 구름). Cloudflare 프록시가 켜져 있으면 서버 IP가 Cloudflare로 대체되어 Let's Encrypt 인증서 발급이 복잡해집니다.

DNS 전파를 확인합니다.

dig +short dbalog.dev
# 서버IP가 나오면 성공

보통 Cloudflare는 수 분 내로 전파됩니다.

3. Hugo 설정 변경

hugo.toml의 baseURL을 변경합니다.

# 변경 전
baseURL = "https://example.com/"
# 변경 후
baseURL = "https://dbalog.dev/"

이 값은 sitemap, RSS, 메타 태그 등 블로그 전체에서 사용되므로 반드시 변경해야 합니다.

4. Nginx server_name 변경

deploy/nginx/blog.conf:

server_name dbalog.dev; # IP 또는 example.com에서 변경

서버에 반영:

scp deploy/nginx/blog.conf ncp-blog:/etc/nginx/conf.d/blog.conf
ssh ncp-blog "nginx -t && systemctl reload nginx"

5. Let's Encrypt SSL 인증서 발급

certbot 설치

Rocky Linux 9 기본 저장소에는 certbot이 없습니다. EPEL 저장소를 먼저 추가합니다.

ssh ncp-blog "dnf install -y epel-release"
ssh ncp-blog "dnf install -y certbot python3-certbot-nginx"

인증서 발급

certbot의 --nginx 플러그인이 인증서 발급과 Nginx 설정 수정을 한번에 처리합니다.

ssh ncp-blog "certbot --nginx -d dbalog.dev \
--non-interactive \
--agree-tos \
--email your@email.com \
--redirect"
  • --nginx: Nginx 설정을 자동으로 수정
  • --redirect: HTTP→HTTPS 301 리다이렉트 자동 추가
  • --non-interactive: 대화형 프롬프트 없이 실행

성공하면 이런 메시지가 나옵니다.

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/dbalog.dev/fullchain.pem
Key is saved at: /etc/letsencrypt/live/dbalog.dev/privkey.pem
This certificate expires on 2026-07-09.

certbot이 Nginx에 추가한 설정

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/dbalog.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dbalog.dev/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

그리고 HTTP→HTTPS 리다이렉트를 위한 별도 server 블록:

server {
if ($host = dbalog.dev) {
return 301 https://$host$request_uri;
}
listen 80;
server_name dbalog.dev;
return 404;
}

인증서 자동 갱신

Let's Encrypt 인증서는 90일마다 만료됩니다. certbot이 systemd 타이머를 자동 등록하므로 수동 갱신이 필요 없습니다.

갱신 테스트:

ssh ncp-blog "certbot renew --dry-run"

6. 재배포

baseURL이 변경되었으므로 빌드 후 재배포합니다.

./deploy.sh

확인

# HTTPS 접속
curl -s -o /dev/null -w "%{http_code}" https://dbalog.dev/
# 200

# HTTP → HTTPS 리다이렉트
curl -s -o /dev/null -w "%{http_code}" http://dbalog.dev/
# 301

브라우저에서 https://dbalog.dev/에 접속하면 자물쇠 아이콘이 표시됩니다.

시리즈 마무리

3편에 걸쳐 Hugo 블로그를 처음부터 세팅했어요.

  1. Hugo + PaperMod 설치 및 설정
  2. NCP 서버에 Nginx + rsync 배포
  3. 도메인 연결 + SSL 인증서

이제 남은 건 글을 쓰는 것뿐이고, 마크다운 파일을 만들고 ./deploy.sh를 실행하면 끝이에요.

참고 자료

NCP Hugo 블로그 배포

· 약 4분

이 글은 Hugo + PaperMod 블로그 세팅 시리즈의 두 번째 글이에요.

  1. Hugo + PaperMod로 기술 블로그 만들기
  2. NCP 서버에 Hugo 블로그 배포하기 ← 현재 글
  3. 커스텀 도메인 연결과 Let's Encrypt SSL 설정

배포 구조

[로컬 macOS] [NCP Rocky Linux]
content/*.md
↓ hugo --minify
public/ ──── rsync ────→ /var/www/blog/ ←── Nginx 서빙

Hugo가 생성한 정적 파일을 rsync로 서버에 전송하고, Nginx가 서빙하는 단순한 구조입니다. DB도 Node.js도 필요 없습니다.

1. SSH 키 인증 설정

매번 비밀번호를 입력하는 건 번거롭고, 스크립트 자동화도 불가능합니다. SSH 키 인증을 먼저 세팅합니다.

키 생성

ssh-keygen -t ed25519 -C "blog-deploy"
  • ed25519: RSA보다 짧고 안전한 알고리즘
  • passphrase: 비워도 되고, 입력하면 macOS Keychain이 기억해 줍니다

서버에 공개키 등록

ssh-copy-id -i ~/.ssh/id_ed25519.pub root@서버IP

이때 마지막으로 서버 비밀번호를 입력합니다. 이후로는 비밀번호 없이 접속 가능합니다.

SSH config 설정 (선택)

~/.ssh/config에 아래를 추가하면 ssh ncp-blog만으로 접속할 수 있습니다.

Host ncp-blog
HostName 서버IP
User root
IdentityFile ~/.ssh/id_ed25519

2. Nginx 설정 파일

Hugo 정적 파일을 서빙하기 위한 Nginx 설정입니다.

server {
listen 80;
server_name 서버IP;

root /var/www/blog;
index index.html;

# gzip 압축
gzip on;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;

# 정적 파일 캐싱 (30일)
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}

# Hugo 정적 사이트 서빙
location / {
try_files $uri $uri/ =404;
}

# 404 페이지
error_page 404 /404.html;
location = /404.html {
internal;
}

# 보안 헤더
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

핵심 설정:

  • try_files $uri $uri/ =404: 파일 → 디렉토리(index.html) → 404 순서로 탐색
  • gzip: 텍스트 기반 파일을 압축해서 전송 (대역폭 절약)
  • expires 30d: CSS/JS/이미지를 30일 캐싱 (재방문 시 빠른 로딩)

3. 서버 세팅

SSH로 서버에 접속해서 실행합니다.

# Nginx 설치
dnf install -y nginx

# 블로그 디렉토리 생성
mkdir -p /var/www/blog

# Nginx 설정 파일 복사 (로컬에서)
scp deploy/nginx/blog.conf ncp-blog:/etc/nginx/conf.d/blog.conf

# 설정 검사 + 시작
ssh ncp-blog "nginx -t && systemctl enable nginx && systemctl start nginx"

systemctl enable은 서버가 재부팅되어도 Nginx가 자동 시작되게 합니다.

4. 배포 스크립트

매번 명령어를 입력하는 것은 번거로우니 스크립트로 만듭니다.

deploy.sh:

#!/bin/bash
set -e

SERVER_USER="root"
SERVER_HOST="서버IP"
SERVER_PORT="22"
REMOTE_DIR="/var/www/blog"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"

build() {
echo "=== Hugo 빌드 시작 ==="
hugo --minify
echo "=== 빌드 완료 ==="
}

deploy() {
echo "=== 서버 배포 시작 ==="
rsync -avz --delete \
-e "ssh -p ${SERVER_PORT}" \
public/ \
"${SERVER_USER}@${SERVER_HOST}:${REMOTE_DIR}/"
echo "=== 배포 완료 ==="
}

case "${1:-all}" in
build) build ;;
deploy) deploy ;;
all) build && deploy ;;
*) echo "사용법: $0 {build|deploy|all}" ; exit 1 ;;
esac

사용법:

chmod +x deploy.sh

./deploy.sh build # 빌드만
./deploy.sh deploy # 배포만
./deploy.sh # 빌드 + 배포

rsync의 --delete 옵션은 서버에서 로컬에 없는 파일을 삭제합니다. 글을 지웠을 때 서버에도 반영되게 하려면 필요합니다.

--delete 함정: 서버에 수동으로 올려둔 파일(임시 백업, 정적 다운로드 자료 등)도 같이 지워집니다. 빌드 산출물 외의 파일은 같은 디렉토리에 두지 마십시오. 보존이 필요하면 별도 디렉토리(/var/www/blog-extra/)로 분리하거나 --exclude='backups/' 패턴을 추가합니다.

5. NCP ACG (방화벽) 설정

서버에서 curl localhost는 되는데 외부에서 접속되지 않는다면, NCP ACG 설정을 확인해야 합니다.

NCP 콘솔 → Server → ACG → 인바운드 규칙:

프로토콜포트허용 소스
TCP800.0.0.0/0
TCP4430.0.0.0/0

NCP는 OS 레벨 방화벽(firewalld)과 별개로 ACG라는 네트워크 방화벽을 사용합니다. 둘 다 확인해야 합니다.

배포 확인

curl -s -o /dev/null -w "HTTP %{http_code}, %{time_total}s" http://서버IP/
# HTTP 200, 0.021s

다음 글

IP 주소로 접속하는 블로그는 아무래도 불편해요. 다음 글에서는 커스텀 도메인을 연결하고 HTTPS를 설정할게요.

커스텀 도메인 연결과 Let's Encrypt SSL 설정