개발자가 ssh user@server를 친다.
터미널에 비밀번호나 키 한 번 입력하면 곧바로 원격 서버 쉘이 열린다. 그런데 그 사이 0.x초 동안 클라이언트와 서버가 실제로 무엇을 주고받는지 설명하라고 하면 막상 말문이 막힌다.
SSH는 신뢰할 수 없는 네트워크 위에서 안전한 통로를 만드는 프로토콜이다. 1995년 Tatu Ylönen이 평문으로 모든 걸 흘려보내던 Telnet을 대체하려고 만들었고, 지금은 서버 관리·배포·터널링의 기반이 됐다. 이 글에서는 SSH가 풀어야 하는 문제부터 시작해, 연결이 맺어지는 전체 과정을 단계별로 따라간다. 다 읽고 나면 known_hosts 경고가 왜 뜨는지, 개인키는 왜 절대 네트워크로 안 나가는지 자연스럽게 이해된다.
SSH가 풀어야 하는 세 가지 문제
암호화 한 줄로 끝나는 얘기가 아니다. SSH는 다음 세 가지를 동시에 보장해야 한다.
| 문제 | 설명 | 막아내는 공격 |
|---|---|---|
| 기밀성(Confidentiality) | 주고받는 데이터를 제3자가 읽을 수 없게 | 도청(Sniffing) |
| 무결성(Integrity) | 데이터가 중간에 변조되지 않았음을 보장 | 패킷 변조(Tampering) |
| 인증(Authentication) | 상대가 진짜 그 서버/사용자인지 확인 | 중간자 공격(MITM), 위장 |
여기서 인증은 다시 두 갈래로 나뉜다.
서버 인증(내가 접속한 곳이 진짜 그 서버가 맞나)과 사용자 인증(접속하는 너는 진짜 권한이 있는 사람이 맞나)이다.
이 둘은 SSH 안에서 완전히 다른 단계에서 처리된다. 이걸 구분해서 보는 게 SSH 이해의 핵심이다.
전체 흐름 한눈에 보기
먼저 큰 그림부터 잡고 가자. ssh 명령 한 줄은 내부적으로 대략 이런 순서로 진행된다.

크게 보면 앞부분(1~6)은 안전한 터널을 까는 단계고, 뒷부분(7~8)은 그 터널 안에서 신원을 확인하고 일을 시작하는 단계다. 이제 하나씩 뜯어본다.
1. TCP 연결과 버전 교환
모든 건 평범한 TCP 연결로 시작한다. 클라이언트가 서버의 22번 포트로 3-way handshake를 맺는다. 이 시점에는 아직 암호화가 없다.
연결되면 양쪽이 평문으로 버전 문자열을 주고받는다.
서버 → 클라이언트 : SSH-2.0-OpenSSH_9.6
클라이언트 → 서버 : SSH-2.0-OpenSSH_9.6
이 문자열은 SSH 프로토콜 버전(2.0)과 구현체·버전을 알려준다. 둘이 호환되지 않으면 여기서 연결을 끊는다. 참고로 요즘은 거의 다 SSH-2만 쓴다. SSH-1은 단일 모놀리식 구조에 약한 CRC-32 무결성 검사를 쓰는 등 설계 결함이 있어 사실상 폐기됐다.
이 버전 문자열은 평문으로 오갈 수밖에 없다. 아직 암호화 키가 존재하지 않기 때문이다. 그래서 더더욱, 이 단계에서 교환된 값은 나중에 무결성 검증용 해시에 함께 묶인다.
2. 알고리즘 협상
다음은 "어떤 암호 방식으로 대화할지" 정하는 단계다. 양쪽이 SSH_MSG_KEXINIT 메시지를 보내며 자기가 지원하는 알고리즘 목록을 선호 순서대로 나열한다.
협상 대상은 크게 네 종류다.
- 키 교환(KEX):
curve25519-sha256,ecdh-sha2-nistp256등 - 대칭 암호화:
aes256-ctr,chacha20-poly1305@openssh.com등 - MAC(무결성):
hmac-sha2-256등 - 압축:
none,zlib등
양쪽 목록을 비교해 공통으로 지원하는 것 중 클라이언트 선호도가 가장 높은 걸 고른다. 겹치는 게 하나도 없으면 협상 실패로 연결이 끊긴다. 이 "목록 + 선호 순서" 구조 덕분에 SSH는 새 알고리즘이 나와도 유연하게 갈아탈 수 있다.
3. 키 교환(KEX) — 핵심 중의 핵심
여기가 SSH에서 가장 영리한 부분이다. 목표는 단 하나, 공유 비밀(shared secret)을 네트워크에 단 한 번도 흘리지 않고 양쪽이 똑같은 값에 도달하는 것이다.
이걸 가능하게 하는 게 Diffie-Hellman 키 교환, 그리고 그 타원곡선 변형인 ECDH(예: Curve25519)다. 직관적으로 보면 이렇다.

