서버를 인터넷에 공개한 지 며칠 만에 /var/log/auth.log를 보니 다른 나라 IP에서 SSH 로그인을 수백 번 시도한 흔적이 있습니다. root 계정 비밀번호 인증이 열려있는 상태였습니다. 보안 감사에서는 웹 서버 프로세스가 root 권한으로 실행되고 있다는 사실도 드러났습니다. 공격자가 SQL 인젝션 하나로 서버를 전부 장악할 수 있는 상태였습니다. OS 하드닝은 서버를 켜는 순간부터 필요한 기본 작업입니다.
OS 보안 하드닝
- 1최소 권한 원칙(PoLP)과 공격 표면(Attack Surface) 축소 전략
- 2sshd_config 보안 강화 — root 로그인 차단·키 인증·접근 계정 제한
- 3fail2ban으로 SSH 브루트포스 자동 차단
- 4불필요한 서비스·패키지 제거 및 계정 보안(PAM·비밀번호 정책)
- 5SUID/SGID 감사와 auditd로 중요 파일 접근 기록
- 6SELinux 강제 모드(MAC) 설정 및 보안 패치 자동화
sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication'systemctl list-units --type=service --state=runningdnf install -y fail2ban || apt-get install -y fail2bangetenforce && sestatus최소 권한의 원칙 — 보안 하드닝의 철학적 기반

보안 감사 결과 웹 애플리케이션 프로세스가 root로 실행되고 있었습니다. 공격자가 SQL 인젝션으로 OS 명령 실행 권한을 얻었을 때, 그 프로세스가 root이면 서버 전체가 즉시 장악됩니다. 반면 전용 서비스 계정으로 실행 중이었다면, 피해는 그 계정이 접근 가능한 파일과 포트로만 제한됩니다. 모든 하드닝 작업은 "이 권한이 정말 필요한가?"를 묻는 이 원칙에서 출발합니다.
**최소 권한의 원칙(Principle of Least Privilege, PoLP)**은 모든 시스템 보안의 출발점입니다. 사용자, 프로세스, 서비스 각각에게 자신의 작업을 수행하는 데 꼭 필요한 최소한의 권한만 부여하는 것이 핵심입니다.
이 원칙이 중요한 이유는 단순합니다. 해커가 낮은 권한의 계정을 탈취하더라도, 그 계정이 할 수 있는 일이 제한되어 있으면 피해 범위가 크게 줄어듭니다. 반대로 모든 사용자가 root 권한을 가지거나, 불필요한 서비스가 열려 있으면 — 공격 표면(attack surface)이 기하급수적으로 넓어집니다.
공격 표면(Attack Surface) 개념 — 외부에서 시스템에 접근할 수 있는 모든 경로의 총합입니다:
보안이 취약한 서버 하드닝된 서버
───────────────── ─────────────────
✗ root 로그인 허용 ✓ root 로그인 차단
✗ 비밀번호 SSH 인증 ✓ 키 기반 인증만 허용
✗ 불필요한 서비스 20개 실행 ✓ 필요한 서비스만 5개 실행
✗ 모든 포트 열림 ✓ 필요한 포트만 방화벽 허용
✗ SUID 바이너리 수백 개 ✓ 불필요한 SUID 제거
✗ 패키지 업데이트 수개월 지연 ✓ 자동 보안 패치 적용
✗ 감사 로그 없음 ✓ auditd로 모든 중요 접근 기록
최소 권한 원칙의 실천 영역 — 계정, 파일, 네트워크, 서비스 각 영역에서 최소 권한을 어떻게 적용하는지 비교합니다:
| 영역 | 나쁜 예 | 좋은 예 |
|---|---|---|
| 사용자 계정 | root로 일상 작업 | 개인 계정 + sudo 필요시만 |
| 서비스 계정 | root로 nginx 실행 | www-data 전용 계정 |
| 파일 권한 | chmod 777 | chmod 640, 필요한 사람만 |
| 네트워크 포트 | 모든 포트 개방 | 방화벽으로 필요 포트만 허용 |
| SSH 접근 | 모든 계정 허용 | AllowUsers로 특정 계정만 |
| 패키지 | 개발 도구 모두 설치 | 운영에 필요한 것만 |
보안은 단 한 번의 설정으로 끝나지 않습니다. 정기적인 보안 감사(security audit)를 통해 새로운 취약점을 발견하고, 불필요해진 권한을 회수해야 합니다. 이 챕터에서는 리눅스 서버를 실전 수준으로 하드닝하는 체계적인 방법을 다룹니다.
보안 하드닝 전체 로드맵

