DEVELOPMENT NOTE2

OCI 방화벽 규칙을 재부팅 뒤에도 유지하기

외부 HTTP timeout을 host firewall에서 찾아 수정하고 두 차례 reboot로 복구를 확인한 기록

#oracle-cloud#ubuntu#iptables#docker#nginx#reboot#operations
아카이브로 돌아가기

내부 요청은 성공하고 외부 요청만 멈췄다

단일 OCI VM에 Docker Compose로 application과 database를 올리고 Nginx를 HTTP 진입점으로 연결했다. 서버 안에서 Nginx를 거친 API와 application health는 정상이었지만 외부 요청은 timeout이 났다.

text
DNS -> OCI Security List -> Ubuntu firewall -> Nginx -> application

DNS는 instance를 가리켰고 Security List는 TCP 80을 허용했다. Nginx도 port 80에서 listen하고 내부 API는 200이었다. 남은 경계는 VM 안쪽 host firewall이었다. OCI의 Security Listhost firewall은 서로 다른 계층이다.

terminal REJECT 앞에 HTTP를 허용했다

Ubuntu는 UFW가 아니라 nftables backend의 iptables-nft를 사용하고 있었다.

bash
sudo iptables -L INPUT -n --line-numbers

INPUT chain에는 기존 연결과 loopback, SSH 허용 뒤 terminal REJECT가 있었고 TCP 80 규칙은 없었다. Cloud 경계를 통과한 packet이 마지막 거부 규칙에서 막힌 것이 원인이었다.

Chain을 flush하거나 기본 policy를 바꾸지 않고 TCP 80 새 연결만 REJECT 앞에서 허용했다. Database와 application 내부 port는 열지 않았다. 이후 외부 API가 200을 반환했다.

Public ingress를 열기 전 Nginx에서는 /actuator와 하위 경로를 404로 막았다. 외부 root와 trailing slash, health path는 모두 404였고 loopback health는 정상이었다. 이는 Nginx 경계의 결과이며 Spring Security까지 요청이 도달했는지는 뜻하지 않는다.

현재 규칙과 다음 boot의 규칙을 구분했다

iptables로 추가한 규칙은 현재 kernel에만 있다. 조사 당시 netfilter-persistentiptables-persistent는 제거 뒤 설정만 남은 상태였고 별도 로더와 활성 nftables.service는 없었다. 반면 /etc/iptables/rules.v4에는 기존 OCI host policy와 terminal REJECT가 남아 있었다.

이 파일을 부팅 원본으로 다시 사용하되 Docker가 실행 중 동적으로 만드는 체인은 넣지 않았다.

text
rules.v4 -> 운영자가 관리하는 host INPUT policy
Docker -> container와 network에 맞는 runtime chain

현재 전체 ruleset을 iptables-save하면 DOCKER, DOCKER-USER, DOCKER-FORWARD 같은 순간의 컨테이너 상태까지 저장될 수 있다. Docker의 firewall 문서처럼 Docker 동적 체인은 daemon이 다시 만들게 했다.

기존 live ruleset과 파일을 backup하고 rules.v4의 순서를 유지한 채 TCP 80 한 줄만 추가했다. iptables-restore --test로 문법과 기존 SSH 규칙, 중복 부재와 Docker chain 미포함을 확인했다. IPv6는 이번 변경 대상이 아니었다.

Package 설치 뒤 규칙이 두 번 생겼다

Installer가 현재 Docker chain을 자동 저장하지 않도록 IPv4와 IPv6 autosave를 껐다. Package 변화를 미리 확인한 뒤 application과 database를 정상 종료하고 Docker daemon과 socket을 잠시 멈춰 persistence package를 설치했다. Volume은 삭제하지 않았고 유지보수 중 Nginx 502 가능성은 받아들였다.

설치 직후 host 규칙이 두 세트로 중복됐다. 원인은 남아 있던 설정이었다.

text
IPTABLES_RESTORE_NOFLUSH=yes

기존 live rules를 비우지 않고 rules.v4를 추가한 것이다. 이는 reboot 장애가 아니라 설치 직후 검증에서 발견한 적용 차이였다. Docker가 멈춘 상태에서 검증한 파일을 정확히 restore해 한 세트로 정리하고 기존 SSH session과 새 SSH 연결, TCP 80을 확인한 뒤 Docker를 시작했다.

파일 문법 test가 통과해도 live 결과까지 원하는 모양인지는 별도로 확인해야 했다.

Reboot 전에 복구 경로를 확인했다

netfilter-persistent와 Docker, Nginx의 부팅 활성화 상태, 컨테이너 재시작 정책과 rules.v4를 확인했다. 별도 SSH session과 외부 API도 시험했다. 비상 키와 OCI Console 경로, firewall backup과 Boot Volume Backup도 다시 확인했다. Lockout을 없애지는 못해도 돌아갈 선택지를 확보하기 위해서였다.

Reboot 필요 표시의 package도 확인했고 이번 작업에 추가 update를 섞지 않았다.

두 차례 reboot로 HTTP와 HTTPS를 확인했다

첫 reboot 뒤 다음 상태가 자동으로 돌아왔다.

  • TCP 80 규칙이 terminal REJECT 앞에 한 번만 존재
  • Docker 동적 체인 재생성
  • Nginx와 application 실행, database healthy
  • Backend health 정상, 외부 API 200
  • Public Actuator path 404

Docker 체인은 실행 중인 ruleset에는 있었지만 rules.v4에는 들어가지 않았다. 수동 서비스 재시작도 없었다.

이후 TCP 443도 같은 방식으로 live와 persistent rule에 추가했다. Nginx TLS termination과 HTTP→HTTPS redirect, 인증서 갱신 dry-run을 확인하고 두 번째 reboot를 실행했다.

  • TCP 80과 443 규칙 복구
  • Docker와 Nginx, application과 database 자동 시작
  • HTTPS list와 detail API 200
  • HTTP request의 HTTPS redirect
  • Actuator 404와 certificate renewal schedule 유지

실제 domain과 certificate, internal port와 host 정보는 공개하지 않았다. 단일 VM의 계획된 reboot 두 번을 검증한 범위이며 갑작스러운 전원 장애나 failover, 부하와 disaster recovery를 증명하지 않는다.

정상의 기준을 외부 요청까지 넓혔다

문제는 host INPUT chain에 HTTP 허용이 없던 것이었다. Live rule로 끝내지 않고 누가 다음 boot에 복원하는지, Docker 동적 체인과 어떻게 분리되는지 확인했다.

text
OCI Security List -> Cloud ingress
rules.v4 + netfilter-persistent -> VM의 boot-time INPUT policy
Docker + systemd + Nginx + application -> runtime과 service startup

현재 연결된다는 사실은 지금의 live state만 설명한다. Reboot 뒤 firewall과 service가 다시 만들어지고 마지막 외부 요청까지 성공하는지 확인해야 운영 복구가 끝난다는 기준을 세운 작업이었다.