리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.
리눅스 시스템 기초 46 / 50 · Part 5. 네트워크·보안·SOC
실습 환경: Rocky Linux 9 (10.0.0.200) / Ubuntu 22.04 · zone 설정 출력은firewall-offline-cmd(firewalld 2.x)의 실제 출력, 런타임 명령 결과는 형식 예시
이전 글: 45. SSH 구조 이해
44편에서 ss -tulnp로 열린 포트를 봤고, "0.0.0.0에 열려 있으면 방화벽이 없는 한 외부에 노출된다"고 했다. 이번 글은 그 방화벽이다.
41편 그림에서 방화벽은 커널의 netfilter 계층에 있었다. 하지만 netfilter 규칙(nftables)을 직접 작성하는 일은 복잡하다. RHEL 계열은 이를 관리하는 도구로 firewalld 를 기본 제공하고, Ubuntu는 ufw 를 제공한다.
관제에서 firewalld를 알아야 하는 이유는 세 가지다.
firewall-cmd (사용자 명령)
↓ D-Bus
firewalld (데몬, 규칙 관리) ← systemd 서비스 (36·37편)
↓
nftables 규칙 (커널에 적재)
↓
netfilter (커널, 실제 패킷 검사)
firewalld가 멈춰도 이미 커널에 적재된 규칙은 남을 수 있다. 반대로 systemctl stop firewalld로 끄면 규칙이 내려가 모든 포트가 열린 상태 가 된다.
firewalld는 규칙을 zone(구역) 단위로 묶는다. zone은 "이 출처의 트래픽을 얼마나 신뢰할 것인가"를 나타낸다.
| zone | 성격 |
|---|---|
drop | 들어오는 모든 연결을 응답 없이 버림 |
block | 들어오는 연결을 거부 응답과 함께 차단 |
public | 기본 zone. 허용한 서비스만 받음 |
external / dmz / work / home / internal | 용도별 신뢰 수준 |
trusted | 모든 연결 허용 |
패킷이 어느 zone으로 판정되는지는 다음 순서로 정해진다.
| 요소 | 예 | 설명 |
|---|---|---|
| service | ssh, http | 미리 정의된 포트 묶음 (ssh = 22/tcp) |
| port | 8080/tcp | 직접 지정 |
| source | 10.0.0.0/24 | 이 출처를 특정 zone으로 |
| rich rule | rule family=ipv4 source address=... service name=ssh accept | 출처·서비스·로그·제한을 조합한 세밀한 규칙 |
| 구분 | 적용 | 유지 |
|---|---|---|
| runtime (기본) | 즉시 | --reload나 재부팅 시 사라짐 |
--permanent | --reload 후 | /etc/firewalld/zones/*.xml에 저장 |
실무에서는 runtime으로 먼저 적용해 확인 → 문제없으면 permanent로 저장 하는 방식을 쓴다. 원격 SSH로 작업하다 규칙을 잘못 넣으면 자기 접속이 끊길 수 있기 때문이다. --runtime-to-permanent로 현재 상태를 한 번에 저장할 수도 있다.

firewall-offline-cmd(데몬 없이 설정 파일을 다루는 도구)로 확인한 기본 public zone의 실제 출력이다.
public (default)
target: default
icmp-block-inversion: no
interfaces:
sources:
services: dhcpv6-client ssh
ports:
forward: yes
masquerade: no
rich rules:
기본적으로 ssh가 모든 출처에 허용 되어 있다. 인터넷에 연결된 서버라면 45편의 무차별 대입에 그대로 노출된다.
다음 순서로 설정을 바꾼 뒤의 실제 출력이다.
firewall-offline-cmd --zone=public --remove-service-from-zone=ssh
firewall-offline-cmd --zone=public --add-service=http
firewall-offline-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" service name="ssh" accept'
firewall-offline-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="10.0.0.128" service name="ssh" log prefix="SSH-BLOCK " level="info" limit value="1/m" drop'
public (default)
services: dhcpv6-client http
rich rules:
rule family="ipv4" source address="10.0.0.0/24" service name="ssh" accept
rule family="ipv4" source address="10.0.0.128" service name="ssh" log prefix="SSH-BLOCK " level="info" limit value="1/m" drop
저장된 설정 파일(/etc/firewalld/zones/public.xml)의 실제 내용이다(설명 문구 생략).
<zone>
<short>Public</short>
<service name="dhcpv6-client"/>
<service name="http"/>
<rule family="ipv4">
<source address="10.0.0.0/24"/>
<service name="ssh"/>
<accept/>
</rule>
<rule family="ipv4">
<source address="10.0.0.128"/>
<service name="ssh"/>
<log prefix="SSH-BLOCK " level="info">
<limit value="1/m"/>
</log>
<drop/>
</rule>
<forward/>
</zone>
rich rule의 평가 순서. 10.0.0.128은 10.0.0.0/24 안에 있으므로 두 규칙 모두 해당된다. firewalld는 rich rule을 적힌 순서가 아니라 동작 종류별로 정렬 해서 평가한다: log → drop/reject(거부) → accept(허용). 그래서 10.0.0.128의 SSH는 accept보다 먼저 drop되고, 차단 시 SSH-BLOCK 접두어로 커널 로그가 남는다(limit으로 분당 1건만 기록해 로그 폭주를 막음).
drop과 reject의 차이. reject는 상대에게 거부 응답을 보내 즉시 "막혔다"는 것을 알린다. drop은 아무 응답도 하지 않아 상대는 타임아웃까지 기다린다. 42편에서 본 것처럼 스캐너는 응답이 없으면 포트를 filtered로 판정한다.
# 1) 상태
systemctl status firewalld
sudo firewall-cmd --state
sudo firewall-cmd --get-default-zone
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all # 기본 zone
sudo firewall-cmd --list-all-zones | less
# 2) 서비스·포트 (runtime → 확인 → permanent)
sudo firewall-cmd --add-service=http
sudo firewall-cmd --add-port=8080/tcp
sudo firewall-cmd --runtime-to-permanent
# 3) SSH 를 관리망으로 제한
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" service name="ssh" accept'
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload
# 4) 공격 IP 차단 (drop zone 에 source 등록 — 모든 포트 차단)
sudo firewall-cmd --zone=drop --add-source=203.0.113.7
sudo firewall-cmd --zone=drop --list-sources
# 5) 긴급 차단 모드 (모든 네트워크 차단 — 원격 접속도 끊김, 주의)
# sudo firewall-cmd --panic-on
# 6) 거부 패킷 로그 켜기와 확인
sudo firewall-cmd --set-log-denied=all
sudo journalctl -k | grep -E 'SSH-BLOCK|filter_IN_.*_REJECT|_DROP' | tail
# 7) 커널에 실제 적재된 규칙
sudo nft list ruleset | less
# Ubuntu ufw
sudo ufw status verbose
sudo ufw allow from 10.0.0.0/24 to any port 22 proto tcp
sudo ufw deny from 203.0.113.7
주의: 원격에서 SSH 규칙을 바꿀 때는 허용 규칙을 먼저 추가한 뒤 기존 규칙을 제거한다. 순서를 반대로 하면 자기 세션이 끊길 수 있다. 콘솔 접근 수단을 확보해 두는 것이 좋다.
아래 출력은 형식 설명용 예시다.
① 차단 로그 (journalctl -k)
kernel: SSH-BLOCK IN=ens33 OUT= MAC=... SRC=10.0.0.128 DST=10.0.0.200 LEN=60 ... PROTO=TCP SPT=41862 DPT=22 WINDOW=64240 RES=0x00 SYN URGP=0
| 필드 | 해석 |
|---|---|
SSH-BLOCK | rich rule의 log prefix — 어떤 규칙에 걸렸는지 |
SRC / DST | 출발·목적지 IP |
DPT=22, SYN | SSH 연결 시도 (42편: SYN = 시도) |
규칙 적용 후에도 이 로그가 계속 쌓인다면 공격이 계속 시도 중이지만 차단되고 있다 는 뜻이다. 반대로 sshd 로그(secure)에는 더 이상 이 IP가 나타나지 않아야 한다.
② 방화벽 비활성화 흔적
$ systemctl status firewalld
Active: inactive (dead) since Tue 02:24:10 KST
$ sudo grep -E 'firewall-cmd|systemctl (stop|disable) firewalld' /var/log/secure
... sudo: devuser : ... COMMAND=/usr/bin/systemctl stop firewalld
37편의 판단 방식 그대로다. enabled 서비스가 inactive이고, 그 시각에 누군가 sudo로 중지했다.
| 공격 | 흔적 | 점검 |
|---|---|---|
| 방화벽 중지 (T1562.004) | firewalld inactive, systemctl stop 기록 | 서비스 상태 감시 (37편) |
| 백도어 포트 허용 | 새 --add-port, trusted zone에 공격자 IP | --list-all-zones 기준값 비교 |
| permanent만 변경 | 재부팅 후에 열리는 규칙 | /etc/firewalld/zones/*.xml 무결성·변경 시각 |
| runtime만 변경 | 파일에는 흔적이 없음 | firewall-cmd --list-all vs 파일 비교 |
| 과도한 허용 | 전체 대역에 SSH·DB 허용 | 출처 제한 rich rule |
runtime과 permanent가 다르다 는 것 자체가 점검 포인트다. 공격자가 runtime에만 규칙을 넣으면 설정 파일에는 흔적이 없고, 반대로 permanent에만 넣으면 지금은 안 보이다가 재부팅 후 열린다.
{
systemctl is-active firewalld; systemctl is-enabled firewalld
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all-zones
sudo firewall-cmd --permanent --list-all-zones > /tmp/perm.txt
ls -l --time-style=full-iso /etc/firewalld/zones/
} > /root/evidence/fw_$(date +%Y%m%d_%H%M).txt 2>&1
# runtime 과 permanent 차이
diff <(sudo firewall-cmd --list-all-zones) <(sudo firewall-cmd --permanent --list-all-zones)
[Alert] secure: 10.0.0.128 Failed password 95회 (02:10~02:19)
↓
[즉시] firewall-cmd --add-rich-rule='... source address="10.0.0.128" service name="ssh" log prefix="SSH-BLOCK " drop'
↓
[확인] secure 에서 10.0.0.128 신규 시도 중단 · journalctl -k 에 SSH-BLOCK 기록
↓
[근본] SSH 를 관리망만 허용, PasswordAuthentication no (45편)
↓
[저장] --runtime-to-permanent · 변경 이력 기록
↓
[조사] 10.0.0.128 은 내부 IP → 그 호스트 자체의 침해 조사 (49·50편)
ufw.다음 글 「47. SELinux 이해」 에서는 방화벽과는 다른 층의 방어인 SELinux 를 다룬다. 파일 권한(DAC, 15편)만으로는 막지 못하는 상황에서 강제 접근 제어(MAC) 가 어떻게 동작하는지, 컨텍스트(ls -Z)와 enforcing/permissive 모드, AVC 거부 로그 읽기, 그리고 Ubuntu의 AppArmor 와의 차이를 정리한다.