SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor

Ubuntu 24.04 자체 서명 TLS 인증서 올바르게 생성하기

Ubuntu 24.04에서 Chrome이 신뢰하는 자체 서명 TLS 인증서 생성법을 다룹니다. SAN 설정부터 nginx 적용, 시스템 신뢰 등록까지 curl -k 없이 안전하게 연결하는 실무 가이드를 제공합니다.

구축할 내용

현대적인 브라우저와 클라이언트가 실제로 수용하는 자체 서명 TLS 인증서, 올바른 subjectAltName, 적절한 키 권한, nginx 또는 Apache 연동, 그리고 거의 모든 가이드에서 생략하는 핵심 내용인 클라이언트가 경고를 무시하고 클릭하거나 스크립트에 curl -k을 영구적으로 하드코딩하는 대신 인증서를 올바르게 신뢰하도록 만드는 방법을 다룹니다. 마지막에는 내부 서비스가 하나에서 여섯 개로 늘어날 때를 대비한 5개 명령어 기반의 사설 CA 구축 방법을 포함합니다.

먼저 결정을 내려야 합니다. 자체 서명 인증서는 사용되는 빈도에 비해 실제로 적합한 도구인 경우가 드뭅니다. 서비스가 실제 DNS 이름을 통해 공용 인터넷에서 접근 가능하다면, 이 글을 읽는 것을 멈추고 nginx에서 certbot을 사용한 무료 Let's Encrypt 인증서 또는 Apache용 동일 설정을 적용하십시오. 비용이 들지 않고 자동으로 갱신되며, 주류 브라우저와 운영체제는 이미 이를 신뢰합니다. 공용 사이트에 자체 서명 인증서를 사용하면 사용자에게 보안 경고를 무시하고 클릭하도록 학습시키게 되는데, 이는 일반 HTTP를 사용하는 것보다 더 나쁜 습관입니다.

자체 서명 인증서가 적합한 경우는 공용 인터넷이 관여하지 않을 때입니다. VPS의 WireGuard 터널 주소에 바인딩된 관리자 패널, 사설 네트워크의 스테이징 서버, 백엔드 간의 서비스 통신, 홈랩 장비, 또는 Webmin이 10000번 포트에서 자체 생성하는 자리 표시자 인증서를 대체하는 경우가 이에 해당합니다. 어차피 Let's Encrypt는 10.8.0.1이나 git.internal.lan에 대해 인증서를 발급할 수 없으며, 어떤 공용 CA도 사설 IP나 임의로 만든 TLD를 인증서에 포함해주지 않습니다. 이러한 이름의 경우, 귀하가 직접 CA가 되어야 합니다.

아래의 모든 내용은 OpenSSL 3.0.x(확인하려면 openssl version 사용)가 포함된 최신 Ubuntu 24.04 환경에서 실행됩니다. 이 과정은 인터넷 연결이 필요 없으며, 외부와 완전히 차단된(air-gapped) 환경에서도 모두 작동합니다.

이전의 한 줄짜리 명령어가 Chrome에서 거부되는 인증서를 생성하는 이유

2017년 이전의 모든 튜토리얼에서 제공하는 명령어는 다음과 같습니다.

# Do not run this: shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key -out selfsigned.crt

이 명령어는 일련의 대화형 질문을 던지고, 호스트 이름을 Common Name 필드에 넣으며, subjectAltName 확장이 없는 인증서를 생성합니다. 해당 인증서는 생성 즉시 사용할 수 없는 상태가 됩니다. Chrome은 2017년 4월 버전 58부터 Common Name을 읽지 않게 되었으며, RFC 2818은 이미 2000년에 Common Name 일치를 폐기했습니다. Firefox와 Safari도 동일하게 동작합니다. curl과 Python의 ssl 모듈은 인증서에 SAN이 전혀 없을 경우 Common Name으로 대체(fallback)하는 방식을 여전히 사용합니다. 이것이 바로 SAN이 없는 인증서가 간단한 curl 테스트는 통과하면서도 모든 브라우저에서는 실패하는 이유입니다. 브라우저 입장에서 인증서는 SAN 확장을 통해서만 서버를 식별하며, 그렇지 않으면 아예 식별하지 못합니다. 브라우저는 정확히 다음과 같은 문구로 이 사실을 알려줍니다.

