본문으로 바로가기
Lab

HTTPS와 HTTP 차이, 맥 터미널로 인증서와 TLS 직접 확인해 보니

HTTPS가 HTTP와 어떻게 다른지 맥 터미널로 확인합니다. 리디렉션, 인증서, TLSv1.3, SNI, 오류 메시지와 example.com 첫 바이트 시간(http 0.242387~0.284200초, https 0.374047~0.439808초)을 담았습니다.

·

잠긴 통신 통로를 지나는 데이터 봉투와, 그 앞에서 봉인된 인증서를 확인하는 터미널 창을 표현한 삽화
HTTPS와 HTTP 차이, 맥 터미널로 인증서와 TLS 직접 확인해 보니 표지 삽화

solodevnote.com에 http로 접속하면 301과 함께 https 주소가 돌아오지만, Strict-Transport-Security 헤더는 없습니다. example.com을 다섯 번씩 열었을 때 첫 바이트까지의 시간은 http가 0.2423870.284200초, https가 0.3740470.439808초였습니다. AI 에이전트로 배포까지 마쳤다면 내 사이트도 맥 터미널에서 이렇게 바로 확인할 수 있습니다.

HTTPS란 무엇이고 HTTP와 무엇이 다를까요?

MDN의 HTTPS 설명을 쉽게 옮기면, HTTPS는 HTTP를 암호화한 버전이고 클라이언트와 서버 사이의 통신을 SSL 또는 TLS로 암호화합니다. 여기에 인증서가 더해집니다. 서버는 인증서로 자기가 그 도메인의 서버임을 보여 주고, curl은 서명과 도메인 이름을 확인합니다.

HTTPS 연결 순서: DNS 조회, TCP 연결, TLS 핸드셰이크와 인증서 확인, 암호화된 HTTP 요청 순서를 단계별로 그린 도식
http는 연결 뒤 바로 요청을 보내고, https는 그 사이에 TLS 핸드셰이크와 인증서 확인이 들어간다
도식출처: 직접 실행한 순서

그래서 터미널에서는 명령 두 개만으로 차이가 드러납니다. solodevnote.com에서 나온 결과는 이렇습니다.

확인할 것명령결과
http 접속 시 이동curl -sI http://solodevnote.com301, https 주소로 안내
인증서 발급 기관openssl s_clientGoogle Trust Services의 WE1
TLS 버전openssl s_clientTLSv1.3

이제 항목별로 명령과 출력을 하나씩 살펴보겠습니다. 도메인만 바꾸면 내 사이트에도 그대로 쓸 수 있습니다.

함께 읽을 글

http 주소로 들어가면 어떻게 될까요?

사이트마다 응답이 다릅니다. 응답 헤더만 받아서 상태 코드와 이동할 주소를 걸러 보면 차이가 바로 보입니다.

$ for d in solodevnote.com naver.com example.com; do echo "[$d]"; curl -sI http://$d | grep -iE '^HTTP|^location'; done
[solodevnote.com]
HTTP/1.1 301 Moved Permanently
Location: https://solodevnote.com/
[naver.com]
HTTP/1.1 301 Moved Permanently
location: https://www.naver.com/
[example.com]
HTTP/1.1 200 OK

solodevnote.com과 naver.com은 301로 https 주소를 알려 줍니다. 반면 example.com은 http 그대로 200을 돌려줍니다. 에이전트가 만든 배포 설정을 믿고 넘어가기보다, 내 도메인의 결과가 어느 쪽인지 한 번 확인해 두는 편이 안전합니다.

HSTS 헤더가 없으면 무엇이 다를까요?

그런데 301만 있으면 충분할까요? MDN의 Strict-Transport-Security 문서에 따르면, 리디렉션 방식에서는 방문자가 이동 전에 암호화되지 않은 버전과 먼저 통신합니다. 이때 중간자 공격의 기회가 생깁니다. HSTS는 앞으로 http 접근을 https로 자동 변환하라고 브라우저에 알리는 응답 헤더입니다.

$ for d in solodevnote.com naver.com ko.wikipedia.org; do printf '%s: ' $d; curl -sI https://$d | grep -i '^strict-transport-security' || echo '(없음)'; done
solodevnote.com: (없음)
naver.com: strict-transport-security: max-age=63072000; includeSubdomains
ko.wikipedia.org: strict-transport-security: max-age=106384710; includeSubDomains; preload

