무작위 로그인 시도 공격 대응하기

로그를 확인하던 중 여러 사용자 이름으로 반복되는 SSH 로그인 실패를 발견했어요.

2023-11-20-image1 반복되는 SSH 로그인 시도 로그

자동화된 무차별 대입 공격을 의심할 만한 기록이었습니다. 다만 실패 로그만으로 공격 주체가 봇넷인지, 어떤 비밀번호를 썼는지, 다른 시도가 성공했는지까지 알 수는 없어요.

당시에는 키 기반 인증을 사용했고, 운영자가 사내 네트워크나 VPN을 통해서만 접속하면 되는 환경이었습니다. 그래서 SSH 접근을 사내·VPN 출발지로 제한하는 방법을 선택했습니다. 여기서는 선택 이유와 함께, 같은 조치를 할 때 접속을 잃지 않으려면 무엇을 확인해야 하는지 정리합니다.

로그인 실패와 침해 여부를 나눠 보기

실패 기록은 시도가 있었다는 증거입니다. 침해가 없었다는 증거는 아니에요. 조치 전후의 시간 범위를 정하고, 알 수 없는 성공 로그인과 계정·키 변경도 함께 확인합니다.

systemd journal에 SSH 로그를 남기는 환경의 조회 예시는 다음과 같습니다. 배포판에 따라 서비스 이름이 ssh 또는 sshd일 수 있어 두 이름을 함께 조회합니다.

1
2
sudo journalctl -u ssh -u sshd --since '1 hour ago' --no-pager
last -ai

CentOS 계열의 파일 로그를 사용하는 당시 환경에서는 /var/log/secure, 일부 Debian·Ubuntu 환경에서는 /var/log/auth.log를 확인할 수 있습니다. 실제 로깅 설정에 따라 파일이 없을 수도 있어요. last 역시 보조 기록이지 단독으로 침해 여부를 판정하는 도구는 아닙니다.

Failed password만 집계하면 키 인증 실패 등 다른 형태를 놓칩니다. 로그의 뒤에서 몇 번째 열을 IP로 가정하는 awk 한 줄도 메시지 형식에 따라 잘못된 값을 뽑을 수 있어요. 먼저 원문을 확인하고 사용하는 로그 형식에 맞게 집계해야 합니다.

접근 제한을 선택한 이유

방법 줄이는 위험 한계·적용 조건
긴 고유 비밀번호 비밀번호 추측 성공 가능성 공개된 SSH에 대한 시도는 계속됨
키 인증만 허용 비밀번호 추측을 통한 로그인 키 탈취와 SSH 자체 취약점은 별개
Root 직접 로그인 차단 관리자 계정 직접 접근 일반 계정과 sudo 권한 관리도 필요
출발지 허용 목록 SSH까지 도달하는 비인가 접속 고정된 VPN·사내 접속 경로가 필요
Fail2ban 같은 IP의 반복 인증 실패 분산·저속 시도와 로그 설정에 영향받음
비표준 포트 기본 포트를 훑는 일부 스캔 접근 제어를 대신하지 못함

운영 접속 경로가 고정되어 있었기 때문에 불필요한 출발지에서 인증 절차에 도달하지 못하도록 하는 것이 적합했습니다. 방화벽이 먼저 차단한 요청은 sshd의 인증 로그에 남지 않을 수 있으며, 방화벽 차단 로그는 별도 설정에 달려 있습니다.

잠기지 않는 순서로 적용하기

방화벽 명령을 복사하기 전에 클라우드 콘솔이나 다른 복구 경로를 준비하고, 현재 SSH 세션을 유지합니다. 기존 세션이 살아 있는 것과 새로 접속할 수 있는 것은 다릅니다.

  1. 일반 운영 계정으로 키 로그인과 필요한 sudo 권한이 동작하는지 새 터미널에서 확인합니다.
  2. 서버가 실제로 보는 사내·VPN 출발지 주소를 확인합니다. 인터넷을 지나는 사내 접속은 PC의 사설 IP가 아니라 NAT의 공인 IP로 보일 수 있습니다.
  3. 클라우드 보안 그룹에 그 주소의 SSH 허용 규칙을 먼저 추가합니다.
  4. 새 연결에 성공하면 기존의 광범위한 SSH 허용 규칙을 제거합니다. 같은 인스턴스에 연결된 다른 보안 그룹도 확인합니다.
  5. 허용된 경로의 새 접속은 성공하고, 통제 가능한 비허용 경로에서는 SSH에 도달하지 못하는지 확인합니다.