NET::ERR_CERT_COMMON_NAME_INVALID

This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.

인증서가 실제로 아무런 이름도 지정하지 않기 때문에 신뢰 저장소(trust-store)를 아무리 수정해도 해당 오류는 해결되지 않습니다. 현재 NET::ERR_CERT_COMMON_NAME_INVALID 오류가 발생하고 있다면, 인증서에 SAN이 없거나 잘못된 SAN이 포함된 것이므로 새로 발급해야 합니다. 다행히 해결 방법은 한 줄의 명령어로 충분합니다.

브라우저가 신뢰하는 인증서 발급: 단일 명령어

OpenSSL은 1.1.1 버전부터 -addext 플래그를 도입했습니다. 따라서 과거 가이드에서 SAN을 삽입하기 위해 사용하던 복잡한 설정 파일 작업이 더 이상 필요하지 않습니다. Ubuntu 24.04 환경에서는 다음과 같이 실행합니다.

sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
  -keyout /etc/ssl/private/git.internal.key \
  -out /etc/ssl/certs/git.internal.crt \
  -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"

각 플래그의 역할은 다음과 같습니다.

  • -x509: 서명 요청(CSR) 대신 자체 서명된 인증서를 즉시 생성합니다.
  • -newkey rsa:4096: 동일한 단계에서 새로운 키를 생성합니다. RSA 4096은 구형 클라이언트와 호환성 문제가 없으며, 연결되는 모든 환경이 최신이라면 -newkey ec -pkeyopt ec_paramgen_curve:P-256을 사용하는 것이 더 작고 빠릅니다.
  • -noenc: 기존 -nodes의 OpenSSL 3.x 버전 표기법으로, 키에 암호를 설정하지 않습니다. 두 표기법 모두 작동합니다. 키에 암호가 설정되어 있으면 부팅할 때마다 nginx가 입력을 대기하며 멈추게 되므로, 서버용 키에는 이 옵션이 필수입니다.
  • -days 730: 유효 기간을 2년으로 설정합니다. 이 수치에 관한 자세한 내용은 만료 섹션을 참조하십시오.
  • -subj: 대화형 질문에 대한 답변을 명령줄에서 바로 입력합니다. CN은 현재 장식적인 용도에 불과하지만, 일부 도구에서 표시하므로 기본 도메인 이름을 설정하는 것이 좋습니다.
  • -addext "subjectAltName=...": 핵심적인 역할을 하는 플래그입니다. 클라이언트가 입력할 모든 이름과 모든 IP를 나열하십시오. 호스트 이름은 DNS: 항목(DNS:*.internal.lan와 같은 와일드카드 사용 가능), 주소는 IP: 항목을 사용합니다. 누군가 https://10.8.0.1로 접속할 예정이라면 IP:10.8.0.1 항목이 반드시 포함되어야 합니다. DNS 전용 SAN만 설정하면 다시 NET::ERR_CERT_COMMON_NAME_INVALID 오류가 발생합니다.

설정을 적용하기 전에 SAN이 제대로 포함되었는지 확인하십시오.

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltName

올바른 출력 결과:

X509v3 Subject Alternative Name:
    DNS:git.internal.lan, IP Address:10.8.0.1

만약 결과에 No extensions in certificate이 출력된다면, 인증서에 SAN이 없는 상태입니다. 브라우저가 이를 거부하므로, 그대로 진행하지 말고 다시 생성하십시오.

키 보안 강화

시스템의 모든 사용자가 읽을 수 있는 개인 키는 더 이상 개인 키가 아닙니다. 기본 Ubuntu 24.04 환경에서 /etc/ssl/private는 이미 700 root:root 상태이므로 root 사용자만 목록을 확인할 수 있습니다. 이 디렉터리가 710 root:ssl-cert 상태가 되는 경우는 ssl-cert 패키지가 설치되었을 때뿐입니다(apache2가 이 패키지를 권장합니다). 어떤 경우든 파일 자체의 권한을 명시적으로 설정하십시오.

sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.key

