본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 26. 09:12

SSH의 내부 구조: 프로토콜, 메커니즘, 그리고 전체 기술적 이야기

요약

SSH(Secure Shell) 프로토콜의 내부 구조와 작동 메커니즘을 심층적으로 다룹니다. TCP 연결부터 버전 교환, 그리고 Diffie-Hellman 등을 이용한 키 교환(KEX) 단계까지의 기술적 과정을 상세히 설명합니다.

핵심 포인트

  • SSH는 클라이언트-서버 아키텍처 기반의 암호화 네트워크 프로토콜임
  • 프로토콜은 TCP 위에서 동작하는 세 가지 계층으로 구성됨
  • 버전 교환 단계 이후의 모든 통신은 암호화되어 보호됨
  • 키 교환(KEX) 단계에서 공유 비밀을 안전하게 설정함

SSH의 실체

SSH — Secure Shell — 는 보안되지 않은 네트워크를 통해 네트워크 서비스를 안전하게 운영하기 위한 암호화 네트워크 프로토콜 (cryptographic network protocol) 입니다. 그 핵심은 클라이언트-서버 아키텍처 (client-server architecture) 입니다. SSH 클라이언트가 연결을 시작하면, SSH 데몬 (sshd)이 서버에서 일반적으로 22번 포트로 대기합니다. 이들 사이를 오가는 모든 데이터는 암호화 (encrypted) 되고, 인증 (authenticated) 되며, 무결성 보호 (integrity-protected) 됩니다.

대부분의 개발자가 일상적으로 접하는 것 — ssh user@host를 입력하는 것 — 은 거대한 빙산의 일각에 불과합니다. 그 단일 명령 아래에는 밀리초 단위로 발생하는 정밀하게 정렬된 암호화 핸드셰이크 (cryptographic handshakes), 키 협상 (key negotiations), 그리고 프로토콜 계층 (protocol layers) 이 존재합니다.

프로토콜 스택: 세 가지 계층

SSH 프로토콜은 RFC 제품군 (RFC 4251–4254)에 공식적으로 정의되어 있으며, TCP 위에 쌓인 세 가지 별개의 서브 프로토콜 (sub-protocols) 로 구성됩니다:

┌─────────────────────────────────────────┐
│         SSH Connection Protocol         │  ← 채널 (channels), 세션 (sessions), 포트 포워딩 (port forwarding)
├─────────────────────────────────────────┤
...

각 계층은 정밀하고 잘 정의된 역할을 수행합니다. 실행 순서에 따라 각 계층을 살펴보겠습니다.

1단계 — TCP 연결 및 버전 교환

모든 것은 22번 포트로의 일반적인 TCP 핸드셰이크 (TCP handshake) 로 시작됩니다. TCP 연결이 수립되면, 양측은 즉시 평문 버전 문자열 (cleartext version string) 을 보냅니다:

SSH-2.0-OpenSSH_9.6

이 문자열은 SSH 프로토콜 버전 (현대적 사용에서는 항상 2.0 — SSH-1은 폐기되었으며 취약함) 과 소프트웨어 구현체를 선언합니다. 양측은 서로의 버전 문자열을 읽으며, 만약 호환되지 않는다면 연결은 즉시 종료됩니다. 이것이 전체 세션에서 유일한 평문 교환 (plaintext exchange) 입니다. 이 이후의 모든 것은 암호화됩니다.

2단계 — 전송 계층: 키 교환 (KEX)

이 부분은 SSH에서 암호학적으로 가장 밀도가 높은 단계입니다. 목표는 클라이언트와 서버 사이에 공유 비밀 (shared secret)을 설정하되, 그 비밀을 암호화된 형태를 포함하여 네트워크 선로를 통해 전송하지 않는 것입니다. 이는 **키 교환 알고리즘 (Key Exchange Algorithm)**을 통해 달성되며, 가장 흔하게는 Diffie-Hellman (DH) 또는 그 타원 곡선 변형인 ECDH가 사용됩니다.

