웹/클라우드/인프라로 돌아가기
웹/클라우드/인프라신윤섭·2026년 7월 13일

AI가 'curl -k 붙이세요'라고 할 때: 인증서 체인과 mTLS

공유

에이전트가 제일 먼저 배우는 나쁜 습관

Claude Code나 Codex한테 "API 붙여줘"라고 시켜본 사람은 이 장면을 한 번쯤 봤을 것이다. 에이전트가 터미널에서 curl을 날렸는데 이런 게 튀어나온다.

curl: (60) SSL certificate problem: unable to get local issuer certificate

빨간 글씨가 뜨자 에이전트가 태연하게 다음 명령을 제안한다. "인증서 검증을 건너뛰면 됩니다." 그러고는 curl -k를 붙인다. 파이썬이면 verify=False, Node면 NODE_TLS_REJECT_UNAUTHORIZED=0. 에러가 사라지고 데이터가 넘어온다. 다 풀린 것처럼 보인다.

풀린 게 아니다. 방금 에이전트는 통신에서 가장 중요한 보호막 하나를 꺼버렸고, 그게 왜 위험한지 알려면 인증서가 실제로 무슨 일을 하는지부터 봐야 한다.

인증서가 지키는 건 비밀이 아니라 신원이다

HTTPS를 두 가지 일을 한꺼번에 하는 것으로 뭉뚱그려 이해하는 사람이 많다. 하나는 암호화, 즉 오가는 내용을 남이 못 보게 봉인하는 것. 다른 하나는 신원 확인, 즉 지금 대화하는 상대가 진짜 그 서버가 맞는지 증명하는 것. 인증서는 두 번째를 담당한다.

이 구분이 왜 중요할까. 검증을 꺼도 암호화는 여전히 걸린다. 그래서 겉보기엔 멀쩡하다. 문제는 "상대가 진짜인지"를 확인하지 않는다는 데 있다. 결국 가짜 서버와 아주 튼튼하게 암호화된 대화를 나누는 셈이 된다. 도둑에게 편지를 정성껏 봉인해서 건네는 꼴이다.

가운데에 누가 끼어드는 공격을 중간자 공격(MITM)이라고 부른다. 카페 와이파이, 사내 프록시, 감염된 라우터 어디서든 공격자가 슬쩍 끼어들어 "내가 그 서버야"라고 속일 수 있다. 인증서 검증은 바로 이 순간 "당신 그 서버 아닌데?"라고 막아서는 장치다. -k는 그 장치를 떼어내는 스위치다.

신뢰는 사슬로 내려온다

그럼 내 노트북은 처음 접속하는 서버가 진짜인지 어떻게 아는 걸까. 세상 모든 서버의 신분증을 미리 갖고 있을 리는 없다. 여기서 인증서 체인, 즉 신뢰의 사슬이 등장한다.

회사 출입증을 떠올리면 편하다. 처음 보는 사람이 목에 건 출입증 한 장만으로는 그 사람을 믿기 어렵다. 하지만 그 출입증을 "우리 회사 보안팀이 발급했다"는 사실이 붙으면 이야기가 달라진다. 그리고 보안팀의 권한은 다시 "본사가 인정한 부서"라는 사실에 기대고 있다. 결국 내가 본사를 신뢰하면, 본사가 인정한 보안팀이 발급한 출입증까지 믿을 수 있다.

인증서도 똑같이 층층이 쌓인다. 방금 이야기에서 출입증은 서버의 인증서, 보안팀은 중간 발급 기관, 본사는 사슬의 뿌리에 해당한다.

  • 리프(leaf) 인증서: 서버가 실제로 내미는 인증서. api.example.com이라는 이름이 박혀 있다.
  • 중간(intermediate) CA: 리프에 서명해준 발급 기관. "이 리프는 내가 보증한다"고 도장을 찍는다.
  • 루트(root) CA: 사슬의 맨 위. 중간 CA에 권한을 준 최종 뿌리다.

서버는 접속할 때 리프와 중간 인증서들을 함께 내민다. 내 클라이언트는 리프가 중간의 서명을 받았는지, 중간이 그 위의 서명을 받았는지 서명을 거슬러 올라가며 확인한다. 그렇게 사슬의 끝에 있는 루트 CA에 도착했을 때, 그 루트가 내 컴퓨터가 미리 신뢰하기로 정해둔 목록에 들어 있으면 검증이 통과한다.

이 신뢰 목록을 신뢰 저장소(trust store)라고 부른다. 운영체제나 브라우저에는 전 세계가 인정하는 루트 CA 수백 개가 기본으로 깔려 있다. 그래서 우리가 아무 설정 없이도 은행 사이트에 안전하게 접속하는 것이다. 핵심은 이거다. 인증서 파일이 있다는 것만으로는 부족하고, 사슬의 뿌리가 내 신뢰 저장소에 미리 들어 있어야 한다.

