SSH란 무엇인가: 원격 접속과 공개 키 인증의 원리

SSH란 무엇인가: 원격 접속과 공개 키 인증의 원리

클라우드에 서버를 하나 만들고 나면 대개 이런 명령어부터 입력하게 됩니다.

$ ssh deploy@server.example.com

명령어 한 줄로 멀리 있는 서버의 터미널이 열리니 무척 간단해 보이는데요. 처음 접속할 때 나타나는 지문이 무엇인지, 비밀번호 없이 어떻게 로그인이 되는지, ~/.ssh에는 왜 여러 파일이 생기는지 파고들기 시작하면 이야기가 금세 복잡해집니다.

SSH를 단순히 원격 접속 명령어로만 외우면 연결 오류가 났을 때 어디부터 확인해야 할지 막막합니다. 반대로 서버 확인과 사용자 인증을 구분해서 이해하면 키를 안전하게 관리하는 방법도 자연스럽게 보이죠. 이번 글에서는 SSH 연결이 만들어지는 순서를 살펴본 뒤 공개 키 인증과 자주 쓰는 설정을 직접 구성해보겠습니다.

SSH란?

보안 셸(Secure Shell, SSH)은 신뢰하기 어려운 네트워크를 통해 다른 컴퓨터에 안전하게 접속하기 위한 프로토콜입니다. 현재 널리 쓰이는 SSH-2의 전체 구조는 RFC 4251에 정의되어 있습니다.

SSH라고 하면 원격 셸부터 떠올리지만 실제 역할은 더 넓습니다. 원격 명령을 실행하거나 scpsftp로 파일을 전송할 수 있습니다. 포트 포워딩(port forwarding)으로 다른 서비스의 트래픽을 터널링할 수도 있죠. Git이 GitHub와 통신할 때도 HTTPS 대신 SSH를 사용할 수 있습니다.

SSH 연결에는 접속을 시작하는 클라이언트(client)와 연결을 받아들이는 서버(server)가 참여합니다. OpenSSH에서는 우리가 터미널에서 실행하는 ssh가 클라이언트이고 서버에서 대기하는 sshd가 서버입니다. 서버는 기본적으로 TCP 22번 포트에서 연결을 기다리지만 운영 환경에서는 다른 포트를 사용할 수도 있습니다.

# 기본 포트로 접속
$ ssh deploy@server.example.com

# 2222번 포트로 접속
$ ssh -p 2222 deploy@server.example.com

# 접속해서 명령 하나만 실행
$ ssh deploy@server.example.com "uname -a"

첫 번째 명령에서 deploy는 원격 서버의 사용자 이름이고 server.example.com은 접속할 호스트입니다. 로컬 컴퓨터의 사용자 이름과 원격 서버의 사용자 이름은 서로 달라도 됩니다.

연결할 때 무슨 일이 일어날까?

SSH 연결에서는 서버와 사용자를 따로 확인합니다. 먼저 클라이언트가 접속한 서버가 진짜 목표 서버인지 확인합니다. 그다음 서버가 접속을 요청한 사용자를 확인하죠.

flowchart TB
    accTitle: SSH 연결과 인증 흐름
    accDescr: 클라이언트가 서버에 TCP로 연결한 뒤 호스트 키를 확인하고 키 교환으로 암호화 통로를 만든다. 이어서 서버가 사용자를 인증하고 성공하면 원격 세션을 연다.

    client["SSH 클라이언트"] --> tcp["TCP 연결"]
    tcp --> host["서버 호스트 키 확인"]
    host --> trusted{"신뢰할 서버인가?"}
    trusted -- "아니요" --> stop(["연결 중단"])
    trusted -- "예" --> tunnel["키 교환과 암호화"]
    tunnel --> auth["사용자 인증"]
    auth --> accepted{"인증에 성공했는가?"}
    accepted -- "아니요" --> stop
    accepted -- "예" --> session(["원격 세션"])

우선 클라이언트와 서버가 지원하는 프로토콜 버전과 암호 알고리즘을 협상합니다. 그다음 키 교환(key exchange)을 거쳐 클라이언트와 서버만 아는 임시 비밀값(shared secret)을 각자 계산하고, 이 값에서 이번 연결에 사용할 대칭키를 파생합니다. 실제 세션(session) 데이터는 빠른 대칭키 암호화로 보호됩니다.

