Zzangss' 기술 블로그

태그

SSH는 PEM 키로 사용자를 어떻게 인증할까?

AWS EC2 인스턴스에 접속할 때 다음과 같은 명령어를 사용한다.

ssh -i my-key.pem ubuntu@server-ip

비밀번호를 입력하지 않았는데도 서버는 사용자를 인증하고 접속을 허용한다. 이 과정을 처음 접하면 PEM 파일 안에 비밀번호가 들어 있거나, 서버가 PEM 파일을 직접 전달받아 검사한다고 생각하기 쉽다.

그러나 실제로 개인키 파일은 서버에 전송되지 않는다. SSH 클라이언트는 개인키로 전자서명을 생성하고, 서버는 미리 가지고 있던 공개키로 서명을 검증한다.

이번 글에서는 PEM 파일이 무엇인지, 공개키와 개인키가 어디에 저장되는지, 그리고 SSH가 개인키의 소유자를 어떻게 확인하는지 정리해보고자 한다.

1. PEM 키란?

PEM(Privacy-Enhanced Mail)은 개인키, 공개키, 인증서와 같은 바이너리 데이터를 텍스트로 표현하기 위한 형식이다.

PEM 파일은 저장된 데이터의 종류를 나타내는 시작 문구, Base64로 인코딩된 데이터, 종료 문구로 구성된다.

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----

각 부분의 의미는 다음과 같다.

  • -----BEGIN RSA PRIVATE KEY-----: 파일 안에 RSA 전용 PKCS#1 형식의 개인키가 들어 있음을 나타낸다.
  • MIIEowIBAAKCAQEA...: 바이너리 개인키 데이터를 Base64로 인코딩한 내용이다.
  • -----END RSA PRIVATE KEY-----: 개인키 데이터가 끝났음을 나타낸다.

따라서 RSA PRIVATE KEY라는 문구는 인증 방식을 뜻하는 것이 아니라, 파일 내부에 저장된 데이터의 종류와 구조를 나타낸다. PEM은 키 데이터를 텍스트로 표현하는 바깥 형식이고, PKCS#1은 그 안에 RSA 개인키를 저장하는 구조이다.

SSH에서 실제로 사용되는 인증 방식은 공개키 인증(Public Key Authentication)이다. 흔히 “PEM 키로 인증한다”고 표현하지만, 정확히는 “PEM 형식으로 저장된 개인키를 이용해 SSH 공개키 인증을 수행한다”는 의미이다.

한편 최신 OpenSSH가 기본적으로 생성하는 개인키는 PEM과 비슷한 텍스트 형태를 사용하지만, 내부 데이터는 OpenSSH 자체 형식으로 저장된다.

-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

PEM 형식의 개인키가 필요하다면 ssh-keygen의 -m PEM 옵션을 이용해 생성하거나 변환할 수 있다. 결국 .pem이라는 파일 확장자만 보는 것보다 첫 줄의 시작 문구를 확인하는 편이 정확하다.

2. 공개키와 개인키의 역할

SSH 공개키 인증에서는 서로 대응하는 두 개의 키를 사용한다.

구분 저장 위치 역할
개인키(Private Key) 클라이언트 사용자가 해당 키의 소유자임을 증명하는 서명을 생성한다.
공개키(Public Key) 서버 클라이언트가 생성한 서명이 올바른지 검증한다.

개인키는 접속을 시도하는 사용자의 컴퓨터에 보관한다. AWS에서 내려받은 .pem 파일이 여기에 해당한다.

공개키는 접속 대상 서버의 사용자 홈 디렉터리에 있는 다음 파일에 등록된다.

~/.ssh/authorized_keys

AWS EC2에서는 인스턴스를 처음 생성할 때 선택한 키 페어의 공개키를 해당 인스턴스의 authorized_keys에 등록한다. AWS는 공개키를 인스턴스에 저장하고, 사용자는 대응하는 개인키를 보관하는 구조이다.

즉, 서버가 개인키를 가지고 있는 것이 아니다. 서버에는 개인키와 대응하는 공개키만 존재한다.

3. SSH 연결은 사용자 인증부터 시작하지 않는다

SSH 연결 과정은 크게 다음과 같이 구분할 수 있다.

  1. 클라이언트와 서버가 SSH 연결을 시작한다.
  2. 서버의 신원을 확인하고 암호화된 통신 채널을 만든다.
  3. 클라이언트 사용자를 인증한다.
  4. 인증이 성공하면 셸이나 명령 실행을 허용한다.

SSH는 사용자 인증을 시작하기 전에 먼저 키 교환을 수행한다. 클라이언트와 서버는 키 교환 결과로부터 세션 암호화 키를 만들고, 이를 이용해 암호화된 통신 채널을 구성한다.