nginx와 Apache는 권한을 낮추기 전에 root 권한으로 인증서를 읽으므로 root:root 모드 600 설정으로 충분합니다. 만약 Node 앱, Gitea, Python 데몬과 같이 별도의 사용자 계정으로 실행되면서 직접 키를 로드하는 서비스라면, chown 명령을 사용하여 해당 서비스 사용자에게 소유권을 부여하십시오. 이때도 권한은 600으로 유지해야 합니다. 절대 하지 말아야 할 행동은 다음과 같습니다: 644 모드 설정, git 저장소에 키 복사본 저장, /tmp 내부에 키 복사본 저장.

Nginx에 연결하기

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name git.internal.lan;

    ssl_certificate     /etc/ssl/certs/git.internal.crt;
    ssl_certificate_key /etc/ssl/private/git.internal.key;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}
sudo nginx -t && sudo systemctl reload nginx

nginx -t은 리로드를 수행하기 전에 반드시 syntax is ok 및 test is successful을 출력해야 합니다. 만약 SSL_CTX_use_PrivateKey_file() failed ... key values mismatch가 출력된다면, 인증서와 키가 서로 다른 생성 과정에서 만들어진 것이므로 실패 유형(failure-modes) 섹션을 확인하십시오.

Apache에 연결하기

sudo a2enmod ssl proxy proxy_http

ssl만으로는 충분하지 않습니다. 아래의 vhost는 ProxyPass을 사용하며, mod_proxy와 mod_proxy_http이 없으면 설정 테스트가 Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration 오류와 함께 실패합니다. vhost를 /etc/apache2/sites-available/git-internal.conf로 저장하십시오:

<VirtualHost *:443>
    ServerName git.internal.lan
    SSLEngine on
    SSLCertificateFile      /etc/ssl/certs/git.internal.crt
    SSLCertificateKeyFile   /etc/ssl/private/git.internal.key

    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2

configtest은 Syntax OK로 응답해야 합니다. 이제 클라이언트 머신에서 테스트하십시오:

curl -v https://git.internal.lan/

그러면 다음과 같은 오류가 발생합니다:

curl: (60) SSL certificate problem: self-signed certificate

이는 버그가 아닙니다. TLS가 정상적으로 작동하고 있는 것입니다. curl은 귀하의 인증서에 대해 알지 못하며, 인증할 수 없는 서버와 통신하기를 거부합니다. 다음 섹션에서 올바른 해결 방법을 다룹니다. 이는 현재 인터넷상의 절반이 수행하는 방식과는 다른 방법입니다.

클라이언트가 인증서를 신뢰하게 만들기 및 피해야 할 안티패턴

먼저 잘못된 해결책을 명확히 정의합니다. 스크립트에 포함된 curl -k(또는 --insecure), Python requests의 verify=False, Node의 NODE_TLS_REJECT_UNAUTHORIZED=0는 인증서를 신뢰하게 만들지 않습니다. 이들은 인증서 검증 기능을 비활성화하며, 이는 클라이언트가 공격자가 중간에 삽입한 인증서를 포함하여 어떤 인증서를 제시하는 어떤 서버와도 통신하게 만듭니다. TLS의 오버헤드는 그대로 유지하면서 TLS의 핵심 목적인 인증 기능을 상실하게 됩니다. 더 나쁜 점은 이러한 플래그가 확산된다는 것입니다. 크론 작업에 붙여넣고, 배포 스크립트에 넣고, 운영 코드에까지 포함되어 결국 어떤 연결이 일시적인 것이었는지 아무도 기억하지 못하게 됩니다. 만약 verify=False이 디버깅 세션이 끝난 후에도 남아 있다면, 설계가 잘못된 것입니다.

올바른 해결책은 각 클라이언트 OS에 이 인증서가 신뢰할 수 있는 루트임을 알리는 것입니다. Ubuntu 및 Debian 클라이언트에서는 다음과 같이 합니다.

sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificates

출력 내용 중 중요한 줄은 다음과 같습니다 (Running hooks in /etc/ca-certificates/update.d... 블록이 뒤따릅니다):

Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

