3편에서 500 오류를 잡고 사이트가 IP로 떴습니다. 여기서 바로 도메인 연결로 넘어가고 싶은 마음을 한 번만 눌러주세요. 서버는 켜진 순간부터 공격받고 있습니다. 비유가 아니라 사실입니다. 제 서버를 만든 지 하루도 안 돼서 로그를 봤더니 이랬습니다.
sudo grep "Failed password" /var/log/auth.log | wc -l
세 자리 숫자가 나왔습니다. 아무한테도 IP를 알려준 적 없는데요. 공개 IP는 전 세계 스캐너가 24시간 훑고 다닙니다. 접속해서 root / admin / ubuntu에 흔한 비밀번호를 넣어보는 봇들입니다. 이들은 제 블로그에 관심이 없습니다. 그냥 열려 있는 문을 찾을 뿐입니다.
그래서 도메인을 붙이기 전에 30분을 씁니다. 다섯 가지고, 순서가 중요합니다. 특히 두 번째는 우분투 24.04에서 대부분의 튜토리얼대로 하면 안 먹힙니다. 제가 그렇게 당했습니다.
| # | 할 일 | 막는 공격 | 시간 |
|---|---|---|---|
| 1 | SSH 키 인증(ed25519) 설정 | 비밀번호 추측 | 10분 |
| 2 | 비밀번호 로그인 차단 | 무차별 대입 전체 | 5분 |
| 3 | root 직접 로그인 차단 | 최고권한 직행 | 2분 |
| 4 | 자동 보안 업데이트 | 알려진 취약점 | 5분 |
| 5 | Fail2ban 설치 | 반복 시도·로그 오염 | 8분 |
SSH 키 인증 — 비밀번호를 아예 없앤다
비밀번호는 아무리 복잡해도 맞힐 수 있는 것입니다. 봇은 지치지 않고 초당 수십 번 시도합니다. 키는 다릅니다. 맞히는 게 아니라 갖고 있거나 없거나입니다.
| 비밀번호 | SSH 키 | |
|---|---|---|
| 공격 방식 | 계속 찍어보면 언젠가 맞음 | 찍는 게 불가능 |
| 유출 경로 | 재사용, 피싱, 어깨너머 | 내 PC가 털려야 함 |
| 서버에 저장되는 것 | 해시(그래도 대조 가능) | 공개키만 — 털려도 무의미 |
| 입력 | 매번 타이핑 | 자동 |
키 방식의 핵심은 개인키가 서버에 절대 올라가지 않는다는 점입니다. 서버엔 공개키만 둡니다. 서버가 통째로 털려도 공개키로는 아무것도 못 합니다.
키 종류는 ed25519를 씁니다.
| RSA 2048/4096 | ed25519 | |
|---|---|---|
| 키 길이 | 길다 (수백 자) | 짧다 (한 줄) |
| 속도 | 느림 | 빠름 |
| 안전성 | 길이를 늘려야 확보 | 기본으로 충분 |
| 호환성 | 어디서나 | OpenSSH 6.5+ (2014년 이후) |
| 추천 | 아주 오래된 장비용 | 기본값으로 이것 |
내 PC에서 키를 만듭니다. 서버가 아닙니다. 개인키가 내 PC를 떠나면 안 되니까요.
ssh-keygen -t ed25519 -C "mj@ddlabx"
저장 경로는 기본값(엔터), 암호구문(passphrase)은 넣는 걸 권합니다. 노트북을 잃어버렸을 때 마지막 방어선입니다. 윈도우면 C:\Users\사용자명\.ssh\에 두 파일이 생깁니다.
| 파일 | 정체 | 취급 |
|---|---|---|
id_ed25519 | 개인키 | 절대 밖으로. 백업은 오프라인으로 |
id_ed25519.pub | 공개키 | 서버에 올리는 것. 공개돼도 무방 |
주의 — 윈도우 PowerShell에는 ssh-copy-id가 없습니다. 리눅스·맥 기준으로 쓰인 튜토리얼을 그대로 따라 하면 “명령을 찾을 수 없습니다”가 납니다. 아래 방법을 쓰세요.
PowerShell에서 공개키를 서버에 등록하는 한 줄입니다.
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh mj@서버IP "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
권한(700/600)이 중요합니다. SSH는 .ssh 폴더나 authorized_keys가 남에게 열려 있으면 보안상 이유로 키를 아예 무시합니다. “키를 분명히 넣었는데 계속 비밀번호를 묻는” 사례의 대부분이 이 권한 문제입니다.
이제 새 창에서 확인합니다. 기존 창은 닫지 마세요.
ssh mj@서버IP
비밀번호를 안 묻고 바로 들어가면 성공입니다.
비밀번호 로그인 차단 — 여기서 다들 속습니다
키가 되니까 이제 비밀번호를 막습니다. 튜토리얼은 하나같이 이렇게 시킵니다.
sudo nano /etc/ssh/sshd_config
# PasswordAuthentication no 로 수정
sudo systemctl restart ssh
그리고 비밀번호 로그인이 여전히 됩니다. 저는 여기서 30분을 날렸습니다. 설정을 몇 번을 다시 봐도 no인데 왜 되는지 이해가 안 갔어요.
범인은 두 가지가 겹친 것이었습니다.
첫째, Ubuntu 24.04의 /etc/ssh/sshd_config는 파일 맨 위에 이 줄이 있습니다.
Include /etc/ssh/sshd_config.d/*.conf
둘째, OpenSSH의 설정 규칙은 “먼저 나온 값이 이긴다”입니다. 대부분의 설정 파일이 “나중 것이 덮어쓴다”인 것과 반대예요. 그러니까 맨 위에서 불러온 파일의 값이 최종값이 되고, 아래에서 내가 고친 건 읽히지도 않습니다.
그리고 Vultr 같은 클라우드 이미지는 cloud-init이 부팅할 때 이런 파일을 몰래 만들어 둡니다.
/etc/ssh/sshd_config.d/50-cloud-init.conf
→ PasswordAuthentication yes
즉 내 no는 처음부터 진 싸움이었습니다. 먼저 진짜 어디에 뭐가 적혀 있는지 찾습니다.
sudo grep -ri passwordauthentication /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
50-cloud-init.conf가 나오면 그 파일을 고칩니다.
sudo nano /etc/ssh/sshd_config.d/50-cloud-init.conf
PasswordAuthentication no
참고 — “번호가 큰 파일을 새로 만들면 덮어쓰지 않나?” 아닙니다. 60-hardening.conf를 만들어도 50번이 먼저 읽히니 50번이 이깁니다. 굳이 별도 파일로 하려면 01-hardening.conf처럼 번호를 더 낮게 줘야 합니다. 이 함정이 진짜 고약합니다.
그리고 우분투 24.04는 SSH 재시작 방법도 다릅니다. 22.10부터 SSH가 소켓 활성화(socket activation) 방식으로 바뀌었습니다. 평소엔 sshd가 떠 있지 않다가 접속이 들어오면 그때 뜹니다. 메모리를 아끼는 구조인데, 설정 반영 방식이 달라집니다.
sudo sshd -t
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
daemon-reload가 빠지면 소켓 설정이 재생성되지 않습니다. 포트를 바꿀 때도 ssh.service가 아니라 ssh.socket을 재시작해야 합니다.
가장 중요한 건 검증입니다. 설정 파일을 눈으로 보지 말고, sshd에게 직접 물어보세요. 이 명령은 모든 파일을 합친 최종 결과를 보여줍니다.
sudo sshd -T | grep -i passwordauthentication
passwordauthentication no가 나와야 진짜 꺼진 겁니다. 이 명령을 몰랐다면 저는 아직도 헤매고 있었을 겁니다.
root 직접 로그인 차단
공격자 입장에서 로그인은 아이디 + 비밀번호 두 개를 맞히는 문제입니다. 그런데 root는 모든 리눅스에 반드시 있고 이름이 정해져 있습니다. 즉 절반을 공짜로 주는 셈입니다. 봇들이 root부터 두드리는 이유가 이겁니다.
2편에서 mj 계정을 만들고 sudo를 준 게 이걸 위한 밑작업이었습니다. 이제 root 문을 닫습니다.
sudo grep -ri permitrootlogin /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
찾은 파일에서 값을 바꿉니다.
PermitRootLogin no
| 값 | 의미 | 평가 |
|---|---|---|
yes | 비밀번호로도 root 접속 가능 | 최악 |
prohibit-password | 키로만 root 접속 가능 (24.04 기본값) | 보통 — root 이름은 여전히 유효 |
no | root 직접 접속 완전 차단 | 권장 — sudo로 쓰면 됩니다 |
sudo sshd -t && sudo systemctl daemon-reload && sudo systemctl restart ssh.socket
sudo sshd -T | grep -i permitrootlogin
주의 — 여기서도 원칙은 같습니다. 지금 접속창을 절대 닫지 말고 새 창에서 ssh mj@서버IP가 되는지 먼저 확인하세요. 확인 전에 창을 닫았다가 설정이 잘못됐으면, 그때부터는 Vultr 웹콘솔로 들어가야 합니다.
자동 보안 업데이트 — 게으름을 인정하는 설정
솔직해집시다. 매일 apt upgrade를 칠 사람이 몇이나 될까요. 저는 못 합니다. 서버 취약점 대부분은 제로데이가 아니라 패치가 나온 지 몇 달 지났는데 아무도 안 올린 것입니다. 그러니 자동화합니다.
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
파란 화면에서 Yes. 이걸로 보안 업데이트만 자동 설치됩니다. 일반 패키지 업그레이드는 건드리지 않아요. 설정이 마음대로 바뀔 걱정은 안 해도 됩니다.
cat /etc/apt/apt.conf.d/20auto-upgrades
Unattended-Upgrade "1";이 보이면 켜진 겁니다. 동작 기록은 여기 쌓입니다.
sudo tail -20 /var/log/unattended-upgrades/unattended-upgrades.log
참고 — 커널이 업데이트되면 재부팅해야 적용됩니다. 자동 재부팅은 기본적으로 꺼져 있어요. /var/run/reboot-required 파일이 있으면 재부팅이 필요하다는 뜻입니다. 개인 블로그면 한 달에 한 번쯤 확인해서 수동 재부팅하는 정도로 충분합니다.
Fail2ban — 반복해서 두드리면 문을 닫는다
키 인증만 켰으면 사실 비밀번호 공격은 이미 무의미합니다. 그런데도 봇은 계속 두드리고, 그 시도가 전부 로그에 쌓입니다. 로그가 오염되면 진짜 문제가 생겼을 때 그 줄을 못 찾습니다. 3편에서 로그가 얼마나 중요한지 봤죠. Fail2ban은 그 로그를 지켜주는 장치이기도 합니다.
sudo apt install fail2ban -y
기본 설정 파일(jail.conf)은 직접 고치지 않습니다. 업데이트 때 덮어써지거든요. jail.local을 새로 만듭니다. 이게 우선합니다.
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
[sshd]
enabled = true
| 항목 | 뜻 | 이 설정에서는 |
|---|---|---|
findtime | 세는 시간 창 | 10분 안에 |
maxretry | 허용 실패 횟수 | 3번 틀리면 |
bantime | 차단 기간 | 1시간 차단 |
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
이 확인을 꼭 하세요. “설치했으니 되겠지” 하고 넘어가면 안 됩니다. Currently banned 숫자가 보이면 정상 작동 중입니다. 며칠 뒤에 다시 보면 차단된 IP가 쌓여 있을 겁니다. 그게 다 나를 노렸던 시도들입니다.
참고 — fail2ban-client status sshd에서 오류가 나거나 jail을 못 찾는다고 하면, jail.local의 [sshd] 아래에 backend = systemd를 추가하고 재시작해 보세요. 로그를 어디서 읽을지 못 정해서 생기는 문제입니다.
SSH 포트를 바꾸는 건 보안일까
많이들 22번 포트를 2222 같은 걸로 바꾸라고 합니다. 솔직하게 말하면 이건 보안이 아닙니다. 포트 스캔 한 번이면 다 드러납니다. 자물쇠를 바꾼 게 아니라 문을 옆으로 옮긴 것뿐이에요.
그럼 무의미하냐 — 그건 아닙니다. 로그가 조용해집니다. 자동화된 봇 대부분은 22번만 훑고 지나가거든요. 로그 소음이 줄면 진짜 이상 신호가 눈에 띕니다. 그게 유일하고 진짜인 이득입니다.
| 얻는 것 | 못 얻는 것 | 비용 | |
|---|---|---|---|
| 포트 변경 | 로그 소음 감소 | 실질 보안 (스캔에 다 걸림) | 접속 때마다 -p, 방화벽 재설정 |
| 키 인증 + 비번 차단 | 무차별 대입 원천 차단 | — | 초기 10분 |
우선순위가 명확합니다. 1~3번을 먼저 하세요. 포트 변경은 그 다음에 취향껏입니다. 게다가 24.04에서는 앞서 말한 ssh.socket 때문에 포트 변경이 한 단계 더 까다롭습니다. sshd_config에서 Port를 바꾼 뒤 반드시 daemon-reload 후 ssh.socket을 재시작해야 하고, UFW에서 새 포트를 열어두는 걸 잊으면 그대로 잠깁니다.
자주 묻는 질문
Q. 개인키를 잃어버리면 서버에 영영 못 들어가나요?
A. SSH로는 못 들어갑니다. 하지만 Vultr 웹콘솔이 있습니다. 대시보드에서 브라우저로 서버 화면에 직접 접속하는 기능인데, SSH를 거치지 않습니다. 여기로 들어가서 새 공개키를 authorized_keys에 넣으면 됩니다. 그러려면 root나 mj 계정의 비밀번호는 알고 있어야 하니, 비밀번호 로그인을 껐더라도 비밀번호 자체는 비밀번호 관리자에 보관해 두세요.
Q. 이 5가지면 충분한가요?
A. 아닙니다. “서버 세우고 30분 안에”에 해당하는 최소한입니다. 침입 경로의 큰 덩어리를 막을 뿐이에요. 워드프레스 자체 보안(관리자 계정명, 로그인 시도 제한, 플러그인 최신화)은 별개 주제고, 백업은 그 무엇보다 중요합니다. 보안의 마지막 보루는 언제나 복구 가능한 백업입니다.
Q. UFW는 왜 없나요?
A. 2편에서 이미 했기 때문입니다. sudo ufw status로 확인해 보세요. OpenSSH와 Nginx Full만 있으면 정상입니다. 뭔가 더 열려 있다면 왜 열었는지 스스로 설명할 수 있어야 합니다. 설명 못 하면 닫으세요.
Q. sudo 쓸 때 비밀번호 안 묻게 하면 편하지 않나요?
A. 편하고, 위험합니다. 그 순간 SSH 세션 하나만 뚫리면 곧바로 root입니다. sudo 비밀번호는 마지막 확인 절차예요. 하루에 몇 번 치는 게 그렇게 큰 비용은 아닙니다.
Q. 이미 공격받고 있는지 어떻게 아나요?
A. 이 두 줄이면 대충 보입니다. 숫자를 보면 왜 이걸 해야 하는지 바로 납득이 됩니다.
sudo grep "Failed password" /var/log/auth.log | wc -l
sudo grep "Accepted" /var/log/auth.log | tail -5
두 번째 줄은 성공한 로그인입니다. 여기에 내가 모르는 접속이 있으면 그건 다른 차원의 문제입니다.
정리하면
- 서버는 켜진 순간부터 스캔당합니다.
grep "Failed password" /var/log/auth.log | wc -l로 직접 확인해 보세요. - ed25519 키 인증이 1순위입니다. 개인키는 내 PC를 떠나지 않습니다.
- 윈도우에는
ssh-copy-id가 없습니다. PowerShell 파이프 방식을 쓰세요. - Ubuntu 24.04의 함정 —
sshd_config.d/50-cloud-init.conf가PasswordAuthentication yes를 먼저 선언합니다. OpenSSH는 먼저 나온 값이 이깁니다. 그 파일을 직접 고치세요. - 설정은 눈으로 믿지 말고
sudo sshd -T | grep -i passwordauthentication으로 최종값을 확인하세요. - 24.04는 SSH가 소켓 활성화입니다.
daemon-reload후ssh.socket을 재시작합니다. - 포트 변경은 보안이 아니라 로그 소음 감소입니다. 순서를 착각하지 마세요.
- 모든 SSH 설정 변경은 기존 창을 열어둔 채 새 창에서 검증합니다.
문을 잠갔으니 이제 간판을 답니다. 다음 편은 가비아에서 산 도메인을 이 서버로 연결하는 A레코드 설정과 DNS 전파 이야기입니다.