이후 클라이언트는 이 채널 안에서 사용자의 SSH 개인키로 전자서명을 생성하여 자신을 인증한다. 인증이 끝난 뒤 주고받는 명령어와 응답도 사용자의 개인키가 아니라 앞서 만든 세션 암호화 키로 보호된다.

즉, 사용자 개인키는 로그인 자격을 증명하고 세션 암호화 키는 실제 통신 내용을 숨기는 역할을 한다.

4. SSH 공개키 인증 과정

SSH 공개키 인증의 전체 흐름은 다음과 같다.

4.1. 클라이언트가 사용할 공개키를 제시한다

사용자가 다음 명령어를 실행했다고 가정해보자.

ssh -i my-key.pem ubuntu@server-ip

SSH 클라이언트는 my-key.pem에서 개인키를 읽고, 이에 대응하는 공개키 정보를 구한다. 이후 서버에 접속하려는 사용자명과 사용할 공개키를 알린다.

쉽게 표현하면 다음과 같은 질문을 보내는 것이다.

ubuntu 사용자로 접속하려고 한다.
이 공개키를 이용한 인증을 허용하는가?

4.2. 서버가 공개키 등록 여부를 확인한다

서버는 ubuntu 사용자의 ~/.ssh/authorized_keys를 확인한다.

클라이언트가 제시한 공개키가 등록되어 있으면 해당 키를 이용한 인증을 계속할 수 있다. 등록되어 있지 않다면 인증을 거부한다.

하지만 공개키가 등록되어 있다는 사실만으로는 인증이 끝나지 않는다. 공개키는 외부에 노출되어도 되는 정보이므로 누구나 다른 사람의 공개키를 가져와 서버에 제시할 수 있기 때문이다.

따라서 클라이언트는 공개키에 대응하는 개인키를 실제로 가지고 있다는 사실을 증명해야 한다.

4.3. 클라이언트가 개인키로 서명한다

SSH 클라이언트는 현재 세션의 식별자와 사용자 인증 요청 데이터를 개인키로 서명한다.

서명 대상에는 다음 정보가 포함된다.

  • 현재 SSH 연결의 세션 식별자
  • 사용자명
  • 요청할 서비스명
  • publickey 인증 방식
  • 공개키 알고리즘
  • 인증에 사용할 공개키

전자서명은 개인키와 서명할 데이터를 입력해 만드는 검증용 데이터이다. 클라이언트는 위 항목을 SSH 프로토콜에 정의된 순서대로 하나의 바이트열로 구성한다. 이후 이 데이터와 개인키를 RSA나 Ed25519 같은 서명 알고리즘에 입력해 서명을 생성한다.

서명 = 서명 알고리즘(개인키, 인증 요청 데이터)

서명을 만드는 내부 계산은 RSA와 Ed25519 등 알고리즘마다 다르다. 하지만 생성된 서명이 개인키와 인증 요청 데이터에 종속된다는 점은 같다. 서버는 동일한 인증 요청 데이터와 공개키를 이용해 서명을 검증하며, 데이터가 변경됐거나 대응하는 개인키로 만든 서명이 아니라면 검증에 실패한다.

이 중 세션 식별자는 SSH 연결 초기에 수행한 키 교환 결과로부터 만들어진다. 각 SSH 연결마다 달라지므로, 이전 연결에서 사용한 서명을 그대로 복사하여 다시 사용하는 재전송 공격을 방지할 수 있다.

클라이언트는 생성한 서명만 서버에 전송한다. 개인키 자체는 서버로 보내지 않는다.

4.4. 서버가 공개키로 서명을 검증한다

서버는 authorized_keys에 등록된 공개키로 클라이언트의 서명을 검증한다.

검증 결과가 올바르다는 것은 다음 두 가지를 의미한다.

  1. 해당 서명은 공개키와 대응하는 개인키로 만들어졌다.
  2. 서명된 데이터가 전송 중에 변경되지 않았다.

개인키를 가지고 있지 않은 사용자는 올바른 서명을 생성할 수 없다. 따라서 서버는 개인키를 직접 전달받지 않고도 클라이언트가 개인키를 소유하고 있다는 사실을 확인할 수 있다.

전체 과정을 단계별로 정리하면 다음과 같다.

단계 클라이언트 전달 방향 서버
준비 개인키를 보관한다. 공개키를 authorized_keys에 등록해 둔다.
1 사용자명과 사용할 공개키를 보낸다. → 제시된 공개키를 인증에 사용할 수 있는지 확인한다.
2 ← 해당 공개키를 사용할 수 있음을 알린다.
3 개인키로 현재 세션의 인증 요청 데이터에 서명한다.
4 공개키와 전자서명을 보낸다. → 등록된 공개키로 서명을 검증한다.
5 인증 성공 응답을 받는다. ← 검증에 성공하면 사용자 인증을 승인한다.

