How to 목록

// blog post

Mac mini 서버 보안 — Tailscale 빼고 모든 인바운드 포트 차단하기

|
macospffirewalltailscalesecurity

공인 IP가 직접 붙어 있다는 것

안녕하세요, 이프입니다.

이전 글에서 Tailscale로 포트 포워딩 없이 Mac mini에 접속하는 방법을 정리했는데요. 한 가지 빠뜨린 게 있었습니다.

제 Mac mini는 공유기 뒤가 아니라 공인 IP가 직접 할당되어 있습니다. ISP에서 브릿지 모드로 연결했거든요. 그래서 Tailscale을 쓰고 있어도, en0 인터페이스에는 모든 포트가 외부에 그대로 노출되어 있었습니다.

nmap으로 스캔해보면 22번(SSH), 그 외 여러 포트가 열려 있다고 나옵니다. Tailscale 덕분에 포트 포워딩은 안 했지만, 포트 자체가 열려 있는 건 별개 문제입니다.

sshd_config의 ListenAddress로는 부족합니다

처음 시도한 방법은 SSH가 Tailscale 인터페이스에서만 리스닝하도록 바꾸는 거였습니다.

# /etc/ssh/sshd_config
ListenAddress 100.64.0.1

그런데 macOS의 sshd는 launchd가 관리합니다. sshd_config를 수정해도 launchd가 별도로 소켓을 열어서, ListenAddress 설정이 무시되는 경우가 있었습니다.

sudo lsof -i :22
# launchd가 0.0.0.0:22를 리스닝하고 있음

launchd plist를 건드리는 방법도 있지만, macOS 업데이트 때마다 초기화될 수 있어서 신뢰하기 어려웠습니다. 결국 방화벽에서 확실하게 차단하기로 했습니다.

macOS pf 방화벽 설정

macOS에는 pf(Packet Filter)라는 BSD 기반 방화벽이 내장되어 있습니다. iptables를 쓰던 분이라면 문법이 좀 다르지만 개념은 비슷합니다.

규칙 파일 작성

sudo vi /etc/pf.anchors/tailscale-only

내용은 이렇습니다:

# 1) en0 아웃바운드는 허용하고, 응답 패킷 추적
pass out quick on en0 keep state

# 2) en0 인바운드 TCP 전부 차단
block in quick on en0 proto tcp to any

# 3) en0 인바운드 UDP도 차단 — 단, Tailscale WireGuard 포트(41641)는 허용
block in quick on en0 proto udp to any port != 41641

pf.conf에 앵커 등록

sudo vi /etc/pf.conf

기존 내용 아래에 한 줄 추가합니다:

anchor "tailscale-only"
load anchor "tailscale-only" from "/etc/pf.anchors/tailscale-only"

적용 및 활성화

# 문법 검사
sudo pfctl -n -f /etc/pf.conf

# 적용
sudo pfctl -f /etc/pf.conf

# pf 활성화 (이미 활성화되어 있으면 생략)
sudo pfctl -e

왜 UDP 41641은 열어두나요

Tailscale은 WireGuard 프로토콜을 사용하는데, 기기 간 직접 연결(direct connection)을 위해 UDP 41641 포트를 씁니다.

이 포트를 막으면 Tailscale이 릴레이(DERP) 서버를 거치게 됩니다. 접속은 되지만 속도가 느려지고, 릴레이 서버 상태에 따라 불안정해질 수 있습니다.

# 직접 연결 확인
tailscale status
# direct 표시가 있으면 41641을 통한 직접 연결

삽질 — pass out keep state를 빠뜨리면

처음에 이렇게만 썼습니다:

block in quick on en0 proto tcp to any
block in quick on en0 proto udp to any port != 41641

인바운드만 막으면 될 줄 알았는데, 규칙을 적용하는 순간 인터넷이 끊겼습니다.

원인은 간단했습니다. Mac mini에서 외부로 요청(예: DNS 쿼리, apt 패키지 다운로드)을 보내면, 응답 패킷이 en0 인바운드로 들어옵니다. 인바운드를 전부 막아버리니 응답도 차단된 거죠.

pass out quick on en0 keep state가 해결해줍니다. keep state가 아웃바운드 연결의 상태를 추적해서, 그 응답 패킷은 인바운드에서 허용합니다. 방화벽의 stateful inspection 기능입니다.

이걸 빠뜨려서 SSH 접속이 끊긴 상태로 Mac mini 앞에 직접 가서 모니터를 연결한 건 비밀입니다.

확인 방법

# pf 상태 확인
sudo pfctl -si | grep Status
# Status: Enabled

# 현재 적용된 규칙 확인
sudo pfctl -a tailscale-only -sr

# 외부에서 포트 스캔 테스트 (다른 기기에서)
nmap -Pn 공인IP주소
# 모든 TCP 포트가 filtered로 나와야 정상

재부팅 후에도 유지하기

macOS는 재부팅하면 pf가 비활성화될 수 있습니다. LaunchDaemon으로 부팅 시 자동 활성화를 걸어두면 됩니다.

plist 파일의 내용은 다음과 같습니다:

파일을 저장한 뒤 로드합니다:

sudo launchctl load /Library/LaunchDaemons/com.pf.tailscale-only.plist

정리

최종 보안 구조를 정리하면 이렇습니다:

  • en0 (공인 IP): TCP 인바운드 전체 차단, UDP는 41641만 허용
  • utun (Tailscale): 제한 없음 — Tailscale ACL에서 접근 제어
  • SSH: Tailscale 네트워크를 통해서만 접속 가능

공인 IP가 직접 할당된 환경이라면, Tailscale만 믿고 방화벽을 안 건드리면 포트가 그대로 열려 있습니다. pf로 en0 인바운드를 확실히 막아두면 외부에서 보이는 포트가 아예 없어집니다.

pass out keep state 빠뜨려서 모니터 들고 달려간 경험은 저 하나면 충분하니, 참고하시길 바랍니다.