이 블로그에는 헤더가 없고, naver.com과 ko.wikipedia.org에는 있습니다. 값의 뜻은 다음과 같습니다.

  • max-age: https로만 접근해야 한다고 브라우저가 기억할 시간(초)입니다.
  • includeSubDomains: 규칙을 모든 하위 도메인에도 적용합니다.
  • preload: 쓰려면 max-age가 31536000 이상이고 includeSubDomains가 있어야 합니다.

브라우저는 https로 처음 접근해 이 헤더를 받은 뒤부터 기록합니다. 또 includeSubDomains는 http로만 제공되는 하위 도메인 접근을 막습니다. 그러니 헤더를 추가하기 전에 하위 도메인이 모두 https로 열리는지 먼저 확인하는 편이 좋습니다.

SSL 인증서에는 무엇이 적혀 있을까요?

'SSL 인증서'를 검색했다면 실제 인증서를 열어 보는 것이 가장 빠릅니다. curl 문서에 따르면 신뢰할 수 있는 서버 인증서는 인증 기관(CA)이 디지털 서명한 것입니다.

$ echo | openssl s_client -connect solodevnote.com:443 -servername solodevnote.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates -text | grep -E 'subject=|issuer=|notBefore|notAfter|DNS:'
subject=CN=solodevnote.com
issuer=C=US, O=Google Trust Services, CN=WE1
notBefore=Sep 27 10:37:37 2026 GMT
notAfter=Dec 26 11:37:34 2026 GMT
                DNS:solodevnote.com, DNS:*.solodevnote.com
  • subject: 인증서의 대상입니다.
  • issuer: 발급 기관입니다. 여기서는 Google Trust Services의 WE1입니다.
  • notBefore, notAfter: 유효 기간의 시작과 끝입니다.
  • DNS:: 인증서에 적힌 도메인 목록(SAN)입니다.

예를 들어 하위 도메인을 새로 붙였다면 마지막 줄을 보면 됩니다. 이 인증서에는 *.solodevnote.com도 함께 적혀 있습니다.

실제로 쓰인 TLS 버전은 어떻게 볼까요?

인증서 다음으로 궁금한 것은 연결 방식입니다. openssl s_client와 curl -sv를 같은 사이트에 한 번씩 실행해 볼 수 있습니다.

$ echo | openssl s_client -connect solodevnote.com:443 -servername solodevnote.com 2>/dev/null | grep -E '^New,|^Protocol:|^Verify return code'
curl -sv -o /dev/null https://solodevnote.com 2>&1 | grep -E 'SSL connection|ALPN'
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Verify return code: 0 (ok)
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2

두 도구 모두 TLSv1.3을 썼고, Verify return code: 0 (ok)로 검증도 통과했습니다. curl은 h2와 http/1.1을 제안했고 서버는 h2, 곧 HTTP/2를 받아들였습니다. 다만 암호화 방식은 openssl이 TLS_AES_256_GCM_SHA384, curl이 AEAD-CHACHA20-POLY1305-SHA256으로 달랐습니다. 이 측정만으로는 원인을 가를 수 없으니, 암호화 방식을 볼 때는 어떤 도구로 확인했는지 함께 적어 두세요.

인증서 체인은 어디까지 이어질까요?

서버가 보낸 인증서를 모두 받아 대상(s:)과 발급자(i:)만 추리면 연결 고리가 보입니다.

$ echo | openssl s_client -connect solodevnote.com:443 -servername solodevnote.com -showcerts 2>/dev/null | grep -E '^ *[0-9] s:|^ *i:'
 0 s:CN=solodevnote.com
   i:C=US, O=Google Trust Services, CN=WE1
 1 s:C=US, O=Google Trust Services, CN=WE1
   i:C=US, O=Google Trust Services LLC, CN=GTS Root R4
 2 s:C=US, O=Google Trust Services LLC, CN=GTS Root R4
   i:C=BE, O=GlobalSign nv-sa, OU=Root CA, CN=GlobalSign Root CA