da, db는 네트워크에 절대 나가지 않는다. 오직 공개값 Qa, Qb만 오간다.
그런데 타원곡선의 수학적 성질 때문에 양쪽이 계산한 결과는 정확히 같은 값 K가 된다.
도청자는 Qa, Qb를 다 봐도 K를 역산할 수 없다.
여기서 중요한 디테일 하나. 이 da, db는 이 연결 한 번만을 위해 만들어졌다 버려지는 임시(ephemeral) 키다. 그래서 순방향 비밀성(Forward Secrecy)이 보장된다. 공격자가 5년 뒤 서버의 영구 키를 통째로 훔쳐도, 오늘 나눈 대화는 복호화할 수 없다. 그날의 임시 키는 세션이 끝나는 순간 영원히 사라졌기 때문이다.
한 가지 짚어둘 것: 이렇게 만들어진 K는 그 자체가 암호화 키가 아니다. 이후 다른 핸드셰이크 데이터와 함께 KDF(키 유도 함수)에 들어가, 여러 개의 대칭 키를 뽑아내는 "원재료"다.
4. 서버 인증 — known_hosts의 정체
키 교환만 끝난 상태에서는 함정이 하나 있다. 도청은 막았지만, 내가 키 교환한 상대가 진짜 그 서버인지는 아직 모른다. 중간에 누가 끼어들어 서버인 척했을 수도 있다(능동적 MITM).
그래서 서버는 자기 신원을 증명해야 한다. 서버에는 프로세스가 뜰 때 만들어진 호스트 키 쌍(공개키 + 개인키)이 있다. 서버는 지금까지의 핸드셰이크 값들을 묶어 만든 exchange hash에 자기 개인키로 서명해서 보낸다. 클라이언트는 서버의 공개키로 그 서명을 검증한다. 검증이 통과하면 "이 상대는 그 호스트 개인키를 가진 게 확실하다"가 증명된다.
문제는, 그 공개키 자체를 믿을 수 있느냐다. 클라이언트는 받은 호스트 공개키를 로컬의 ~/.ssh/known_hosts와 대조한다.
# 처음 접속할 때 뜨는 바로 그 메시지
The authenticity of host '10.10.10.10 (10.10.10.10)' can't be established.
ED25519 key fingerprint is SHA256:abc123...
Are you sure you want to continue connecting (yes/no)?
이 경고는 "이 서버의 키가 내 known_hosts에 없다"는 뜻이다. yes를 누르면 키가 known_hosts에 저장되고, 다음부터는 조용히 통과한다. 반대로 저장된 키와 다른 키가 오면 SSH는 강하게 경고하며 연결을 막는다. MITM 가능성을 의심하는 것이다.
❌ 위험: 경고가 떠도 습관적으로 yes
→ 진짜 MITM 공격을 그냥 통과시킬 수 있다
✅ 안전: fingerprint를 서버 관리자가 공유한 값과 대조
→ 처음 접속 시 딱 한 번만 확인하면 이후는 자동 검증
규모가 큰 환경에서는 호스트마다 키를 신뢰하는 대신, SSH 인증서(CA) 방식으로 CA 하나만 known_hosts에 넣고 그 CA가 서명한 모든 호스트를 통째로 신뢰하게 만들기도 한다.
5. 세션 키 생성과 암호화 전환
서버 인증까지 끝나면, 키 교환에서 얻은 공유 비밀 K를 KDF에 넣어 실제 세션 키들을 유도한다. 방향별·용도별로 여러 개가 나온다.
- 클라이언트→서버 암호화 키 / 서버→클라이언트 암호화 키
- 각 방향의 MAC 키 (무결성 검증용)
이 키들이 준비되면 양쪽은 SSH_MSG_NEWKEYS 메시지를 교환한다. 이 메시지 이후의 모든 통신은 새로 만든 대칭 키로 암호화·보호된다. 실제 암호화에는 협상해둔 AES-256이나 ChaCha20-Poly1305 같은 대칭 암호가 쓰인다.
여기서 가장 흔한 오해 하나를 정리하고 넘어가자.
| 구분 | 비대칭 암호(공개키/개인키) | 대칭 암호(공유 키) |
|---|---|---|
| 어디 쓰나 | 키 교환 보조, 서버 인증, 사용자 공개키 인증 | 실제 세션 데이터 암호화 |
| 속도 | 느림 | 빠름 |
| 대표 예 | RSA, Ed25519, ECDH | AES-256, ChaCha20 |
"SSH 키 쌍(공개키/개인키)으로 세션을 암호화한다"는 건 틀린 생각이다. 키 쌍은 인증에만 쓰이고, 정작 주고받는 데이터는 키 교환으로 만든 대칭 키가 암호화한다. 대칭 암호가 훨씬 빠르기 때문이다.
무결성은 MAC(Message Authentication Code)가 책임진다. 패킷마다 MAC이라는 "봉랍 도장"이 찍히는데, 전송 중 누가 비트 하나라도 뒤집으면 MAC 검증이 실패하고 SSH는 즉시 연결을 끊는다.
6. 사용자 인증
이제야 비로소 "너 누구냐"를 묻는다. 중요한 건 이 단계가 이미 암호화된 터널 안에서 일어난다는 점이다. 그래서 비밀번호 인증조차 도청으로부터 보호된다.
클라이언트와 서버는 먼저 허용된 인증 방법 목록을 합의한다(공개키, 비밀번호, keyboard-interactive 등). 클라이언트는 이 중 하나씩 시도한다. 대표적인 두 가지를 보자.
비밀번호 인증
말 그대로 비밀번호를 입력한다. 이미 암호화된 터널 안이라 평문 노출 위험은 없지만, 추측·무차별 대입 공격에 약하다.
공개키 인증 (권장)
비밀번호보다 안전하고, 자동화에 적합하다. 흐름은 "챌린지-서명-검증"이다.

