반응형

작년에 마이데이터 프로젝트에서 MTLS를 알게되었는데,

당시 openresty(nginx)에서 테스트를 하고 실제론 L7에서 검증하는 방식으로 진행하였다.

 

참고로 마이데이터는 EV급 인증서를 기준으로하기때문에 인증서도입비용이 크다.

그리고 EV급인증서는 공인된 CA기관으로부터 발급을 받는데 마이데이터 오픈당시에 3개의 rootCA기관이 있었다.

 

MTLS란

MTLS란 기존 TLS(https) 통신에서 더 나아가 클라이언트인증에 대한 부분을 추가한 상호인증이다.

그래서 클라이언트가 서버에 접근하고자하면 서버에서 검증할 rootCA기관에 서명을 받은 인증서를 제출해야한다.

이렇게 서로 검증된 MTLS는 B2B(기업간 비즈니스 통신)에서 널리 사용되고 있다. 

 

만약 클라이언트 인증서가 잘못되었거나 제출되지 않았을 경우 서버는 400 Bad Request 에러를 리턴해준다.

 

MTLS구현

필자는 MTLS 구현에 nginx를 사용한다.

간단히 설명하자면 아래와 같다.

1. rootCA 기관 인증서를 생성한다.(crt파일)

2. domain 인증요청서 생성한다.(csr파일)

3. rootCA에 서명받아 domain 인증서를 생성한다.(crt파일)

4. nginx에 tls설정과 client 인증에 대한 설정을 한다.(nginx.conf)

 

RootCA 인증기관

ca.key 생성

아래 명령어로 key를 생성한 뒤 password를 입력해준다.

$ openssl genrsa -aes256 -out ca.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
........................................................................+++++
...+++++
e is 65537 (0x010001)
Enter pass phrase for ca.key:
Verifying - Enter pass phrase for ca.key:

 

csr 생성

csr이란 Certificate Signing Request의 약자로 ssl 발급 신청을 위해 ca에 제출하는 요청서 파일이다.

위에서 생성한 key파일을 사용하며 지역, 도시, 위치, 기관, 팀, 이름, 이메일 내용을 기입하여 csr을 생성한다.

(일일이 입력하기 싫으면 파일로 생성하여 넣어줘도 된다)

$ openssl req -new -key ca.key -out ca.csr
Enter pass phrase for ca.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:Seoul
Locality Name (eg, city) []:Jongno-gu
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Metanet
Organizational Unit Name (eg, section) []:PlatformT
Common Name (e.g. server FQDN or YOUR name) []:DigisertJOONHYEOK
Email Address []:joon95

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:meta1234
An optional company name []:

 

인증서 생성(crt)

이제 key파일과 csr파일로 ca 인증서를 생성해보자.

필자는 인증서 유효기간을 30일로 주었으니 참고하라.

$ openssl x509 -req -days 30 -extensions v3_ca -set_serial 103 -in ca.csr -signkey ca.key -out ca.crt
Signature ok
subject=C = KR, ST = Seoul, L = Jongno-gu, O = Metanet, OU = PlatformT, CN = DigisertJOONHYEOK, emailAddress = joon95
Getting Private key
Enter pass phrase for ca.key:

 

생성한 rootCA 인증서 확인하기

이제 생성된 인증서의 내용을 확인해보자.

(생성시 입력한 Serial과 유효기간, 인증기관 이름 등)

