Jaeilit

EC2 인스턴스 갑작스러운 다운, 2806번의 SSH 브루트포스 공격 본문

TIL

EC2 인스턴스 갑작스러운 다운, 2806번의 SSH 브루트포스 공격

Jaeilit 2025. 12. 14. 21:23
728x90

배경

목요일 퇴근길에 서비스를 한번 들어가봤는데 페이지에서 로딩이 엄청 걸렸습니다. 별도의 모니터링 서비스가 없어서 핸드폰으로 직접 클라우드에 접속해서 봤습니다. CPU가 100%를 찍고 서버가 다운이 됐었습니다. 이런 적이 없는데.. 이상함을 느끼고 즉시 집으로 돌아와서 확인 해봤습니다.

 

EC2 인스턴스 갑작스러운 다운 트러블슈팅: SSH 브루트포스 공격과 OOM Killer 분석

🚨 문제 발생

운영 중이던 EC2 인스턴스에 SSH 접속을 시도하는 과정에서 연결 실패 에러가 발생했습니다.

Failed to connect to your instance
Error establishing SSH connection to your instance. Try again later.

 

1년 이상 안정적으로 운영해온 프로덕션 환경이었기에, 즉각적인 원인 파악이 필요한 상황이었습니다. AWS 콘솔을 통해 인스턴스 상태를 확인한 결과, 인스턴스가 "stopping" 상태에 머물러 있었으며, 재시작 시도 시 다음과 같은 에러가 발생했습니다.

The instance 'i-009a8a3ea45a9bdb2' is not in a state from which it can be started.

🔍 원인 파악 과정

1단계: 초기 진단 - 시스템 로그 분석

EC2 콘솔의 "작업(Actions) → 모니터링 및 문제 해결 → 시스템 로그 가져오기"를 통해 부팅 로그를 확인했습니다.

[    1.600432] EXT4-fs (xvda1): INFO: recovery required on readonly filesystem
[    1.613122] EXT4-fs (xvda1): 24 orphan inodes deleted
[    1.614650] EXT4-fs (xvda1): recovery complete

분석 결과:

  • ext4 파일시스템 저널 복구가 수행됨
  • 24개의 orphan inode 삭제 → 비정상 종료 증거
  • 파일시스템이 read-only 모드로 마운트 후 복구 진행

이는 시스템이 정상적인 shutdown 절차 없이 강제 종료되었음을 의미합니다.

2단계: 커널 파라미터 및 부팅 설정 분석

시스템 로그에서 커널 부팅 파라미터를 확인했습니다.

Dec 11 09:45:49 kernel: Command line: BOOT_IMAGE=/vmlinuz-6.14.0-1018-aws 
root=PARTUUID=3bc7495a-004e-409a-8296-171507c04602 ro console=tty1 console=ttyS0 
nvme_core.io_timeout=4294967295 panic=-1

주목할 파라미터:

  • panic=-1: 커널 패닉 발생 시 즉시 재부팅 설정
  • 이 설정은 시스템이 복구 불가능한 상태에 빠졌을 때 자동 재부팅을 수행

커널 패닉이 발생했을 가능성을 염두에 두고 추가 조사를 진행했습니다.

3단계: 부팅 이력 및 로그 타임라인 분석

sudo journalctl --list-boots
-3  0553c30b95314c2aa759a1b9b3a46d52  Sun 2025-09-07 17:11:03  Sat 2025-12-06 11:41:13
-2  7042579562b345429184319db04352f2  Sat 2025-12-06 11:46:23  Thu 2025-12-11 09:36:59
-1  15456917fc104573b4a3e0b3727b0be8  Thu 2025-12-11 09:40:18  Thu 2025-12-11 09:43:27
 0  6f68a6a380cc4dfbb310106d53b4a538  Thu 2025-12-11 09:45:49  Thu 2025-12-11 09:48:32

타임라인 분석:

  • 12월 6일 11:46 → 12월 11일 09:36: 4일 21시간 50분 운영
  • 12월 11일 09:36 → 09:40: 4분간 다운타임
  • 12월 11일 09:40 → 09:43: 재부팅 후 3분간만 운영 후 재차 다운
  • 12월 11일 09:45: 최종 재부팅 후 안정화