"OS 하드닝 해주세요"라는 요청을 받았습니다. 어디서부터 시작해야 할지, 어디까지 해야 끝인지 기준이 없으면 중요한 항목을 빠뜨리거나 우선순위 없이 작업하게 됩니다. CIS Benchmark를 처음 열면 수백 개 항목에 압도됩니다. 계층별로 로드맵을 잡아두면 어느 레이어를 먼저 다룰지, 각 레이어에서 무엇을 확인해야 하는지 순서가 생깁니다.
OS 하드닝은 계층별로 접근해야 합니다. 각 계층이 독립적으로 방어하면서 서로를 보완하는 심층 방어(Defense in Depth) 전략입니다.
┌─────────────────────────────────────────────┐
│ 계층 7: 감사 & 모니터링 │
│ auditd, 로그 분석, ISMS 점검 │
├─────────────────────────────────────────────┤
│ 계층 6: 파일 시스템 보안 │
│ SUID/SGID 제거, chattr, 파일 권한 │
├─────────────────────────────────────────────┤
│ 계층 5: 계정 보안 │
│ PAM, 비밀번호 정책, /etc/shadow │
├─────────────────────────────────────────────┤
│ 계층 4: 네트워크 최소화 │
│ ss -tulnp, 불필요 서비스 중지 │
├─────────────────────────────────────────────┤
│ 계층 3: 접근 제어 (MAC) │
│ SELinux enforcing 모드 │
├─────────────────────────────────────────────┤
│ 계층 2: SSH & 원격 접근 보안 │
│ sshd_config 10개 항목, fail2ban │
├─────────────────────────────────────────────┤
│ 계층 1: 패치 & 업데이트 │
│ unattended-upgrades, dnf-automatic │
└─────────────────────────────────────────────┘
하드닝 순서는 아래에서 위로 — 기초부터 쌓아야 합니다. 패치가 안 된 커널에 아무리 정교한 감사 설정을 해도 알려진 취약점으로 뚫릴 수 있습니다.
SSH는 서버의 정문입니다. 기본 설정 그대로 두면 전 세계 봇넷이 24시간 비밀번호를 대입 공격합니다. 아래 10개 항목을 순서대로 설정하세요.
실습 전 디렉토리와 예제 파일을 먼저 준비합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/linux/part5/exam_7 && cd /tmp/linux/part5/exam_7
# 현재 보안 상태 스냅샷
cat > audit_baseline.sh << 'EOF'
#!/bin/bash
echo "=== SSH 설정 점검 ==="
grep -E "^(PermitRootLogin|PasswordAuthentication|Port)" /etc/ssh/sshd_config 2>/dev/null
echo "=== SUID 파일 목록 ==="
find /usr/bin /usr/sbin -perm -4000 -type f 2>/dev/null
echo "=== 빈 패스워드 계정 ==="
sudo awk -F: '($2 == "") {print $1}' /etc/shadow 2>/dev/null || echo "(root 권한 필요)"
echo "=== 열린 포트 ==="
ss -tlnp 2>/dev/null | head -20
EOF
chmod +x audit_baseline.sh
이제 실습을 진행합니다.
설정 파일 열기 — sshd_config 파일을 편집해 보안 설정을 적용합니다:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak # 백업 필수!
sudo nano /etc/ssh/sshd_config
항목 1: 포트 변경 (기본 22 → 비표준 포트) — 자동화된 스캐너의 22번 포트 공격을 피하는 첫 번째 조치입니다:
# 기본값: #Port 22
Port 2222 # 또는 다른 비표준 포트 (1024~65535)
포트를 변경하면 자동화된 스캐닝 봇의 90% 이상을 차단합니다. 보안의 근본 해결책은 아니지만, 노이즈를 극적으로 줄입니다.
항목 2: root 직접 로그인 차단 — root 계정으로 SSH 직접 접속을 차단해 권한 탈취 리스크를 줄입니다:
# 기본값: #PermitRootLogin prohibit-password
PermitRootLogin no
root 계정으로 직접 SSH 접속을 허용하면, 공격자가 한 번만 성공해도 서버 전체를 장악합니다. 반드시 비활성화하세요.
항목 3: 비밀번호 인증 차단 (키 기반만 허용) — 브루트포스 공격의 여지를 완전히 없애는 가장 강력한 설정입니다:
# 기본값: #PasswordAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
키 기반 인증을 설정한 후에만 이 항목을 변경하세요. 순서를 바꾸면 Lock-out됩니다.
항목 4: 인증 시도 횟수 제한 — 잘못된 인증 시도가 일정 횟수를 초과하면 연결을 강제 종료합니다:
# 기본값: #MaxAuthTries 6
MaxAuthTries 3
3회 실패 후 연결을 끊습니다. fail2ban과 함께 사용하면 더욱 효과적입니다.
항목 5: 특정 사용자만 SSH 허용 — AllowUsers로 SSH 접속 가능한 계정을 명시적으로 제한합니다:
# 이 줄을 추가 (기본값에는 없음)
AllowUsers deploy ubuntu ec2-user
# 또는 그룹 기반
AllowGroups sshusers
명시된 사용자/그룹 외에는 SSH 접속 자체가 거부됩니다. 가장 강력한 접근 제어 방법 중 하나입니다.
항목 6: 유휴 세션 타임아웃 — 방치된 SSH 세션을 자동으로 끊어 무단 접근 가능성을 줄입니다:
ClientAliveInterval 300 # 300초(5분) 간격으로 클라이언트 확인
ClientAliveCountMax 2 # 2회 무응답 시 연결 종료
자리를 비웠을 때 열려있는 세션을 자동으로 종료합니다.
항목 7: X11 포워딩 비활성화 — 서버에서 GUI 포워딩 기능을 끄면 불필요한 공격 경로를 하나 더 닫습니다:
X11Forwarding no
서버에서 GUI 애플리케이션을 실행할 일이 없다면 반드시 비활성화하세요. X11 포워딩은 여러 보안 취약점의 벡터가 됩니다.
항목 8: SSH 프로토콜 버전 고정 — 취약점이 있는 SSH 프로토콜 1 버전 사용을 명시적으로 차단합니다:
# OpenSSH 7.0 이상은 SSHv2만 지원하지만, 명시적으로 확인
# /etc/ssh/sshd_config에 Protocol 옵션이 있다면
Protocol 2
SSH Protocol 1은 심각한 취약점이 있습니다. 현대 OpenSSH는 기본적으로 v2만 사용하지만, 레거시 설정 파일에 Protocol 1이 남아있을 수 있습니다.
항목 9: 빈 비밀번호 계정 접근 차단 — 빈 비밀번호로 설정된 계정은 SSH 로그인을 허용하지 않습니다:
PermitEmptyPasswords no
항목 10: 로그인 유예 시간 단축 — 인증에 걸리는 최대 시간을 줄여 느린 브루트포스 공격을 어렵게 합니다:
LoginGraceTime 30 # 기본값 120초 → 30초로 단축
MaxSessions 4 # 동시 세션 수 제한
설정 적용 및 검증 — 수정한 설정 파일의 문법을 확인하고 sshd를 재시작합니다:
# 설정 파일 문법 검사 (적용 전 필수!)
sudo sshd -t
# 문법 오류 없으면 sshd 재시작
sudo systemctl restart sshd
# 포트가 올바르게 열렸는지 확인
ss -tulnp | grep sshd
경고: 새 포트로 접속 테스트를 완료하기 전에 기존 세션을 닫지 마세요. 항상 두 개의 터미널을 열어두고 하나로 테스트하세요.
- sshd -T | grep permitrootlogin 에서 'permitrootlogin no' 가 표시된다
- sshd -T | grep passwordauthentication 에서 'passwordauthentication no' 가 표시된다
- systemctl restart sshd 후 새 터미널에서 키 없이 SSH 접속 시도 시 즉시 거부된다
- grep -c 'Failed password' /var/log/auth.log 실행 시 설정 전과 비교해 신규 시도가 줄어드는지 확인한다
fail2ban은 로그 파일을 실시간으로 모니터링하여 반복 실패 패턴을 감지하고, 해당 IP를 방화벽(iptables/nftables)에서 자동 차단합니다.
설치 — fail2ban은 패키지 관리자로 설치합니다:
# Ubuntu/Debian
sudo apt update && sudo apt install -y fail2ban
# RHEL/CentOS/Rocky
sudo dnf install -y epel-release
sudo dnf install -y fail2ban
# 서비스 활성화
sudo systemctl enable --now fail2ban
로컬 설정 파일 생성 (jail.local)
fail2ban의 기본 설정은 jail.conf에 있지만, 이 파일은 패키지 업데이트 시 덮어씌워집니다. 반드시 jail.local에 커스텀 설정을 작성하세요.
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# 차단 제외 IP (자신의 관리용 IP 반드시 추가!)
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
# 차단 지속 시간 (초 단위, -1이면 영구 차단)
bantime = 3600 # 1시간
# 이 시간 안에
findtime = 600 # 10분
# 이 횟수만큼 실패하면 차단
maxretry = 5
# 차단 액션 (iptables 또는 nftables)
banaction = iptables-multiport
[sshd]
enabled = true
port = 2222 # 변경한 SSH 포트에 맞게 수정
filter = sshd
logpath = /var/log/auth.log # Ubuntu
# logpath = /var/log/secure # RHEL/CentOS
maxretry = 3
bantime = 7200 # SSH는 더 엄격하게 2시간 차단
fail2ban 재시작 및 상태 확인 — 설정 변경 후 서비스를 재시작하고 차단 현황을 확인합니다:
sudo systemctl restart fail2ban
# 전체 jail 상태 확인
sudo fail2ban-client status
# SSH jail 상세 상태
sudo fail2ban-client status sshd
# 출력 예시:
# Status for the jail: sshd
# |- Filter
# | |- Currently failed: 2
# | |- Total failed: 47
# | `- File list: /var/log/auth.log
# `- Actions
# |- Currently banned: 1
# |- Total banned: 8
# `- Banned IP list: 185.220.101.45
수동 IP 차단 및 해제 — 관리자가 특정 IP를 직접 차단하거나 오탐을 해제할 때 씁니다:
# 특정 IP 수동 차단
sudo fail2ban-client set sshd banip 1.2.3.4
# 특정 IP 차단 해제 (실수로 자신의 IP가 차단된 경우)
sudo fail2ban-client set sshd unbanip 1.2.3.4
# 차단된 IP 목록
sudo fail2ban-client status sshd | grep "Banned IP"
fail2ban 로그 모니터링 — fail2ban의 동작과 차단 이력을 로그에서 실시간으로 확인합니다:
sudo tail -f /var/log/fail2ban.log
# 차단 이벤트만 필터링
sudo grep "Ban " /var/log/fail2ban.log | tail -20
운영 서버에는 필요한 것만 있어야 합니다. 설치된 패키지와 실행 중인 서비스가 많을수록 취약점이 발생할 가능성이 높아집니다.
현재 실행 중인 서비스 전체 목록 확인 — 시스템에서 동작 중인 서비스를 나열해 불필요한 것을 파악합니다:
# 실행 중인 서비스만
sudo systemctl list-units --type=service --state=running
# 활성화된 서비스 (부팅 시 자동 시작)
sudo systemctl list-unit-files --type=service --state=enabled
불필요한 서비스 예시 — 서버 역할에 따라 비활성화를 검토할 서비스 목록입니다:
| 서비스 | 설명 | 일반 서버에서 필요? |
|---|---|---|
avahi-daemon | mDNS/Bonjour 서비스 | 거의 불필요 |
cups | 프린터 스풀러 | 불필요 |
bluetooth | 블루투스 | 서버에서 불필요 |
postfix | 메일 서버 | 메일 서버 아니면 불필요 |
rpcbind | NFS RPC | NFS 미사용 시 불필요 |
nfs-server | NFS 서버 | NFS 미사용 시 불필요 |
# 서비스 중지 및 비활성화
sudo systemctl stop avahi-daemon
sudo systemctl disable avahi-daemon
# 완전히 제거 (서비스 + 패키지)
sudo apt purge avahi-daemon # Ubuntu
sudo dnf remove avahi # RHEL
불필요한 패키지 목록 확인 및 제거 — 설치된 패키지를 검토하고 사용하지 않는 것을 삭제합니다:
# Ubuntu: 더 이상 필요 없는 패키지
sudo apt autoremove --purge
# 특정 패키지 검색
dpkg -l | grep telnet # telnet 설치 여부 확인
# telnet은 평문 전송 — 반드시 제거
sudo apt purge telnet telnetd rsh-client rsh-server
# 컴파일러 제거 (운영 서버)
# 공격자가 서버 내에서 악성코드를 컴파일하는 것을 방지
sudo apt purge gcc g++ make # 개발 서버가 아니라면
네트워크 서비스 최소화 — 열린 포트 확인 — 외부에 노출된 포트를 목록화해 불필요한 포트를 닫습니다:
# 현재 열린 포트 전체 확인
sudo ss -tulnp
# 출력 예시:
# Netid State Recv-Q Send-Q Local Address:Port
# tcp LISTEN 0 128 0.0.0.0:2222 # SSH (변경된 포트)
# tcp LISTEN 0 511 0.0.0.0:80 # nginx
# tcp LISTEN 0 511 0.0.0.0:443 # nginx HTTPS
# tcp LISTEN 0 128 127.0.0.1:5432 # PostgreSQL (로컬만)
# tcp LISTEN 0 128 0.0.0.0:8080 # 불필요한 서비스?
# 특정 포트를 사용하는 프로세스 확인
sudo ss -tulnp | grep :8080
# 또는
sudo lsof -i :8080
외부에 열려있을 필요가 없는 서비스(DB 등)는
127.0.0.1에 바인딩하거나 방화벽으로 차단하세요.
계정 보안은 내부 위협(퇴사자, 권한 남용)과 외부 위협(탈취된 계정) 모두를 방어합니다.
PAM을 이용한 비밀번호 복잡도 설정
PAM(Pluggable Authentication Modules)은 리눅스 인증 프레임워크입니다. pam_pwquality 모듈로 비밀번호 정책을 강제합니다.
# Ubuntu
sudo apt install -y libpam-pwquality
# RHEL
sudo dnf install -y libpwquality
# 비밀번호 정책 설정
sudo nano /etc/security/pwquality.conf
# 최소 길이
minlen = 12
# 소문자 최소 개수
lcredit = -1
# 대문자 최소 개수
ucredit = -1
# 숫자 최소 개수
dcredit = -1
# 특수문자 최소 개수
ocredit = -1
# 이전 비밀번호와 달라야 하는 문자 수
difok = 8
# 사전에 있는 단어 사용 금지 (1=활성화)
dictcheck = 1
# 사용자 이름 포함 금지
usercheck = 1
비밀번호 만료 정책 (/etc/login.defs) — 시스템 전체에 적용되는 기본 비밀번호 만료 정책을 설정합니다:
sudo nano /etc/login.defs
# 비밀번호 최대 사용 기간 (90일)
PASS_MAX_DAYS 90
# 비밀번호 최소 사용 기간 (1일 — 연속 변경 방지)
PASS_MIN_DAYS 1
# 만료 7일 전 경고
PASS_WARN_AGE 7
개별 계정 비밀번호 만료 설정 — chage 명령으로 특정 계정의 만료 정책을 개별 지정합니다:
# 특정 계정의 비밀번호 만료 정보 확인
sudo chage -l username
# 비밀번호 만료일 설정
sudo chage -M 90 -m 1 -W 7 username
# 계정 즉시 만료 (다음 로그인 시 비밀번호 변경 강제)
sudo chage -d 0 username
계정 잠금 정책 (pam_tally2 / pam_faillock) — 로그인 실패 횟수를 초과하면 계정을 자동으로 잠급니다:
# RHEL 9 / Ubuntu 22.04+ : pam_faillock 사용
sudo nano /etc/security/faillock.conf
# 잠금 전 허용 실패 횟수
deny = 5
# 잠금 해제까지 대기 시간 (초)
unlock_time = 900 # 15분
# root 계정도 잠금 대상 포함
even_deny_root = true
잠긴 계정 확인 및 해제 — 잠금 상태인 계정을 확인하고 관리자가 직접 해제합니다:
# 계정 잠금 상태 확인
sudo faillock --user username
# 계정 잠금 해제
sudo faillock --user username --reset
# 모든 계정 잠금 상태 확인
sudo faillock
/etc/shadow 파일 이해 — 암호화된 비밀번호와 만료 정보가 저장된 파일 구조를 봅니다:
# shadow 파일 형식 (root만 읽기 가능)
sudo cat /etc/shadow | head -3
# 출력 형식:
# username:$6$salt$hashedpassword:19000:0:99999:7:::
# 필드: 이름:해시:마지막변경일:최소:최대:경고:비활성:만료:예약
# 비밀번호 없는 계정 찾기 (보안 위험)
sudo awk -F: '($2 == "" || $2 == "!!" ) {print $1}' /etc/shadow
# 비밀번호 잠금 상태 계정 (!! 또는 ! 접두어)
sudo awk -F: '$2 ~ /^!/ {print $1, "- 잠금됨"}' /etc/shadow
# 서비스 계정은 로그인 불가 상태로 설정
sudo usermod -s /sbin/nologin serviceuser
sudo passwd -l serviceuser # 비밀번호 잠금
SUID(Set User ID) 비트가 설정된 파일은 실행 시 파일 소유자의 권한으로 동작합니다. root 소유의 SUID 바이너리가 취약하면 권한 상승(privilege escalation) 공격에 악용됩니다.
SUID/SGID 파일 전체 검색 — 루트 권한으로 실행 가능한 SUID 파일을 전체 파일시스템에서 탐색합니다:
# SUID 파일 찾기
sudo find / -perm -4000 -type f 2>/dev/null | sort
# SGID 파일 찾기
sudo find / -perm -2000 -type f 2>/dev/null | sort
# SUID + SGID 동시에
sudo find / -perm /6000 -type f 2>/dev/null | sort
# 결과를 파일로 저장 (기준선 생성)
sudo find / -perm /6000 -type f 2>/dev/null | sort > /root/suid_baseline_$(date +%Y%m%d).txt
일반적으로 허용되는 SUID 바이너리 예시 — 시스템 운영에 필요한 SUID 바이너리 목록입니다:
/usr/bin/sudo # sudo 명령어
/usr/bin/passwd # 비밀번호 변경
/usr/bin/su # 계정 전환
/usr/bin/ping # ICMP (일부 배포판)
/usr/bin/newgrp # 그룹 전환
위 목록에 없는 SUID 바이너리는 의심하세요. 특히 인터프리터(python, bash, perl)에 SUID가 설정되어 있으면 즉시 제거해야 합니다.
불필요한 SUID 비트 제거 — 용도가 불분명한 SUID 바이너리는 특수 권한 비트를 제거합니다:
# SUID 비트 제거
sudo chmod u-s /path/to/suspicious/binary
# 예시: python3에 SUID가 있다면
sudo chmod u-s /usr/bin/python3
# 변경 전후 확인
ls -la /usr/bin/python3
# -rwxr-xr-x (SUID 제거 후)
# -rwsr-xr-x (SUID 있을 때)
auditd로 중요 파일 접근 감사
auditd는 커널 수준의 감사 프레임워크입니다. 파일 접근, 시스템 콜, 사용자 활동을 기록합니다.
# 설치
sudo apt install -y auditd audispd-plugins # Ubuntu
sudo dnf install -y audit # RHEL
# 활성화
sudo systemctl enable --now auditd
감사 규칙 설정 — auditd 규칙으로 민감한 파일 접근과 명령 실행을 추적합니다:
sudo nano /etc/audit/rules.d/hardening.rules
# 감사 로그 삭제 시도 모니터링
-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F dir=/var/log -k log_tampering
# /etc/passwd, /etc/shadow 변경 감사
-w /etc/passwd -p wa -k identity_changes
-w /etc/shadow -p wa -k identity_changes
-w /etc/group -p wa -k identity_changes
-w /etc/sudoers -p wa -k sudoers_changes
# SSH 설정 변경 감사
-w /etc/ssh/sshd_config -p wa -k sshd_config
# SUID/SGID 파일 실행 감사
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -k privilege_escalation
# 실패한 파일 접근 시도
-a always,exit -F arch=b64 -S open -F exit=-EACCES -k access_denied
-a always,exit -F arch=b64 -S open -F exit=-EPERM -k access_denied
# 모듈 로드/언로드
-w /sbin/insmod -p x -k module_insertion
-w /sbin/rmmod -p x -k module_removal
# 규칙 적용
sudo augenrules --load
sudo systemctl restart auditd
# 현재 적용된 규칙 확인
sudo auditctl -l
감사 로그 조회 — ausearch와 aureport로 감사 이벤트를 검색하고 요약합니다:
# identity_changes 태그 이벤트 조회
sudo ausearch -k identity_changes | aureport -f
# 특정 파일에 대한 접근 기록
sudo ausearch -f /etc/passwd
# 특정 사용자의 활동
sudo ausearch -ua 1001 # UID 1001의 활동
# 오늘 발생한 권한 상승 이벤트
sudo ausearch -k privilege_escalation --start today
# 전체 감사 요약 보고서
sudo aureport --summary
chattr로 파일 불변 속성 설정
chattr은 파일에 특수 속성을 부여합니다. +i(immutable) 속성이 설정되면 root도 파일을 수정하거나 삭제할 수 없습니다.
# 파일에 불변 속성 부여
sudo chattr +i /etc/passwd
sudo chattr +i /etc/shadow
sudo chattr +i /etc/hosts
# 속성 확인
sudo lsattr /etc/passwd
# ----i---------e------- /etc/passwd
# 속성 제거 (변경이 필요할 때만 임시로)
sudo chattr -i /etc/passwd
# 변경 후 반드시 다시 설정
sudo useradd newuser
sudo chattr +i /etc/passwd
# 디렉토리 전체에 적용 (하위 포함)
sudo chattr -R +i /etc/cron.d/
주의: 중요 설정 파일에
+i를 설정하면 패키지 업데이트나 시스템 관리 도구가 실패할 수 있습니다. 신중하게 적용하세요.
SELinux(Security-Enhanced Linux)는 NSA가 개발한 커널 수준의 보안 모듈입니다. 일반적인 리눅스 권한 체계(DAC)와 달리, 정책에 명시적으로 허용되지 않은 모든 접근을 거부합니다.
SELinux 상태 확인 — 현재 SELinux 모드와 정책을 확인합니다:
# 현재 모드 확인
getenforce
# 출력:
# Enforcing — 정책 위반 시 차단 + 기록
# Permissive — 차단 없이 기록만 (디버깅용)
# Disabled — SELinux 완전 비활성화
# 상세 상태 확인
sestatus
SELinux 모드 임시 변경 — 재부팅 전까지만 모드를 바꾸는 방법으로, 트러블슈팅에 주로 씁니다:
# Enforcing → Permissive (재부팅 후 원래대로)
sudo setenforce 0
# Permissive → Enforcing
sudo setenforce 1
SELinux 모드 영구 설정 — /etc/selinux/config를 수정해 재부팅 후에도 모드가 유지되도록 합니다:
sudo nano /etc/selinux/config
# SELINUX=enforcing ← 프로덕션 권장
# SELINUX=permissive ← 디버깅/마이그레이션
# SELINUX=disabled ← 권장하지 않음
SELINUX=enforcing
SELINUXTYPE=targeted
disabled → enforcing으로 직접 변경하면 다음 부팅 시 파일 레이블 재작성으로 시간이 오래 걸립니다.disabled → permissive → (재부팅) → enforcing순서로 전환하세요.
SELinux 컨텍스트 이해 — 파일과 프로세스에 붙는 보안 레이블의 의미와 형식을 확인합니다:
# 파일의 SELinux 컨텍스트 확인
ls -Z /var/www/html/
# 출력:
# system_u:object_r:httpd_sys_content_t:s0 index.html
# 프로세스의 SELinux 컨텍스트
ps auxZ | grep nginx
# system_u:system_r:httpd_t:s0 ... nginx
컨텍스트 형식: user:role:type:level에서 type이 가장 중요합니다. nginx(httpd_t)는 httpd_sys_content_t 레이블이 붙은 파일에만 접근할 수 있습니다.
SELinux 거부 로그 확인 — SELinux가 차단한 동작을 로그에서 찾아 정책 추가 여부를 판단합니다:
# AVC 거부 메시지 확인
sudo ausearch -m avc -ts recent | head -30
# 또는
sudo tail -f /var/log/audit/audit.log | grep AVC
# audit2why로 거부 이유 분석
sudo ausearch -m avc -ts recent | audit2why
# audit2allow로 허용 정책 생성 제안
sudo ausearch -m avc -ts recent | audit2allow
실용적인 SELinux 명령어 — 현장에서 자주 쓰는 SELinux 관련 명령어 모음입니다:
# 파일 컨텍스트 수동 변경
sudo chcon -t httpd_sys_content_t /data/webfiles/
# 기본 컨텍스트로 복구
sudo restorecon -Rv /var/www/html/
# SELinux boolean 확인 (서비스별 세부 정책)
sudo getsebool -a | grep httpd
# boolean 변경 (예: nginx의 네트워크 연결 허용)
sudo setsebool -P httpd_can_network_connect on
알려진 취약점의 60% 이상이 패치가 이미 나와있는 상태에서 발생합니다. 자동 보안 패치 적용은 가장 기본적이고 효과적인 보안 수단입니다.
Ubuntu: unattended-upgrades — 보안 패치를 자동으로 설치하는 Ubuntu 전용 패키지입니다:
# 설치
sudo apt install -y unattended-upgrades apt-listchanges
# 설정 파일
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
// "${distro_id}:${distro_codename}-updates"; // 보안 패치만 원하면 주석
};
// 자동 재부팅 설정
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00"; // 새벽 3시
// 이메일 알림
Unattended-Upgrade::Mail "admin@example.com";
Unattended-Upgrade::MailReport "on-change";
// 사용하지 않는 패키지 자동 제거
Unattended-Upgrade::Remove-Unused-Dependencies "true";
# 자동 업데이트 주기 설정
sudo nano /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1"; // 매일 패키지 목록 갱신
APT::Periodic::Unattended-Upgrade "1"; // 매일 자동 업그레이드
APT::Periodic::AutocleanInterval "7"; // 7일마다 캐시 정리
# 즉시 실행 테스트
sudo unattended-upgrades --dry-run --debug
# 서비스 활성화
sudo systemctl enable --now unattended-upgrades
RHEL/Rocky/AlmaLinux: dnf-automatic — RHEL 계열에서 자동 업데이트를 처리하는 방법입니다:
# 설치
sudo dnf install -y dnf-automatic
# 설정 파일
sudo nano /etc/dnf/automatic.conf
[commands]
# 업그레이드 유형: security, bugfix, enhancement, all
upgrade_type = security
# 자동 적용 (download_updates만 하고 apply_updates=no 로 안전하게 가능)
apply_updates = yes
# 재부팅 필요 시 동작 (never, when-changed, always)
reboot = when-needed
reboot_command = "shutdown -r +5 'Rebooting for security updates'"
[email]
email_to = admin@example.com
email_from = dnf-automatic@server.example.com
# 타이머 활성화 (기본: 매일 랜덤 시간)
sudo systemctl enable --now dnf-automatic.timer
# 타이머 상태 확인
sudo systemctl status dnf-automatic.timer
sudo systemctl list-timers | grep dnf
수동 보안 패치 현황 확인 — 현재 시스템에 적용되지 않은 보안 패치 목록을 확인합니다:
# Ubuntu: 적용 가능한 보안 업데이트 목록
sudo apt list --upgradable 2>/dev/null | grep -i security
# RHEL: 보안 업데이트만 확인
sudo dnf check-update --security
# CVE 특정 취약점 패치 여부 확인
sudo dnf updateinfo list security | grep CVE-2024
증상
보안 패치 적용 후 nginx나 Apache가 시작되지 않습니다. systemctl start nginx를 실행하면 오류 없이 종료되거나, 특정 디렉토리의 파일을 읽지 못합니다.
sudo systemctl start nginx
# Job for nginx.service failed. 오류 메시지
journalctl -xe -u nginx
# Permission denied: /data/app/static/
원인 분석
SELinux enforcing 모드에서 보안 패치로 SELinux 정책이 업데이트되거나, 새 설정 파일/디렉토리의 컨텍스트가 잘못 설정된 경우입니다.
# 1단계: SELinux 거부 확인
sudo ausearch -m avc -ts recent | tail -20
# 출력 예시:
# type=AVC msg=audit(1711000000.123:456): avc: denied { read } for
# pid=12345 comm="nginx" name="static" dev="sda1" ino=67890
# scontext=system_u:system_r:httpd_t:s0
# tcontext=system_u:object_r:var_t:s0 ← 잘못된 컨텍스트!
# tclass=dir
# 2단계: 파일 컨텍스트 확인
ls -Z /data/app/static/
# system_u:object_r:var_t:s0 . ← httpd_sys_content_t 이어야 함
해결 방법 — SELinux 컨텍스트를 올바른 타입으로 수정해 서비스 접근을 복구합니다:
# 방법 1: 올바른 컨텍스트로 수동 변경
sudo chcon -R -t httpd_sys_content_t /data/app/static/
# 방법 2: 기본 컨텍스트 규칙 추가 (영구적)
sudo semanage fcontext -a -t httpd_sys_content_t "/data/app/static(/.*)?"
sudo restorecon -Rv /data/app/static/
# 방법 3 (임시 디버깅): Permissive 모드로 전환 후 확인
sudo setenforce 0
sudo systemctl start nginx # 이제 시작되면 SELinux 문제 확정
sudo setenforce 1 # 확인 후 반드시 복구
# SELinux boolean 확인 (httpd 관련)
sudo getsebool -a | grep httpd | grep -v "off$"
# 필요한 boolean 활성화
sudo setsebool -P httpd_read_user_content on
sudo setsebool -P httpd_can_network_connect on # 프록시 사용 시
재발 방지 — 포트 변경 시 방화벽 확인을 체크리스트에 포함시킵니다:
# 새 디렉토리 생성 시 컨텍스트 함께 설정
sudo mkdir -p /data/app/newdir
sudo semanage fcontext -a -t httpd_sys_content_t "/data/app/newdir(/.*)?"
sudo restorecon -Rv /data/app/newdir
증상
SSH 포트를 변경했는데 새 포트로 접속이 안 됩니다. 기존 연결도 끊겼고, 서버에 접근할 방법이 없습니다.
원인 분석
이 상황은 보통 다음 중 하나입니다:
- 방화벽(UFW/firewalld)에서 새 포트를 열지 않음
- 클라우드 보안 그룹(Security Group)에서 새 포트를 허용하지 않음
sshd_config문법 오류로 sshd가 재시작되지 않음
복구 방법 — 잠금 상태의 서버는 클라우드 콘솔 접근이나 rescue 모드를 활용합니다:
# 복구 순서: 클라우드 콘솔 → EC2 Instance Connect / Serial Console 접속
# AWS EC2 경우:
# 1. AWS 콘솔 → EC2 → 인스턴스 선택
# 2. "연결" → "EC2 Instance Connect" 또는 "직렬 콘솔"
# 3. 브라우저에서 직접 터미널 접속
# GCP 경우:
# 1. GCP 콘솔 → Compute Engine → VM 선택
# 2. SSH 버튼 클릭 (브라우저 기반 SSH)
# Azure 경우:
# 1. Azure 포털 → 가상 머신 → Bastion 연결
접속 후 문제 진단:
# sshd 상태 확인
sudo systemctl status sshd
journalctl -xe -u sshd
# sshd_config 문법 검사
sudo sshd -t
# 오류가 있으면 메시지 출력됨
# 방화벽 상태 확인
sudo ufw status verbose # Ubuntu
sudo firewall-cmd --list-all # RHEL
# 새 포트 방화벽 허용
sudo ufw allow 2222/tcp # Ubuntu
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload # RHEL
# sshd 재시작
sudo systemctl restart sshd
# 포트 확인
sudo ss -tulnp | grep :2222
클라우드 보안 그룹 수정 (AWS 예시) — AWS CLI로 보안 그룹에 포트를 추가해 SSH 접속을 복구합니다:
# AWS CLI로 보안 그룹 규칙 추가
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxxx \
--protocol tcp \
--port 2222 \
--cidr 내_IP/32
재발 방지 체크리스트 — 방화벽 변경 전 확인해야 할 항목입니다:
SSH 포트 변경 절차:
□ 1. sshd_config 수정
□ 2. sshd -t 로 문법 검사
□ 3. 방화벽에서 새 포트 허용 (UFW/firewalld)
□ 4. 클라우드 보안 그룹에서 새 포트 허용
□ 5. sshd 재시작 (기존 세션은 유지됨)
□ 6. 새 터미널에서 새 포트로 접속 테스트
□ 7. 성공 확인 후 방화벽에서 기존 포트 닫기
□ 8. 클라우드 보안 그룹에서 기존 포트 제거
증상
실수로 여러 번 잘못된 비밀번호를 입력했거나, 자동화 스크립트가 잘못 동작하여 자신의 IP가 fail2ban에 의해 차단되었습니다. SSH 연결이 즉시 끊기거나 Connection refused가 발생합니다.
원인 분석 — fail2ban 상태를 직접 조회해 어떤 IP가 차단됐는지 확인합니다:
# 로컬 접근(같은 서버 내)으로 확인
sudo fail2ban-client status sshd
# Banned IP list: 203.0.113.55 ← 자신의 IP가 차단됨
# iptables에서도 확인
sudo iptables -n -L f2b-sshd
# DROP 규칙에 내 IP가 보임
즉각 해제 방법 — 차단된 IP를 즉시 언밴 처리합니다:
# 방법 1: fail2ban 명령으로 차단 해제
sudo fail2ban-client set sshd unbanip 203.0.113.55
# 방법 2: 콘솔 접근이 가능한 경우 fail2ban 일시 중지
sudo systemctl stop fail2ban
# iptables 규칙 직접 삭제
sudo iptables -D f2b-sshd -s 203.0.113.55 -j REJECT
# fail2ban 재시작
sudo systemctl start fail2ban
근본 해결: ignoreip 설정 — 사내 IP나 VPN 대역을 화이트리스트에 등록해 오탐을 예방합니다:
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# 반드시 자신의 관리 IP를 추가!
ignoreip = 127.0.0.1/8 ::1 203.0.113.55 10.0.0.0/8
sudo systemctl restart fail2ban
# 설정 확인
sudo fail2ban-client get sshd ignoreip
관리 IP가 유동 IP라면 VPN을 통해 고정 IP를 확보하거나, 클라우드 Bastion Host를 경유하는 접근 방식을 고려하세요.
ISMS(정보보호관리체계) 인증과 서버 하드닝
국내 기업이 ISMS 또는 ISMS-P 인증을 받거나 유지하려면, 서버 보안 설정에 대한 정기적인 점검이 필수입니다. 인증 심사관이 실제로 확인하는 항목들을 알아두면, 실무에서 바로 적용할 수 있습니다.
ISMS 기술적 보호조치 — 서버 보안 점검 항목 — 국내 ISMS 인증에서 요구하는 서버 보안 점검 기준입니다:
[계정 관리]
□ root 계정 직접 로그인 차단 여부
□ 불필요한 계정 존재 여부 (퇴사자, 테스트 계정)
□ 비밀번호 복잡도 정책 적용 여부
□ 비밀번호 최대 사용기간 설정 여부 (90일 이내 권장)
□ 계정 잠금 정책 설정 여부 (5회 실패 시 잠금)
□ 서비스 계정 로그인 쉘 제한 여부 (/sbin/nologin)
[접근 통제]
□ SSH 키 기반 인증 사용 여부
□ 접근 가능 IP 제한 여부 (AllowUsers, hosts.allow)
□ 불필요한 서비스/포트 차단 여부
□ 방화벽 정책 적용 여부
□ Sudo 권한 최소 부여 여부
[패치 관리]
□ OS 보안 패치 적용 주기 (월 1회 이상)
□ 보안 취약점 스캔 실시 여부
□ 자동 업데이트 정책 적용 여부
[감사 로그]
□ 로그인/로그아웃 기록 보관 여부
□ 특권 명령어 사용 기록 여부 (sudo 로그)
□ 파일 접근/변경 이력 기록 여부 (auditd)
□ 로그 보관 기간 (6개월~1년 이상 권장)
□ 로그 위변조 방지 조치 여부
[악성코드 방어]
□ SUID/SGID 파일 정기 점검 여부
□ SELinux/AppArmor 활성화 여부
□ 침입 탐지 시스템(IDS) 운영 여부
보안 점검 스크립트 예시 — 위 점검 항목을 자동으로 확인하는 쉘 스크립트입니다:
#!/bin/bash
# quick_audit.sh — 빠른 보안 점검 스크립트
echo "=== OS 보안 점검 보고서 ==="
echo "점검 일시: $(date)"
echo "호스트명: $(hostname)"
echo ""
echo "[1] root 로그인 차단 확인"
grep "^PermitRootLogin" /etc/ssh/sshd_config
echo ""
echo "[2] 비밀번호 인증 상태"
grep "^PasswordAuthentication" /etc/ssh/sshd_config
echo ""
echo "[3] 실행 중인 서비스"
systemctl list-units --type=service --state=running --no-legend | wc -l
echo "개의 서비스 실행 중"
echo ""
echo "[4] 열린 포트"
ss -tulnp | grep LISTEN
echo ""
echo "[5] SUID 파일 수"
find / -perm -4000 -type f 2>/dev/null | wc -l
echo "개의 SUID 파일 발견"
echo ""
echo "[6] SELinux 상태"
getenforce 2>/dev/null || echo "SELinux 미설치"
echo ""
echo "[7] 최근 로그인 실패"
grep "Failed password" /var/log/auth.log 2>/dev/null | tail -5
grep "Failed password" /var/log/secure 2>/dev/null | tail -5
echo ""
echo "[8] 비밀번호 없는 계정"
awk -F: '($2 == "" ) {print "[경고] " $1 " — 비밀번호 없음"}' /etc/shadow
echo ""
echo "=== 점검 완료 ==="
chmod +x quick_audit.sh
sudo ./quick_audit.sh | tee /var/log/security_audit_$(date +%Y%m%d).txt
보안은 나중에 붙이는 게 아니다
과거에는 서버를 구성한 후 보안 담당자가 별도로 하드닝을 수행했습니다. 현대의 DevSecOps 문화에서는 인프라 코드 자체에 보안 설정이 내재화됩니다.
Ansible로 하드닝 자동화 — SSH 보안 설정을 Ansible 플레이북으로 전 서버에 일괄 적용합니다:
# hardening.yml — Ansible Playbook 예시
---
- name: OS Security Hardening
hosts: all
become: yes
tasks:
- name: SSH 포트 변경
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port'
line: 'Port 2222'
- name: Root 로그인 차단
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
- name: 비밀번호 인증 차단
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
- name: fail2ban 설치 및 활성화
package:
name: fail2ban
state: present
- name: fail2ban 서비스 시작
service:
name: fail2ban
state: started
enabled: yes
- name: auditd 설치 및 활성화
package:
name: audit
state: present
- name: 감사 규칙 배포
copy:
src: files/hardening.rules
dest: /etc/audit/rules.d/hardening.rules
notify: restart auditd
handlers:
- name: restart sshd
service:
name: sshd
state: restarted
- name: restart auditd
service:
name: auditd
state: restarted
CI/CD 파이프라인에 보안 스캔 추가 — 배포 전에 자동으로 보안 취약점을 검사하는 파이프라인 설정입니다:
# .github/workflows/security.yml 예시 (참고용)
# Lynis를 사용한 서버 보안 점수 측정
# Trivy를 사용한 컨테이너 이미지 취약점 스캔
# OpenSCAP을 사용한 STIG/CIS 벤치마크 준수 여부 확인
CIS 벤치마크 — 업계 표준 하드닝 가이드라인
CIS(Center for Internet Security) Benchmark는 OS별 보안 설정 기준을 제시하는 업계 표준입니다.
# Lynis로 CIS 스타일 보안 점수 측정
sudo apt install -y lynis # Ubuntu
sudo dnf install -y lynis # RHEL
# 전체 시스템 감사
sudo lynis audit system
# 결과 확인 (Hardening Index 점수 제공)
# Hardening index : 65 [################ ]
# 100점 만점에서 현재 보안 수준 측정
# 각 항목별 권고사항 제시
# CI 모드 (비대화식)
sudo lynis audit system --quiet --report-file /var/log/lynis-report.dat
실무 팁: 보안 설정 변경 관리
모든 보안 설정 변경은 변경 관리 프로세스를 거쳐야 합니다:
- 변경 계획 문서화 (무엇을, 왜, 어떻게, 롤백 방법)
- 스테이징 환경에서 테스트
- 변경 전 스냅샷/백업
- 유지보수 윈도우 내에서 적용
- 모니터링 강화 (변경 직후 1~2시간)
- 롤백 준비 상태 유지
보안 담당자(또는 DevSecOps 엔지니어)는 시스템을 "잠그는 사람"이 아니라, 팀이 안전하게 빠르게 움직일 수 있도록 레일을 깔아주는 사람입니다.
핵심 명령어 요약
| 명령어 | 용도 |
|---|---|
sudo sshd -t | sshd_config 문법 검사 |
sudo fail2ban-client status sshd | fail2ban SSH jail 상태 |
sudo fail2ban-client set sshd unbanip IP | 차단 IP 해제 |
sudo ss -tulnp | 열린 포트 및 서비스 확인 |
getenforce | SELinux 모드 확인 |
sudo setenforce 0/1 | SELinux 모드 임시 변경 |
sudo ausearch -k tag_name | 감사 로그 태그 검색 |
sudo chattr +i /path | 파일 불변 속성 설정 |
sudo lsattr /path | 파일 속성 확인 |
sudo find / -perm -4000 -type f | SUID 파일 검색 |
sudo lynis audit system | 전체 보안 감사 |
sudo chage -l username | 비밀번호 만료 정보 |
sudo faillock --user username | 계정 잠금 상태 확인 |
sudo restorecon -Rv /path | SELinux 기본 컨텍스트 복구 |
sudo audit2why | SELinux 거부 이유 분석 |
실습 과제
이번 챕터에서 배운 내용을 직접 적용해보세요:
- SSH 하드닝: sshd_config에서 10개 항목을 모두 설정하고
sshd -t로 검증하세요 - fail2ban 설치: 설치 후
fail2ban-client status sshd로 동작을 확인하세요 - 포트 감사:
ss -tulnp로 열린 포트를 확인하고, 불필요한 서비스를 중지하세요 - SUID 기준선: SUID 파일 목록을 저장해두고, 1주일 후 변경 사항을 비교하세요
- auditd 설정: /etc/passwd 변경 감사 규칙을 설정하고,
useradd실행 후 로그를 확인하세요 - Lynis 실행:
lynis audit system을 실행하고 Hardening Index 점수를 기록하세요
파일시스템 마운트 옵션 — noexec·nosuid·nodev 실무 적용