알고리즘 협상 (Algorithm Negotiation)

실제 키 교환이 시작되기 전에, 클라이언트와 서버는 어떤 알고리즘을 사용할지 협상합니다. 양측은 각 카테고리별로 선호하는 순서대로 지원 가능한 알고리즘 목록을 담은 SSH_MSG_KEXINIT 패킷을 전송합니다:

  • 키 교환 알고리즘 (Key Exchange algorithms): curve25519-sha256, ecdh-sha2-nistp256, diffie-hellman-group14-sha256
  • 호스트 키 알고리즘 (Host key algorithms): ssh-ed25519, ecdsa-sha2-nistp256, rsa-sha2-512
  • 암호화 사이퍼 (Encryption ciphers): chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-ctr
  • MAC 알고리즘 (MAC algorithms): hmac-sha2-256, hmac-sha2-512, umac-128-etm@openssh.com
  • 압축 (Compression): none, zlib@openssh.com

각 카테고리에 대해 합의된 알고리즘은 클라이언트의 목록 중 서버도 지원하는 첫 번째 알고리즘입니다.

Diffie-Hellman 키 교환

실제 키 교환은 다음과 같이 작동합니다 (DH를 표준적인 예시로 사용):

  1. 양측은 커다란 소수 p와 생성자 g에 합의합니다 (이 값들은 공개된 표준 값입니다).
  2. 클라이언트는 무작위 개인 정수 x를 생성하고, e = g^x mod p를 계산하여 서버로 e를 보냅니다.
  3. 서버는 무작위 개인 정수 y를 생성하고, f = g^y mod p를 계산하여 클라이언트로 f를 보냅니다.
  4. 클라이언트는 공유 비밀 K = f^x mod p를 계산합니다.
  5. 서버는 공유 비밀 K = e^y mod p를 계산합니다.

양측은 xy를 전혀 전송하지 않고도 동일한 값인 K(공유 비밀)에 도달합니다. ef를 엿보는 도청자는 이산 로그 문제 (discrete logarithm problem)를 해결하지 않고서는 K를 유도할 수 없으며, 이는 충분히 큰 소수에 대해 계산적으로 불가능합니다.

최신 OpenSSH에서는 Curve25519가 선호되는 KEX (Key Exchange, 키 교환) 알고리즘입니다. 이는 Curve25519 타원 곡선 상의 타원 곡선 디피-헬먼 (ECDH, Elliptic-Curve Diffie-Hellman)을 사용하며, 클래식 DH (Diffie-Hellman)보다 훨씬 작은 키로 128비트 보안을 제공하고 사이드 채널 공격 (side-channel attacks)에 저항하도록 설계되었습니다.

호스트 키 검증 및 교환 해시 (Host Key Verification and the Exchange Hash)

