1GB 클라우드 서버 인스턴스, 왜 이렇게 느렸을까 — 원인 분석과 SSH 보안 강화 기록

작은 클라우드 인스턴스(RAM 약 1GB)에서 도커로 모니터링 스택(netdata, uptime-kuma)을 운영하던 중, 접속이 느려지고 SSH 연결까지 불안정해지는 문제를 겪었습니다. 원인을 하나씩 추적하고 해결한 과정을 정리합니다.

1. 증상: CPU는 멀쩡한데 시스템이 멈춘 것처럼 느림

top으로 확인한 첫 화면에서 눈에 띈 건 CPU 사용률이 아니라 I/O wait(wa) 94.8% 였습니다. CPU 연산은 거의 없는데, 프로세스 대부분이 디스크 응답을 기다리며 멈춰 있는 상태였습니다.

%Cpu(s):  2.2 us,  2.2 sy,  0.0 ni,  0.0 id, 94.8 wa, ...
MiB Mem :    958.3 total,     66.5 free, ...
MiB Swap:      0.0 total,      0.0 free,      0.0 used.

원인은 명확했습니다.

  • 전체 메모리 1GB 미만, 여유 메모리는 66MB뿐
  • 스왑이 아예 설정되어 있지 않음

메모리가 거의 꽉 찬 상태에서 스왑조차 없으니, 커널이 페이지 캐시를 밀어내며 디스크 I/O가 폭주하고 있었던 것입니다.

2. 1차 조치: 스왑 추가

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

적용 직후 wa는 94.8%에서 **0%**로 완전히 떨어졌습니다. 재부팅해도 스왑이 유지되도록 /etc/fstab에도 등록해두었습니다.

3. SSH 접속 불안정 대응

메모리 압박 상황에서 SSH 데몬(sshd)이 새 연결을 처리하지 못해 접속이 안 되는 경우가 있었습니다. systemd가 메모리 부족 시 어떤 프로세스를 먼저 죽일지 판단하는 OOM 우선순위를 sshd에 유리하게 조정했습니다.

sudo systemctl edit ssh
[Service]
OOMScoreAdjust=-500

이렇게 하면 메모리가 다시 빠듯해지더라도 sshd가 최대한 살아남아, 원격 접속 자체는 유지할 수 있습니다.

4. 반복해서 재발한 CPU 부하 — 원인이 여러 개였다

스왑을 추가한 뒤에도 시간이 지나면서 CPU/I/O 부하가 다시 튀는 일이 반복됐습니다. 매번 원인이 달랐습니다.

netdata의 apps.plugin 과부하
모든 프로세스를 1초마다 스캔하는 기본 설정이 1GB급 인스턴스에는 부담이었습니다.

[plugin:apps]
	update every = 5

[plugins]

apps = yes netflow = no

스캔 주기를 5초로 늘리고, 안 쓰는 netflow 플러그인은 껐습니다.

이중으로 떠 있던 모니터링 에이전트
클라우드 제공사가 기본으로 깔아주는 자체 모니터링 에이전트가 netdata와 별개로 계속 백그라운드에서 리소스를 수집하고 있었습니다. 심지어 API 연결에 계속 실패하면서도 재시도를 반복하며 자원만 소모하는 상태였습니다. 이미 netdata로 모니터링하고 있었기 때문에 완전히 중지했습니다.

자동 업데이트와 재부팅 직후 부하 몰림
apt의 unattended-upgrades와 snapd의 자동 패키지 갱신이 재부팅 직후에 동시에 실행되며 일시적으로 CPU/디스크 부하가 치솟는 구간이 있었습니다. 이건 일회성이라 시간이 지나면 자연히 가라앉는 것으로 확인했습니다.

5. SSH 브루트포스 공격 확인 및 방어

로그를 들여다보니 특정 IP에서 흔한 계정명(debian, root 등)으로 지속적인 로그인 시도가 있었습니다. 자동화된 봇이 인터넷 전체 IP 대역을 스캔하다가 열려 있는 22번 포트를 찾아낸, 흔한 케이스였습니다. 클라우드 IP 대역은 공개적으로 알려져 있어 특히 더 자주 스캔 대상이 됩니다.