실제 프로토콜에서는 클라이언트가 공개키 사용 가능 여부를 먼저 묻는 단계를 생략하고, 공개키와 서명을 한 번에 보낼 수도 있다.

5. 개인키로 암호화하고 공개키로 복호화하는 것인가?

공개키 인증을 설명할 때 다음과 같이 표현하는 경우가 있다.

클라이언트가 개인키로 데이터를 암호화하고 서버가 공개키로 복호화한다.

직관적인 설명일 수 있지만, SSH 사용자 인증을 정확하게 나타내는 표현은 아니다.

SSH 클라이언트가 수행하는 작업은 암호화가 아니라 전자서명이다. 클라이언트는 개인키로 서명을 생성하고, 서버는 공개키로 서명의 유효성을 검증한다.

또한 SSH 키 페어는 연결 초기의 사용자 인증에 사용된다. 인증 이후 셸 명령과 응답 데이터를 암호화할 때는 키 교환 과정에서 별도로 만든 세션 키를 사용한다.

구분 사용되는 키 목적
사용자 인증 사용자의 공개키·개인키 개인키 소유 여부 증명
SSH 통신 암호화 키 교환으로 합의한 세션 키 명령과 응답 데이터 보호
서버 인증 서버의 Host Key 접속 대상 서버의 신원 확인

이 세 가지를 구분해야 SSH의 전체 동작을 정확하게 이해할 수 있다.

6. chmod 400을 설정하는 이유

AWS에서 받은 PEM 파일로 접속할 때 다음 명령어를 먼저 실행하는 경우가 많다.

chmod 400 my-key.pem

400 권한은 파일 소유자만 읽을 수 있고, 다른 사용자는 읽거나 수정할 수 없다는 의미이다.

개인키를 다른 사용자가 읽을 수 있다면 그 사용자도 동일한 개인키로 전자서명을 생성하여 서버에 접속할 수 있다. OpenSSH는 이러한 위험을 막기 위해 개인키 파일의 권한이 지나치게 개방되어 있으면 해당 키의 사용을 거부한다.

WARNING: UNPROTECTED PRIVATE KEY FILE!
Permissions for 'my-key.pem' are too open.

chmod 400 외에 소유자에게 읽기와 쓰기 권한을 주는 chmod 600도 일반적으로 사용할 수 있다. 핵심은 그룹이나 다른 사용자가 개인키를 읽을 수 없도록 제한하는 것이다.

7. 개인키의 Passphrase는 서버 비밀번호가 아니다

개인키를 만들 때 Passphrase를 설정할 수도 있다.

ssh-keygen
Enter passphrase:

Passphrase는 SSH 서버에서 사용자를 인증하기 위한 비밀번호가 아니다. 디스크에 저장된 개인키 파일을 암호화하여 보호하는 로컬 비밀번호이다.

개인키 파일이 유출되더라도 Passphrase를 모르면 바로 사용할 수 없도록 만드는 역할을 한다. 클라이언트는 접속 전에 Passphrase로 개인키 파일을 복호화한 뒤, 복호화된 개인키로 전자서명을 생성한다.

ssh-agent를 사용하면 복호화한 개인키를 메모리에 보관하여 접속할 때마다 Passphrase를 다시 입력하는 과정을 줄일 수 있다.

8. 정리

PEM은 SSH의 인증 방식이 아니라 개인키를 저장할 수 있는 파일 형식이다. 실제 인증은 공개키 암호화 기술을 이용한 전자서명 검증으로 이루어진다.

서버에는 공개키가 저장되고 클라이언트에는 개인키가 저장된다. 클라이언트는 현재 SSH 세션과 사용자 정보가 포함된 데이터를 개인키로 서명한다. 서버는 등록된 공개키로 서명을 검증하고, 검증에 성공하면 클라이언트가 대응하는 개인키를 가지고 있다고 판단한다.

이 과정에서 개인키는 서버에 전달되지 않는다. 또한 개인키는 사용자를 인증하는 데 사용될 뿐, 인증 이후의 SSH 통신 내용은 별도로 합의한 세션 키로 암호화된다.

결국 다음 명령어에서 -i 옵션은 PEM 파일을 서버로 보내는 옵션이 아니다.

ssh -i my-key.pem ubuntu@server-ip

SSH 클라이언트가 전자서명을 생성할 때 사용할 개인키 파일을 지정하는 옵션이다. 평소에는 단순히 접속 명령어로만 사용했지만, 내부에서는 공개키 등록 여부 확인과 전자서명 검증을 통해 비밀번호 없이 사용자의 신원을 증명하고 있었던 것이다.