SSH 프로토콜 보안 원격 접속 | 공개키·ProxyJump·포트 포워딩·OpenSSH 실전
이 글의 핵심
SSH의 키 교환·서버 인증·사용자 인증 흐름, 공개키 설정, ssh config, 로컬·원격 포트 포워딩과 ProxyJump, SCP/SFTP·보안 운영까지 정리한 중급 가이드입니다.
들어가며
SSH(Secure Shell)는 암호화된 원격 셸에 그치지 않고 파일 복사(SCP/SFTP), 포트 포워딩, SOCKS 프록시, Git·Ansible의 전송 채널로도 쓰이는 인프라 실무의 기본 도구입니다. Telnet과 rsh가 평문으로 주고받던 원격 접속을 기밀성, 무결성, 서버 인증으로 보완한 프로토콜입니다.
이 글은 프로토콜 관점의 키 교환·인증 흐름과 OpenSSH 설정 실전을 함께 다룹니다. 공개키 관리, 다단 점프, 최소 권한은 한 번의 실수가 전체 서버로 번지는 환경에서 특히 중요합니다.
왜 SSH가 표준이 되었나
원격 셸, 파일 전송, 포트 포워딩을 하나의 암호화 세션에 묶을 수 있고, 공개키 인증과 호스트 검증으로 평문 원격 접속의 문제를 해결했기 때문입니다. CI 배포나 Ansible 같은 운영 자동화도 같은 신뢰 모델을 그대로 재사용할 수 있습니다.
프로덕션에서 주의할 점
- 비밀번호 로그인을 허용하면 무차별 대입 공격에 노출되는 면이 넓어지므로, 공개키 인증에 필요하면 MFA를 더하는 구성이 일반적입니다.
ForwardAgent는 편리하지만, 원격 서버가 침해되면 에이전트를 통해 다른 서버로 피해가 번질 수 있어 호스트와 시간을 제한해야 합니다.authorized_keys에command=나from=제한 없이 키를 등록하면, 그 키가 유출됐을 때 의도보다 큰 권한이 열립니다.
점프 호스트의 역할
리버스 프록시가 외부 요청을 받아 내부 서비스로 나눠 주듯, SSH 점프 호스트(bastion)는 내부 서버의 IP를 인터넷에 직접 노출하지 않고 검증된 경로만 통과시키는 역할을 맡습니다.
프로토콜 개요
역사
SSH는 1995년 Telnet·rlogin·rsh의 평문 전송과 취약한 인증 문제를 해결하기 위해 등장했고, 현재 널리 쓰이는 SSH-2는 RFC 4251~4254로 표준화됐습니다. 1999년 시작된 OpenSSH가 사실상의 표준 구현이며, 현재 OpenSSH와 libssh 등은 Ed25519·ECDSA·RSA 키와 chacha20-poly1305·AES-GCM 같은 AEAD 암호를 지원합니다. 알고리즘 권고가 계속 바뀌므로 보안 업데이트를 꾸준히 따라가야 합니다.
계층에서의 위치
SSH는 응용 계층 프로토콜이며 전송 계층으로 TCP(기본 22번 포트)를 사용합니다. TLS가 다른 응용 프로토콜 밑에 깔리는 범용 보안 계층이라면, SSH는 대화형 셸, 서브시스템(SFTP 등), 포트 포워딩까지 하나의 보안 연결 안에 채널로 묶는 설계입니다.
핵심 특징
| 특징 | 설명 |
|---|---|
| 암호화 세션 | 키 교환 후 대칭키로 페이로드 보호. |
| 서버 인증 | 호스트 키 핑거프린트로 중간자 공격 완화. |
| 사용자 인증 | 비밀번호, 공개키, 키보드 인터랙티브(2FA) 등. |
| 채널·포워딩 | 여러 논리 채널(셸, SFTP, 포트 포워드)을 한 연결에 멀티플렉싱. |
동작 원리
전체 흐름
- TCP 연결을 맺습니다(기본 22번 포트).
- 버전 문자열을 교환하고 사용할 알고리즘을 협상한 뒤, 키 교환(예: Curve25519)으로 세션 키를 도출합니다.
- 서버는 호스트 키로 키 교환 결과에 서명해 자신을 증명하고, 클라이언트는 그 호스트 키를
known_hosts와 비교합니다. - 암호화된 채널 안에서 사용자 인증을 합니다(공개키 방식이면 서명으로 증명).
- 셸, SFTP 같은 서브시스템, 포워딩용 채널을 엽니다.
세 가지 키의 역할
- 키 교환(KEX): 연결마다 새 대칭키를 안전하게 합의합니다.
- 호스트 키: 서버의 정체를 검증하는 장기 키입니다.
- 사용자 키: 클라이언트가 개인키로 서명해 로그인 권한을 증명합니다. 개인키는 패스프레이즈로 보호합니다.
세 종류의 키가 하는 일이 다르다는 점을 이해하면 오류 메시지가 훨씬 잘 읽힙니다. 키 교환은 매 연결마다 새로 만드는 일회용 비밀(Curve25519 등)이라, 나중에 호스트 키나 사용자 키가 유출되더라도 과거에 녹화된 세션을 복호화할 수 없습니다(전방 비밀성). 호스트 키는 서버가 키 교환 결과에 서명해 “나는 지난번과 같은 서버”임을 증명하는 데 쓰이고, 사용자 키는 그 암호화된 채널 안에서 세션 고유 값에 서명해 “나는 이 공개키의 주인”임을 증명합니다. 서명 대상에 세션 고유 값이 들어가므로, 중간에서 서명을 가로채 다른 세션에 재사용할 수 없습니다. 비밀번호 인증과 달리 개인키는 네트워크로 전송되지 않고 서버에 저장되는 것도 공개키뿐이라, 서버가 털려도 로그인 수단이 유출되지 않는다는 것이 공개키 인증의 근본적인 장점입니다.
처음 접속할 때 나오는 The authenticity of host ... can't be established. ED25519 key fingerprint is SHA256:... 질문은 SSH 신뢰 모델에서 가장 약한 고리입니다. 이 시점에는 클라이언트가 서버의 진짜 핑거프린트를 알 방법이 없어서(TOFU, trust on first use), 무심코 yes를 입력하면 그 순간 중간자가 있었더라도 그 키를 믿게 됩니다. 서버를 새로 만들 때 콘솔에서 ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub로 핑거프린트를 확인해 두거나, 클라우드의 인스턴스 콘솔 출력·DNS의 SSHFP 레코드·SSH 인증서(CA가 서명한 호스트 키)로 검증 경로를 마련하는 것이 규모가 커질수록 중요해집니다.
인증 방식
| 방식 | 설명 |
|---|---|
| password | 편리하지만 무차별 대입과 유출에 취약해, 쓰지 않거나 MFA와 함께 씁니다. |
| publickey | 패스프레이즈로 보호한 키를 에이전트에 올려 쓰는 방식이 일반적입니다. |
| keyboard-interactive | OTP 같은 2단계 인증에 활용합니다. |
sequenceDiagram participant C as Client participant S as SSH Server C->>S: TCP connect :22 C->>S: 알고리즘 협상 C->>S: 키 교환(KEX) S->>C: 호스트 키 + 증명 C->>C: known_hosts 검증 C->>S: 사용자 인증(공개키 서명 등) S->>C: 성공 → 셸/SFTP 채널
실전 사용
ssh-keygen
# Ed25519 권장(짧고 강함)
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519
# 기존 RSA 필요 시(레거시 호환)
ssh-keygen -t rsa -b 4096 -C "[email protected]" -f ~/.ssh/id_rsa
# 공개키를 서버에 등록 (~/.ssh/authorized_keys 한 줄)
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
개인키 파일 권한은 chmod 600 ~/.ssh/id_ed25519로 둡니다.
Ed25519를 권하는 이유는 강도보다 실수할 여지가 적다는 데 있습니다. 키가 짧아 복사·붙여넣기가 쉽고, 서명 과정에 좋은 난수가 필요 없도록 설계되어 있어(ECDSA는 서명마다 쓰는 난수가 약하면 개인키가 복원될 수 있습니다) 구현 실수의 영향이 적습니다. RSA가 필요한 경우는 오래된 장비나 일부 하드웨어 보안 모듈 정도입니다. 반대 방향의 호환성 문제도 알아 두어야 합니다. OpenSSH 8.8부터 SHA-1 기반 ssh-rsa 서명 알고리즘이 기본으로 비활성화되어, 오래된 서버나 네트워크 장비에 접속하면 no matching host key type found. Their offer: ssh-rsa 에러가 납니다. RSA 키 자체가 금지된 것이 아니라 SHA-1 서명이 막힌 것이므로, 서버를 업데이트해 rsa-sha2-256/512를 지원하게 하는 것이 정답이고, 당장 접속해야 한다면 해당 호스트에만 HostKeyAlgorithms +ssh-rsa와 PubkeyAcceptedAlgorithms +ssh-rsa를 제한적으로 추가합니다.
ssh config
~/.ssh/config:
Host bastion
HostName bastion.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519
Host internal-*
User app
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519
Host db-tunnel
HostName app.internal
LocalForward 15432 localhost:5432
ProxyJump bastion
접속:
ssh internal-api
ssh db-tunnel # 로컬 15432 → 원격 postgres
포트 포워딩
# 로컬: 내 PC의 8080 → 서버가 보는 target:80
ssh -L 8080:target.internal:80 user@bastion
# 원격: 서버의 9090 → 내 로컬 3000
# (기본은 서버의 127.0.0.1:9090에만 바인딩. 외부에서 접속하게 하려면
# 서버 sshd_config에 GatewayPorts가 허용되어 있어야 함)
ssh -R 9090:localhost:3000 user@public-host
포워딩 명세의 주소가 어느 쪽 기준으로 해석되는지가 가장 헷갈리는 부분입니다. -L 8080:target.internal:80에서 8080은 내 PC에서 열리는 포트이고, target.internal은 SSH 서버(bastion)가 이름을 풀어 연결하는 대상입니다. 그래서 -L 8080:localhost:80의 localhost는 내 PC가 아니라 bastion 자신을 뜻합니다. 로컬 포트도 기본적으로 127.0.0.1에만 바인딩되어 같은 네트워크의 다른 사람은 쓸 수 없는데, 이는 의도된 안전장치이므로 -L 0.0.0.0:8080:...처럼 풀 때는 신중해야 합니다. 로컬 포트가 이미 사용 중이면 bind [127.0.0.1]:8080: Address already in use와 함께 Could not request local forwarding 경고만 나오고 셸은 그대로 열리므로, 터널이 안 된다 싶으면 접속 직후 출력되는 경고부터 확인합니다. 셸 없이 터널만 필요하면 -N을, 백그라운드로 보내려면 -f를 함께 씁니다.
ProxyJump
ssh -J [email protected] [email protected]
매번 -J를 입력하기보다 앞의 ~/.ssh/config처럼 ProxyJump를 설정 파일에 고정해 두는 편이 실수를 줄입니다.
ProxyJump가 중요한 이유는 편리함보다 보안 모델 때문입니다. 흔한 대안은 bastion에 먼저 접속한 뒤 거기서 다시 내부 서버로 ssh하는 것인데, 이 방식에서 내 개인키를 쓰려면 개인키를 bastion에 복사하거나 ForwardAgent를 켜야 합니다. 에이전트 포워딩을 켜면 bastion의 root(또는 bastion을 장악한 공격자)가 내 에이전트 소켓을 이용해 내가 접속해 있는 동안 내 키로 어디든 로그인할 수 있습니다. ProxyJump는 bastion을 단순한 TCP 중계로만 쓰고, 암호화와 인증은 내 PC와 최종 서버 사이에서 끝까지 이루어지므로 bastion은 내 키에 전혀 접근할 수 없습니다. 제가 점프 호스트 구성을 정리할 때 가장 먼저 하는 일도 팀의 ForwardAgent yes 설정을 ProxyJump로 바꾸는 것입니다.
SCP / SFTP
scp -i ~/.ssh/id_ed25519 ./build.tar.gz user@host:/var/app/
sftp user@host
# sftp> put local.bin /remote/path/
scp 명령은 그대로지만 내부 프로토콜은 바뀌었습니다. 전통적인 scp 프로토콜은 원격 셸 명령 해석에 의존하는 구조라 파일 이름 처리와 관련된 보안 문제가 반복되었고, OpenSSH 9.0부터 scp 명령은 기본적으로 SFTP 프로토콜로 전송합니다. 대부분은 차이를 느끼지 못하지만, 원격 경로에 ~user나 셸 와일드카드를 쓰던 스크립트가 다르게 동작하거나, SFTP 서브시스템을 꺼 둔 오래된 서버에서 실패할 수 있습니다. 그런 경우 scp -O로 예전 프로토콜을 쓸 수 있습니다. 큰 디렉터리를 자주 동기화한다면 변경된 부분만 보내는 rsync -e ssh가 더 효율적입니다.
보안 고려사항
키 관리
- 키 타입: 새 키는 Ed25519를 우선하고, RSA가 필요하면 3072비트 이상으로 만듭니다.
- 분리: 개인용, CI용, 배포용 키를 나누고, 유출 시 키를 폐기하고 교체하는 절차를 문서로 남깁니다.
- authorized_keys:
command=,from="IP"옵션으로 키마다 권한을 최소화합니다.
2단계 인증
pam_google_authenticator 같은 TOTP 모듈이나, OpenSSH 8.2부터 지원하는 FIDO2 보안 키(ed25519-sk, ecdsa-sk 키 타입)로 인증을 강화할 수 있습니다. 공개키와 MFA를 함께 요구하는 구성이 많습니다.
sshd 하드닝과 fail2ban
- 비밀번호 로그인 비활성화:
PasswordAuthentication no - root 직접 로그인 금지:
PermitRootLogin no - 허용 사용자 제한:
AllowUsers - fail2ban: 로그인 실패가 반복되는 IP를 일정 시간 차단합니다. SSH 포트가 인터넷에 열려 있을 때 효과가 있습니다.
에이전트 포워딩
ForwardAgent yes는 편리하지만, 원격 서버가 침해되면 에이전트 소켓을 통해 내 키가 악용될 수 있습니다. 필요한 호스트에서만, 필요한 동안만 켭니다. 점프 목적이라면 앞에서 설명한 ProxyJump가 대부분의 경우 더 안전한 대안이고, 원격 서버에서 Git 저장소를 받아야 하는 경우처럼 정말 필요하다면 ssh-add -c로 키를 올려 두어 키가 사용될 때마다 로컬에서 확인 창이 뜨게 하는 방법도 있습니다.
실무 활용 사례
| 영역 | 설명 |
|---|---|
| 서버 관리 | 셸, systemd, 로그 tail, 패치. |
| 배포 | CI에서 SSH로 릴리스 스크립트, Docker context over SSH. |
| 터널링 | 사내망 DB·Redis를 로컬 포트로 안전히 매핑. |
| Git | [email protected]:... SSH URL, 호스트 키 핀닝. |
| 에어갭 근접 | 점프 호스트 체인으로 세그먼트 분리 유지. |
최적화 팁
ControlMaster(멀티플렉싱)
~/.ssh/config:
Host *
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
같은 호스트로의 반복 연결이 빨라집니다. 보안 정책에 따라 제한할 수 있습니다.
첫 연결이 마스터가 되어 소켓 파일을 만들고, 이후 연결은 새 TCP 연결·키 교환·인증 없이 그 소켓을 통해 채널만 추가하므로 Ansible처럼 같은 서버에 수십 번 접속하는 도구에서 효과가 큽니다. 주의할 점은 마스터가 살아 있는 동안 포워딩 옵션을 새로 줘도 적용되지 않을 수 있다는 것입니다. -L을 추가한 두 번째 ssh가 기존 마스터에 붙으면서 포워딩이 조용히 무시되는 경우가 있어, 이럴 때는 ssh -O exit host로 마스터를 끊거나 -o ControlMaster=no로 새 연결을 만듭니다. 소켓 경로는 유닉스 도메인 소켓 길이 제한(약 100바이트)에 걸리기 쉬워, 긴 호스트 이름을 쓴다면 ControlPath ~/.ssh/cm-%C처럼 해시값(%C)을 쓰는 편이 안전합니다.
압축
-C 옵션은 느린 링크에서 유용하지만 빠른 LAN에서는 CPU만 쓸 수 있습니다.
KeepAlive
Host *
ServerAliveInterval 30
ServerAliveCountMax 4
클라이언트가 30초마다 서버에 응답을 요청해, 유휴 연결이 NAT나 방화벽의 타임아웃으로 끊기는 일을 줄입니다. 응답이 4번 연속 없으면 연결을 끊습니다.
ssh-agent
eval "$(ssh-agent -s)"
ssh-add --apple-use-keychain ~/.ssh/id_ed25519 # macOS 예
에이전트는 복호화한 키를 메모리에만 두므로, 디스크에는 패스프레이즈로 암호화된 개인키만 남깁니다.
흔한 문제와 해결
| 증상 | 점검 |
|---|---|
| Permission denied (publickey) | 서버 authorized_keys 경로·권한(~/.ssh 700, 파일 600), 홈 디렉터리가 그룹·다른 사용자 쓰기 가능하지 않은지, 올바른 공개키 등록, 계정명 일치. |
| WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! | 서버 재설치·IP 재사용 시 ssh-keygen -R host로 해당 항목만 제거 후 새 핑거프린트를 별도 경로로 확인. |
| 연결 타임아웃 | 보안 그룹·방화벽, NAT, IPv4/IPv6 혼동, 점프 경로 누락. |
| Too many authentication failures | 에이전트에 많은 키—IdentitiesOnly yes, IdentityFile 명시. |
| 포트 포워딩 안 됨 | 서버 설정 AllowTcpForwarding(기본은 보통 허용이나 정책 확인), 바인드 주소(GatewayPorts). |
Permission denied (publickey)는 원인이 서버 쪽에 있는데 클라이언트에는 이 한 줄만 보여서 가장 오래 헤매게 되는 오류입니다. 서버의 sshd는 보안상 거부 이유를 클라이언트에 알려 주지 않기 때문입니다. 먼저 클라이언트에서 ssh -v user@host로 어떤 키를 제시했는지(Offering public key: ...)와 서버가 받아들였는지를 보고, 원인이 보이지 않으면 서버의 인증 로그(journalctl -u ssh 또는 /var/log/auth.log)를 확인합니다. 권한이 문제라면 Authentication refused: bad ownership or modes for directory /home/user처럼 서버 로그에 정확한 이유가 남습니다. sshd의 StrictModes는 다른 사용자가 authorized_keys를 바꿀 수 있는 상태를 거부하므로, 홈 디렉터리를 chmod 775로 열어 둔 것만으로도 공개키 로그인이 막힙니다. Windows의 OpenSSH 서버에서는 관리자 그룹 계정의 공개키를 사용자 폴더가 아니라 C:\ProgramData\ssh\administrators_authorized_keys에서 읽는다는 점도 자주 걸리는 부분입니다.
REMOTE HOST IDENTIFICATION HAS CHANGED 경고는 대부분 서버 재설치나 같은 IP에 다른 서버가 뜬 정상적인 경우지만, 이 경고는 중간자 공격이 실제로 일어날 때 보이는 메시지와 똑같습니다. 그래서 습관적으로 known_hosts 파일을 통째로 지우는 것은 위험합니다. 변경 이유를 확인할 수 있을 때만 ssh-keygen -R로 해당 호스트 줄만 지우세요. 오토스케일링으로 IP가 계속 재사용되는 환경이라면 경고를 끄는 대신 SSH 호스트 인증서를 도입해 CA 키 하나만 신뢰하는 방식이 근본적인 해결책입니다.
자주 묻는 질문 (FAQ)
Q. Too many authentication failures 오류는 왜 생기나요?
ssh-agent에 키가 여러 개 올라가 있으면 클라이언트가 키를 차례로 시도하다가 서버의 인증 시도 한도를 넘겨 이 오류가 납니다. ~/.ssh/config에서 해당 호스트에 IdentitiesOnly yes와 IdentityFile을 명시해 필요한 키만 제시하도록 하면 해결됩니다. Permission denied (publickey)가 난다면 이와 별개로 ~/.ssh(700)와 authorized_keys(600) 권한, 계정명을 확인해야 합니다.
참고
man ssh,man sshd_config,man ssh_config- OpenSSH 릴리스 노트(알고리즘 권고)
- NIST IR 7966, Security of Interactive and Automated Access Management Using Secure Shell (SSH)
같이 보면 좋은 글
- HTTP vs FTP vs SSH 프로토콜 비교 | 용도·보안·파일 전송 선택 가이드
- FTP 프로토콜 실전 활용 | Active·Passive·FTPS·SFTP와 파일 전송 운영
- HTTP 프로토콜