이 과정에는 두 가지 주의사항이 있습니다. 파일은 반드시 .crt로 끝나야 하며, .pem 확장자는 무시되고 오류 메시지 없이 0 added이 발생합니다. 또한 내용은 PEM 형식이어야 하며, 파일은 -----BEGIN CERTIFICATE-----로 시작해야 합니다. DER 바이너리 형식이라면 먼저 openssl x509 -inform der -in file.der -out file.crt로 변환하십시오. 자체 서명된 인증서 자체를 루트로 추가하는 방식은 자체 서명된 인증서가 그 자체로 자신의 루트이기 때문에 정상적으로 작동합니다.

그 후에는 curl, wget, git, apt 및 시스템 번들을 사용하는 OpenSSL 기반의 모든 도구가 별도의 플래그 없이 서버를 신뢰하게 됩니다. 자체적인 신뢰 저장소를 사용하는 일부 클라이언트는 개별적인 처리가 필요합니다.

  • Linux용 Chrome/Chromium은 시스템 저장소가 아닌 NSS 데이터베이스를 읽습니다. sudo apt install libnss3-tools을 사용한 다음 사용자별로 certutil -d sql:$HOME/.local/share/pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt을 수행하십시오. 해당 경로는 버전 146부터 Chrome의 기본값입니다. 만약 사용자가 이전 설치 버전의 레거시 ~/.pki/nssdb를 여전히 가지고 있다면 Chrome은 계속해서 해당 파일을 읽으므로, 해당 장비에서는 -d이 sql:$HOME/.pki/nssdb을 가리키도록 설정하십시오.
  • Firefox는 자체 저장소를 사용합니다. 설정(Settings) → 개인정보 및 보안(Privacy & Security) → 인증서(Certificates) → 가져오기(Import)를 선택하십시오. Windows 및 macOS에서는 about:config에서 security.enterprise_roots.enabled를 true으로 변경하여 OS 저장소를 읽게 할 수 있습니다. Mozilla는 이 설정을 Windows와 macOS에 대해서만 문서화하고 있으므로, Ubuntu에서는 가져오기 기능을 사용하십시오.
  • Python requests는 자체 CA 번들(certifi)을 포함하고 있으며 시스템 저장소를 무시합니다. verify="/usr/local/share/ca-certificates/git.internal.crt"를 전달하거나 REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt을 내보내십시오(export).
  • Node.js는 기본적으로 자체 번들 루트를 사용합니다. NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt을 내보내거나(export), 방금 업데이트한 시스템 저장소를 읽도록 --use-system-ca 플래그(Linux용 Node 23.9 이상) 또는 NODE_USE_SYSTEM_CA=1(Node 24.6 및 22.19 이상)를 사용하십시오.

Windows 클라이언트에서는 .crt을 더블 클릭하여 신뢰할 수 있는 루트 인증 기관(Trusted Root Certification Authorities)에 설치하십시오. macOS에서는 키체인 접근(Keychain Access)의 시스템 키체인에 추가하고 항상 신뢰(Always Trust)로 표시하십시오.

여러 서비스를 위한 하나의 루트: 소규모 사설 CA

인증서별로 신뢰를 설정하는 방식은 즉시 확장성을 잃습니다. 6개의 서비스와 4대의 클라이언트 머신이 있다면 24번의 신뢰 설치가 필요하며, 서비스가 추가될 때마다 작업량은 늘어납니다. 해결책은 사설 CA를 구축하는 것입니다. 클라이언트는 하나의 루트 인증서를 신뢰하고, 각 서비스의 인증서는 이 루트로 서명합니다.

가장 편리한 선택지는 mkcert입니다. 이 도구는 Ubuntu 24.04 저장소에 포함되어 있으며, update-ca-certificates가 놓치는 NSS 저장소(Firefox 및 아래에서 설명할 한 가지 예외를 제외한 Chrome)를 처리합니다.

sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1