짧은 운영 시간 후 반복적인 크래시는 리소스 고갈을 시사합니다.

4단계: 이전 부팅 세션 상세 로그 분석

sudo journalctl -b -2 --no-pager | tail -200

 

로그 끝부분에서 중요한 패턴을 발견했습니다.

Dec 11 09:03:27 systemd-journald[129]: Under memory pressure, flushing caches.
Dec 11 09:10:40 systemd-journald[129]: Under memory pressure, flushing caches.
Dec 11 09:11:05 systemd-journald[129]: Under memory pressure, flushing caches.
...
(총 30회 이상 반복)
...
Dec 11 09:36:59 systemd-journald[129]: Under memory pressure, flushing caches.

메모리 압박(Memory Pressure) 분석:

  • systemd-journald가 메모리 부족으로 캐시를 강제로 비우고 있음
  • 09:03부터 09:36까지 약 33분간 지속적인 메모리 압박
  • 마지막 메시지(09:36:59) 직후 시스템 다운 → 메모리 고갈이 직접적 원인

5단계: SSH 로그 분석 - 공격 패턴 발견

메모리 압박의 원인을 찾기 위해 인증 로그를 분석했습니다.

sudo journalctl -b -2 | grep "Invalid user\|Failed password" | wc -l
2806

 

2,806건의 실패한 로그인 시도!

공격자 IP 추출 및 통계 분석:

sudo journalctl -b -2 | grep "Invalid user\|Failed password" | \
grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | \
sort | uniq -c | sort -rn | head -20
465 76.8.66.186
266 164.90.155.58
266 161.35.152.121
266 142.111.244.35
266 116.148.226.140
 88 14.103.85.199
 79 39.105.3.55
 76 46.105.28.181
...

 

WHOIS 조회 결과:

whois 76.8.66.186 | grep -E "Country|OrgName"
# OrgName: Crown Castle Fiber LLC
# Country: US

whois 164.90.155.58 | grep -E "Country|OrgName"
# OrgName: DigitalOcean, LLC
# Country: US