하지만 키 교환만으로는 상대가 진짜 서버라는 사실을 알 수 없습니다. 중간자 공격(man-in-the-middle attack)을 시도하는 쪽이 클라이언트와 서버 사이에 끼어 양쪽과 각각 다른 비밀값을 만들 수 있기 때문인데요. 그래서 서버는 장기간 보관하는 호스트 키(host key)로 키 교환 결과에 서명합니다. 클라이언트는 이 서명을 검증하고요.

서버의 신원을 확인하고 암호화된 통로를 만든 다음에야 사용자 인증이 시작됩니다. 비밀번호나 공개 키로 사용자를 확인하고 인증에 성공하면 셸, 명령 실행, 파일 전송 같은 용도별 채널을 엽니다. 비밀번호도 암호화된 통로 안에서 전송되므로 네트워크에 평문으로 노출되지는 않습니다.

known_hosts는 서버의 신분증 보관함

처음 보는 서버에 접속하면 SSH 클라이언트는 서버의 호스트 키 지문(fingerprint)을 신뢰할지 묻습니다. 승인한 호스트 키는 기본적으로 ~/.ssh/known_hosts에 저장됩니다. 다음 접속부터는 서버가 제시한 키를 저장된 키와 비교하기 때문에, 같은 주소를 가로챈 공격자가 다른 키를 내밀면 연결을 중단할 수 있습니다.

첫 접속에서 아무 생각 없이 yes를 입력하면 이 보호 장치의 절반만 사용하는 셈입니다. 클라우드 콘솔이나 서버 관리자가 알려준 지문처럼 이미 신뢰하는 경로를 통해 실제 지문과 일치하는지 확인하는 게 가장 안전합니다. 첫 만남을 믿고 이후의 변화를 감시하는 방식을 흔히 최초 사용 시 신뢰(Trust On First Use, TOFU)라고 부릅니다.

호스트 키가 달라졌다는 경고가 나오면 known_hosts를 곧바로 지우지 마세요. 서버를 재설치했거나 호스트 키를 정상적으로 교체했을 수도 있지만 중간자 공격일 가능성도 있습니다. 먼저 관리자가 제공한 새 지문을 별도 경로로 확인한 뒤 기존 항목을 제거해야 합니다.

# 서버의 새 지문을 확인한 뒤 실행
$ ssh-keygen -R server.example.com

자동화 환경에서는 StrictHostKeyChecking accept-new를 사용하면 처음 보는 키는 저장하고 기존 서버의 키가 바뀐 경우에는 연결을 거부할 수 있습니다. 검증을 완전히 끄는 StrictHostKeyChecking noUserKnownHostsFile /dev/null 조합은 중간자 공격을 알아챌 방법까지 없애므로 피하는 편이 좋습니다.

공개 키 인증은 어떻게 동작할까?

비밀번호 인증은 서버가 비밀번호를 검증합니다. 비밀번호가 약하면 추측 공격에 노출됩니다. 여러 서버에서 재사용했다면 한 곳의 유출이 다른 서버로 번질 수 있죠.

공개 키 인증(public-key authentication)은 공개 키와 개인 키로 이루어진 비대칭키 쌍(key pair)을 사용합니다. 개인 키(private key)는 클라이언트에만 보관하고 공개 키(public key)는 서버의 ~/.ssh/authorized_keys에 등록합니다. 접속할 때 클라이언트는 현재 SSH 세션에 묶인 인증 데이터에 개인 키로 서명합니다. 서버는 등록된 공개 키로 서명을 검증하죠. 개인 키 자체는 네트워크로 전송되지 않습니다.

여기서 호스트 키와 사용자 키를 헷갈리기 쉬운데요. 두 용어 모두 키 하나가 아니라 공개 키와 개인 키로 구성된 키 쌍 전체를 가리킵니다. 호스트 키 쌍은 서버의 신원을 증명하고 사용자 키 쌍은 접속을 요청한 사용자의 신원을 증명합니다. 둘 다 디지털 서명을 사용하지만 개인 키를 보관하고 공개 키를 확인하는 쪽은 서로 반대입니다.

구분호스트 키 쌍사용자 키 쌍
확인 대상접속한 서버접속을 요청한 사용자
개인 키서버가 보관클라이언트가 보관
공개 키클라이언트가 known_hosts에 저장서버가 authorized_keys에 등록
목적중간자 공격 방지비밀번호 없이 안전하게 사용자 인증