공격자가 서버에 파일을 업로드하는 데 성공했더라도, /tmp에 noexec가 걸려 있으면 거기서 악성 바이너리를 실행할 수 없습니다. 업로드 디렉터리나 임시 디렉터리는 파일 쓰기를 허용해야 하지만, 실행까지 허용할 이유는 없습니다. 마운트 옵션은 설정 한 줄로 공격 경로 하나를 닫는 가장 저비용 보안 조치 중 하나입니다. 특히 /tmp와 /var/tmp는 많은 exploit 코드가 기본적으로 사용하는 경로입니다.
마운트 옵션으로 파일시스템 단위의 권한을 제한해 공격 표면을 줄입니다.
| 옵션 | 효과 | 적용 대상 |
|---|---|---|
noexec | 이 파티션의 바이너리 실행 불가 | /tmp, /var/tmp, /home |
nosuid | SUID/SGID 비트 무시 | /tmp, /var/tmp, /home, /dev |
nodev | 문자/블록 디바이스 파일 무시 | /tmp, /home |
/etc/fstab 적용 예시 — 마운트 옵션에 noexec, nosuid를 추가해 특정 파티션의 권한을 제한합니다:
# 현재 마운트 옵션 확인
findmnt -o TARGET,OPTIONS | grep -E "tmp|home|var"
# /etc/fstab 보안 강화 예시
# /tmp — 실행 및 SUID 금지
tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev,size=2G 0 0
# /var/tmp — 임시 파일 디렉토리 보안
/dev/sda3 /var/tmp ext4 defaults,noexec,nosuid,nodev 0 2
# /home — 사용자 디렉토리 실행 금지 (필요 시 예외 처리)
/dev/sda4 /home ext4 defaults,nosuid,nodev 0 2
# 변경 후 재마운트 (재부팅 없이)
sudo mount -o remount /tmp
sudo mount -o remount /home
# 검증
mount | grep /tmp | grep noexec
영향도 체크 — 설정 변경 전에 해당 파티션을 사용하는 프로세스를 확인합니다:
# noexec 적용 후 /tmp에서 스크립트 실행 시도
cp /bin/ls /tmp/test_exec
/tmp/test_exec # → Permission denied (noexec 작동 확인)
# 일부 애플리케이션이 /tmp에 실행 파일을 만들 경우 예외 처리
# (예: Java, Node.js 빌드 도구) → 전용 디렉토리를 noexec 없이 마운트
커널 보안 sysctl 하드닝 — rp_filter·accept_redirects 적용