$ openssl x509 -text -in ca.crt
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number: 103 (0x67)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = KR, ST = Seoul, L = Jongno-gu, O = Metanet, OU = PlatformT, CN = DigisertJOONHYEOK, emailAddress = joon95
        Validity
            Not Before: Sep  7 02:10:24 2022 GMT
            Not After : Oct  7 02:10:24 2022 GMT
        Subject: C = KR, ST = Seoul, L = Jongno-gu, O = Metanet, OU = PlatformT, CN = DigisertJOONHYEOK, emailAddress = joon95
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:d9:95:d3:ca:3b:69:79:f6:82:9f:bb:4a:1c:25:
                    35:5e:e1:24:14:ee:86:3b:f3:46:79:cb:12:51:d4:
                    f6:d3:1e:09:8c:c8:63:c2:46:1b:3a:fc:0f:86:f7:
                    74:36:5d:11:6d:4b:e9:1e:65:cd:88:92:3d:24:69:
                    68:67:15:d1:29:9a:0a:00:e1:47:76:4b:3a:ce:27:
                    0b:6f:7d:a7:24:09:79:bd:76:84:91:45:3c:34:e7:
                    33:d9:6d:65:3b:b0:0a:a5:11:ea:e3:fb:5c:82:df:
                    29:42:5a:88:70:00:81:18:2a:58:38:7b:0b:5b:83:
                    02:ac:b5:00:6f:3b:af:92:c4:a8:6e:58:10:46:b7:
                    6f:c0:e9:fc:cd:bf:b6:3d:43:e7:a2:4c:04:1a:8b:
                    24:ee:f1:d4:36:c6:7e:ee:cc:fa:f6:36:84:06:28:
                    43:2e:6a:14:cc:76:9a:d7:a2:4e:0d:1b:2f:9a:53:
                    a3:4a:df:02:fb:50:f6:ef:91:08:e1:cb:de:6d:44:
                    ac:66:36:93:8d:35:73:46:2b:d9:27:83:d1:20:7c:
                    de:e2:90:cb:30:16:93:c3:56:8b:82:e4:25:6e:ae:
                    d8:b5:a5:99:e6:9b:53:eb:b8:46:81:0d:33:a9:a4:
                    6b:cd:b6:e5:75:62:c5:45:15:2d:ba:54:d7:0a:d6:
                    ce:97
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha256WithRSAEncryption
         55:cf:b5:8d:29:b8:86:11:ac:00:b5:11:7f:56:8c:0b:d0:85:
         52:ff:4c:eb:44:5c:4b:ca:34:f7:49:d2:c9:59:06:c8:1f:93:
         e8:3d:24:5e:54:1e:bb:e5:ba:e8:e0:b6:c3:b3:51:5c:11:46:
         39:f2:f0:4c:72:d6:1b:46:d4:29:f7:10:0f:87:ba:31:4c:bc:
         e9:88:c3:7f:bd:46:5f:bd:47:26:e0:03:48:e5:f7:33:31:2e:
         d7:cd:7d:86:2c:d8:26:97:a2:7b:db:36:74:88:cc:64:90:33:
         a7:b9:50:75:7d:be:86:19:97:72:ff:31:57:5c:8a:ea:81:12:
         f2:15:d7:c0:ff:3f:f8:33:6d:e9:01:54:cc:27:43:d4:88:df:
         78:8f:cc:68:2c:16:0d:d1:be:54:ff:fd:17:24:5c:a6:83:15:
         89:55:63:e4:5e:db:ba:94:53:fe:c9:a9:0c:ae:c9:e8:77:81:
         34:5c:ae:1c:8b:e9:73:b4:53:ce:11:18:e2:67:ae:0c:a3:43:
         67:a2:a4:63:bb:1b:8e:ce:f6:99:5b:24:52:f9:ff:dc:e0:f2:
         94:dd:82:5d:c6:ba:b8:24:58:3d:17:e0:27:2f:36:45:0c:63:
         da:5f:0e:75:ae:7b:18:83:20:85:d0:93:a8:c9:d9:bd:ba:c2:
         13:61:6b:8d

 

Domain 인증서

이제 위에서 만든 rootCA에 서명을 받을 인증서를 만들어야한다.

필자는 myapi.joon95.com 라는 도메인을 임시로 사용할 예정이다.

 

key 생성 및 암호 제거

ca 생성시 key 파일을 만든 방법과 동일하다.

$ openssl genrsa -aes256 -out myapi.joon95.com.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
..........+++++
...+++++
e is 65537 (0x010001)
Enter pass phrase for myapi.joon95.com.key:
Verifying - Enter pass phrase for myapi.joon95.com.key:

이제 key 파일 암호를 제거하자.

제거하는 이유는 nginx에 tls 인증서를 넣고 사용하면 서비스 재기동마다 password를 입력해줘야하기 때문에 불편해서이다.

$ openssl rsa -in myapi.joon95.com.key -out myapi.joon95.com.nopass.key
Enter pass phrase for myapi.joon95.com.key:
writing RSA key

 

csr 생성

rootCA 생성과 동일한 명령어로, 실제 사용할 도메인만 다르게 해도 된다. 