호스트 키 검증에 실패하면 진짜 서버인지 확신할 수 없습니다. 사용자 키 검증에 실패하면 서버에 로그인할 수 없고요. 두 단계가 모두 성공해야 안전한 원격 세션이 열립니다.

Ed25519 키 만들기

새 사용자 키는 ssh-keygen으로 만듭니다. 특별한 호환성 제약이 없다면 사용자 인증에 Ed25519를 기본으로 선택할 수 있습니다. Ed25519는 키와 서명이 작은 디지털 서명 알고리즘이며 데이터를 암호화하는 알고리즘은 아닙니다.

$ ssh-keygen -t ed25519 -C "dale@laptop"

-t ed25519는 키 유형을 지정하고 -C는 키를 구분하기 위한 설명을 붙입니다. 명령을 실행하면 저장 경로와 패스프레이즈(passphrase)를 묻습니다. 기본 경로를 선택하면 다음 두 파일이 만들어집니다.

  • ~/.ssh/id_ed25519: 외부에 공개하면 안 되는 개인 키
  • ~/.ssh/id_ed25519.pub: 서버에 등록할 수 있는 공개 키

패스프레이즈는 개인 키 파일을 한 번 더 암호화합니다. 노트북이나 백업에서 개인 키 파일이 유출되더라도 바로 사용하지 못하게 막아주므로 설정하는 편이 안전합니다. 서버 로그인 비밀번호와는 전혀 다른 값이며 서버로 전송되지도 않습니다.

서버에 비밀번호로 접속할 수 있다면 ssh-copy-id로 공개 키를 등록할 수 있습니다.

$ ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@server.example.com

ssh-copy-id를 사용할 수 없는 환경에서는 공개 키 파일의 내용을 복사해 원격 사용자의 ~/.ssh/authorized_keys에 한 줄로 추가하면 됩니다. GitHub처럼 일반 셸 계정을 제공하지 않는 서비스에는 웹 설정 화면을 통해 공개 키를 등록합니다. 구체적인 과정은 GitHub 최초 사용자를 위한 Git 설정 가이드에서 확인할 수 있습니다.

파일 권한도 빼놓을 수 없습니다. OpenSSH 서버의 StrictModes가 기본값으로 켜져 있으면 authorized_keys나 상위 디렉터리를 다른 사용자가 수정할 수 있을 때 인증을 거부합니다. 일반적으로 다음 권한을 사용하면 됩니다.

# 클라이언트
$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/id_ed25519

# 서버
$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/authorized_keys

개인 키 파일을 메신저로 보내거나 여러 사람이 공유해서는 안 됩니다. 같은 공개 키를 여러 서버에 등록할 수도 있습니다. 다만 유출됐을 때의 피해 범위와 폐기 절차를 단순하게 만들려면 기기별이나 용도별로 키를 나누는 편이 좋습니다.

ssh-agent로 패스프레이즈 입력 줄이기

패스프레이즈를 설정하면 개인 키를 사용할 때마다 잠금을 풀어야 합니다. 보안을 위해 설정한 값이지만 접속할 때마다 입력하면 번거롭죠.

SSH 에이전트(SSH agent)는 잠금이 풀린 키를 세션 동안 관리하고 클라이언트의 서명 요청을 대신 처리합니다. 키를 에이전트에 한 번 추가하면 이후 접속에서는 패스프레이즈를 반복해서 입력하지 않아도 됩니다.

# 에이전트가 없는 셸 환경에서 시작
$ eval "$(ssh-agent -s)"

# 개인 키 추가
$ ssh-add ~/.ssh/id_ed25519

# 현재 등록된 키 확인
$ ssh-add -l

macOS나 여러 데스크톱 환경은 로그인할 때 에이전트를 자동으로 실행하므로 첫 번째 명령이 필요하지 않을 수 있습니다. 1Password CLI의 SSH 키 관리처럼 별도의 키 관리 도구가 에이전트 역할을 맡도록 구성할 수도 있습니다.

에이전트 포워딩(agent forwarding)을 켜면 원격 서버에서도 로컬 에이전트를 거쳐 다른 서버에 접속할 수 있습니다. 편리하지만 침해된 원격 서버가 연결된 동안 에이전트에 서명을 요청할 수 있으므로, 신뢰할 수 있는 중간 서버에서 꼭 필요한 경우에만 사용하세요.