외부에서 들어오는 패킷의 출발 IP를 위조한 IP 스푸핑 공격, ICMP 리디렉션 메시지로 라우팅 테이블을 바꾸는 공격, TCP SYN 패킷으로 연결 자원을 고갈시키는 SYN Flood — 이 세 가지는 네트워크 레이어의 고전적 공격 유형이지만 지금도 자주 사용됩니다. 방화벽이 있어도 커널 자체가 이런 패킷을 처리하는 방식을 강화하면 공격 표면이 줄어듭니다. sysctl 파라미터 몇 줄로 이 방어를 구성할 수 있습니다.
커널 파라미터를 통해 네트워크 레벨의 공격을 방어합니다.
보안 핵심 sysctl 설정 — 커널 파라미터로 IP 스푸핑, 리디렉션, SYN 플러딩을 방어합니다:
# /etc/sysctl.d/99-security-hardening.conf
# IP 스푸핑 방어 (Reverse Path Filtering)
net.ipv4.conf.all.rp_filter = 1 # strict mode
net.ipv4.conf.default.rp_filter = 1
# ICMP 리다이렉트 무시 (라우팅 조작 공격 방어)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# ICMP 리다이렉트 전송 비활성화 (게이트웨이가 아닌 경우)
net.ipv4.conf.all.send_redirects = 0
# 소스 라우팅 패킷 거부 (경로 조작 공격 방어)
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# SYN Flood 방어 (SYN Cookies)
net.ipv4.tcp_syncookies = 1
# IP 포워딩 비활성화 (라우터/게이트웨이 아닌 경우)
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# 브로드캐스트 ping 응답 비활성화 (Smurf 공격 방어)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Bogus ICMP 오류 응답 무시
net.ipv4.icmp_ignore_bogus_error_responses = 1
적용 및 검증 — sysctl 설정을 영구 적용하고 실제로 반영됐는지 확인합니다:
# 설정 적용
sudo sysctl -p /etc/sysctl.d/99-security-hardening.conf
# 적용 확인
sysctl net.ipv4.conf.all.rp_filter
# net.ipv4.conf.all.rp_filter = 1 ← 정상
# 보안 감사 도구로 전체 검증
sudo lynis audit system | grep -A2 "kernel"
rp_filter 모드 선택 — 서버 유형에 따라 역방향 경로 필터링 모드를 적절히 선택합니다:
| 값 | 동작 | 적합한 환경 |
|---|---|---|
| 0 | 비활성화 | 복잡한 멀티홈 환경 |
| 1 | strict — 최적 경로로만 수신 | 대부분의 서버 (권장) |
| 2 | loose — 어떤 인터페이스든 허용 | 비대칭 라우팅 환경 |
GRUB 부트로더 보호 — 단일 사용자 모드 접근 방지