핵심은 개인키도 비밀번호도 네트워크로 절대 나가지 않는다는 점이다.
클라이언트는 개인키로 챌린지에 서명만 하고, 서버는 등록된 공개키로 그 서명이 맞는지 검증할 뿐이다.
개인키를 가졌다는 사실을, 개인키 자체를 노출하지 않고 증명하는 셈이다.
# 키 쌍 생성 (ed25519 권장)
ssh-keygen -t ed25519 -C "hyunmin@laptop"
# 공개키를 서버에 등록
ssh-copy-id user@server
# 내부적으로 ~/.ssh/authorized_keys 에 공개키가 추가된다
개인키가 유출되면 누군가 나인 척 서버에 들어올 수 있으니, 반드시 passphrase로 보호하고 유출 시 서버의 공개키를 즉시 삭제 후 새 쌍을 발급해야 한다.
실전 팁
오랫동안 SSH를 쓰며 정리한 것들이다.
- 비밀번호 인증은 끄고 공개키만 쓴다. 서버의
/etc/ssh/sshd_config에서:
PasswordAuthentication no
PubkeyAuthentication yes
- 키는 ed25519를 쓴다. RSA보다 짧고 빠르며 안전하다. 새로 만든다면 RSA 2048을 고집할 이유가 없다.
- known_hosts 경고를 습관적으로 무시하지 않는다. fingerprint가 바뀌었다면 서버 재설치 같은 정상적 이유일 수도, MITM일 수도 있다. 이유를 확인하기 전엔 yes를 누르지 않는다.
~/.ssh/config로 접속을 단축한다. 호스트별 설정을 모아두면ssh myserver한 줄로 끝난다.
Host myserver
HostName XXX.XX.XX.XXX
User username
Port 22
IdentityFile ~/.ssh/id_ed25519
- passphrase + ssh-agent 조합을 쓴다. 개인키에 passphrase를 걸되, ssh-agent에 한 번만 등록해두면 매번 입력하는 번거로움 없이 보안과 편의를 둘 다 챙긴다.
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
마치며
ssh user@server 한 줄 뒤에는 이런 일들이 순서대로 일어난다.
- TCP 연결과 버전 교환으로 판을 깔고
- 알고리즘을 협상한 뒤
- Diffie-Hellman으로 공유 비밀을 흘리지 않고 양쪽이 도출하고
- 호스트 키로 서버가 진짜인지 검증하고(known_hosts)
- 거기서 유도한 대칭 키로 터널을 암호화한 다음
- 그 안에서 비로소 사용자를 인증한다
기억할 핵심은 두 가지다. 첫째, 데이터를 실제로 암호화하는 건 빠른 대칭 키이고, 공개키/개인키 쌍은 인증에만 쓰인다. 둘째, 비밀번호든 개인키든 그 자체는 절대 네트워크로 나가지 않는다. SSH는 비밀을 직접 보내지 않고도 그 비밀을 안다는 사실만 증명하는 방식으로 동작한다.
다음에 known_hosts 경고가 뜨거나 비밀번호 없이 서버에 로그인될 때, 그 짧은 순간에 어떤 핸드셰이크가 지나갔는지 한 번쯤 떠올려봐도 좋다.
더 읽을거리
- DigitalOcean — Understanding the SSH Encryption and Connection Process
- Teleport — SSH Handshake Explained
- Cisco — Understand Secure Shell Packet Exchange
- RFC 4251~4254 (SSH 프로토콜 아키텍처 / 전송 / 인증 / 연결)