mkcert -install은 루트 인증서를 생성하여 시스템 저장소와 브라우저가 찾을 수 있는 NSS 저장소에 등록하며, JAVA_HOME가 설정된 경우 Java 저장소에도 등록합니다. Node는 별도의 지시가 없으면 시스템 저장소를 읽지 않으므로, 위와 같이 --use-system-ca(또는 NODE_USE_SYSTEM_CA=1)를 사용하여 Node 애플리케이션을 시작하거나 NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"을 내보내야 합니다. 세 번째 명령은 git.internal.lan+2.pem과 git.internal.lan+2-key.pem를 출력하며, 이는 앞서 언급한 nginx 또는 Apache 스니펫에 바로 적용할 수 있습니다. 이 도구는 개발 머신을 전제로 설계되었으며, 루트 키는 -install을 실행한 장비에 저장됩니다. 따라서 개발용 노트북에는 적합하지만 서버 팜(fleet)에는 적합하지 않습니다.

2026년 10월 기준, 주의할 점이 하나 있습니다. mkcert는 Chrome의 NSS 데이터베이스를 레거시 경로인 ~/.pki/nssdb에서만 찾으며, 버전 146부터 Chrome이 기본으로 사용하는 ~/.local/share/pki/nssdb는 확인하지 않습니다. 레거시 데이터베이스가 없는 머신에서는 -i "$(mkcert -CAROOT)/rootCA.pem"를 사용하여 이전 섹션의 certutil 명령으로 직접 루트 인증서를 추가해야 합니다.

서버의 경우, 일반 OpenSSL을 사용하여 5개의 명령어로 전체 CA를 구축할 수 있습니다.

openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
  -out lab-ca.crt -subj "/CN=Lab Internal CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
  -CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crt

마지막 명령에서 주의할 점이 있습니다. openssl x509 -req는 기본적으로 CSR에서 모든 확장 필드를 제거하며, 여기에는 공들여 추가한 SAN도 포함됩니다. -copy_extensions copy(OpenSSL 3.x 옵션으로 24.04에서 작동)을 사용하면 확장 필드가 유지됩니다. 이 옵션을 생략하면 서명된 인증서에 SAN이 포함되지 않으며, Chrome은 다시 NET::ERR_CERT_COMMON_NAME_INVALID 오류를 표시합니다. 이전과 동일한 openssl x509 -noout -ext subjectAltName 확인 명령으로 검증하십시오.

lab-ca.crt는 위에서 설명한 신뢰 저장소 등록 단계를 통해 클라이언트에 배포하며, 머신당 한 번만 수행하면 됩니다. lab-ca.key은 현재 가장 중요한 자산이므로 철저히 보호해야 합니다. 권한을 600으로 설정하고, 가급적 서명 대상 서버가 아닌 별도의 장비에 보관하십시오. 이 키를 가진 사람은 클라이언트가 신뢰하는 어떤 도메인으로든 인증서를 위조할 수 있기 때문입니다.

만료 및 갱신

공용 CA 인증서의 유효 기간은 점차 짧아지고 있습니다. CA/Browser Forum은 2026년 3월부터 신규 발행되는 공용 신뢰 인증서의 최대 유효 기간을 200일(기존 398일)로 제한했으며, 2027년에는 100일, 2029년 3월까지는 47일로 단축할 예정입니다. 하지만 이러한 규칙은 공용 신뢰 CA에만 적용됩니다. 사설 CA는 해당 규정의 적용을 받지 않으며, 브라우저 역시 수동으로 설치된 루트 인증서에 대해서는 이를 강제하지 않습니다. 다만 실제 환경에서 적용되는 제약 사항이 하나 있습니다. Apple 플랫폼은 발행 주체와 관계없이 유효 기간이 825일을 초과하는 모든 TLS 서버 인증서를 거부합니다. 따라서 iPhone이나 Mac이 연결되는 환경이라면 리프 인증서의 유효 기간을 2년 이하로 유지해야 합니다. -days 730는 모든 환경에서 이 기준을 충족하며, 10년 유효 기간의 루트 인증서와 2년 유효 기간의 리프 인증서를 조합하는 것이 내부 운영상 적절한 형태입니다.

수명이 긴 인증서는 단 한 가지 방식으로 실패합니다. 아무도 기억하지 못하는 특정 날짜에, 아무런 예고 없이 한꺼번에 만료됩니다. 현재 보유한 인증서를 확인하십시오:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate

