| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- 코어자바스크립트
- js배열 알고리즘
- 실행컨텍스트
- JS module system
- this
- 웹 크롤링
- chromatic error
- 항해99 사전스터디
- 리액트
- Js module
- 웹팩 기본개념
- 항해99 미니프로젝트
- 렌더링 최적화
- 자바스크립트 엔진 v8
- 항해99 부트캠프
- 리액트 메모
- 함수형 프로그래밍 특징
- FP 특징
- jwt
- gql restapi 차이
- toggle-btn
- 타입스크립트
- 리액트 메모이제이션
- 리액트 렌더링 최적화
- 테스트 코드 툴 비교
- 알고리즘
- 항해99
- next js
- v8 원리
- 리덕스
- Today
- Total
Jaeilit
EC2 인스턴스 갑작스러운 다운, 2806번의 SSH 브루트포스 공격 본문
배경
목요일 퇴근길에 서비스를 한번 들어가봤는데 페이지에서 로딩이 엄청 걸렸습니다. 별도의 모니터링 서비스가 없어서 핸드폰으로 직접 클라우드에 접속해서 봤습니다. 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
공격자 정보 정리:
- 76.8.66.186 (465건) - 미국 뉴욕주 - Crown Castle Fiber (People's Choice Communications)
- 164.90.155.58 (266건) - 미국 콜로라도 - DigitalOcean (클라우드 서버)
- 161.35.152.121 (266건) - 미국 콜로라도 - DigitalOcean (클라우드 서버)
- 142.111.244.35 (266건) - 미국 유타/와이오밍 - IB Compute Systems (데이터센터)
- 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 설정:
- Connection Settings > Main Tab:
- Host: localhost Port: 5432 Database: lotto_prod
- SSH Tab:
- ✓ 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)
'TIL' 카테고리의 다른 글
| 타입스크립트을 써야하는 이유 (0) | 2025.12.13 |
|---|---|
| AWS S3를 활용한 프로필 이미지 업로드 구현하기 (0) | 2025.02.21 |
| swagger-typescript-api 도입 후 fetchUtils 마이그레이션 (0) | 2025.02.12 |
| DNS를 Route 53에서 Cloudflare로 이전하면서 디도스 방어하기 (0) | 2025.01.20 |
| 내 사이트 더 많이 노출 시키기(SEO) (0) | 2025.01.16 |