각 줄의 발급자가 다음 줄의 대상이 됩니다. 사이트 인증서에서 WE1으로, 다시 GTS Root R4로 올라가고, 마지막 발급자는 GlobalSign Root CA입니다. 이 출력에 GlobalSign Root CA는 발급자 이름으로만 나옵니다. curl 문서가 말하는 로컬 CA 저장소가 바로, 클라이언트가 서버 확인에 쓰는 신뢰 기관 인증서 모음입니다.

같은 IP에서 왜 다른 인증서가 올까요?

RFC 6066에 답이 있습니다. TLS 자체에는 접속하려는 서버 이름을 알리는 수단이 없어서, 클라이언트가 server_name 확장(SNI)에 이름을 담아 보낼 수 있습니다. 서버는 이 정보를 참고해 돌려줄 인증서를 고를 수 있고요. 하나의 주소에서 여러 서버를 운영할 때를 위한 장치입니다.

$ ip=$(dig +short solodevnote.com | head -1); echo "IP $ip"
for n in solodevnote.com example.com; do printf 'SNI %s → ' $n; echo | openssl s_client -connect $ip:443 -servername $n 2>/dev/null | openssl x509 -noout -subject; done
printf 'SNI 없이 → '; echo | openssl s_client -connect $ip:443 2>&1 | grep -oE 'alert handshake failure|SSL alert number [0-9]+' | tr '\n' ' '; echo
IP 104.21.68.112
SNI solodevnote.com → subject=CN=solodevnote.com
SNI example.com → subject=CN=example.com
SNI 없이 → alert handshake failure SSL alert number 40

같은 IP인데 이름만 바꾸니 다른 인증서가 왔고, 이름을 빼니 핸드셰이크가 실패했습니다. 그래서 IP로 서버를 점검할 때는 -servername을 빠뜨리지 않아야 합니다. 빠뜨리면 이 IP에서처럼 인증서 내용을 보기도 전에 연결이 끊길 수 있습니다.

하나의 서버 입구에 서로 다른 이름표를 단 두 요청이 도착해 각각 다른 인증서를 받고, 이름표가 없는 요청은 막히는 모습을 표현한 개념 삽화
SNI 이름에 따라 다른 인증서가 오고, 이름이 없으면 연결이 끊긴 흐름을 표현한 개념 삽화입니다.

HTTPS가 HTTP보다 빠를까요?

AWS 글에는 HTTPS 웹 애플리케이션의 로드 속도가 HTTP보다 빠르다는 주장이 있습니다. 그렇다면 연결 단계별로는 어떨까요? example.com을 http와 https로 다섯 번씩 열어 curl이 내는 시간 값을 쟀습니다.

$ for u in http://example.com/ https://example.com/; do echo "$u"; for i in 1 2 3 4 5; do curl -s -o /dev/null -w '  DNS %{time_namelookup}s 연결 %{time_connect}s TLS %{time_appconnect}s 첫 바이트 %{time_starttransfer}s HTTP/%{http_version}\n' $u; done; done
http://example.com/
  DNS 0.002641s 연결 0.137089s TLS 0.000000s 첫 바이트 0.278439s HTTP/1.1
  DNS 0.003257s 연결 0.138771s TLS 0.000000s 첫 바이트 0.280221s HTTP/1.1
  DNS 0.002643s 연결 0.139064s TLS 0.000000s 첫 바이트 0.284200s HTTP/1.1
  DNS 0.003693s 연결 0.118578s TLS 0.000000s 첫 바이트 0.242387s HTTP/1.1
  DNS 0.002734s 연결 0.116992s TLS 0.000000s 첫 바이트 0.246281s HTTP/1.1
https://example.com/
  DNS 0.002522s 연결 0.121197s TLS 0.259231s 첫 바이트 0.390318s HTTP/2
  DNS 0.003081s 연결 0.121567s TLS 0.252977s 첫 바이트 0.380291s HTTP/2
  DNS 0.002545s 연결 0.122273s TLS 0.247918s 첫 바이트 0.374047s HTTP/2
  DNS 0.003532s 연결 0.142627s TLS 0.292143s 첫 바이트 0.439808s HTTP/2
  DNS 0.002401s 연결 0.133572s TLS 0.275059s 첫 바이트 0.412915s HTTP/2

