엔티티 인증 없는 키 교환 (Insecure Host Key Verification)

Key Exchange without Entity Authentication

설명

이 취약점은 SSH 클라이언트가 서버의 host key(서버 공개키)를 검증하지 않고 그냥 신뢰할 때 발생합니다. Go에서 ssh.InsecureIgnoreHostKey() 를 사용하면, 클라이언트는 접속하는 서버가 진짜 서버인지 확인하지 않게 됩니다. 공격자는 네트워크 중간에서 위조된 SSH 서버를 띄우고 사용자의 트래픽을 가로채는 Man-in-the-Middle(MitM) 공격을 할 수 있습니다. 사용자는 정상 서버에 접속한다고 생각하지만, 실제로는 공격자 서버에 접속하게 되고, 비밀번호 인증에 사용한 값과 그 연결로 전송되는 중요 데이터가 노출될 수 있습니다. 공개키 인증만으로 클라이언트의 개인키 자체가 서버에 전송되는 것은 아닙니다.

잠재적 영향

  • 중간자 공격(MitM, Man-in-the-Middle): 서버의 진위를 확인하지 않아 공격자가 가짜 SSH 서버로 트래픽을 가로채서 세션 내용을 엿보거나 변조할 수 있음
  • 자격 증명 탈취: SSH 비밀번호, 토큰, 명령 실행 결과 등의 민감 정보가 공격자에게 노출될 수 있음
  • 명령·응답 노출 및 조작: 악성 연결 상대가 클라이언트가 보낸 명령을 수신하고 반환하는 응답을 위조할 수 있음

해결 방법

  • 운영 코드에서 ssh.InsecureIgnoreHostKey()를 사용하지 마세요.
  • golang.org/x/crypto/ssh/knownhosts의 knownhosts.New로 신뢰한 OpenSSH known_hosts 파일을 읽고 HostKeyCallback에 설정하세요.
  • 제한된 환경에서는 ssh.FixedHostKey()로 미리 신뢰한 서버 공개키를 고정하세요.
  • known_hosts나 고정 키를 안전하게 배포·관리하고, 서버 키 변경 시 신뢰한 절차로 새 키를 확인하세요.
  • 테스트에서도 검증을 생략하지 말고 테스트용 known_hosts나 고정 키를 사용하세요.

예시

호스트 키 검증만 비교합니다. 예시 비밀번호를 운영에 사용하지 말고 보호된 설정에서 값을 공급하세요. known_hosts는 신뢰한 경로로 배포하고, 홈 디렉터리 조회 오류도 실제 코드에서 처리해야 합니다.

변경 전

go
package main

import (
    "log"

    "golang.org/x/crypto/ssh"
)

func newSSHClientInsecure(addr, user, password string) (*ssh.Client, error) {
    config := &ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{
            ssh.Password(password),
        },
        // ❌ 서버 host key를 전혀 검증하지 않음
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    client, err := ssh.Dial("tcp", addr, config)
    if err != nil {
        return nil, err
    }
    return client, nil
}

func main() {
    client, err := newSSHClientInsecure("example.com:22", "deploy", "secret-password")
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // ... 중요 작업 수행 ...
}

변경 후

go
package main

import (
    "log"
    "os"

    "golang.org/x/crypto/ssh"
    "golang.org/x/crypto/ssh/knownhosts"
)

func newSSHClientSecure(addr, user, password, knownHostsPath string) (*ssh.Client, error) {
    // ✅ known_hosts 파일을 사용한 host key 검증 설정
    hostKeyCallback, err := knownhosts.New(knownHostsPath)
    if err != nil {
        return nil, err
    }

    config := &ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{
            ssh.Password(password),
        },
        HostKeyCallback: hostKeyCallback,
    }

    client, err := ssh.Dial("tcp", addr, config)
    if err != nil {
        return nil, err
    }
    return client, nil
}

func main() {
    home, _ := os.UserHomeDir()
    knownHosts := home + "/.ssh/known_hosts"

    client, err := newSSHClientSecure("example.com:22", "deploy", "secret-password", knownHosts)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // ... 안전하게 SSH 작업 수행 ...
}

설명:

  • 변경 전: HostKeyCallback: ssh.InsecureIgnoreHostKey() 설정은 SSH 서버의 host key를 전혀 검증하지 않습니다. 따라서 공격자가 네트워크 중간에 가짜 SSH 서버를 띄워도 클라이언트는 이를 정상 서버로 믿고 접속합니다. 이 과정에서 비밀번호, 명령, 결과 값 등 민감 정보가 모두 공격자에게 노출될 수 있고, 세션이 완전히 장악될 수 있습니다.
  • 변경 후: knownhosts.New 를 이용해 OpenSSH 형식의 known_hosts 파일을 읽고, HostKeyCallback 으로 설정함으로써 서버 host key가 신뢰된 값인지 자동 검증합니다. 서버 키가 다를 경우 SSH 연결이 실패하므로, 중간자 공격이나 가짜 서버에 대한 접속이 차단됩니다. 이렇게 서버의 엔티티(정체)를 인증함으로써 키 교환과 통신 채널의 기밀성과 무결성을 보호할 수 있습니다.

참조