공격자 정보 정리:

  1. 76.8.66.186 (465건) - 미국 뉴욕주 - Crown Castle Fiber (People's Choice Communications)
  2. 164.90.155.58 (266건) - 미국 콜로라도 - DigitalOcean (클라우드 서버)
  3. 161.35.152.121 (266건) - 미국 콜로라도 - DigitalOcean (클라우드 서버)
  4. 142.111.244.35 (266건) - 미국 유타/와이오밍 - IB Compute Systems (데이터센터)
  5. 116.148.226.140 - 중국 - China Unicom

 

공격 특성 분석:

  • 총 150개 이상의 고유 IP에서 공격
  • DigitalOcean, AWS 등 클라우드 서비스를 악용한 분산 공격
  • 전형적인 봇넷 기반 브루트포스 공격 패턴
  • 사전 대입(Dictionary Attack) 공격으로 추정되는 다양한 사용자명 시도
sudo journalctl -b -2 | grep "Invalid user" | awk '{print $9}' | sort | uniq
usuario
sysadmin
zimbra
admin
oracle
...

6단계: 결정적 증거 - OOM Killer 로그 발견

sudo journalctl -b -2 --since "2025-12-11 08:55:00" --until "2025-12-11 08:56:00" | grep -A 50 "oom-killer"
Dec 11 08:55:22 kernel: containerd invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=-999
Dec 11 08:55:33 kernel: Mem-Info:
Dec 11 08:55:33 kernel: active_anon:351464kB inactive_anon:393564kB active_file:372kB inactive_file:5912kB unevictable:40248kB
Dec 11 08:55:33 kernel: Free swap  = 0kB
Dec 11 08:55:33 kernel: Total swap = 0kB
Dec 11 08:55:35 kernel: free:12229kB

OOM Killer 분석:

  • gfp_mask=0x140cca: GFP_HIGHUSER_MOVABLE 플래그 → 유저 공간 메모리 할당 실패
  • order=0: 단일 페이지(4KB) 할당조차 실패
  • oom_score_adj=-999: containerd는 보호되어야 할 프로세스지만, 시스템 전체가 메모리 부족 상태
  • 스왑 공간 0KB: 메모리 버퍼 전무
  • 가용 메모리 12MB: 1GB 중 단 1.2%만 남음

메모리 사용 분석:

active_anon: 351MB (프로세스 활성 메모리)
inactive_anon: 393MB (비활성 프로세스 메모리)
총 사용: ~744MB + 시스템 오버헤드 = 거의 전체 메모리 고갈

7단계: 침해 여부 확인 - 보안 감사

공격이 성공했는지 확인하기 위한 종합 보안 감사를 수행했습니다.

# 1. 성공한 로그인 확인
sudo grep "Accepted password\|Accepted publickey" /var/log/auth.log* | tail -30

# 결과: 모두 정상 IP (13.209.1.59, 13.209.1.60 - AWS Korea region)
# 공격자 IP의 성공 로그인 기록 전무
# 2. 계정 생성 여부 확인
awk -F: '$3 >= 1000 {print $1 " (UID: " $3 ")"}' /etc/passwd

# 결과: ubuntu (UID: 1000) - 단일 사용자 계정만 존재
# 3. sudo 권한 변조 확인
sudo grep -i "sudo:" /var/log/auth.log | tail -50

# 결과: 모든 sudo 사용이 ubuntu 계정에서만 발생, 비정상 활동 없음
# 4. 파일 권한 감사
ls -la ~/.ssh/
# drwx------  .ssh (700)
# -rw-------  authorized_keys (600)
# 권한 설정 정상
# 5. Cron 작업 점검
sudo crontab -l
crontab -l
sudo cat /etc/crontab

# 결과: 시스템 기본 작업만 존재, 악성 스케줄러 없음

 

보안 감사 결론: ✅ 모든 공격 시도 실패 ✅ 시스템 무결성 유지 ✅ 악성 코드 또는 백도어 흔적 없음

🛠️ 문제 해결 과정

1. 긴급 복구: 인스턴스 재시작

강제 중지 시도:

aws ec2 stop-instances --instance-ids i-009a8a3ea45a9bdb2 --force
aws ec2 start-instances --instance-ids i-009a8a3ea45a9bdb2

EC2 콘솔을 통한 강제 중지 후, 인스턴스가 정상적으로 부팅되는 것을 확인했습니다.

2. 스왑 공간 구성 (Critical Fix)

# 2GB 스왑 파일 생성
sudo fallocate -l 2G /swapfile

# 보안을 위한 권한 설정 (root만 읽기/쓰기)
sudo chmod 600 /swapfile

# 스왑 영역 초기화
sudo mkswap /swapfile

# 스왑 활성화
sudo swapon /swapfile

# 재부팅 후에도 유지되도록 fstab 설정
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 검증
free -h
swapon --show

 

3. 네트워크 보안 강화

AWS 보안 그룹 재구성

변경 전 (취약한 구성):

Type: SSH
Protocol: TCP
Port: 22
Source: 0.0.0.0/0, ::/0
Description: Allow SSH from anywhere

변경 후 (보안 강화):

Type: SSH
Protocol: TCP  
Port: 22
Source: 180.224.124.175/32
Description: SSH - Restricted to authorized IP only

IP 화이트리스트 전략:

  • /32 CIDR: 단일 IP 주소 (최대 보안)
  • 고정 IP가 없는 경우: VPN 게이트웨이 IP 사용 권장
  • 복수 위치에서 접속: 각 위치별 IP를 개별 규칙으로 추가

포트 노출 최소화

Docker Compose 포트 바인딩 재구성:

# PostgreSQL - 외부 노출 차단
postgres:
  ports:
    - '127.0.0.1:5432:5432'  # localhost only

# Prometheus - 내부 네트워크만
prometheus:
  ports:
    - '127.0.0.1:9090:9090'

# Grafana - nginx reverse proxy 통해서만 접근
grafana:
  ports:
    - '127.0.0.1:3000:3000'

# node_exporter - 모니터링 수집용
node_exporter:
  ports:
    - '127.0.0.1:9100:9100'

# postgres-exporter
postgres-exporter:
  ports:
    - '127.0.0.1:9187:9187'

검증:

sudo netstat -tulpn | grep LISTEN | grep -E "5432|9090|3000|9100|9187"

# 예상 결과:
# tcp  127.0.0.1:5432  0.0.0.0:*  LISTEN
# tcp  127.0.0.1:9090  0.0.0.0:*  LISTEN
# ...

4. 보안 설정 강화

SSH 하드닝

sudo nano /etc/ssh/sshd_config
# 비밀번호 인증 비활성화 (키 기반만 허용)
PasswordAuthentication no
ChallengeResponseAuthentication no

# Root 로그인 차단
PermitRootLogin no

# 인증 시도 횟수 제한
MaxAuthTries 3

# 빈 비밀번호 차단
PermitEmptyPasswords no

# X11 포워딩 비활성화 (불필요한 경우)
X11Forwarding no

# 로그인 타임아웃 설정
LoginGraceTime 30

설정 적용:

sudo systemd restart sshd

Docker 보안 강화

Grafana 인증 강화:

grafana:
  environment:
    - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} 
    - GF_SECURITY_SECRET_KEY=${GRAFANA_SECRET_KEY}
    - GF_SERVER_ROOT_URL=https://grafana.도메인
    - GF_AUTH_ANONYMOUS_ENABLED=false