가장 작은 값과 가장 큰 값만 추리면 다음과 같습니다. 표의 값은 curl이 낸 그대로이며, 단계끼리 빼서 따로 계산하지 않았습니다.

항목httphttps
DNS0.002641~0.003693초0.002401~0.003532초
연결0.116992~0.139064초0.121197~0.142627초
TLS0.000000초0.247918~0.292143초
첫 바이트0.242387~0.284200초0.374047~0.439808초
HTTP 버전HTTP/1.1HTTP/2
첫 바이트 시간 막대 도표. http 최솟값 0.242387초, http 최댓값 0.284200초, https 최솟값 0.374047초, https 최댓값 0.439808초
다섯 번씩 연 값 가운데 가장 작은 값과 가장 큰 값입니다. 이 맥에서 이 시각에 잰 값이며 네트워크에 따라 달라집니다.
도식출처: 근거를 바탕으로 코드가 그린 도식

이번 측정에서는 https의 첫 바이트가 다섯 번 모두 http보다 늦었습니다. 하지만 이것은 한 사이트에서 연결 하나로 첫 바이트까지만 잰 결과입니다. 여러 파일을 내려받는 페이지 전체 로드는 재지 않았으니, 어느 쪽이 항상 빠르다고 말할 수 없습니다. 내 제품이 궁금하다면 같은 명령의 주소만 바꿔 실제 페이지로 재 보세요.

인증서 만료는 어떻게 미리 점검할까요?

속도보다 놓치기 쉬운 것이 만료일입니다. openssl x509 -checkend 뒤에 초 단위 값을 주면, 그 시간 안에 만료되는지 문구와 종료 코드로 알려 줍니다.

$ echo | openssl s_client -connect solodevnote.com:443 -servername solodevnote.com 2>/dev/null | openssl x509 -noout -enddate -checkend 2592000; echo "종료 코드 $?"
notAfter=Dec 26 11:37:34 2026 GMT
Certificate will not expire
종료 코드 0

반대 경우도 있습니다. 다음은 이 맥에서 하루짜리 자체 서명 인증서를 만들어 같은 방식으로 검사한 결과입니다. 이 인증서는 다음 절의 오류 실험에도 그대로 씁니다.

$ openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 1 -subj '/CN=localhost' -addext 'subjectAltName=DNS:localhost' 2>/dev/null && ls key.pem cert.pem
openssl x509 -in cert.pem -noout -subject -issuer -dates -text | grep -E 'subject=|issuer=|notAfter|DNS:'
openssl x509 -in cert.pem -noout -checkend 172800; echo "종료 코드 $?"
cert.pem
key.pem
subject=CN=localhost
issuer=CN=localhost
notAfter=Nov  3 02:07:42 2026 GMT
                DNS:localhost
Certificate will expire
종료 코드 1

subject와 issuer가 같은 점이 자체 서명 인증서의 모습입니다. 그리고 종료 코드가 0과 1로 갈리니, 에이전트에게 정기 점검 스크립트를 맡길 때 이 값을 성공과 실패 기준으로 삼으라고 요청할 수 있습니다.

인증서가 잘못되면 어떤 오류가 날까요?

curl은 기본으로 인증서 서명을 검증하고, 인증서가 URL의 서버 이름에 맞게 만들어졌는지 확인합니다. 자체 서명 인증서로 localhost에 띄운 서버에 접속하면 종료 코드 60으로 실패합니다.

$ openssl s_server -accept 18443 -cert cert.pem -key key.pem -www >/dev/null 2>&1 &
pid=$!; sleep 1
curl -sS https://localhost:18443/ -o /dev/null; code=$?
kill $pid; exit $code
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

그럼 이 인증서를 믿게 하거나, 이름을 다르게 하면 어떻게 될까요?

