새벽 3시, 고객사 서버 로그에 이상한 패턴이 쌓입니다. root, admin, ubuntu 계정으로 초당 수십 번 로그인을 시도하는 기록들. 전 세계 수백 개 IP에서 쏟아지는 무차별 대입 공격입니다. 패스워드 123456, password, admin1234를 하나씩 넣어봅니다. 운이 나쁘면 한 번에 뚫립니다.
공개 IP에 22번 포트가 열려있는 모든 서버는 지금 이 순간도 같은 공격을 받고 있습니다. 이 모듈은 그 공격을 구조적으로 차단하는 방법을 다룹니다.
- 1SSH 공개키 인증 구조를 이해하고 키를 생성해 서버에 등록할 수 있다
- 2sshd_config 핵심 보안 설정을 적용하고 sshd -t로 검증한 뒤 안전하게 재로드할 수 있다
- 3배스천 호스트 구조를 이해하고 ProxyJump로 내부 서버에 접속할 수 있다
- 4SSH 접속 시도 로그를 확인하고 fail2ban으로 무차별 대입을 차단할 수 있다
- 5Permission denied (publickey) 등 흔한 SSH 오류를 권한 문제와 설정 관점에서 해결할 수 있다
systemctl status sshd 2>/dev/null || systemctl status ssh 2>/dev/nullsudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak && echo backup OKSSH 인증 방식
패스워드 인증 vs 공개키 인증
공개 IP에 22번 포트를 열어둔 서버는 매 순간 자동화된 무차별 대입 공격의 표적이 됩니다. 패스워드 인증은 네트워크로 전달되는 값이 노출되거나 추측될 수 있어, 짧거나 단순한 비밀번호는 수만 건의 시도 끝에 뚫립니다. 공개키 인증은 비밀키가 클라이언트를 절대 벗어나지 않는 구조로, 서버에 탈취당할 정보가 없고 수학적으로 추측이 불가능합니다.
확대
22번 포트를 인터넷에 열어둔 서버는 지금 이 순간에도 자동화된 무차별 대입 공격을 받고 있습니다. 사무실 동료 계정이 password123이었다면, 수십만 개 비밀번호를 자동으로 시도하는 봇이 결국 뚫습니다. 한 번 뚫리면 그 서버를 발판 삼아 내부 네트워크로 이동하고, DB 서버까지 도달합니다. 공개키 인증은 이 경로를 구조적으로 차단합니다. 비밀키(private key)는 절대 클라이언트를 벗어나지 않습니다. 서버는 공개키(public key)만 갖고 있으며, 로그인 시 수학적 서명 검증으로 인증합니다. 추측하거나 가로챌 것이 없습니다.
# 키 쌍 생성
ssh-keygen -t ed25519 -C "infra-admin@company.com"
# 프로덕션에서는 패스프레이즈 반드시 설정
# 공개키를 서버에 등록
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@192.168.1.100
# 서버에서 권한 설정 (틀리면 인증 자체를 거부함)
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
핵심 권한 규칙:
| 대상 | 권한 |
|---|---|
~/.ssh/ 디렉터리 | 700 — 본인만 접근 |
~/.ssh/authorized_keys | 600 — 본인만 읽기/쓰기 |
~/.ssh/id_ed25519 (비밀키) | 600 — 본인만 읽기/쓰기 |
sshd 설정 강화
sshd_config 핵심 보안 설정
기본 SSH 설정 그대로 운영 중인 서버는 보안 감사에서 예외 없이 지적을 받습니다. root 직접 로그인이 허용되어 있거나, 패스워드 인증이 켜져 있으면 — 공개 IP에 노출된 서버는 무차별 대입 공격의 직접 타겟입니다. 설정 하나하나가 공격 표면을 줄이는 역할을 하며, 잘못 적용하면 서버에 영구히 잠길 수 있습니다. 반드시 현재 세션을 유지한 채 새 터미널로 테스트합니다.
sudo vi /etc/ssh/sshd_config
# 기본 22는 자동화 스캐너가 항상 시도 — 비표준 포트로 노이즈 감소
Port 22222
# root 직접 로그인 금지 — 감사 추적을 위해 일반 계정 로그인 후 sudo
PermitRootLogin no
# 패스워드 인증 비활성화 — 공개키 등록 완료 확인 후 변경
PasswordAuthentication no
PubkeyAuthentication yes
# 허용할 계정 명시 — 목록에 없는 계정은 키가 있어도 접속 불가
AllowUsers infra-admin deploy-bot
# 인증 시간 초과 및 재시도 제한
LoginGraceTime 60
MaxAuthTries 4
PermitEmptyPasswords no
X11Forwarding no
설정 후 반드시 이 순서를 지킵니다.
# 1단계: 문법 검증 (오류 있으면 여기서 중단)
sudo sshd -t && echo CONFIG OK
# 2단계: 재로드 (기존 연결 유지, restart 아님)
sudo systemctl reload sshd
# 3단계: 새 터미널에서 접속 테스트 — 현재 세션 유지한 채
SSH 접속이 허용되거나 거부되는 순서 — 접근 제어 게이트 6관문
인증 흐름(키 교환·서명 검증)이 "어떻게 신원을 증명하나"라면, 접근 제어는 "증명에 성공해도 들여보낼 것인가"입니다. SSH 접속 한 번은 방화벽부터 세션 개설까지 여러 관문을 순서대로 지나며, 어느 하나라도 막으면 거부됩니다. 이 순서를 알면 Connection timed out, Too many authentication failures, Permission denied (publickey)가 서로 다른 관문의 증상임을 구분해 진단을 좁힐 수 있습니다.
[클라이언트] ssh deploy@server
│
① 방화벽·보안그룹 (SG/iptables Inbound 22·비표준 포트에 도달?)
│
② fail2ban·차단 목록 (이 출발지 IP가 밴돼 있나)
│
③ 호스트키·인증 방식 협상 (서버가 허용한 방식 통보: publickey? password?)
│ → PasswordAuthentication no면 비번 경로 자체가 닫힘
│
④ 사용자 인증 (공개키: authorized_keys 대조·서명 검증 / 비번)
│ → MaxAuthTries 초과 시 여기서 끊김
│
⑤ 계정 인가 게이트 (PermitRootLogin · AllowUsers/AllowGroups · DenyUsers · PAM)
│ → 자격이 맞아도 정책·목록에 막히면 거부
│
⑥ 세션 개설 (셸 실행 → 프롬프트)
▼
[서버] deploy@server:~$ (①~⑤ 중 하나라도 막으면 세션 없음)
각 관문에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 관문 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① 방화벽·보안그룹 | 클라이언트가 서버의 SSH 포트(22 또는 비표준)에 TCP로 도달할 수 있는지. 클라우드면 Security Group·NACL, 서버면 firewalld·iptables가 앞단에서 판정 | 차단 → Connection timed out(응답 없음) / sshd 미기동·포트 닫힘 → Connection refused(즉시 거부) |
| ② fail2ban·밴 목록 | 반복 실패로 밴된 출발지 IP는 방화벽 레벨에서 미리 차단돼 인증까지 가지도 못한다 | 내 IP가 밴됨 → 갑자기 timed out·거부 → fail2ban-client status sshd로 확인, unbanip로 해제 |
| ③ 방식 협상 | 서버가 PubkeyAuthentication · PasswordAuthentication 등 허용한 인증 방식을 통보. 서버가 끈 방식은 아예 시도 불가 | PasswordAuthentication no인데 등록된 키가 없음 → 시도할 방식이 없어 Permission denied (publickey) |
| ④ 사용자 인증 | 공개키면 서버가 ~/.ssh/authorized_keys에서 내 공개키를 찾아 서명을 검증. 이때 .ssh(700)·authorized_keys(600) 권한이 틀리면 StrictModes가 그 키를 무시 | 권한·키 불일치 → Permission denied (publickey) / 실패 누적 → Too many authentication failures |
| ⑤ 계정 인가 | 인증 성공이 곧 통과는 아니다. PermitRootLogin no, AllowUsers/AllowGroups 화이트리스트, DenyUsers(우선), PAM 정책이 이 계정을 들여보낼지 최종 판정 | 목록에 없는 계정·root 직접 로그인 → 자격이 맞아도 거부(User ... not allowed) / 셸이 /sbin/nologin이면 붙자마자 끊김 |
| ⑥ 세션 개설 | 모든 관문 통과 후 sshd가 해당 사용자 권한으로 셸을 실행하고 프롬프트를 준다 | 여기까지 왔으면 접속 성공 |
즉 SSH 접근 제어는 "네트워크 관문(①②) → 인증 관문(③④) → 인가 관문(⑤)"의 3층이고, 증상이 곧 층을 가리킵니다 — 연결 자체가 안 서면(timed out·refused) ①②의 방화벽·밴, 비밀번호·키를 묻는 단계까지 갔는데 거부면(Permission denied) ③④의 인증, 키는 맞는데 특정 계정만 막히면 ⑤의 AllowUsers · PermitRootLogin입니다. ssh -vvv로 클라이언트가 어디까지 진행됐는지 보고, 서버 쪽은 journalctl -u sshd의 Authentication refused · User ... not allowed 메시지로 관문을 특정합니다.
배스천 호스트 구조
단일 진입점으로 공격 표면 최소화
내부 서버 10대가 각각 22번 포트를 인터넷에 열어두면 공격자에게 10개의 진입 기회를 주는 셈입니다. 그중 하나라도 취약한 설정이 있으면 전체 내부망이 위험해집니다. 배스천 호스트는 외부에서 SSH로 접근 가능한 서버를 단 한 대로 줄이고, 나머지 내부 서버는 프라이빗 네트워크에만 존재하게 만듭니다. 접속 경로가 단일화되면 보안 설정 집중도가 높아지고, 모든 SSH 접근 기록이 한 곳에 남아 감사 추적도 훨씬 쉬워집니다.
확대
내부 서버 10대가 각각 22번 포트를 인터넷에 열면 공격 목표가 10개입니다. 배스천 호스트(Bastion Host)는 인터넷과 내부 네트워크 사이의 단일 진입점으로, 내부 서버는 공인 IP 없이 배스천을 통해서만 접근합니다. 모든 SSH 접속 로그가 한 곳에 집중되어 감사(audit) 추적도 쉬워집니다.
# ProxyJump 방식 — 배스천을 경유해 내부 서버로 직접 접속
ssh -J infra-admin@bastion.company.com admin@10.0.1.50
# ~/.ssh/config에 등록해 단축 사용
# ~/.ssh/config
Host bastion
HostName bastion.company.com
User infra-admin
Port 22222
IdentityFile ~/.ssh/id_ed25519
Host web-01
HostName 10.0.1.50
User admin
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519
Host db-01
HostName 10.0.2.10
User dbadmin
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519
# 권한 설정
chmod 600 ~/.ssh/config
# 등록 후 사용 — bastion 경유가 자동으로 처리됨
ssh web-01
ssh db-01
SSH 접속 모니터링
로그 확인과 fail2ban 무차별 대입 방어
무차별 대입 공격은 막고 있다고 생각했는데, 로그를 보니 하루에 수만 건의 실패 기록이 쌓이고 있었습니다. 동일한 IP에서 반복 시도하는데도 차단이 안 됐고, 결국 어느 날 취약한 비밀번호를 가진 계정이 뚫렸습니다. fail2ban은 지정 횟수 이상 실패한 IP를 방화벽에서 자동으로 차단합니다. 로그를 모니터링하지 않으면 이미 뚫리고 있다는 사실을 모를 수 있습니다.
# 접속 시도 로그 확인
sudo tail -50 /var/log/secure # RHEL/Rocky
sudo tail -50 /var/log/auth.log # Ubuntu/Debian
sudo journalctl -u sshd --since "1 hour ago" --no-pager # systemd 공통
# 실패한 로그인 시도만 필터
sudo grep "Failed password" /var/log/secure | tail -20
# 무차별 대입 IP 상위 10개
sudo grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10
# 성공한 로그인 이력 / 실패 이력 / 계정별 마지막 로그인
last -20 && sudo lastb | head -10 && lastlog | grep -v "Never logged"
fail2ban은 로그를 감시하다가 지정 횟수 이상 실패하면 해당 IP를 방화벽에서 자동 차단합니다.
sudo dnf install -y fail2ban # RHEL/Rocky
sudo apt-get install -y fail2ban # Ubuntu
# /etc/fail2ban/jail.local — jail.conf는 수정 금지, local에 오버라이드
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = 22222
logpath = /var/log/secure
maxretry = 3
bantime = 86400
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd # 차단 현황 확인
sudo fail2ban-client set sshd unbanip 45.33.32.156 # 실수 차단 해제
1. ed25519 키 쌍 생성
ssh-keygen -t ed25519 -C "infra-admin@company.com" -f ~/.ssh/id_ed25519_infra2. 공개키를 서버에 등록
ssh-copy-id -i ~/.ssh/id_ed25519_infra.pub -p 22 user@TARGET_SERVER_IP3. 서버 측 권한 확인 및 수정
ssh user@TARGET_SERVER_IP "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && ls -la ~/.ssh/"4. 공개키 인증으로 접속 확인
ssh -i ~/.ssh/id_ed25519_infra -o PasswordAuthentication=no user@TARGET_SERVER_IP echo SSH-OK- 먼저 ls -la ~/.ssh/ 로 키 파일 권한 확인, 그 다음 ssh -o PasswordAuthentication=no 로 실제 접속 테스트 — 권한이 맞아야 SSH 데몬이 공개키 인증을 수락
- 권한 기준: 비밀키 600(소유자만 읽기), ~/.ssh 디렉터리 700(소유자만 접근), authorized_keys 600 — 이 중 하나라도 더 열려 있으면 sshd가 공개키 인증을 거부
- authorized_keys 600인데 접속 거부 발생하면 → journalctl -u sshd | grep "Authentication refused" 로 원인 확인 — 대부분 .ssh 디렉터리 소유자가 다르거나 권한이 700이 아닌 것
1. 현재 설정 백업
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak && ls -la /etc/ssh/sshd_config*2. 변경 전 현재 설정 확인
sudo grep -E 'PasswordAuthentication|PermitRootLogin|Port|AllowUsers' /etc/ssh/sshd_config3. 설정 변경 후 문법 검증
sudo sshd -t && echo CONFIG OK4. sshd 재로드 (기존 연결 유지)
sudo systemctl reload sshd && systemctl status sshd --no-pager | head -55. 새 터미널에서 접속 테스트
ssh -i ~/.ssh/id_ed25519_infra user@TARGET_SERVER_IP echo SSH-OK- sshd -t → systemctl reload sshd → 새 터미널 접속 테스트 순서로 확인 — sshd -t가 실패하면 reload 금지, reload 후 반드시 기존 세션 유지한 채 새 터미널로 테스트
- 보안 강화 기준: PasswordAuthentication no이고 PermitRootLogin no여야 외부 공격 차단 — 이 두 항목 중 하나라도 yes이면 무차별 대입 공격에 노출
- 새 터미널 접속 성공인데 패스워드 시도 시 "Permission denied (publickey)"가 안 뜨면 → PasswordAuthentication이 여전히 yes 상태 — grep PasswordAuthentication /etc/ssh/sshd_config 로 재확인
심화 — 배스천을 넘는 두 방법, 그리고 그 위험
심화: Agent Forwarding vs ProxyJump — 왜 -A는 위험하고 -J가 나은가
배스천을 경유해 내부 서버에 로그인할 때, 내부 서버도 내 키로 인증해야 합니다. 이 '키를 배스천 너머로 전달하는' 방식이 두 가지인데, 보안 차이가 큽니다.
- Agent Forwarding(
ssh -A,ForwardAgent yes): 내 로컬ssh-agent의 유닉스 소켓을 배스천 위에 노출시켜, 배스천에서 실행한 ssh가 그 소켓으로 내 로컬 에이전트에 서명을 요청하게 합니다. 비밀키 자체는 안 넘어가지만, 배스천에 있는 소켓을 쓸 수 있는 사람(특히 root)은 누구나 내 에이전트에 서명을 시킬 수 있습니다. 배스천이 침해되면 공격자가 내 신원으로 내부 전 서버에 로그인할 수 있다는 뜻입니다. 소켓 경로는 배스천의SSH_AUTH_SOCK환경변수에 그대로 드러납니다. - ProxyJump(
ssh -J,ProxyJump): 배스천을 단순 TCP 터널로만 씁니다. 내 클라이언트가 배스천을 통과해 내부 서버까지 암호화 연결을 뚫고, 인증은 내 로컬에서 내부 서버 끝단까지 직접 이뤄집니다. 비밀키도 에이전트 소켓도 배스천에 남지 않으므로, 배스천이 뚫려도 내 자격증명은 안전합니다. 그래서 현대 표준은 ProxyJump입니다(이 모듈 본문의~/.ssh/config의ProxyJump설정). - 부득이 Agent Forwarding을 써야 한다면: 신뢰하는 호스트에만, 그리고
ssh-add -c(서명 때마다 확인 프롬프트)나ssh-add -t(짧은 만료)로 위험을 줄입니다. 그래도 기본 선택은 ProxyJump입니다.
요약: 배스천은 '통과하는 문'이지 '내 키를 맡겨 두는 곳'이 아닙니다. 터널만 빌려주는 ProxyJump가 원칙입니다.
상황: 내부 서버로 편하게 넘어가려고 ssh -A bastion으로 접속한 뒤 거기서 ssh web-01, ssh db-01을 이어서 실행하는 습관이 있었습니다. 잘 동작해서 문제 없다고 여겼는데, 보안 점검에서 이 방식이 위험하다는 지적을 받았습니다.
원인: -A(Agent Forwarding)는 내 로컬 에이전트 소켓을 배스천에 열어 둡니다. 배스천에서 env | grep SSH_AUTH_SOCK를 보면 그 소켓 경로가 그대로 노출되고, 배스천의 root(또는 배스천을 장악한 공격자)는 그 소켓을 통해 내 키로 서명을 시켜 나로 위장해 내부 서버에 접속할 수 있습니다. 배스천 한 대의 침해가 내부 전체로 번지는 통로가 됩니다.
진단: 배스천에 접속한 상태에서 echo $SSH_AUTH_SOCK가 값을 갖고 있고 ls -l "$SSH_AUTH_SOCK" 소켓이 존재하면 agent forwarding이 켜진 것입니다. 로컬 ~/.ssh/config나 접속 명령에 ForwardAgent yes/-A가 있는지 확인합니다.
해결: agent forwarding을 끄고(ForwardAgent no, -A 제거) ProxyJump로 전환합니다 — ssh -J infra-admin@bastion admin@10.0.1.50, 또는 ~/.ssh/config에 내부 호스트마다 ProxyJump bastion을 등록합니다. 그러면 배스천은 터널 역할만 하고 인증은 내 클라이언트에서 끝단까지 직접 이뤄져, 키·에이전트가 배스천에 남지 않습니다. 정 forwarding이 필요하면 ssh-add -c로 서명마다 확인을 받게 하고 신뢰 호스트로 제한합니다.
실제 업무에서 이 지식이 쓰이는 상황:
금융권과 공공기관 서버 운영에서 SSH 접근 통제는 정보보호 관리체계(ISMS-P) 심사 항목입니다. 심사관이 실제로 확인하는 것:
grep "PermitRootLogin" /etc/ssh/sshd_config # → no
grep "PasswordAuthentication" /etc/ssh/sshd_config # → no
grep "AllowUsers\|AllowGroups" /etc/ssh/sshd_config # 허용 목록 존재
sudo fail2ban-client status sshd # 차단 현황
sudo lastb | head -20 # 실패 로그인 이력
AWS로 처음 이전하는 스타트업이 흔히 겪는 실수가 내부 서버 보안 그룹에 0.0.0.0/0:22를 열어두는 것입니다. 올바른 구성은 배스천 EC2(공인 IP, 특정 사무실 IP만 허용) → 내부 서버(프라이빗 서브넷, 배스천 IP만 허용) 구조입니다. 이 구조에서 개발자 PC가 탈취되어도 배스천 계정과 키까지 없으면 내부 서버에 도달할 수 없습니다.
명령어·단축키 빠른 참조
SSH 키 인증·sshd 강화·배스천 접근 제어에서 다룬 명령을 모았습니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
ssh-keygen | 키 쌍 생성(ed25519 권장) | ssh-keygen -t ed25519 -C "admin@co" -f ~/.ssh/id_ed25519 |
ssh-copy-id | 공개키를 서버에 등록 | ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 user@IP |
chmod(SSH 권한) | 키·디렉터리 권한(틀리면 인증 거부) | chmod 700 ~/.ssh, chmod 600 ~/.ssh/authorized_keys |
sshd -t | sshd_config 문법 검증(reload 전 필수) | sshd -t && systemctl reload sshd |
systemctl reload sshd | 무중단 설정 재적용(restart 아님) | systemctl reload sshd(기존 세션 유지) |
ssh -J(ProxyJump) | 배스천 경유 내부 서버 접속 | ssh -J infra-admin@bastion admin@10.0.1.50 |
ssh -vvv | 인증 실패 지점 디버깅 | ssh -vvv -i key user@server | grep -E 'Offering|denied' |
grep … sshd_config | 강화 항목 확인 | grep -E 'PermitRootLogin|PasswordAuthentication' /etc/ssh/sshd_config |
tail /var/log/secure | SSH 접속 시도 로그 확인 | grep "Failed password" /var/log/secure | tail |
last / lastb / lastlog | 성공·실패·계정별 로그인 이력 | last -20, lastb | head, lastlog |
fail2ban-client status | 무차별 대입 차단 현황·해제 | fail2ban-client status sshd, set sshd unbanip <IP> |
ssh-add -c / -t | Agent Forwarding 위험 축소 | ssh-add -c(서명마다 확인), ssh-add -t 3600(만료) |
echo $SSH_AUTH_SOCK | 배스천에 에이전트 노출 점검 | 배스천에서 값이 있으면 Agent Forwarding 켜진 위험 상태 |
grep … | sort | uniq -c | 실패 로그에서 공격 IP 상위 집계 | grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head |
관련 모듈로 더 깊이:
- 네트워크 방화벽 정책과 요청서 작성 실무 — SSH 포트와 배스천 IP를 방화벽 정책으로 통제하는 법
- Linux 사용자/그룹/권한 체계와 서비스 계정 관리 — AllowUsers로 제한할 계정과 sudo 권한 설계의 기반
- OS 보안 패치와 미들웨어 버전 업 절차 — OpenSSH 취약점 패치와 미들웨어 버전 업 절차
다음 모듈에서는 OS 보안 패치와 미들웨어 버전 업 절차를 다룹니다.