클라이언트 인증서
고친 사람 github-actions[bot]
클라이언트 인증서는 서버에 접속하는 쪽이 자기가 누구인지 서버에게 증명해 줍니다. 보통의 암호화된 연결에서는 서버만 인증서를 내밉니다. 클라이언트 인증서를 쓰면 서버도 접속한 쪽의 인증서를 확인합니다. 비밀번호나 토큰을 따로 보내지 않아도 연결을 맺는 순간 상대가 누구인지 정해집니다.
쉽고 빠른 이해
클라이언트 인증서는 접속하는 프로그램이 서버에 내미는 신분증입니다. 사내 주문 서비스가 결제 서비스를 부를 때 인증서를 내밀어 「나는 주문 서비스다」를 보이는 것이 그런 쓰임입니다.
이게 없으면 서버는 연결만 보고 상대를 가릴 수 없습니다. 비밀 문자열을 요청마다 실어 보내야 합니다. 그 문자열이 새면 누구든 주인 행세를 합니다.
- 서버가 연결을 맺는 도중에 「인증서를 보여 달라」고 요청합니다
- 접속하는 쪽이 자기 인증서를 보냅니다. 주인만 가진 비밀 키로 만든 서명도 함께 보냅니다
- 서버가 인증서를 내준 곳과 서명을 확인합니다. 그다음 인증서에 적힌 이름으로 상대를 알아봅니다
대가는 발급과 관리입니다. 접속하는 장비마다 인증서를 나눠 줘야 합니다. 기간이 끝나기 전에 새것으로 바꾸고, 새어 나간 것은 무효로 돌려야 합니다. 사용자가 브라우저로 들어오는 서비스보다는 서비스끼리, 장비와 서버 사이에서 주로 씁니다.
상세
회사 건물에 손님으로 갈 때는 우리가 간판을 보고 맞는 건물인지 확인합니다. 건물은 손님이 누구인지 묻지 않습니다. 직원 출입문은 반대입니다. 직원이 회사가 내준 사원증을 문에 대야 문이 열립니다.
클라이언트 인증서는 연결을 받는 서버가 연결을 거는 쪽을 확인할 때 쓰는 인증서입니다. 접속하는 쪽이 사람이든 프로그램이든 장비이든, 그 주인이 누구인지를 서버에게 보이는 데 씁니다.
인증서 안에는 공개키가 들어 있습니다. 공개키는 누구에게 보여 줘도 되는 키입니다. 주인만 가진 짝 키로 만든 서명이 맞는지를 이 키로 검사합니다. 인증서는 이 공개키가 누구의 것인지를 발급자가 서명으로 보증해 주는 파일입니다.
이 인증서가 오가는 무대는 대개 TLS(Transport Layer Security, 전송 계층 보안)입니다. TLS 는
주고받는 내용을 암호화하고 상대가 누구인지 확인해 주는 프로토콜입니다. https:// 로 시작하는
주소가 그 위에서 돕니다.
평소의 TLS 는 한쪽 확인입니다. 서버만 서버 인증서를 내밉니다. 클라이언트가 그것을 확인하면 끝납니다.
클라이언트 인증서를 더하면 확인이 양쪽으로 일어납니다. 서버가 클라이언트에게서도 인증서를 받아 확인합니다. 이렇게 양쪽이 서로 인증서를 확인하는 구성을 상호 TLS(mutual TLS, mTLS)라고 부릅니다.
서버 인증서와 같은 틀
클라이언트 인증서는 생김새로는 서버 인증서와 다르지 않습니다. 둘 다 X.509 라는 표준이 정한 칸을 채운 인증서입니다. 주체 이름, 공개키, 발급자, 유효 기간, 용도, 그리고 발급자의 서명이 들어갑니다.
갈리는 것은 칸에 적히는 내용입니다. 서버 인증서의 주체 칸에는 api.example.com 같은 도메인
이름이 들어갑니다. 클라이언트 인증서의 주체 칸에는 order-service 같은 서비스 이름, 장비 일련번호,
사용자 계정처럼 서버가 상대를 알아볼 이름이 들어갑니다.
용도 칸도 다릅니다. 인증서에는 이 키를 무엇에 쓰라고 냈는지 적는 칸이 있습니다. 클라이언트 인증서는 그 칸에 「클라이언트 인증」 용도를 적습니다. 서버는 이 용도가 없는 인증서를 거절할 수 있습니다. 서버 확인용으로 낸 인증서가 엉뚱하게 클라이언트 신분증으로 쓰이는 것을 막으려는 것입니다.
발급자도 대개 다릅니다. 인증 기관은 인증서에 서명해 주는 발급자입니다. 서버 인증서는 누구나 접속하므로 공개 인증 기관이 냅니다. 공개 인증 기관은 브라우저와 운영체제가 처음부터 믿기로 정해 둔 발급자입니다.
사설 인증 기관은 조직이 직접 세워 조직 안에서만 믿기로 한 발급자입니다. 클라이언트 인증서는 그 서버를 운영하는 쪽만 믿으면 됩니다. 그래서 사설 인증 기관이 내는 경우가 많습니다.
인증서를 보내는 것만으로는 모자란 까닭
인증서는 비밀이 아닙니다. 공개키와 이름과 서명만 들어 있어서 누구에게 보여 줘도 괜찮습니다. 한 번 보낸 인증서는 도중에 누군가 복사해 둘 수도 있습니다.
서버는 인증서만 받고 믿지 않습니다. 접속한 쪽이 공개키의 짝인 비밀 키를 가졌는지를 따로 확인합니다. 이 비밀 키를 개인키라고 부릅니다. 주인만 갖고 있습니다.
확인하는 방법은 서명입니다. TLS 연결을 맺을 때 두 쪽은 본론에 앞서 메시지를 몇 차례 주고받습니다. 여기서 암호 방식을 정하고 서로를 확인합니다. 이 첫 대화를 핸드셰이크라고 부릅니다.
접속한 쪽은 그때까지 두 쪽이 주고받은 핸드셰이크 메시지를 자기 개인키로 서명해 보냅니다. 서버는 인증서에 든 공개키로 그 서명을 검사합니다. 서명이 맞으면 상대가 개인키를 가졌다는 뜻입니다. 개인키를 가진 쪽이 곧 인증서의 주인입니다.
서명하는 내용은 이번 연결에서만 나온 메시지입니다. 지난 연결에서 엿본 서명을 다시 보내도 이번 연결의 메시지와는 안 맞습니다. 개인키 자체는 한 번도 연결 위로 나가지 않습니다.
핸드셰이크에서 오가는 순서
클라이언트 인증서는 핸드셰이크 안에서 오갑니다. 이 소절은 그 대화 가운데 클라이언트 인증서가 끼어드는 메시지만 따라갑니다.
핸드셰이크는 클라이언트의 연결 시작 인사로 열립니다. 클라이언트는 서버가 달라고 하기 전에는 인증서를 보내지 않습니다.
서버는 자기 인증서를 보낼 때 요청을 함께 보냅니다. 이 요청을 인증서 요청(CertificateRequest)이라고 부릅니다. 서버는 이 요청에 자기가 믿는 발급자 목록을 실어 보낼 수 있습니다. 클라이언트는 그 목록을 보고 갖고 있는 인증서 가운데 하나를 고릅니다.
클라이언트가 인증서와 함께 보내는 서명은 인증서 검증(CertificateVerify) 메시지에 실립니다. 앞 소절에서 본 「개인키를 가졌다는 증명」이 이 메시지입니다.
sequenceDiagram
participant 클라이언트
participant 서버
클라이언트->>서버: 연결 시작 인사
서버->>클라이언트: 서버 인증서 + 인증서 요청
Note over 클라이언트: 서버 인증서를 확인한다
Note over 클라이언트: 요청에 맞는 인증서를 고른다
클라이언트->>서버: 클라이언트 인증서
클라이언트->>서버: 인증서 검증 · 개인키로 한 서명
Note over 서버: 발급자 · 기간 · 서명을 확인한다
서버->>클라이언트: 핸드셰이크 완료
그림에서 보듯 클라이언트 인증서는 서버 인증서보다 뒤에 갑니다. 클라이언트는 서버를 먼저 확인한 뒤에야 자기 신분을 밝힙니다.
서버는 요청을 필수로 둘 수도 있고 선택으로 둘 수도 있습니다. 필수면 인증서가 없는 클라이언트와는 연결을 끊습니다. 선택이면 인증서 없이도 연결을 맺습니다. 그때는 인증서가 있는지 없는지를 애플리케이션이 보고 판단합니다.
서버가 확인하는 것
서버는 받은 클라이언트 인증서를 서버 인증서를 확인할 때와 같은 순서로 봅니다. 발급자의 서명이 맞는지, 오늘이 유효 기간 안인지, 용도 칸이 맞는지를 봅니다.
발급자를 믿는지는 서버가 가진 신뢰 저장소로 판단합니다. 신뢰 저장소는 「이 발급자가 서명한 인증서는 믿는다」고 미리 정해 둔 인증서 목록입니다. 클라이언트 인증서를 받는 서버는 이 목록에 보통 자기 조직의 사설 인증 기관 하나만 넣어 둡니다. 운영체제에 딸려 오는 공개 인증 기관 목록을 넣으면, 공개 인증 기관에서 인증서를 받은 아무나 들어옵니다.
기간이 남았는데 개인키가 샌 인증서는 서명이 멀쩡해서 위의 확인을 다 통과합니다. 이런 인증서를 막으려고 서버는 인증서 폐기 목록도 봅니다. 발급자가 「이 인증서는 이제 무효」라고 알린 목록입니다. 거기 오른 인증서는 기간과 무관하게 거절합니다.
인증은 여기까지, 권한은 따로
확인을 통과한 연결에는 「상대는 이 인증서의 주체다」라는 결론 하나가 남습니다. 서버는 주체 칸의 이름을 꺼내 애플리케이션의 사용자나 서비스 계정에 이어 붙입니다.
상대가 누구인지와 그 상대가 무엇을 해도 되는지는 다른 물음입니다. 앞의 것을 인증이라 하고 뒤의 것을 인가라고 합니다(인증과 인가). 클라이언트 인증서가 답하는 것은 인증뿐입니다.
서비스가 바깥에 열어 둔 호출 창구를 API(Application Programming Interface, 애플리케이션
프로그래밍 인터페이스)라고 부릅니다. order-service 라는 주체가 확인됐다고 해서 그 서비스가
모든 API 를 부를 수 있는 것은 아닙니다. 「이 주체는 주문 조회만 된다」 같은 규칙은 애플리케이션이
따로 갖고 있어야 합니다.
프록시 뒤에서 신원이 넘어오는 길
백엔드 애플리케이션이 TLS 를 직접 받지 않는 구성이 많습니다. 애플리케이션 앞에 서서 연결을 대신 받는 서버를 둡니다. 리버스 프록시나 로드 밸런서가 그런 서버입니다.
앞에 선 서버가 암호화된 연결을 받아 풉니다. 뒤의 애플리케이션에는 평문 요청을 넘깁니다. 이렇게 앞에서 암호를 푸는 것을 TLS 종료라고 부릅니다.
TLS 가 프록시에서 끝나면 클라이언트 인증서를 확인하는 쪽도 프록시입니다. 애플리케이션은 핸드셰이크를 못 봤으므로 상대가 누구인지 모릅니다. 그래서 프록시가 확인한 주체 이름을 요청 헤더에 실어 애플리케이션에 넘기는 구성이 흔합니다.
flowchart TD
C["클라이언트 · 인증서와 개인키를 가짐"] -->|"TLS · 클라이언트 인증서 확인"| P["리버스 프록시"]
P -->|"평문 요청 + 주체 이름 헤더"| A["애플리케이션"]
A --> R["주체 이름으로 권한을 판단"]
이 구성에서 애플리케이션은 그 헤더를 프록시만 붙인다고 믿고 있습니다. 바깥 요청이 같은 이름의 헤더를 스스로 달고 오면 프록시가 그 헤더를 지우고 자기 값으로 덮어써야 합니다. 애플리케이션에 프록시를 거치지 않는 길이 열려 있으면 누구든 헤더 한 줄로 다른 주체 행세를 합니다.
API 키 · 토큰과 견주기
클라이언트를 알아보는 방법은 클라이언트 인증서 말고도 있습니다. 흔히 쓰는 API 키와 베어러 토큰과 나란히 놓으면 무엇이 다른지 드러납니다. API 키는 서버가 발급한 비밀 문자열입니다. 베어러 토큰은 가진 쪽을 주인으로 쳐 주는 접근 토큰입니다.
| API 키 | 베어러 토큰 | 클라이언트 인증서 | |
|---|---|---|---|
| 연결 위로 나가는 것 | 비밀 문자열 자체 | 토큰 자체 | 인증서와 서명. 개인키는 안 나간다 |
| 확인하는 때 | 요청마다 | 요청마다 | 연결을 맺을 때 한 번 |
| 확인하는 쪽 | 애플리케이션 | 애플리케이션 | TLS 를 받는 쪽 |
| 도중에 엿보면 | 그대로 다시 쓸 수 있다 | 만료 전까지 다시 쓸 수 있다 | 개인키가 없어 다시 못 쓴다 |
표의 핵심은 첫 줄입니다. 앞의 둘은 비밀 자체를 보내서 증명합니다. 클라이언트 인증서는 비밀을 보내지 않고 비밀로 만든 서명만 보냅니다.
셋째 줄은 반대로 짐이 됩니다. 확인이 TLS 계층에서 끝납니다. 애플리케이션이 신원을 쓰려면 앞 소절처럼 그 결과를 넘겨받는 길을 따로 만들어야 합니다.
쓰는 곳과 안 쓰는 곳
접속하는 쪽이 정해져 있고 그 쪽을 운영하는 사람이 인증서를 챙길 수 있을 때 잘 맞습니다. 사내 서비스끼리 부르는 통신이 대표적입니다. 서비스 메시는 서비스마다 인증서를 자동으로 발급하고 새것으로 바꿔 이 일을 사람 손에서 덜어 냅니다.
현장에 깔리는 장비도 같은 경우입니다. 공장에서 장비마다 인증서를 한 장씩 넣어 내보내면, 서버는 접속한 장비가 자기 회사 제품인지 연결만 보고 가립니다. 기업끼리 주고받는 API 에서도 상대 회사 서버를 알아보는 데 씁니다.
일반 사용자가 브라우저로 접속하는 서비스에는 잘 안 씁니다. 사용자에게 인증서를 설치하게 하고 기간마다 새것으로 바꾸게 하는 일이 어렵습니다. 인증서가 여럿이면 브라우저가 어느 것을 쓸지 고르는 창을 띄웁니다. 이 창도 처음 보는 사람에게는 낯섭니다.
운영하며 드는 손
클라이언트 인증서를 쓰면 인증서가 접속하는 쪽 수만큼 생깁니다. 서버 인증서는 서버 몇 대분이면 되지만, 클라이언트 인증서는 서비스 수백 개나 장비 수만 대분이 될 수 있습니다.
인증서 하나하나를 발급해 나눠 주고, 기간 전에 갱신하고, 새면 폐기해야 합니다. 갱신을 놓친 인증서는 기간이 끝나는 순간 연결이 끊깁니다. 서버 쪽에서는 멀쩡한 클라이언트가 갑자기 거절당하는 장애로 보입니다.
개인키가 접속하는 쪽 장비에 있어야 한다는 것도 짐입니다. 개인키를 파일로 두면 장비를 손에 넣은 사람이 복사해 갈 수 있습니다. 이를 막으려고 키를 밖으로 꺼낼 수 없게 만든 보안 칩이나 하드웨어 보안 모듈 안에 두기도 합니다.
명령줄에서 내밀어 보기
클라이언트 인증서를 요구하는 서버를 부를 때는 인증서 파일과 개인키 파일을 둘 다 넘깁니다. 인증서만 넘기면 앞에서 본 서명을 만들 수 없어 핸드셰이크가 실패합니다.
curl --cert client.pem --key client.key \
https://api.example.com/orders
curl 에서는 --cert 가 인증서 파일을, --key 가 그 짝인 개인키 파일을 받습니다. 두 파일은
대개 PEM(Privacy-Enhanced Mail, 보안 강화 메일) 형식입니다. PEM 은 인증서나 키를
-----BEGIN CERTIFICATE----- 같은 머리줄로 감싼 텍스트 파일 형식입니다.
관련 항목
클라이언트 인증서가 속하는 상위 분류
인증서 · X.509 · 공개키 기반구조 · 인증 · HTTP 인증
클라이언트 인증서를 이루는 구성 요소
공개키 · 개인키 · 디지털 서명 · 주체 이름 · 주체 대체 이름 · 키 용도 · 확장 키 용도 · 유효 기간
클라이언트 인증서를 주고받는 프로토콜
TLS · 상호 TLS · TLS 핸드셰이크 · HTTPS · TLS 종료
클라이언트 인증서를 발급하고 믿는 주체
인증 기관 · 사설 인증 기관 · 루트 인증서 · 중간 인증서 · 신뢰 저장소 · 인증서 서명 요청
클라이언트 인증서를 못 쓰게 하는 수단
인증서 폐기 · 인증서 폐기 목록 · OCSP · 인증서 만료 · 인증서 갱신
클라이언트 인증서와 나란한 인증서의 하위 종류
서버 인증서 · 자기 서명 인증서 · 와일드카드 인증서 · 코드 서명 인증서
클라이언트 인증서 대신 클라이언트를 알아보는 수단
API 키 · 베어러 토큰 · JWT · OAuth 2.0 · 클라이언트 자격 증명 · 사전 공유 키 · 비밀번호 인증
클라이언트 인증서를 운영에 쓰는 구성
서비스 메시 · 제로 트러스트 · 리버스 프록시 · 로드 밸런서 · 하드웨어 보안 모듈 · 키 회전 · 시크릿 관리
클라이언트 인증서를 다루는 도구와 파일 형식
curl · OpenSSL · PEM · DER
클라이언트 인증서를 잘못 다뤘을 때 나는 오류
인증서 사슬 누락 · 만료된 인증서 · 신뢰할 수 없는 발급자 · 중간자 공격
다른 이름: client certificate · 클라이언트 인증서 인증 · client certificate authentication · 클라이언트 측 인증서