그래서 인증서 오류는 대부분 사슬이 끊긴 것이다

앞의 unable to get local issuer certificate 에러로 돌아가 보자. 이 문구는 사실 꽤 친절하다. "리프는 받았는데, 이걸 서명해준 발급자(issuer)를 못 찾겠다"는 뜻이다. 사슬 중간이 비어 있다는 신호다. 인증서 오류는 보통 이런 이유에서 난다.

  • 중간 인증서 누락: 서버가 리프만 내밀고 중간 CA를 빼먹었다. 서버 설정에서 제일 흔한 실수다.
  • 뿌리가 신뢰 저장소에 없음: 회사 내부용 사설 CA로 발급한 인증서인데, 그 루트가 내 컴퓨터에 등록되어 있지 않다.
  • 만료: 인증서에는 유효 기간이 있고, 지나면 검증이 실패한다.
  • 호스트명 불일치: 인증서에 적힌 이름과 실제 접속한 주소가 다르다. localhost로 접속했는데 인증서에는 다른 도메인이 박힌 경우.

여기서 중요한 판단이 갈린다. 이 네 가지는 전부 원인이 분명하고 고칠 수 있는 문제다. 그런데 -k는 원인을 고치는 게 아니라 원인을 안 보이게 덮는다. 감기 걸린 사람에게 체온계를 뺏는 것과 같다. 열이 사라진 게 아니라 열을 못 재게 된 것뿐이다.

-k가 실제로 여는 문

검증을 끄면 무슨 일이 벌어지는지 장면으로 그려보자. 바이브 코딩으로 만든 앱이 결제 API에 카드 정보를 보낸다고 하자. 정상이라면 앱은 결제사 서버의 인증서를 확인하고, 사슬이 진짜일 때만 카드번호를 넘긴다.

그런데 코드에 verify=False가 박혀 있다. 이제 공격자가 같은 네트워크에서 자기 서버를 결제사인 척 세워둔다. 앱은 인증서를 확인하지 않으니 순순히 카드번호를 넘긴다. 암호화는 걸려 있으니 전송 구간에서 누가 훔쳐볼 일은 없다. 다만 그 암호화된 데이터를 받는 상대가 도둑일 뿐이다.

에이전트와 함께 일할 때 이게 특히 위험한 이유는, 한 번 들어간 -k가 조용히 눌러앉기 때문이다. 로컬에서 급하게 뚫으려고 붙인 NODE_TLS_REJECT_UNAUTHORIZED=0이 커밋에 섞여 들어가고, 그대로 배포되고, 운영 환경에서 몇 달을 돈다. 게다가 이 환경변수는 그 프로세스의 모든 통신을 대상으로 검증을 끈다. 문제의 API 하나가 아니라 앱이 맺는 모든 연결이 무방비가 된다. 에이전트는 눈앞의 에러를 없애는 데는 뛰어나지만, 몇 달 뒤의 보안 사고까지 책임지지는 않는다.

로컬 개발은 검증을 끄지 말고 사슬을 만들어라

물론 로컬 개발에서 HTTPS가 필요한 순간은 진짜로 있다. 쿠키의 Secure 옵션을 테스트하거나, 브라우저가 HTTPS에서만 열어주는 기능을 써야 할 때다. 이때 공인 CA는 localhost에 인증서를 발급해주지 않는다. 그렇다고 -k로 도망갈 필요는 없다. 제대로 된 길이 있다.

바로 mkcert다. 이 도구는 내 컴퓨터 안에만 존재하는 개인 CA를 하나 만들고, 그 CA를 내 신뢰 저장소에 등록해준다. 그런 다음 이 CA로 localhost용 인증서를 발급한다. 그러면 브라우저 입장에서는 사슬의 뿌리가 신뢰 저장소에 있으니 검증이 통과한다. 핵심을 다시 강조하면, mkcert는 검증을 끄는 게 아니라 검증이 통과하도록 개발용 신뢰 사슬을 제대로 세우는 방식이다. 구조는 그대로 살아 있다.

한 가지 함정이 있다. Node.js는 운영체제의 신뢰 저장소를 그대로 쓰지 않는다. 그래서 mkcert를 깔아도 Node는 그 인증서를 모른다. 이때 검증을 끄는 대신 이렇게 알려주면 된다.

export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"