$ openssl req -new -key myapi.joon95.com.nopass.key -out myapi.joon95.com.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:Incheon
Locality Name (eg, city) []:Namdung-gu
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Mansu-jugong
Organizational Unit Name (eg, section) []:ljh
Common Name (e.g. server FQDN or YOUR name) []:myapi.joon95.com
Email Address []:joonhyeok

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

 

인증서 생성(crt)

위에서 rootca 인증서를 생성하는 것과 다른 점은 아래 파라메타이다.

-CA ca.crt

-CAcreateserial

-CAkey ca.key

 

이 옵션들로인해 myapi.joon95.com 도메인의 인증서는 DigisertJOONHYEOK 라는 root기관에 서명된다.

$ openssl x509 -req -days 10 -extensions v3_user -in myapi.joon95.com.csr -CA ca.crt -CAcreateserial -CAkey  ca.key -out myapi.joon95.com.crt
Signature ok
subject=C = KR, ST = Incheon, L = Namdung-gu, O = Mansu-jugong, OU = ljh, CN = myapi.joon95.com, emailAddress = joonhyeok
Getting CA Private Key
Enter pass phrase for ca.key:

 

인증서 확인

rootCA기관에 서명된 도메인인증서를 보면, 발급자(Issuer)에 rootCA기관정보가 들어간 것을 볼 수있다.

$ openssl x509 -text -in myapi.joon95.com.crt
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            54:69:44:d4:db:1d:af:28:7d:4e:6b:ba:ca:d8:22:a5:73:f9:e0:3c
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = KR, ST = Seoul, L = Jongno-gu, O = Metanet, OU = PlatformT, CN = DigisertJOONHYEOK, emailAddress = joon95
        Validity
            Not Before: Sep  7 02:18:38 2022 GMT
            Not After : Sep 17 02:18:38 2022 GMT
        Subject: C = KR, ST = Incheon, L = Namdung-gu, O = Mansu-jugong, OU = ljh, CN = myapi.joon95.com, emailAddress = joonhyeok
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:bf:5f:e1:c9:01:61:3c:24:17:f1:b0:ee:4a:de:
                    4b:36:ae:a5:1f:5c:e3:e6:73:12:4b:05:20:b2:7a:
                    6a:cb:25:8f:11:4f:a7:2a:67:b2:48:d2:ea:d5:80:
                    ed:75:0d:af:c2:3a:9a:1c:55:e4:c7:3f:e7:ca:21:
                    99:fb:db:e0:08:9b:e6:6c:82:67:f0:29:bf:e6:86:
                    c2:9b:00:ab:24:0b:03:4a:ce:3a:b6:c4:11:d4:12:
                    fd:87:de:32:34:56:04:b8:31:bf:a5:0b:4d:07:92:
                    74:38:ae:f9:7d:b5:36:9a:c8:3d:1f:c1:b4:54:f0:
                    8c:4a:39:4d:5f:90:ab:b1:bd:5a:78:82:e6:b9:39:
                    08:d0:9a:27:41:03:0e:e9:e6:f5:e1:97:97:75:33:
                    2b:f5:f0:a9:bd:a1:79:3d:4e:4c:f5:79:d8:9b:22:
                    c2:50:29:53:c7:97:5a:55:5a:71:b4:a7:8f:4d:a4:
                    57:c9:c6:ee:d6:6d:72:ac:29:8b:6b:8f:41:6a:af:
                    78:0c:20:13:cf:52:f5:b6:ec:8b:de:32:db:a0:58:
                    10:be:fb:10:9a:21:7e:b3:34:ec:c2:0f:b1:66:a4:
                    4d:46:61:00:a3:e4:b1:84:90:e0:5d:0a:54:2b:0c:
                    7d:83:1f:15:85:a9:06:8d:37:a9:16:bb:da:a3:cb:
                    4d:0b
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha256WithRSAEncryption
         36:1e:cd:86:00:0b:43:25:2b:63:49:bc:53:5a:6b:88:b3:99:
         26:15:54:0b:31:d2:f7:04:4a:49:7b:2b:b9:a3:e5:09:26:b7:
         05:22:8b:bd:aa:87:0f:f2:c3:b0:df:be:43:91:8f:1b:57:84:
         b2:24:37:d4:99:06:4f:fd:ee:d4:d1:b4:09:bb:a1:d6:45:31:
         ec:4f:16:0e:70:da:f3:7a:d2:ed:6d:fe:99:88:07:1e:f9:2e:
         11:9a:ec:b8:15:8a:69:06:5f:01:a1:08:fb:c4:d8:50:c6:b1:
         a4:9e:96:a1:dc:ae:50:f8:31:1d:2a:54:0a:16:34:7e:18:cd:
         fc:ec:a0:21:47:e4:88:05:3f:af:84:73:7f:c6:6c:78:a6:d4:
         37:57:ef:51:9d:3e:c0:6f:96:55:e8:68:ef:2a:96:00:ef:5e:
         30:8f:c7:ce:a5:23:3a:0c:c9:08:e0:6e:aa:44:bb:a8:64:ab:
         07:d3:f1:36:da:6d:6e:26:61:63:40:28:c6:7d:94:aa:b9:78:
         1d:86:79:09:96:2b:2c:6b:0f:11:51:2c:44:67:5e:01:64:0c:
         b9:1d:2d:89:4b:0b:8c:c0:4b:db:36:b3:81:8b:e6:8f:7d:95:
         b0:f8:07:78:18:40:c0:02:29:23:bf:14:1f:12:e3:50:59:c9:
         a0:77:81:db

 