물리적 접근이 가능한 환경이나 콘솔 접근이 가능한 클라우드 환경에서 GRUB 비밀번호 없이 단일 사용자 모드로 진입하면 root 쉘을 얻을 수 있습니다.
GRUB 비밀번호 설정 — 물리적 접근자가 부팅 옵션을 임의로 변경하지 못하도록 GRUB에 비밀번호를 설정합니다:
# GRUB 비밀번호 해시 생성
sudo grub-mkpasswd-pbkdf2
# 입력: 비밀번호 두 번 입력
# 출력: grub.pbkdf2.sha512.10000.AAAA...
# /etc/grub.d/40_custom에 추가
sudo tee -a /etc/grub.d/40_custom << 'EOF'
set superusers="grubadmin"
password_pbkdf2 grubadmin grub.pbkdf2.sha512.10000.AAAA...
EOF
# GRUB 설정 재생성
sudo update-grub # Ubuntu/Debian
sudo grub2-mkconfig -o /boot/grub2/grub.cfg # RHEL
# 특정 항목만 비밀번호 없이 부팅 허용 (일반 부팅은 자유, 편집만 제한)
# /etc/grub.d/10_linux 파일에서 --unrestricted 추가
클라우드 환경 주의:
- AWS EC2, GCP: 시리얼 콘솔 접근이 가능하므로 IAM 권한으로 제어
- GRUB 비밀번호보다 SSH 키 관리 + sudo 감사 로그가 더 실용적
/boot파티션을 별도 마운트하고 noexec,nosuid로 보호
마무리
OS 하드닝은 목적지가 아닌 지속적인 여정입니다. 오늘 완벽하게 설정했더라도, 내일 새로운 취약점이 발견되거나 팀원이 설정을 변경할 수 있습니다. 자동화된 감사 도구(Lynis, OpenSCAP)를 정기적으로 실행하고, 그 결과를 추적하는 습관을 들이세요.
"보안은 제품이 아니라 과정이다." — 브루스 슈나이어(Bruce Schneier)
다음 모듈에서는 헬스체크 스크립트를 작성하고 cron과 systemd OnFailure를 활용해 서비스 장애를 자동 감지·복구하는 방법을 다룹니다.