~/.ssh/config로 접속 정보 정리하기

서버가 늘어나면 긴 사용자 이름과 호스트, 포트, 키 경로를 매번 입력하기 어렵습니다. SSH 클라이언트 설정 파일인 ~/.ssh/config에 별칭을 만들면 접속 정보를 한곳에서 관리할 수 있습니다.

~/.ssh/config
Host prod
  HostName server.example.com
  User deploy
  Port 2222
  IdentityFile ~/.ssh/id_ed25519_prod
  IdentitiesOnly yes

이제 아래처럼 짧게 접속할 수 있습니다.

$ ssh prod

Host는 명령에서 사용할 별칭이고 HostName은 실제 도메인이나 IP 주소입니다. IdentityFile은 사용할 개인 키를 지정합니다. IdentitiesOnly yes를 함께 쓰면 에이전트가 가진 여러 키를 무작정 제시하지 않고 설정한 키만 사용하므로, 키가 많은 환경에서 Too many authentication failures 오류를 피하는 데 도움이 됩니다.

모든 호스트에 공통 설정을 적용할 때는 와일드카드(wildcard)를 사용할 수 있습니다. 구체적인 호스트 블록을 먼저 쓰고 공통 블록을 뒤에 두면 읽기도 편합니다.

~/.ssh/config
Host prod
  HostName server.example.com
  User deploy
  IdentityFile ~/.ssh/id_ed25519_prod
  IdentitiesOnly yes

Host *
  ServerAliveInterval 60

OpenSSH는 각 설정 항목에서 처음 얻은 값을 사용합니다. 그래서 구체적인 설정을 먼저 둬야 합니다. 실제로 어떤 값이 적용되는지는 ssh -G prod로 확인할 수 있습니다. 설정 파일에는 서버 주소와 사용자 이름 같은 정보가 들어가므로 chmod 600 ~/.ssh/config로 다른 사용자의 쓰기 권한을 막아두세요.

SSH는 원격 셸보다 더 많은 일을 한다

같은 암호화 연결에서 셸을 열지 않고 여러 채널을 사용할 수 있다는 점도 SSH의 강점입니다. 예를 들어 로컬 포트 포워딩을 사용하면 외부에 공개하지 않은 원격 데이터베이스에 안전하게 접근할 수 있습니다.

$ ssh -N -L 5433:db.internal:5432 bastion

이 명령은 로컬의 5433번 포트로 들어온 연결을 bastion 서버를 거쳐 db.internal의 5432번 포트로 전달합니다. -N은 원격 명령이나 셸을 실행하지 않겠다는 뜻입니다. 데이터베이스 포트를 인터넷에 직접 노출하지 않고도 관리 도구를 연결할 수 있죠.

서버가 공인 인터넷에서 접근할 수 없는 곳에 있다면 먼저 안전한 네트워크 경로가 필요합니다. Tailscale로 원격 기기에 접속하는 방법을 사용하면 포트 포워딩 없이 사설 네트워크 안에서 SSH 서버에 접근할 수 있습니다. SSH는 접속 이후의 암호화와 인증을 맡습니다. Tailscale은 두 기기가 서로 도달할 수 있는 네트워크를 제공한다고 구분하면 이해하기 쉽습니다.

서버 설정을 강화할 때 주의할 점

공개 키 로그인이 확인되면 서버의 비밀번호 인증을 끄는 방안을 고려할 수 있습니다. 다만 설정을 먼저 바꾸고 현재 연결까지 닫아버리면 서버에 다시 들어가지 못할 수 있는데요. 기존 세션을 유지한 채 두 번째 터미널에서 공개 키 로그인이 성공하는지 반드시 확인하세요.

OpenSSH 서버는 보통 /etc/ssh/sshd_config에서 설정합니다.

/etc/ssh/sshd_config
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin no

설정을 적용하기 전에 sudo sshd -t로 문법을 검사하세요. 그다음 운영 체제의 서비스 관리 방식에 맞춰 sshd를 다시 불러와야 합니다. 배포판의 설정 포함 순서에 따라 /etc/ssh/sshd_config.d/ 안의 값이 먼저 적용될 수 있으므로 실제 적용값도 확인하세요.