갱신 일정을 실제 달력에 기록하거나, cron을 사용하여 30일 전부터 알림을 받도록 설정하십시오. openssl x509 -checkend 2592000 -in cert.crt는 만료 시점이 지정된 초 단위 이내로 다가오면 0이 아닌 값을 반환하며 종료됩니다. 이미 상태 모니터링을 위한 Uptime Kuma를 운영 중이라면, 해당 도구의 HTTPS 모니터 기능을 통해 인증서 만료 임박 알림을 무료로 받을 수 있습니다.

사설 CA를 이용한 인증서 교체는 매우 단순한 작업입니다. CSR 생성 및 서명 명령을 다시 실행하고, 파일을 교체한 뒤 웹 서버를 리로드하면 됩니다. 루트 인증서는 변경되지 않았으므로 클라이언트는 아무런 변화를 감지하지 못합니다.

오류 유형 및 확인되는 메시지

NET::ERR_CERT_AUTHORITY_INVALID, 신뢰를 설정하기 전의 예상되는 상태이며 인증서 결함이 아닙니다. 루트 인증서를 설치한 후에도 이 문제가 지속된다면 다음을 확인하십시오. 리눅스에서 Chrome은 시스템 저장소가 아닌 NSS를 참조하며(certutil 단계 참조), Chrome 146 버전부터는 레거시 ~/.pki/nssdb 경로가 존재하지 않는 한 데이터베이스가 ~/.local/share/pki/nssdb에 위치하므로 다른 경로에 추가된 루트 인증서는 인식되지 않습니다. 또는 복사한 파일의 확장자가 .crt가 아니어서 update-ca-certificates가 0 added 오류를 반환했을 수 있습니다. 혹은 서버가 신뢰한 인증서와 다른 인증서를 제공하고 있을 수 있으니 openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256 명령으로 지문(fingerprint)을 비교하십시오.

NET::ERR_CERT_COMMON_NAME_INVALID, 인증서에 SAN이 없거나 주소창의 도메인 이름이 SAN에 포함되지 않은 경우입니다. 흔한 사례로 SAN에는 DNS:git.internal.lan만 기재되어 있는데 사용자가 https://10.8.0.1으로 접속하는 경우입니다. 신뢰 저장소 변경으로는 해결할 수 없으며, 누락된 항목을 포함하여 인증서를 재발급해야 합니다.

curl: (60) SSL certificate problem: self-signed certificate, curl이 해당 인증서를 신뢰하지 않는 상태입니다. self-signed certificate in certificate chain 변형 메시지 역시 사설 CA가 서명한 인증서에 대해 동일한 의미를 갖습니다. 일회성 해결책은 curl --cacert lab-ca.crt https://...이며, 영구적인 해결책은 신뢰 저장소에 추가하는 것입니다. -k를 사용하지 마십시오.

unable to load certificate ... Expecting: TRUSTED CERTIFICATE(또는 Expecting: CERTIFICATE REQUEST, no start line), PEM 형식 혼동입니다. OpenSSL이 인증서를 기대하는 곳에 키나 CSR 파일을 제공했거나, PEM을 기대하는 곳에 DER 바이너리 파일을 제공한 경우입니다. head -1 filename 명령으로 실제 파일 형식을 확인할 수 있으며, 인증서는 -----BEGIN CERTIFICATE-----로 시작해야 합니다. DER 형식이라면 openssl x509 -inform der -in file.der -out file.crt 명령으로 변환하십시오.

nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch), 인증서와 키가 서로 일치하지 않는 경우입니다. 보통 생성 명령을 두 번 실행하여 파일이 뒤섞였을 때 발생합니다. openssl x509 -in git.internal.crt -noout -pubkey | sha256sum와 openssl pkey -in git.internal.key -pubout | sha256sum 명령을 각각 실행하여 확인하십시오. 해시 값이 일치하면 올바른 쌍입니다. 값이 다르다면 두 파일을 모두 다시 생성하십시오.

FAQ

자체 서명 인증서를 생성했는데도 Chrome에서 왜 "주의 요함"이라고 표시됩니까?