fail2ban 설치 및 설정

sudo apt install fail2ban -y
[DEFAULT]
findtime = 600
maxretry = 5
ignoreip = 127.0.0.1/8 <본인 고정 IP들>

[sshd]

enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600 bantime.increment = true bantime.factor = 2 bantime.maxtime = 604800 bantime.rndtime = 300

[recidive]

enabled = true logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s bantime = 604800 findtime = 86400 maxretry = 3

  • sshd jail: 3회 실패 시 차단, 재범할수록 차단 시간이 2배씩 증가(최대 7일)
  • recidive jail: 24시간 안에 3번 밴 당한 상습 IP는 별도로 1주일 차단하는 안전망
  • ignoreip: 본인이 접속하는 고정 IP는 실수로도 차단되지 않도록 예외 처리

적용 후 실제로 여러 공격 IP가 자동으로 차단되는 것을 확인했습니다.

Currently banned: 4
Banned IP list: <차단된 IP들>

참고: 로그 백엔드 차이

서버마다 fail2ban이 로그를 읽는 방식이 다르게 나타났습니다. 어떤 서버는 /var/log/auth.log 파일을 직접 읽고, 어떤 서버는 systemd journal(journalctl)을 직접 조회했습니다. 두 방식 다 정상이며, journal 방식은 rsyslog 같은 중간 로깅 서비스에 의존하지 않아 오히려 더 안정적인 경우도 있습니다.

6. 침해 여부 점검

공격 시도가 많았던 만큼, 혹시 이미 뚫린 적이 있는지도 점검했습니다.

last -a | head -20      # 로그인 이력 확인
lastlog                 # 계정별 마지막 로그인
cat ~/.ssh/authorized_keys   # 등록된 SSH 키 확인
crontab -l               # 예약 작업에 낯선 스크립트 없는지
sudo ss -tulpn            # 비정상적인 네트워크 연결 확인

로그인 이력은 본인이 쓰는 IP로만 이루어져 있었고, 낯선 계정이나 백도어 흔적은 없었습니다. 무수히 많은 로그인 실패 시도가 있었지만, 실제로 뚫린 흔적은 확인되지 않았습니다.

7. netdata 웹 대시보드 접근 제한

모니터링 대시보드가 전 세계 어디서든 접근 가능하도록 열려 있던 설정도 함께 손봤습니다.

allow connections from = localhost 127.0.0.1 <내부망 대역>

리버스 프록시(nginx)를 통해 접속하는 구조라면, 이렇게 제한해도 정상적인 접속 경로에는 영향이 없고 외부에서 포트로 직접 찌르는 시도만 차단됩니다. 단, 도커 컨테이너를 재생성하면 내부 네트워크 대역이 바뀔 수 있어, 이로 인해 일시적으로 502 에러가 발생하기도 했습니다 — 컨테이너 재생성 후에는 실제 네트워크 대역을 다시 확인하고 설정을 맞춰야 합니다.

정리

문제원인조치
I/O wait 94.8%메모리 부족 + 스왑 없음스왑 2GB 추가
SSH 간헐적 접속 불가메모리 압박 시 sshd 자원 부족OOMScoreAdjust 조정
CPU 반복 급등netdata 과다 스캔, 중복 모니터링 에이전트, 자동 업데이트 몰림스캔 주기 조정, 중복 에이전트 제거
지속적 SSH 공격 시도인터넷 전역 스캔 봇fail2ban + 점진적 차단 시간 증가
모니터링 대시보드 전체 공개접근 제어 미설정내부망/로컬로 접근 제한

작은 인스턴스일수록 여러 프로세스가 리소스를 나눠 쓰다 보면 문제가 겹쳐서 나타나는 경우가 많았습니다. top의 각 지표(특히 wa, 스왑 사용량)를 꾸준히 관찰하는 습관이 원인 파악에 큰 도움이 됐습니다.