SSH 포트를 22번에서 다른 번호로 바꾸면 무작위 로그인 시도와 로그 소음은 줄어들 수 있습니다. 하지만 인증 강도를 높이는 조치는 아닙니다. 강한 키와 패스프레이즈를 쓰고 계정 권한을 꼭 필요한 만큼만 부여하세요. 방화벽이나 사설 네트워크로 접근 범위를 좁히는 편이 더 실질적인 방어가 됩니다.

연결이 안 될 때 확인할 순서

SSH 문제는 어느 단계에서 실패했는지 찾으면 훨씬 쉽게 해결할 수 있습니다. -v 옵션을 붙이면 호스트 이름 해석부터 키 교환과 인증에 사용한 키까지 차례로 확인할 수 있습니다. 로그 수준은 최대 -vvv까지 높일 수 있고요.

$ ssh -v prod

Connection refused라면 주소와 포트까지 도달했지만 sshd가 듣고 있지 않거나 방화벽이 연결을 거부했을 가능성이 큽니다. 연결 시간이 초과된다면 주소, 라우팅, 보안 그룹, 방화벽부터 확인하세요.

Permission denied (publickey)라면 네트워크 연결과 서버 확인은 끝났지만 사용자 인증에 실패한 상태입니다. 원격 사용자 이름이 맞는지, 올바른 개인 키를 제시했는지, 짝이 되는 공개 키가 authorized_keys에 있는지, 파일 소유자와 권한이 올바른지 순서대로 확인합니다.

Too many authentication failures는 에이전트에 등록된 키를 너무 많이 제시했을 때 자주 발생합니다. 다음처럼 사용할 키를 직접 지정하거나 ~/.ssh/configIdentitiesOnly yes를 추가해보세요.

$ ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519_prod deploy@server.example.com

호스트 키 변경 경고는 인증 오류가 아니라 서버 신원 확인 실패입니다. 앞서 살펴본 것처럼 서버의 새 지문을 먼저 확인해야 합니다. 오류 메시지를 없애려고 검증 기능부터 끄면 정작 필요한 보안 경고를 놓칠 수 있습니다.

안전한 SSH 사용을 위한 원칙

SSH 설정은 복잡해 보여도 몇 가지 원칙으로 정리할 수 있습니다.

  • 특별한 호환성 제약이 없다면 새 키는 Ed25519로 생성합니다.
  • 개인 키에는 패스프레이즈를 설정하고 에이전트로 입력 부담을 줄입니다.
  • 개인 키를 공유하지 않고 기기별이나 용도별로 나눕니다.
  • 담당자가 떠났거나 기기를 잃어버린 경우, 키가 유출된 경우에는 서버의 authorized_keys에서 즉시 공개 키를 제거합니다.
  • 첫 접속과 호스트 키 변경 시 지문을 신뢰할 수 있는 경로로 확인합니다.
  • StrictHostKeyChecking을 무작정 끄지 않습니다.
  • 에이전트 포워딩은 꼭 필요한 신뢰 서버에만 허용합니다.
  • 공개 키 로그인을 검증한 뒤 비밀번호와 루트 로그인을 제한합니다.

공개 키는 이름 그대로 공개해도 됩니다. 반면 개인 키는 그 키를 소유한 사용자임을 증명하는 수단입니다. 키 파일을 안전하게 보관하는 일과 서버에서 더 이상 필요 없는 공개 키를 제거하는 일은 같은 인증 수명 주기의 양쪽 끝이라고 볼 수 있습니다.

마치며

SSH 연결은 단순히 비밀번호를 암호화해 보내는 과정이 아닙니다. 클라이언트는 호스트 키로 서버의 신원을 확인합니다. 키 교환으로 암호화된 통로를 만든 다음 그 안에서 공개 키나 비밀번호로 사용자를 인증하죠. known_hostsauthorized_keys가 서로 다른 이유도 이 흐름을 알고 나면 분명해집니다.

처음에는 ssh user@host 한 줄로 시작하되, 서버가 늘어나면 키를 용도별로 분리하고 ~/.ssh/config로 접속 정보를 정리해보세요. 문제가 생겼을 때는 ssh -v로 연결, 호스트 확인, 사용자 인증 가운데 어느 단계에서 멈췄는지 찾는 습관이 큰 도움이 됩니다.

각 명령과 설정 항목의 정확한 의미는 OpenBSD의 ssh 매뉴얼ssh_config 매뉴얼을 참고하세요.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord