Repository navigation
fix: 내부 전용 API 의 외부 노출 차단을 nginx 설정 코드에 반영 #76
Description
어떤 작업인가요?
내부 전용 API(/internal/**)를 외부 인터넷에서 호출할 수 없도록 하는 nginx 차단 규칙을, 운영 인스턴스에는 이미 수동 적용했고 코드에는 아직 반영되지 않은 상태입니다. 인스턴스가 재생성되면 유실되므로 modules/app_stack/scripts/nginx_setup.sh.tftpl 에 반영합니다.
배경
백업 실패 알림 API(POST /internal/alarms/db-backup)가 v2.6.1 로 prod 에 배포되면서, 이 경로가 외부 인터넷에 노출되었습니다.
서버의 SecurityConfiguration 은 /internal/** 을 permitAll() 로 두고 공유 토큰(internal-alarm.token)으로만 인증합니다. 즉 nginx 차단이 없으면 토큰 하나가 유일한 방어선입니다.
적용 전 외부에서 확인한 결과입니다.
POST https://api.solid-connection.com/internal/alarms/db-backup
→ 401 {"message":"요청을 인증할 수 없습니다."}
401 은 요청이 nginx 를 통과해 애플리케이션까지 도달했다는 뜻입니다.
DB EC2 는 API 서버의 app 포트(8080/9080)로 직접 요청하므로 nginx 를 거치지 않습니다. 따라서 외부 443 경로만 막으면 알림 기능에는 영향이 없습니다.
운영 인스턴스 반영 현황 (완료)
API EC2(i-016e57974be976a59)의 /etc/nginx/sites-available/solid-connection-server 에 아래 블록을 추가하고 nginx -t 통과 후 graceful reload 했습니다.
# 3차 차단: 내부 전용 API 는 VPC 내부에서 app 포트로 직접 호출하므로 외부 노출을 막는다
# DB EC2 의 백업 실패 알림은 8080/9080 직접 경로를 쓰므로 이 차단에 영향받지 않는다
location ^~ /internal {
return 444;
}443 server 블록(server_name api.solid-connection.com) 안, dotfile 차단 블록과 location / 사이에 위치합니다.
설계 결정
| 항목 | 결정 | 이유 |
|---|---|---|
| 응답 | return 444 |
기존 차단(IP 직접 접근, 확장자 스캔, dotfile)과 동일한 컨벤션. deny all(403)은 "경로는 있지만 금지" 를 알려주는데, 444 는 응답 없이 연결을 끊어 경로 존재 자체를 숨김 |
| 접두사 | /internal (trailing slash 없음) |
^~ /internal/ 로 두면 /internal 이 매칭되지 않아 앱까지 도달함. 서버의 /internal/** 범위와 맞춤 |
| 위치 | location / 앞 |
^~ 는 prefix priority match 라 정규식 location 보다 먼저 평가됨 |
검증 결과
외부 인터넷에서 차단됨 (000 = 응답 없이 연결 종료):
GET /internal → 000
GET /internal/ → 000
GET /internal/alarms/db-backup → 000
GET /internal/anything → 000
POST /internal/alarms/db-backup → 000
일반 경로는 그대로 앱으로 전달됨 (500 은 앱이 반환한 응답):
GET / → 500
GET /universities → 500
GET /actuator/health → 500
DB EC2 에서 직접 호출하는 알림 경로는 영향 없음:
health 9081 → {"status":"UP"}
알림 9080 → 401
되돌릴 필요가 있으면 /root/nginx-sc-before-internal-block.bak 에 적용 전 파일이 있습니다.
작업 내용
-
modules/app_stack/scripts/nginx_setup.sh.tftpl의 443 server 블록에location ^~ /internal { return 444; }를 추가합니다. - dotfile 차단 블록과
location /사이에 두고, 기존 차단 주석 스타일(1차/2차 차단)에 맞춰 설명을 남깁니다. - stage 환경도 같은 모듈을 사용하므로 함께 적용되는지 확인합니다.
-
terraform plan에서 인스턴스 교체가 발생하지 않는지 확인합니다.
주의: 코드 반영만으로는 운영에 적용되지 않습니다
aws_instance.api_server 의 lifecycle.ignore_changes 에 user_data 와 user_data_base64 가 있고, cloud-init 의 scripts-user 는 once-per-instance 로 동작합니다. 따라서 nginx_setup.sh.tftpl 을 수정해도 plan 에 아무 변화가 나타나지 않고 실행 중인 인스턴스에도 반영되지 않습니다.
이 이슈의 목적은 인스턴스가 재생성되는 시점에 설정이 유지되도록 하는 것입니다. 운영 반영은 위와 같이 이미 수동으로 완료했습니다.
같은 성격의 괴리가 다른 곳에도 있습니다.
- 코드의
db_ec2_ami_id(ami-0501a03cd31b53e82)와 실제 인스턴스 AMI(ami-0d45c1f91adea31b8) mysql_tuning.cnf(변경해도 반영되지 않음, binlog 설정이 MySQL 기본값에 의존)
런타임 설정을 배포 경로로 옮기는 작업은 별도 주제이며, 현재 튜닝 수요가 없어 보류한 상태입니다.
완료 조건
nginx_setup.sh.tftpl에 차단 블록이 포함되어 있습니다.- 새 인스턴스를 기동했을 때
/internal접근이 외부에서 차단됩니다. - DB EC2 의 백업 실패 알림이 정상 동작합니다.
Refs #66
Activity
Hexeong commentedon Aug 30, 2026
정정: "코드 반영만으로는 운영에 적용되지 않습니다" 는 사실과 다릅니다
본문의 마지막 문단은 nginx 가
user_data로 배포된다는 전제를 두고 있는데, 실제 배포 경로가 다릅니다.data.cloudinit_config.app_init에 들어 있는 것은docker_setup.sh하나뿐이고, nginx 는modules/app_stack/ec2.tf의null_resource.update_nginx가 SSM RunShellScript 로 실행합니다. 트리거는 템플릿 렌더 결과의 sha256 입니다.state 로 확인했습니다.
script_hash4b9df925…758c5a4b9df925…758c5a(일치)553605dd…d8d07a553605dd…d8d07a(일치)따라서
nginx_setup.sh.tftpl을 수정하면 해시가 바뀌고 →null_resource가 replace 되고 → apply 때 SSM 으로 nginx 가 재설정·reload 됩니다. plan 에 변화가 나타나고, 인스턴스 교체 없이 운영에 반영됩니다.거꾸로 말하면 지금이 더 위험한 상태였습니다. 코드에
/internal이 없는 채로 다른 이유로update_nginx가 한 번이라도 트리거되면 수동 차단이 그대로 사라집니다.추가로 확인한 것
운영 설정과 코드의 차이는 정확히 6줄뿐입니다.
nginx_setup.sh.tftpl을 prod 변수로 렌더해 나오는 conf 와/etc/nginx/sites-available/solid-connection-server를 기계 대조했고, 차이가 이 이슈의location ^~ /internal블록 하나임을 확인했습니다. 다른 드리프트는 없습니다.롤백 파일 경로가 본문과 다릅니다.
/root/nginx-sc-before-internal-block.bak은 현재 존재하지 않습니다. 대신sites-available/solid-connection-server.pre-fix-20260219-1033이 남아 있는데, 이쪽은 #37(upstream 분리) 이전의 구버전이라 롤백용으로 쓸 수 없습니다. 되돌릴 일이 생기면 해당 블록만 제거하고 reload 하는 편이 안전합니다.작업 범위 조정
체크리스트에 없던 항목을 하나 추가했습니다.
null_resource.update_nginx는 트리거가 스크립트 해시뿐이라 인스턴스가 교체되어도 SSM 명령이 실행되지 않습니다.user_data에는 docker 설치만 있어 새 인스턴스는 nginx 없이 뜨게 되므로, 이 이슈가 목표하는 "재생성 시점에 설정 유지" 가 현재 코드로는 성립하지 않습니다.lifecycle.replace_triggered_by로 함께 해결했습니다.PR: #82