집에 작은 홈서버를 한 대 두고 여러 앱을 돌리고 있습니다. 그런데 어느 날 갑자기 서버에 SSH로 접속이 안 되더라고요. 분명 어제까지 잘 되던 서버인데, 비밀번호도 키도 그대로인데 Connection refused만 반복됐습니다.
범인은 다름 아닌 제가 보안을 위해 설치해 둔 fail2ban이었습니다. 외부 공격자를 막아주라고 깔아둔 도구가, 정작 주인인 저를 차단해 버린 거죠.
저는 홈서버를 구성할 때부터 주먹구구식으로 배우면서 설정해서, 솔직히 아직도 개념과 동작 원리가 어렵습니다. 이 글은 그런 분들도 끝까지 따라올 수 있도록 개념부터 차근차근 풀어 보았습니다.
이 글에서는 세 가지를 다룹니다. 먼저 fail2ban이 무엇이고 왜 이런 일이 벌어지는지 개념을 잡고, 실제로 어떤 증상이 나타났는지 살펴본 뒤, 어떻게 진단하고 해결했는지를 순서대로 정리할게요.
글에 등장하는 공인 IP, 도메인, 계정명은 모두 익명화된 예시값입니다. 공인 IP는 문서 전용 대역(203.0.113.x, 198.51.100.x, 192.0.2.x, RFC 5737), 도메인은 example.com을 사용했어요. 공개 포스트라 보안을 위해 실제 주소·도메인·로그인 계정명을 노출하지 않은 점 참고 바랍니다.
인터넷에 SSH 포트(22번)를 열어두면, 가만히 있어도 전 세계의 자동화된 봇들이 쉴 새 없이 로그인을 시도합니다. admin, root, test 같은 흔한 계정 이름과 비밀번호를 무작위로 대입하는 무차별 대입 공격(brute force) 이죠.
fail2ban은 이런 공격을 막아주는 도구입니다. 동작 원리는 의외로 단순해요.
- 서버의 로그 파일(누가 로그인을 시도하고 실패했는지 기록된 곳)을 실시간으로 감시합니다.
- 같은 IP 주소에서 짧은 시간에 로그인 실패가 여러 번 반복되면 "공격자"로 판단합니다.
- 방화벽에 규칙을 추가해서 그 IP에서 오는 모든 접속을 일정 시간 차단(ban) 합니다.
쉽게 비유하면, 현관문 비밀번호를 계속 틀리는 사람을 건물 입구에서 아예 들여보내지 않는 경비원입니다. 차단의 기준은 접속해 온 IP 주소예요.
저는 집 인터넷에 연결된 미니 PC에 Ubuntu를 올리고, 그 위에서 Dokku라는 도구로 여러 앱을 배포해 운영하고 있습니다. 외부에서 접속할 수 있도록 SSH 포트를 열어둔 구조죠.
홈서버는 보안을 직접 신경써서 처리해야 하고, 그래서 가장 기본적인 방어선으로 fail2ban을 설치했어요. 실제로 잡아낸 공격 IP를 확인해 보면 이런 식입니다.
sudo fail2ban-client status sshd # Banned IP list: 198.51.100.25 198.51.100.112 192.0.2.224 ...
대부분 유럽이나 해외 호스팅 업체 대역에서 들어오는 봇 트래픽이었습니다. fail2ban이 제 역할을 잘 하고 있었던 셈이죠.
fail2ban의 설정은 jail(감옥)이라는 단위로 관리됩니다. 보호하려는 서비스마다 하나씩 jail을 두는 구조예요. 제 서버는 SSH를 보호하는 sshd jail이 켜져 있습니다.
초심자가 알아두면 좋은 핵심 설정값은 네 가지입니다.
예를 들어 기본값 기준으로는 "10분 안에 5번 로그인에 실패한 IP를 1시간 동안 차단"하는 셈입니다. 현재 설정값은 아래 명령으로 직접 확인할 수 있어요.
sudo fail2ban-client get sshd maxretry sudo fail2ban-client get sshd findtime sudo fail2ban-client get sshd bantime sudo fail2ban-client get sshd ignoreip
이 중에서 이번 사건의 열쇠가 되는 건 마지막 ignoreip입니다. "이 IP는 아무리 실패해도 봐준다"는 예외 목록인데, 여기에 무엇을 넣느냐가 자가 격리를 막는 핵심이 됩니다. 뒤에서 자세히 다룰게요.
자동화 작업을 돌리던 중 갑자기 SSH가 끊기기 시작했습니다. 처음 본 메시지는 이랬어요.
Received disconnect from 203.0.113.42 port 22:2: Too many authentication failures
그러다 잠시 후에는 아예 접속 자체가 거부됐습니다.
ssh: connect to host server.example.com port 22: Connection refused
문제는 SSH 접속에서 끝나지 않았습니다. 저는 Dokku로 앱을 배포할 때 git push를 사용하는데, Dokku의 배포 경로 역시 SSH의 22번 포트를 그대로 타고 들어갑니다.
$ git push dokku-api-st master Permission denied (publickey,password). fatal: Could not read from remote repository.
결국 PR은 머지됐는데 서버에 배포가 안 되는, 운영 입장에서는 꽤 곤란한 상황이 벌어졌습니다. SSH 차단 하나가 곧 배포 파이프라인 전체의 마비로 이어진 거죠.
홈서버에서 SSH와 배포(git push)가 같은 22번 포트를 공유하고 있으면, SSH가 막히는 순간 배포도 함께 막힙니다. 두 통로가 하나의 문을 쓰고 있었던 셈이에요.
가장 먼저 한 일은 "포트 자체가 살아있는지" 확인하는 것이었습니다. nc 명령으로 각 포트에 TCP 연결만 시도해 봤어요.
nc -zv 203.0.113.42 22 # → Connection refused nc -zv 203.0.113.42 80 # → succeeded nc -zv 203.0.113.42 443 # → succeeded
80번(HTTP)과 443번(HTTPS)은 멀쩡하게 응답하는데 22번만 거부됐습니다. 이 결과 하나로 많은 게 좁혀집니다.
80·443이 살아있다는 건 서버 자체와 네트워크는 멀쩡하다는 뜻입니다. 그런데 22번만 refused라면, 이건 인터넷 장애나 서버 다운이 아니라 서버 내부에서 22번 포트로 오는 접속을 콕 집어 거부하고 있다는 강력한 신호예요. fail2ban이 가장 유력한 용의자로 떠오른 순간이었습니다.
여기서 결정적인 확인을 했습니다. 제 노트북의 공인 IP를 조회해 봤어요.
curl ifconfig.me # → 203.0.113.42
이 주소가 홈서버의 공인 IP와 똑같았습니다. 당연한 일이에요. 노트북과 홈서버가 같은 집 공유기 뒤에 있으니까요. 이 한 줄이 뒤에 설명할 hairpin NAT라는 개념으로 연결됩니다.
물리적으로 서버 앞에 앉아(모니터를 직접 연결해) 차단 목록을 확인했습니다.
sudo fail2ban-client status sshd # Banned IP list: 192.168.0.1 198.51.100.112 198.51.100.25 121.130.xxx.xxx 192.0.2.195
차단된 5개 IP 중에서 제 눈길을 끈 건 192.168.0.1이었어요.
198.51.100.x,192.0.2.x→ 해외 호스팅 대역. 전형적인 공격 봇입니다.121.130.xxx.xxx→ 한국 IP라서 순간 "내 IP인가?" 싶었지만, 로그를 보니 이상한 계정 이름을 마구 시도하는 봇이었습니다. 한국 대역이라고 본인이라 단정하면 안 됩니다.192.168.0.1→ 사설 IP. 외부 인터넷에서는 존재할 수 없는, 집 안 네트워크에서만 쓰는 주소입니다. 바로 이게 범인이었어요.
192.168.x.x나 10.x.x.x처럼 생긴 주소는 집·회사 내부에서만 쓰는 사설 IP입니다. 그중 192.168.0.1은 보통 공유기 자신(게이트웨이)의 주소예요. 외부 공격자가 이 주소로 위장하는 건 불가능합니다.
로그를 함께 보니 더 확실해졌습니다.
Disconnecting invalid user localuser 192.168.0.1 port 57285: Too many authentication failures [preauth]
시도한 계정 이름이 localuser, 즉 제 노트북(macOS)의 로컬 계정 이름이었고, 그 출처가 192.168.0.1이었습니다. 제가 보낸 접속이 서버에는 192.168.0.1에서 온 것처럼 보였다는 뜻이죠.
왜 제 접속이 192.168.0.1로 둔갑했을까요? 이걸 이해하려면 집 네트워크 구조를 먼저 그려봐야 합니다.
인터넷 (공인 IP 203.0.113.42) │ ┌─────┴─────┐ │ 공유기 │ ← 포트포워딩 담당 │ 192.168.0.1│ ← 게이트웨이(공유기 자신) └─┬───────┬──┘ │ │ 내 노트북 홈서버 192.168.0.2 192.168.0.100
집 안의 모든 기기는 공유기를 거쳐 인터넷에 나갑니다. 외부에서 server.example.com로 접속하면, 그 주소는 공유기의 공인 IP(203.0.113.42)를 가리키고, 공유기가 포트포워딩으로 내부의 홈서버에게 연결을 넘겨주는 구조예요.
문제는 노트북과 홈서버가 같은 공유기 뒤에 있을 때 생깁니다.
집 안 노트북에서 server.example.com로 접속하면, 주소는 여전히 공유기의 공인 IP로 해석됩니다. 그래서 패킷은 일단 공유기 바깥쪽을 향했다가, 공유기가 "어, 이거 사실 내 안쪽 서버로 가는 거네?" 하고 다시 안으로 되돌려 보냅니다. 이렇게 나갔다 그대로 U턴해서 들어오는 동작을 hairpin NAT(머리핀처럼 꺾여 돌아온다는 뜻, NAT loopback이라고도 합니다)라고 불러요.
이때 공유기는 한 가지 일을 더 합니다. 되돌려 보내는 패킷의 출발지 주소를 자기 자신(192.168.0.1)으로 바꿔치기해요.
내 노트북 (192.168.0.2) │ "server.example.com:22로 접속" ▼ 공유기 ── ① 목적지를 홈서버로 변경 (포트포워딩) └─ ② 출발지를 192.168.0.1로 변경 ← 핵심 │ ▼ 홈서버: "192.168.0.1에서 누가 접속해 왔네"
②번 단계가 없으면 응답 패킷이 길을 잃어 통신이 깨지기 때문에, 거의 모든 가정용 공유기가 이렇게 출발지를 바꿉니다. 즉 정상 동작이에요. 문제는 이것이 fail2ban과 만났을 때 생깁니다.
이제 두 가지 사실을 겹쳐보면 답이 나옵니다.
- fail2ban은 "IP 하나"를 단위로 차단합니다.
- hairpin NAT는 집 안의 모든 기기를 "
192.168.0.1하나"로 뭉뚱그려 보여줍니다.
이 둘이 어긋나면서 사고가 납니다. 정리하면 이렇게 흘러갔어요.
- 자동화 작업이 사용자 이름을 지정하지 않은 채 SSH 접속을 여러 번 시도했습니다.
- 사용자 이름이 비면 SSH는 현재 컴퓨터의 로컬 계정(
localuser)으로 붙으려 합니다. 서버에는 없는 계정이라 인증이 계속 실패했어요. - 이 실패가 서버 입장에서는 전부
192.168.0.1에서 온 것으로 기록됐습니다(hairpin NAT 때문). - fail2ban이
192.168.0.1을 공격자로 판단해 차단했습니다. - 그 순간부터 그 공유기를 쓰는 집 안의 모든 기기가 서버에서 차단됐습니다.
fail2ban이 차단한 IP: 192.168.0.1 (공격자라고 판단) 실제 차단된 대상: 집 안의 모든 기기 (= 나)
외부 공격자를 차단하라고 만든 도구가, hairpin NAT 환경에서는 집 전체를 하나로 묶어 차단해 버립니다. fail2ban이 보는 단위(IP 1개)와 NAT가 만들어내는 단위(집 전체)가 어긋나는 게 자가 격리의 본질이에요.
참고로 휴대폰 테더링처럼 다른 네트워크에서 접속하면 멀쩡히 됩니다. 그땐 출발지가 진짜 외부 IP라서 192.168.0.1 차단에 걸리지 않거든요. "집에 있을 때만 막히고 밖에서는 된다"는 묘한 증상의 정체가 바로 이것입니다.
이 글의 핵심입니다. 크게 응급 복구 → 재발 방지 두 단계로 진행했습니다.
차단된 상태에서는 집 네트워크로 서버에 못 들어가니, 먼저 차단을 풀 통로부터 확보해야 합니다. 두 가지 방법이 있어요.
- 휴대폰 테더링으로 네트워크를 바꿔 접속 (출발지 IP가 달라져 차단을 우회)
- 서버에 모니터·키보드를 직접 연결해 물리 콘솔로 접근
둘 중 편한 방법으로 서버에 들어갑니다. 저는 홈서버 PC에 키보드와 마우스를 연결해 접근했습니다.
서버에 들어왔으면 차단된 내 IP를 풀어줍니다.
# 현재 차단 목록 확인 sudo fail2ban-client status sshd # 192.168.0.1 차단 해제 sudo fail2ban-client set sshd unbanip 192.168.0.1
이 명령을 실행하자마자 집 네트워크에서 SSH 접속이 곧바로 정상화됐습니다. 다만 이건 응급 처치일 뿐이에요. bantime이 지나거나 다시 인증 실패가 쌓이면 똑같이 또 막힙니다. 그래서 근본 처방이 필요합니다.
근본 해결은 간단합니다. fail2ban에게 사설 IP는 절대 차단하지 말라고 알려주면 됩니다. 아까 살펴본 ignoreip 설정에 집 안 대역을 추가하는 거예요.
설정 파일에 다음 블록을 넣습니다. 패키지를 업그레이드해도 보존되도록 jail.d/ 폴더에 별도 파일로 만드는 방식을 클로드가 추천해주었는데요, 이유를 몰라서 좀 더 찾아봤습니다.
fail2ban의 기본 설정 파일은 jail.conf인데, 이 파일은 패키지를 업그레이드할 때 새 버전으로 통째로 덮어쓰입니다. 여기에 직접 손대면 업데이트 한 번에 내 설정이 날아가요. 그래서 fail2ban은 jail.d/ 폴더 안의 .local 파일을 나중에 읽어 기본값에 덧씌우는 구조로 동작합니다. 패키지가 건드리지 않는 내 전용 영역이라, 업그레이드를 해도 그대로 보존돼요. (jail.conf → jail.local → jail.d/*.local 순서로 읽히고, 뒤에 읽힌 값이 우선합니다.)
sudo tee /etc/fail2ban/jail.d/ignoreip.local > /dev/null <<'EOF' [DEFAULT] ignoreip = 127.0.0.1/8 ::1 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12 EOF
[DEFAULT] ignoreip = 127.0.0.1/8 ::1 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12
설정을 반영하고 잘 들어갔는지 확인합니다.
sudo fail2ban-client reload sudo fail2ban-client get sshd ignoreip # → 192.168.0.0/16 이 포함되어 있으면 성공
192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12는 전 세계 어디서나 "내부 전용"으로 약속된 사설 IP 대역(RFC 1918)입니다. 세 대역을 다 넣어두면 나중에 공유기를 바꿔 주소 체계가 달라져도 안전해요. 사설 IP는 외부 공격자가 위장할 수 없으므로 화이트리스트에 넣어도 보안에 해가 없습니다.
[DEFAULT] 블록에 넣었기 때문에 이 화이트리스트는 SSH뿐 아니라 fail2ban이 보호하는 모든 서비스에 한 번에 적용됩니다. 같은 원리로 데이터베이스 같은 다른 서비스가 자가 차단되는 것도 함께 예방돼요.
사실 이 사건의 방아쇠는 "사용자 이름을 지정하지 않은 SSH 접속"이었습니다. 애초에 그게 안 일어나게 막는 게 가장 깔끔한 예방이에요.
내 노트북의 ~/.ssh/config 파일에 서버용 설정을 추가해, 사용자 이름을 아예 고정합니다.
Host server.example.com home-server HostName server.example.com User myuser IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes
이렇게 해두면 어떤 스크립트가 ssh server.example.com를 호출하더라도 자동으로 myuser 계정으로 접속합니다. 로컬 OS 계정 이름이 엉뚱하게 새어 나가 인증 실패를 쌓는 일이 사라져요.
그런데 4단계를 적용한 직후 git push로 Dokku에 배포하려 하자 이번엔 Permission denied (publickey)가 떴습니다.
원인은 이렇습니다. Dokku 배포는 같은 서버에 dokku라는 다른 계정으로 접속하는데, 방금 추가한 설정의 User myuser + IdentitiesOnly yes가 배포 접속에까지 적용되면서 엉뚱한 계정·키로 시도하게 된 거예요.
해결은 배포용 접속을 별도 별명(alias)으로 분리하는 것입니다.
Host server.example.com home-server HostName server.example.com User myuser IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes # Dokku 배포는 dokku 계정으로 접속하므로 별도 별명으로 분리 Host dokku-home HostName server.example.com User dokku IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes
그리고 Dokku 원격 저장소 주소를 이 별명으로 바꿔줍니다.
git remote set-url dokku-api-st dokku-home:my-app-st git remote set-url dokku-api-pr dokku-home:my-app-pr
이제 Host server.example.com는 "서버에 직접 들어갈 때", Host dokku-home는 "배포할 때"로 역할이 깔끔하게 갈립니다. 하나의 서버라도 용도에 따라 접속 통로를 나눠두면 서로 간섭하지 않아요.
이번 사건에서 정리한 예방책을 한눈에 모아봤습니다.
- 사설 IP를 ignoreip에 등록 —
192.168.0.0/16등 내부 대역을 화이트리스트에 넣어, hairpin NAT로 뭉뚱그려진 집 트래픽이 차단되지 않게 합니다. - SSH 사용자 이름 고정 —
~/.ssh/config에User를 박아두어 로컬 계정 이름이 새어 나가는 걸 막습니다. - 용도별 접속 분리 — 서버 접속용과 배포용 별명을 나눠 키·계정 충돌을 피합니다.
- 본인 IP는 추측하지 말고 로그로 확인 — 차단 목록에 사설 IP(
192.168.x.x)가 보이면 그게 곧 본인일 가능성이 높습니다.
서버는 생각보다 저를 잘 모릅니다. 서버가 아는 건 오직 접속해 온 IP 주소와 등록된 키뿐이고, 그 IP는 네트워크 환경이나 공유기 설정에 따라 얼마든지 달라질 수 있어요.
- fail2ban의 차단 대상은 늘 외부 공격자가 아닐 수 있습니다. 홈서버처럼 공유기 뒤에 있는 환경에서는, 공유기 자신의 IP가 차단되면서 집 전체가 통째로 격리될 수 있어요.
- 증상은 층위를 나눠서 봅니다.
nc로 TCP는 통하는데ssh만 거부된다면, 네트워크 장애가 아니라 서버 내부의 차단 규칙을 의심해야 합니다. - 보안 설정의 예외 목록(ignoreip)은 사설 IP 기준으로 잡는 게 안정적입니다. 매번 바뀌는 공인 IP를 일일이 등록하는 것보다 오래갑니다.
홈서버를 운영하다 보면 이렇게 평소엔 안 보이던 네트워크의 층위를 한 번씩 직접 마주하게 되는데요. 덕분에 fail2ban과 NAT가 어떻게 맞물리는지 몸으로 이해하게 됐습니다. 같은 증상으로 헤매는 분들께 도움이 되셨길 바랍니다. 감사합니다.