오류가 NET::ERR_CERT_AUTHORITY_INVALID인 경우, 인증서 자체는 문제가 없으나 Chrome이 아직 해당 인증서를 신뢰할 근거가 없기 때문입니다. 클라이언트의 신뢰 저장소(trust store)에 인증서(또는 사설 CA 루트)를 설치하십시오. Linux 환경에서 Chrome은 시스템 저장소가 아닌 certutil를 통해 NSS 데이터베이스를 사용한다는 점을 유의하십시오(Chrome 146 버전부터는 ~/.local/share/pki/nssdb 경로를 사용하며, 기존 ~/.pki/nssdb 파일이 존재할 경우 해당 파일을 사용합니다). 오류가 NET::ERR_CERT_COMMON_NAME_INVALID라면 인증서에 URL과 일치하는 Subject Alternative Name이 누락된 것이므로 -addext "subjectAltName=..."을 사용하여 다시 발급해야 합니다.

-k 옵션 없이 curl이 자체 서명 인증서를 신뢰하게 하려면 어떻게 합니까?

인증서(PEM 형식, .crt 확장자)를 /usr/local/share/ca-certificates/ 경로에 복사한 뒤 sudo update-ca-certificates를 실행하십시오. 출력 결과에 1 added이 포함되어야 합니다. 이후부터 curl은 공인 인증서와 동일하게 해당 인증서를 검증합니다. 시스템 설정을 변경하지 않고 일회성으로 요청하려면 curl --cacert /path/to/cert.crt을 사용하여 해당 파일로 검증하십시오. -k는 검증 과정을 완전히 비활성화하므로 스크립트 작성 시 사용해서는 안 됩니다.

자체 서명 인증서의 유효 기간은 어느 정도가 적당합니까?

기술적으로는 원하는 만큼 길게 설정할 수 있습니다. CA/Browser Forum의 제한(현재 200일, 2029년까지 47일로 단축 예정)은 공인 CA에만 적용되며 사설 인증서에는 적용되지 않습니다. 실무적으로는 Apple 기기에서 발급 기관과 관계없이 825일 이상의 인증서를 거부하므로 서버 인증서의 유효 기간을 825일 이하로 제한하십시오. 10년 유효 기간의 사설 루트 인증서와 2년(-days 730) 유효 기간의 리프(leaf) 인증서를 사용하는 것이 합리적인 기본값입니다. 만료된 내부 인증서는 아무도 기억하지 못하는 날짜에 모든 서비스를 예고 없이 중단시키므로, 갱신 일정을 반드시 기록해 두십시오.

자체 서명 인증서와 Let's Encrypt 중 무엇을 사용해야 합니까?

서비스에 공인 DNS 이름이 있고 인터넷에서 접근 가능하다면 항상 Let's Encrypt를 사용하십시오. 무료이며 자동화가 가능하고 대부분의 브라우저와 운영체제에서 이미 신뢰합니다(매우 오래된 기기와의 연결이 필요하다면 Let's Encrypt 호환성 목록을 확인하십시오). 자체 서명 인증서(또는 사설 CA)는 Let's Encrypt에서 발급할 수 없는 경우, 즉 사설 IP, .lan와 같은 내부 전용 호스트 이름, 폐쇄망(air-gapped network), VPN 뒤에 의도적으로 숨겨진 서비스에 사용합니다. 이 결정은 보안 강도가 아닌 접근성과 이름 지정 방식에 따른 것이며, 암호화 기술 자체는 동일합니다.

/usr/local/share/ca-certificates에 인증서를 추가했는데도 왜 거부됩니까?

다음 세 가지를 확인하십시오. 첫째, 파일 확장자가 반드시 .crt로 끝나야 합니다. .pem 확장자는 무시되며 update-ca-certificates 실행 시 0 added 오류가 발생합니다. 둘째, 파일 내용은 -----BEGIN CERTIFICATE-----로 시작하는 PEM 텍스트 형식이어야 하며 DER 바이너리 형식이어서는 안 됩니다. 셋째, 애플리케이션이 실제로 시스템 저장소를 사용하는지 확인하십시오. Linux용 Chrome, Firefox, Python requests, Node.js(--use-system-ca 옵션으로 시작하지 않은 경우), Java는 각각 별도의 신뢰 저장소를 유지하므로 인증서를 개별적으로 추가해야 합니다.