IPv4 규칙만 제한하고 IPv6의 ::/0을 남겨두면 별도의 진입 경로가 열려 있을 수 있습니다. 서버 방화벽을 함께 쓴다면 실제 관리 도구가 firewalld·nftables·iptables 중 무엇인지 확인하고 그 도구로 일관되게 관리해요. 기존 규칙을 모르는 상태에서 iptables -A 세 줄을 덧붙이면 앞선 허용 규칙 때문에 차단이 무효가 되거나, 반대로 운영자까지 차단할 수 있습니다.

/etc/hosts.allow/etc/hosts.deny만 수정하는 방법도 일반적인 OpenSSH 대응책으로 쓰지 않습니다. OpenSSH 본류는 6.7에서 TCP Wrappers 지원을 제거했습니다.

키 인증을 사용 중이어도 설정은 확인하기

키로 로그인해 왔다고 비밀번호 인증까지 비활성화된 것은 아닙니다. 다음은 일반 운영 계정의 공개키 인증만 허용하려는 경우의 설정입니다. SSH의 PAM 기반 MFA를 사용한다면 이 구성으로 그대로 바꾸지 않고 기존 인증 정책을 유지해야 합니다.

1
2
3
4
5
6
# /etc/ssh/sshd_config 또는 배포판에서 사용하는 설정 파일
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey

OpenSSH는 대체로 먼저 얻은 설정값을 사용하며 IncludeMatch 조건도 영향을 줍니다. 파일 맨 끝에 추가하는 것만으로 적용됐다고 판단하지 않습니다. sshd_config 매뉴얼

1
2
sudo sshd -t
sudo sshd -T

sshd -t는 구문·키 설정 검사, sshd -T는 적용될 설정 확인에 사용합니다. Match가 있다면 sshd -T -C에 실제 사용자·출발지 조건을 넣어 확인해야 해요. 검사에 성공한 뒤 배포판의 서비스 이름에 맞게 sudo systemctl reload sshd 또는 sudo systemctl reload ssh를 실행하고 새 세션으로 재접속합니다. 구형 OpenSSH는 KbdInteractiveAuthentication 대신 이전 이름인 ChallengeResponseAuthentication을 사용하므로 설치 버전의 매뉴얼을 우선합니다.

공개 SSH가 필요하다면 Fail2ban을 보완책으로

출발지를 좁힐 수 없는 환경에서는 Fail2ban을 고려할 수 있습니다. 패키지 설치는 지원 중인 배포판의 저장소 안내를 따릅니다. 아래는 systemd journal과 Fail2ban의 systemd backend를 사용할 수 있는 환경의 예시예요.

1
2
3
4
5
6
7
8
# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
backend = systemd
port = 22
findtime = 600
maxretry = 3
bantime = 3600

이 예시는 600초 안에 해당 필터가 3회 실패를 감지하면 3,600초 차단하는 정책입니다. SSH 포트를 변경했다면 port도 바꿉니다. systemd backend에는 파일 logpath를 함께 지정하지 않으며, 필요한 Python systemd 라이브러리가 설치되어 있어야 합니다. 파일 로그를 사용하는 환경에서는 backend와 실제 로그 경로를 그에 맞춰 구성해요. Fail2ban 설정 매뉴얼

1
2
3
4
sudo fail2ban-client -t
sudo systemctl enable --now fail2ban
sudo fail2ban-client reload
sudo fail2ban-client status sshd

서비스 실행 여부뿐 아니라 올바른 로그를 읽는지, 차단 action이 현재 방화벽과 맞는지도 확인해야 합니다. VPN이 없는 다른 기기에서 무작정 실패를 반복하기보다는, 복구 가능한 테스트 환경에서 실패 감지와 해제를 검증하는 편이 좋습니다.

처음 로그를 봤을 때는 등에 식은땀이 났지만, 필요한 운영 경로를 기준으로 접근 범위를 줄일 수 있었습니다. 이 경험을 다시 정리하며 더 분명해진 기준은 두 가지예요. 인증 실패를 줄이는 것과 침해 여부를 확인하는 것은 따로 수행하고, 보안 설정의 완료는 정상 운영자의 새 접속까지 확인한 시점으로 잡는 것입니다.

참고 자료