서브타이틀: 유니티 클라이언트와 rustls 서버 환경을 위한 신뢰 체인 구축
안녕하세요, 띵앤띵입니다! 😊
오늘은 OpenSSL을 이용해서 자체 서명된 Root CA, Intermediate CA, 그리고 최종 서버 인증서(Leaf)를 생성하고 체인으로 연결하는 과정을 소개할

게요.
💡 왜 이걸 딥다이빙하게 되었을까요?
최근 유니티 클라이언트를 사용하는 프로젝트에서 rustls 기반 HTTPS 통신을 구성하게 되었는데요. 이 rustls는 CA 인증 체인에 굉장히 엄격한 기준을 가지고 있더라고요. 예를 들면, 중간 CA가 없거나 잘못 서명된 경우 바로 통신을 차단해버려요. 😱
그래서 신뢰할 수 있는 인증서 체인을 스스로 만드는 방법을 제대로 정리해봤습니다. HTTPS나 내부 서비스의 보안을 강화하고 싶은 분들께 큰 도움이 될 거예요!
1. openssl.cnf 설정 (SAN 및 X.509v3 포함)
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = req_distinguished_name
req_extensions = req_ext
[ req_distinguished_name ]
C = KR
ST = Seoul
L = Seoul
O = THINKANDTHING
CN = THINKANDTHING # ← 도메인 주소를 THINKANDTHING으로 치환했어요
[ req_ext ]
subjectAltName = @alt_names
[ v3_ca ]
subjectAltName = @alt_names
[ alt_names ]
IP.1 = THINKANDTHING # ← 실제 IP는 변수로 치환했습니다
2. 루트 인증서 생성 (Root CA)
1. 개인키 생성 (비밀키)
openssl genrsa -out root-ca.key.pem 4096
2. X.509 v3 self-signed 루트 인증서 서명
자신의 루트 비밀키(root-ca.key.pem) 로 자체 서명한 인증서 생성 → root-ca.cert.pem
openssl req -x509 -new -nodes \
-key root-ca.key.pem \
-sha256 -days 3650 \
-out root-ca.cert.pem \
-subj "/C=KR/ST=Seoul/O=THINKANDTHING/CN=MyRootCA"
3. 중간 인증서 생성 (Intermediate CA)
# 개인키 생성(비밀키)
openssl genrsa -out intermediate-ca.key.pem 4096
# 중간인증서 CSR 생성
openssl req -new -sha256 \
-key intermediate-ca.key.pem \
-out intermediate-ca.csr.pem \
-subj "/C=KR/ST=Seoul/O=THINKANDTHING/CN=MyIntermediateCA"
# 중간인증서 서명 (Root 인증서를 KEY로 사용함)
openssl x509 -req -in intermediate-ca.csr.pem \
-CA root-ca.cert.pem \
-CAkey root-ca.key.pem \
-CAcreateserial \
-out intermediate-ca.cert.pem \
-days 3650 -sha256 \
-extfile openssl.cnf \
-extensions v3_ca
# 중간인증서가 CA 인지 확인 명령어
openssl x509 -in intermediate-ca.cert.pem -noout -text | grep -A 3 "X509v3"
정상 예시)
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
# rootCA 인증서로 intermediateCA 인증서가 제대로 발급되었는지 openssl 인증서 검증
openssl verify -CAfile root-ca.cert.pem intermediate-ca.cert.pem
# 정상 예시
intermediate-ca.cert.pem: OK
4. 서버 인증서 생성 (Leaf Certificate)
Leaf 인증서는 해당 인증서로 다른 인증서를 발급하지 못하는 인증서를 말합니다. 즉, CA 인증서가 아니라는 뜻입니다.
# 개인키 생성(비밀키)
openssl genrsa -out server.key.pem 2048
# CSR 생성 (SAN 포함, X.509 v3)
openssl req -new -key server.key.pem \
-out server.csr.pem \
-config openssl.cnf
# 서버 인증서 서명 (중간인증서의 KEY로 사용함)
openssl x509 -req -in server.csr.pem \
-CA intermediate-ca.cert.pem \
-CAkey intermediate-ca.key.pem \
-CAcreateserial \
-out server.cert.pem \
-days 3650 -sha256 \
-extensions req_ext \
-extfile openssl.cnf
5. 인증서 체인 구성 (fullchain)
rustls 는 해당 구조를 신뢰합니다.
# 서버인증서(1)와 중간인증서(2)를 연결한 인증서 체인 구성 → fullchain.pem은 서버 구성 시 클라이언트에 인증 체인을 전달하는 용도로 사용. 반드시 순서 지켜야함
cat server.cert.pem intermediate-ca.cert.pem > fullchain.pem
6. 인증서 검증
# 루트 인증서로 중간 체인을 참조하여 Leaf 인증서가 유효한지 확인
openssl verify -CAfile root-ca.cert.pem -untrusted fullchain.pem server.cert.pem
# 정상 예시
server.cert.pem: OK
## 이 조합은 "루트를 신뢰하면서, 중간 체인을 참조하여 leaf 인증서가 유효한지를 확인한다"
이 명령어는 다음의 의미를 가집니다:
-CAfile root-ca.cert.pem → 신뢰의 시작점(루트 인증서)
-untrusted fullchain.pem → 중간 인증서 제공 (이 파일에 중간 인증서가 포함되어 있어야 함)
server.cert.pem → 검증 대상 (leaf 인증서)
7. PFX 파일로 변환
# 자바 게이트웨이에 적용할 thinkandthing.pfx 생성
openssl pkcs12 -export \
-inkey server.key.pem \
-in fullchain.pem \
-out thinkandthing.pfx \
-password pass:123456
# 버전 확인
openssl x509 -in thinkandthing.pfx -text -noout
# 정상 예시
Enter pass phrase for PKCS12 import pass phrase:
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
11:e7:33:b0:dd:93:c8:02:95:38:b6:24:77:ae:55:80:6d:2e:21:9d
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=KR, ST=Seoul, O=thinkandthing, CN=MyIntermediateCA
Validity
Not Before: May 22 03:07:36 2025 GMT
Not After : Aug 25 03:07:36 2027 GMT
Subject: C=KR, ST=Seoul, L=Seoul, O=thinkandthing CO., LTD., CN=thinkandthing.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
~~
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
IP Address:192.168.0.178
X509v3 Subject Key Identifier:
2F:A9:C0:F2:D8:EB:83:23:59:94:CB:35:64:91:E0:A3:06:15:EA:4B
X509v3 Authority Key Identifier:
A9:57:3D:4D:AF:57:6E:54:97:FF:09:EC:00:B7:A2:BA:E4:6C:8B:04
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
~~
No Trusted Uses.
No Rejected Uses.
Key Id: 5F:24:05:73:0B:70:36:7D:06:1D:41:FC:68:F3:FB:3C:D8:7A:35:2D
# SAN 필드 확인
openssl x509 -in thinkandthing.pfx -noout -text | grep -A1 "Subject Alternative Name"
# 정상 예시
Enter pass phrase for PKCS12 import pass phrase:
X509v3 Subject Alternative Name:
IP Address:192.168.0.178
8. 서버에 적용 후 연결 확인
'7. PFX 파일로 변환' 과정에서 생성된 thinkandthing.pfx 파일을 SSL 적용이 필요한 서버에 적용 후 테스트
예를들면, 스프링클라우드에서 게이트웨이가 될 수도 있고 WAS (Nginx, Tomcat) 에 적용될 수 있음
# Gateway 서버에 반영 후 로컬에서 서버 반영된 CA 검증
openssl s_client -connect THINKANDTHING:443 -CAfile root-ca.cert.pem
# 정상 예시
Connecting to 192.168.0.178
CONNECTED(00000003)
Can't use SSL_get_servername
depth=2 C=KR, ST=Seoul, O=THINKANDTHING, CN=MyRootCA
verify return:1
depth=1 C=KR, ST=Seoul, O=THINKANDTHING, CN=MyIntermediateCA
verify return:1
depth=0 C=KR, ST=Seoul, L=Seoul, O=THINKANDTHING CO., LTD., CN=THINKANDTHING.com
verify return:1
---
Certificate chain
0 s:C=KR, ST=Seoul, L=Seoul, O=THINKANDTHING CO., LTD., CN=THINKANDTHING.com
i:C=KR, ST=Seoul, O=THINKANDTHING, CN=MyIntermediateCA
a:PKEY: RSA, 2048 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: May 22 03:07:36 2025 GMT; NotAfter: Aug 25 03:07:36 2027 GMT
1 s:C=KR, ST=Seoul, O=THINKANDTHING, CN=MyIntermediateCA
i:C=KR, ST=Seoul, O=THINKANDTHING, CN=MyRootCA
a:PKEY: RSA, 4096 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: May 22 03:04:28 2025 GMT; NotAfter: May 21 03:04:28 2030 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIEijCCAnKgAwIBAgIUEeczsN2TyAKVOLYkd65VgG0uIZ0wDQYJKoZIhvcNAQEL
...
xhuaRn1ZZ76rs0KSqIA=
-----END CERTIFICATE-----
subject=C=KR, ST=Seoul, L=Seoul, O=THINKANDTHING CO., LTD., CN=THINKANDTHING.com
issuer=C=KR, ST=Seoul, O=THINKANDTHING, CN=MyIntermediateCA
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: rsa_pss_rsae_sha256
Peer Temp Key: X25519, 253 bits
---
SSL handshake has read 3129 bytes and written 1604 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Server public key is 2048 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: ....
Session-ID-ctx:
Resumption PSK: ....
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 86400 (seconds)
TLS session ticket:
0000 - ...
0010 - ...
Start Time: 1748235412
Timeout : 7200 (sec)
Verify return code: 0 (ok)
Extended master secret: no
Max Early Data: 0
---
9. 해당 인증서를 사용할 클라이언트에게 전달
클라이언트에게는 2단계 과정에서 생성했던 root-ca.cert.pem 해당 인증서만 전달해야함!
서버(Gateway) 측 인증서 파일 관리 원칙
| 파일 | Git 업로드 | 외부 배포(클라이언트) | 비고 |
| server.key.pem | ❌ 절대 금지 | ❌ 절대 금지 | 개인키(비밀키) → 유출 시 인증서 무력화됨 |
| server.cert.pem | ❌ (가능한 제한) | ❌ | Leaf 인증서, 공개여도 되지만 독립적 의미 없음 |
| intermediate-ca.cert.pem | ❌ | ❌ | 중간 인증서, 서버에서 체인 전달용 |
| fullchain.pem | ❌ | ❌ | 서버 내부에서 TLS 체인용 사용 |
| root-ca.cert.pem | ❌ (선택사항) | ✅ 유일하게 전달 가능 | 클라이언트 신뢰 시작점 |
✨ 마무리
이제 복잡해 보였던 인증서 발급과 체인 구성도 문제없죠?
특히 엄격한 보안 요구사항을 가진 환경(rustls, 유니티 클라이언트 등)에서는 필수적인 구성입니다.
궁금한 점은 댓글로 남겨주세요! 😊
'기타' 카테고리의 다른 글
| [AWS] EC2 pem 키 SSH 접속시 bad permissions (0) | 2022.10.12 |
|---|---|
| [MiniO] minio 사용법 (0) | 2022.09.22 |
| [GraphQL] 전체 스키마 조회 쿼리 (0) | 2022.09.22 |
댓글