키 교환만으로는 중간자 공격 (man-in-the-middle attack)을 방지할 수 없습니다. 공격자가 양측의 통신을 가로채어 두 개의 별도 키 교환을 실행할 수 있기 때문입니다. 여기서 **서버의 호스트 키 (server's host key)**가 등장합니다.

공유 비밀값 K를 계산한 후, 서버는 교환 해시 (exchange hash) H를 구성합니다:

H = hash(client_version || server_version || client_kexinit || server_kexinit || server_host_public_key || e || f || K)

그 다음 서버는 자신의 개인 호스트 키(예: /etc/ssh/ssh_host_ed25519_key에 저장된 Ed25519 키)로 H에 **서명 (sign)**을 합니다. 이 서명은 서버의 공개 호스트 키와 함께 클라이언트로 전송됩니다.

이제 클라이언트는 다음과 같이 결정해야 합니다: 이 호스트 키를 신뢰할 것인가?

  • 클라이언트가 이전에 이 서버를 방문한 적이 있다면, ~/.ssh/known_hosts 파일에서 일치하는 항목이 있는지 확인합니다.
  • 키가 일치하면, 클라이언트는 해당 공개 키를 사용하여 H에 대한 서명을 검증합니다. 유효한 서명은 이 데이터를 보낸 이가 그에 대응하는 개인 호스트 키를 소유하고 있음, 즉 이 서버가 실제 서버임을 증명합니다.
  • 키가 새로운 경우, 클라이언트는 사용자에게 다음과 같이 묻습니다: The authenticity of host X can't be established. Are you sure you want to continue? (호스트 X의 인증을 확인할 수 없습니다. 계속하시겠습니까?)

이것이 바로 유명한 TOFU (Trust On First Use, 최초 사용 시 신뢰) 모델입니다. 이는 중간자 공격에 대한 주요 방어 수단입니다.

세션 키 유도 (Session Key Derivation)

공유 비밀값 K와 교환 해시 H로부터, 양측은 KDF (Key Derivation Function, 키 유도 함수)를 사용하여 독립적으로 동일한 대칭 세션 키 세트를 유도합니다:

Encryption key (client → server):  hash(K || H || "C" || session_id)
Encryption key (server → client):  hash(K || H || "D" || session_id)
IV (client → server):              hash(K || H || "A" || session_id)
...

각 방향에 대해 별도의 키를 사용한다는 것은 한쪽 방향이 침해되더라도 다른 쪽 방향은 침해되지 않음을 의미합니다. 이 단계 이후, 양측은 이후의 모든 패킷이 이 세션 키(session keys)로 암호화될 것임을 알리기 위해 SSH_MSG_NEWKEYS를 전송합니다. 이제 전송 계층(transport layer)이 활성화되었습니다.

3단계 — 사용자 인증 프로토콜 (The User Authentication Protocol)

암호화되고 무결성이 보호되는 채널이 구축됨에 따라, 서버는 이제 클라이언트가 안전하게 통신할 수 있다는 사실을 알게 되지만, 클라이언트가 _누구_인지는 아직 알지 못합니다. 사용자 인증(User Auth) 프로토콜이 바로 이 문제를 해결합니다.

클라이언트는 ssh-userauth를 위한 SSH_MSG_SERVICE_REQUEST를 보냅니다. 서버가 이를 확인하면 인증이 시작됩니다.

비밀번호 인증 (Password Authentication)

가장 단순한 방법입니다. 클라이언트는 이미 보안이 확보된 채널 내부에서 암호화된 사용자 이름과 비밀번호를 전송합니다. 서버는 /etc/shadow (또는 PAM, LDAP)를 통해 이를 검증합니다. 정보가 일치하면 인증에 성공합니다.

이 방법은 무차별 대입 공격(brute force)과 크리덴셜 스터핑(credential stuffing)에 취약하기 때문에 운영 환경에서는 권장되지 않습니다. 대부분의 보안이 강화된 서버는 sshd_config에서 PasswordAuthentication no 설정을 통해 이 기능을 완전히 비활성화합니다.

공개 키 인증 (Public Key Authentication)

이것이 표준(gold standard)입니다. 프로토콜은 다음과 같이 작동합니다:

  1. 클라이언트가 의도를 선언합니다: "나는 이 공개 키를 사용하여 사용자 alice로 인증하고 싶습니다."
  2. 서버는 해당 공개 키가 사용자 alice~/.ssh/authorized_keys에 등록되어 있는지 확인합니다.
  3. 키가 발견되면, 서버는 챌린지(challenge)로 고유한 데이터 블록(blob)을 보냅니다.
  4. 클라이언트는 그에 대응하는 개인 키 (private key) (~/.ssh/id_ed25519 또는 유사한 경로에 저장됨)로 챌린지에 서명합니다.
  5. 클라이언트는 서명(signature)을 다시 보냅니다.
  6. 서버는 공개 키로 서명을 검증합니다. 개인 키의 소유자만이 유효한 서명을 생성할 수 있습니다. 인증에 성공합니다.

개인 키는 클라이언트 머신을 절대 떠나지 않습니다. 단 한 조각의 파편도 네트워크 선을 넘어가지 않습니다. 이것이 공개 키 인증을 매우 강력하게 만드는 핵심입니다.

SSH 에이전트 및 에이전트 포워딩 (SSH Agent and Agent Forwarding)

실제 환경에서 개인 키 (Private Key)는 종종 암호 (Passphrase)로 보호됩니다. 매번 암호를 입력하는 것은 비현실적입니다. **SSH 에이전트 (SSH agent, ssh-agent)**가 이 문제를 해결합니다. 에이전트는 복호화된 개인 키를 메모리에 보유하며, SSH 클라이언트를 대신하여 서명 작업 (Signing operations)을 수행합니다. 클라이언트는 로컬 Unix 소켓 ($SSH_AUTH_SOCK에 저장됨)을 통해 에이전트와 통신합니다.

**에이전트 포워딩 (Agent forwarding, ssh -A)**은 이를 확장합니다. 머신 A에서 머신 B로, 그리고 다시 B에서 C로 SSH 접속을 할 때, 서명 요청이 체인을 따라 머신 A에 있는 에이전트로 다시 전달될 수 있습니다. 머신 B는 개인 키를 절대 볼 수 없습니다. 이는 매우 편리하지만 위험을 수반합니다. 머신 B에서 루트 (Root) 권한을 가진 사람은 누구나 귀하의 에이전트 소켓을 사용하여 머신 C에서 귀하를 사칭할 수 있습니다.

인증서 기반 인증 (Certificate-Based Authentication)

대규모의 현대적인 SSH 배포 환경에서는 가공되지 않은 공개 키 (Raw public keys) 대신 **SSH 인증서 (SSH certificates)**를 사용합니다. 인증 기관 (Certificate Authority, CA)은 사용자 또는 호스트의 공개 키에 서명하며, 여기에 허가된 주체 (Authorized principals), 유효 기간, 확장 플래그 (Extension flags)를 포함합니다. authorized_keys 방식은 모든 서버가 모든 허가된 사용자의 공개 키를 목록화해야 합니다. 반면 인증서를 사용하면 모든 서버가 CA의 공개 키만 신뢰하면 되며, CA는 사용자에게 수명이 짧은 인증서를 발급합니다. 이는 대규모 환경에서 키 관리 (Key management)를 획기적으로 단순화합니다.

4단계 — 연결 프로토콜: 다중화된 채널 (The Connection Protocol: Multiplexed Channels)

인증이 완료되면 SSH 연결 프로토콜 (SSH Connection Protocol)이 제어권을 넘겨받습니다. 이 프로토콜은 단일 암호화된 TCP 연결 위에 여러 개의 논리적 **채널 (Channels)**을 다중화 (Multiplexes)합니다. 각 채널은 번호로 식별되며, 동시에 서로 다른 유형의 트래픽을 전송할 수 있습니다.

채널 유형 (Channel Types)

  • session: 원격 명령 실행 (Remote command execution) 또는 대화형 셸 (Interactive shell). 단순히 ssh user@host를 실행할 때 얻게 되는 것입니다.
  • direct-tcpip: 로컬 포트 포워딩 (Local port forwarding). 로컬 포트로 전송된 트래픽이 SSH 서버를 통해 목적지로 터널링됩니다.
  • forwarded-tcpip: 원격 포트 포워딩 (Remote port forwarding). 서버가 자신의 포트 중 하나로부터 오는 트래픽을 SSH 터널을 통해 클라이언트로 전달합니다.
  • x11: X11 포워딩 (X11 forwarding) — 그래픽 애플리케이션 디스플레이 세션을 터널링합니다.

채널 작동 방식 (How Channels Work)

채널 열기:

Client → Server:  SSH_MSG_CHANNEL_OPEN (type="session", sender_channel=0, window_size=2MB, max_packet=32KB)
Server → Client:  SSH_MSG_CHANNEL_OPEN_CONFIRMATION (recipient_channel=0, sender_channel=1, ...)

각 채널은 윈도우 크기 (Window sizes)를 통해 독립적인 흐름 제어 (Flow control)를 수행합니다. 즉, 송신자는 수신자가 광고한 윈도우 크기보다 더 많은 데이터를 밀어넣을 수 없습니다. 수신자가 데이터를 처리하면, 윈도우를 확장하기 위해 SSH_MSG_CHANNEL_WINDOW_ADJUST를 보냅니다.

데이터는 SSH_MSG_CHANNEL_DATA 패킷 내부에서 흐릅니다. 채널은 SSH_MSG_CHANNEL_CLOSE로 닫힙니다. 여러 채널이 동시에 열려 있을 수 있으며, 이들은 모두 단일 TCP 연결 위에서 멀티플렉싱 (Multiplexed)됩니다.

대화형 셸 세션 (Interactive Shell Sessions)

대화형 셸을 원하는 경우, 클라이언트는 PTY (Pseudo-Terminal, 가상 터미널)를 요청합니다:

SSH_MSG_CHANNEL_REQUEST (request-type="pty-req", term="xterm-256color", columns=220, rows=50, ...)
SSH_MSG_CHANNEL_REQUEST (request-type="shell")

서버는 PTY 쌍을 할당합니다: 마스터 측 (Master side, sshd에 의해 제어됨)과 슬레이브 측 (Slave side, 원격 셸에서 보이는 터미널). 터미널 크기 조정 이벤트는 SSH_MSG_CHANNEL_REQUEST (request-type="window-change")와 함께 전송됩니다. 비대화형 명령(ssh user@host ls -la)의 경우, PTY 요청은 건너뛰고 exec만 요청됩니다.

포트 포워딩: 터널로서의 SSH (Port Forwarding: SSH as a Tunnel)

SSH의 채널 메커니즘은 강력한 터널링 기능을 가능하게 합니다.

로컬 포트 포워딩 (Local Port Forwarding)

ssh -L 5432:db.internal:5432 user@jumphost

SSH 클라이언트가 로컬 포트 5432에서 대기하도록 지시합니다. 해당 포트로의 모든 연결은 SSH 채널 (SSH channel) 내에 래핑되어, 점프 호스트 (jump host)에서 바라보는 db.internal:5432로 전달됩니다. 사용자의 머신과 점프 호스트 사이의 트래픽은 암호화되지만, 점프 호스트가 데이터베이스에 연결할 때는 평문 (plaintext)으로 연결되거나 자체적인 암호화 연결을 사용합니다.

원격 포트 포워딩 (Remote Port Forwarding)

ssh -R 8080:localhost:3000 user@publicserver

SSH 서버가 publicserver:8080에서 대기합니다. 해당 포트로의 연결은 SSH를 통해 터널링되어 사용자의 머신에 있는 localhost:3000으로 되돌아옵니다. 이는 개발자들이 공인 IP (public IP) 없이 로컬 개발 서버를 인터넷에 노출할 때 사용하는 방식입니다.

동적 포트 포워딩 (Dynamic Port Forwarding, SOCKS Proxy)

ssh -D 1080 user@host

로컬 포트 1080에 SOCKS5 프록시 (SOCKS5 proxy)를 생성합니다. 이 프록시를 사용하도록 설정된 애플리케이션은 트래픽을 SSH 터널을 통해 전송하며, 서버가 해당 애플리케이션을 대신하여 외부 연결을 수행합니다. 이를 통해 SSH 서버를 임시 VPN 출구 노드 (VPN exit node)로 전환할 수 있습니다.

패킷 구조: 실제로 전송되는 데이터는 무엇인가

전송 계층 핸드셰이크 (transport layer handshake) 이후의 모든 SSH 패킷은 다음과 같은 이진 구조 (binary structure)를 따릅니다:

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0