PostgreSQL 환경 변수 보안:

postgres-exporter:
  environment:
    # 하드코딩 제거, 환경 변수 사용
    DATA_SOURCE_NAME: "postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}?sslmode=disable"

5. DBeaver 접속 설정 (SSH 터널링)

외부 노출 없이 안전한 DB 접속:

DBeaver 설정:

  1. Connection Settings > Main Tab:
  2. Host: localhost Port: 5432 Database: lotto_prod
  3. SSH Tab:
  4. ✓ Use SSH Tunnel Host: 13.125.156.172 Port: 22 Username: ubuntu Auth Method: Public Key Private Key: /path/to/your-key.pem

장점:

  • PostgreSQL 포트를 인터넷에 노출하지 않음
  • SSH 암호화 레이어 추가
  • IP 변경에 무관하게 접속 가능

📋 장기 대책 및 모범 사례

1. 모니터링 및 알림 시스템 구축

CloudWatch 알람 설정

메모리 사용률 알람:

# CloudWatch Agent 설치
wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
sudo dpkg -i amazon-cloudwatch-agent.deb

# 설정 파일 생성
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard

알람 임계값 설정:

  • Memory Usage > 80%: Warning
  • Memory Usage > 90%: Critical
  • Swap Usage > 50%: Investigation needed

Prometheus + Grafana 대시보드

핵심 메트릭:

# prometheus.yml
scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['node_exporter:9100']
    
  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres-exporter:9187']

주요 쿼리:

# 메모리 사용률
100 - ((node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100)

# 스왑 사용률
(node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes) / node_memory_SwapTotal_bytes * 100

# SSH 실패 시도 (실시간 감지)
rate(node_auth_ssh_invalid_user_total[5m])

2. 침입 탐지 시스템 고도화

fail2ban 구현 (향후 계획)

현재는 AWS 보안 그룹으로 대응 중이나, 추가 계층 방어를 위해 fail2ban 도입을 고려할 수 있습니다:

# 설치
sudo apt install fail2ban -y

# 설정
sudo tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
# 차단 시간: 24시간
bantime = 86400
# 관찰 시간: 10분
findtime = 600
# 최대 실패 허용: 3회
maxretry = 3
# 로그 백엔드
backend = systemd

[sshd]
enabled = true
port = 22
filter = sshd
logpath = /var/log/auth.log
maxretry = 3

# 더 엄격한 설정 (선택)
[sshd-aggressive]
enabled = true
filter = sshd
logpath = /var/log/auth.log
maxretry = 1
bantime = 604800  # 7일
EOF

Slack/Email 알림 통합:

[DEFAULT]
# Slack webhook
action = %(action_mw)s
         slack-notify[name=%(__name__)s, dest="your-webhook-url"]

OSSEC HIDS (선택사항)

더 포괄적인 침입 탐지가 필요한 경우:

# 파일 무결성 모니터링
# 로그 분석
# Rootkit 탐지
# 활성 응답 (Active Response)

 

728x90