생성된 파일 목록

이제 아래 파일을 nginx conf.d 폴더 안에 ca라는 폴더를 생성한뒤 넣어줄 것이다.

실제로 사용될 파일은 ca.crt / myapi.joon95.com.crt / myapi.joon95.com.nopass.key 총 3개 파일이다.

# 생성된 파일
-rw-r--r-- 1 root root 1289 Sep  7 11:10 ca.crt
-rw-r--r-- 1 root root 1086 Sep  7 11:06 ca.csr
-rw------- 1 root root 1766 Sep  7 10:58 ca.key
-rw-r--r-- 1 root root   41 Sep  7 11:18 ca.srl
-rw-r--r-- 1 root root 1322 Sep  7 11:18 myapi.joon95.com.crt
-rw-r--r-- 1 root root 1058 Sep  7 11:17 myapi.joon95.com.csr
-rw------- 1 root root 1766 Sep  7 11:12 myapi.joon95.com.key
-rw------- 1 root root 1675 Sep  7 11:13 myapi.joon95.com.nopass.key

 

nginx.conf 설정

ubuntu 에서 nginx 기본 설치 시 경로는 /etc/nginx 이며,

nginx.conf 파일을 보면 설정파일들을 ./conf.d/ 폴더안에 모두 include 하고있기 때문에 /etc/nginx/conf.d 폴더 안에 테스트할 conf파일을 작성한다.

$ vi /etc/nginx/conf.d/mtls.conf
server {
	listen                     443 ssl;
	ssl_certificate            conf.d/ca/myapi.joon95.com.crt;
	ssl_certificate_key        conf.d/ca/myapi.joon95.com.nopass.key;

	ssl_client_certificate     conf.d/ca/ca.crt;
	ssl_verify_client          on;
	ssl_protocols              TLSv1.3;

	location / {
			return 200 'You client certificate OK';
	}
}

이제 클라이언트가 성공적으로 인증이 된다면 status code 200에 'You client certificate OK' 라는 문구를 보여줄 것이다.

(인증서파일은 conf.d/ca/ 안에 위치하게 이동시켜놓자)

 

호출하기

간단하게 curl 에서 --cert, --key 옵션으로 인증서를 제출할 수 있다.

 

제일먼저 hosts에 등록한 도메인을 로컬로 설정한다.
127.0.0.1       myapi.joon95.com

 

비정상(인증서 제출안할 때)

$ curl https://myapi.joon95.com -k
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

정상(인증서 제출시)

$ curl https://myapi.joon95.com -k --cert myapi.joon95.com.crt --key myapi.joon95.com.nopass.key
You client certificate OK

 

마치며..

이렇게 MTLS를 실제로 구성해보았다.

내용은 정말 간단한데 당시에 많이 고생했었던 부분이다..

추가로 상위기관 / 1차 중간인증기관 / 2차 중간인증기관 등등.. 레이어가 겹치면 depth를 설정할 수도 있다.

이미 많은 B2B 통신에 사용하고 있었다고 하니 당시에 놀랐었는데,

기업간의 비즈니스 로직을 구성하고 타 기업의 API 를 사용하게 될 때 MTLS와 같은 통신은 필수사항으로 보인다.

반응형
복사했습니다!