$ openssl s_server -accept 18443 -cert cert.pem -key key.pem -www >/dev/null 2>&1 &
pid=$!; sleep 1
echo '[--cacert cert.pem]'; curl -sS --cacert cert.pem https://localhost:18443/ -o /dev/null -w 'HTTP %{http_code}\n'
echo '[이름이 다른 주소 wrong.test]'; curl -sS --cacert cert.pem --resolve wrong.test:18443:127.0.0.1 https://wrong.test:18443/ -o /dev/null; echo "종료 코드 $?"
echo '[-k]'; curl -sSk https://localhost:18443/ -o /dev/null -w 'HTTP %{http_code}\n'
kill $pid
[--cacert cert.pem]
HTTP 200
[이름이 다른 주소 wrong.test]
curl: (60) SSL: no alternative certificate subject name matches target host name 'wrong.test'
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
종료 코드 60
[-k]
HTTP 200
상황옵션결과뜻
자체 서명 인증서없음오류 60, self signed certificate신뢰할 수 있는 서명을 확인하지 못함
자체 서명 인증서--cacert cert.pemHTTP 200이 전송에 쓸 CA 인증서를 직접 지정함
이름이 다른 주소--cacert cert.pem오류 60, no alternative certificate subject name믿는 인증서여도 이름이 맞지 않음
자체 서명 인증서-kHTTP 200검증을 건너뜀

-k로 200이 나왔다고 인증서가 정상이라는 뜻은 아닙니다. curl 문서는 이 옵션을 피하라고 강하게 권하고, 실험이나 개발 중이더라도 운영 환경에서는 절대 검증을 건너뛰지 말라고 합니다. 에이전트가 만든 스크립트나 배포 명령에 -k가 보인다면, 먼저 지우고 원래 오류를 해결하는 편이 맞습니다.

Claude Code에 점검 명령을 맡기면 어떨까요?

매번 명령을 외울 필요는 없습니다. Claude Code에 점검 명령 세 줄을 만들게 하고, 파일로 저장해 그대로 실행하면 이런 결과가 나옵니다. 전체 8.8초가 걸렸습니다.

$ claude --model opus -p "맥 터미널에서 solodevnote.com의 HTTPS 상태를 점검하는 셸 명령을 3줄만 출력해: 1) http로 접속했을 때 상태 코드와 Location, 2) 인증서 만료일, 3) Strict-Transport-Security 헤더(없으면 '없음' 출력). 설명이나 코드 블록 표시는 붙이지 마." < /dev/null > check.sh && cat check.sh && zsh -f check.sh
curl -sI http://solodevnote.com | grep -iE '^(HTTP/|location:)'
echo | openssl s_client -connect solodevnote.com:443 -servername solodevnote.com 2>/dev/null | openssl x509 -noout -enddate
curl -sI https://solodevnote.com | grep -i '^strict-transport-security:' || echo '없음'
HTTP/1.1 301 Moved Permanently
Location: https://solodevnote.com/
notAfter=Dec 26 11:37:34 2026 GMT
없음

세 결과 모두 앞에서 직접 실행한 값과 일치합니다. 다만 한 번 실행한 기록이니, 생성된 명령은 cat으로 읽어 본 뒤 실행하세요. 이 세 줄은 요청한 대로 이동, 만료일, HSTS만 봅니다. TLS 버전이나 만료 임박 여부도 보고 싶다면 앞 절의 명령을 프롬프트에 함께 적어 주면 됩니다.

측정 조건과 재지 않은 것

모든 명령은 2026-11-02 11:07 KST에 darwin arm64 맥의 새 임시 폴더에서 차례로 한 번씩 실행했습니다. 시간은 이 맥에서 이 시각에 잰 값이라 네트워크에 따라 달라집니다. curl은 macOS에 들어 있는 것을, openssl은 Homebrew 판을 썼습니다.

$ openssl version && curl --version | head -1
OpenSSL 4.1.0 23 Oct 2026 (Library: OpenSSL 4.1.0 23 Oct 2026)
curl 8.7.1 (x86_64-apple-darwin26.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.69.0

이 기록으로 답할 수 없는 것도 있습니다.

  • 이미 만료된 인증서에 접속할 때의 오류 메시지는 확인하지 않았습니다.
  • SNI 값이 네트워크에서 어떻게 보이는지는 살피지 않았습니다.
  • 연결 시간은 example.com 한 곳만 쟀으므로 다른 사이트에 그대로 옮길 수 없습니다.
  • HSTS는 헤더 값만 확인했고, 브라우저가 실제로 http를 https로 바꾸는 동작은 시험하지 않았습니다.

참고 자료