Node에게 "이 루트 CA도 믿어도 돼"라고 사슬의 뿌리 하나를 추가해주는 것이다. NODE_TLS_REJECT_UNAUTHORIZED=0과 겉보기 효과는 비슷해 보여도 성격이 정반대다. 하나는 검증을 통째로 끄고, 다른 하나는 믿을 대상을 딱 하나 늘린다. 참고로 mkcert가 만든 rootCA-key.pem은 내 컴퓨터에서 나가는 통신을 가로챌 수 있는 열쇠다. 절대 공유하거나 커밋하지 말아야 한다.

mTLS: 이번엔 서버가 나를 확인한다

지금까지는 클라이언트가 서버를 확인하는 이야기였다. 서버는 아무한테나 문을 열어주고, 진짜인지 따지는 쪽은 언제나 접속하는 클라이언트였다. mTLS(mutual TLS), 상호 TLS는 이 관계를 양방향으로 만든다. 서버도 접속해온 클라이언트에게 인증서를 요구하고, 그 인증서가 진짜일 때만 문을 열어준다.

비밀번호나 API 키와 뭐가 다른지 궁금할 수 있다. API 키는 결국 문자열이라, 로그에 찍히거나 코드에 박히거나 유출되면 아무나 흉내 낼 수 있다. mTLS는 클라이언트가 개인 키를 실제로 쥐고 있다는 걸 증명해야 통과하므로, 열쇠 자체가 새지 않는 한 흉내 내기 어렵다. "이 요청이 정말 그 서비스에서 왔는가"를 훨씬 강하게 보장하는 셈이다.

주로 쓰이는 자리는 사람이 직접 접속하는 공개 웹사이트가 아니라, 서비스끼리 서로를 부르는 내부 통신이다. 마이크로서비스 사이, 회사 간 B2B 연동, 그리고 요즘 부쩍 늘어난 제로 트러스트 구성이 대표적이다. 제로 트러스트는 "내부망이라고 안전한 건 아니다"라는 전제에서 출발한다. 방화벽 안쪽이라도 서비스끼리 통신할 때마다 서로 신분증을 확인하게 만드는데, 그 확인 방법으로 mTLS가 자주 쓰인다.

바이브 코더에게 이게 남 일이 아닌 이유는 에이전트 도구 때문이다. Claude Code나 Codex가 MCP 서버에 붙거나, 자체 호스팅한 내부 API를 호출하는 구성이 흔해지고 있다. 이런 내부 통신 구간은 공개 웹처럼 자동으로 인증서가 붙지 않는다. 아무 검증 없이 열어두면, 같은 네트워크에 들어온 누구든 그 에이전트 엔드포인트를 부를 수 있다. mTLS는 "정해진 클라이언트만 이 API를 부를 수 있다"는 경계선을 긋는 도구다. 대신 인증서를 발급하고, 배포하고, 주기적으로 갈아주고, 필요하면 폐기하는 운영 부담이 따라온다. 그래서 아무 데나 쓰기보다는 통제된 서비스 환경에서 값어치를 한다.

AI와 함께 코딩할 때의 원칙

정리하면 인증서 오류를 만났을 때 판단 기준은 하나다. 검증을 끄는 방향인가, 사슬을 고치는 방향인가.

에이전트가 -kverify=False, NODE_TLS_REJECT_UNAUTHORIZED=0을 제안하면, 그건 문제를 푼 게 아니라 덮은 것이다. 대신 이렇게 되물어보는 편이 낫다. 사슬 중간이 빠졌나, 뿌리가 신뢰 저장소에 없나, 인증서가 만료됐나, 아니면 접속한 주소가 인증서 이름과 다른가. 원인을 짚으면 대개 서버 설정을 고치거나, 로컬이라면 mkcert로 개발용 사슬을 세우는 것으로 끝난다. openssl s_client -connect 도메인:443 한 줄이면 서버가 어떤 인증서를 내미는지 눈으로 확인할 수 있다.

검증을 끄는 건 언제나 마지막의 마지막 선택지여야 한다. 그마저도 정말 격리된 실험 환경에서, 그 줄이 절대 커밋에 섞이지 않는다는 확신이 있을 때만이다. 에이전트는 눈앞의 빨간 에러를 지우는 데 최적화되어 있다. 그 빨간 글씨가 원래 무엇을 막고 있었는지 기억하는 건 결국 사람의 몫이다.

YS

신윤섭

데이너스 대표 | AI 교육 & AX 컨설팅

81개 이상의 AI/AX 교육 과정을 설계하고, 50여 기업과 기관에서 강의했습니다. 강남세브란스, 삼성전자, 현대자동차 등 다양한 조직의 AI 역량 강화를 지원하고 있습니다.

AI 교육이 필요하신가요?

조직에 맞는 맞춤형 AI/AX 교육 프로그램을 설계해드립니다. 커리큘럼 상담부터 시작해보세요.

같은 주제의 다른 글