Demo App Github : https://github.com/Great-Stone/vault-mtls-demo
SSL(Secure Sokets Layer, 보안 소캣 계층)는 클라이언트와 서버 사이에 전송된 데이터를 암호화 하고 인터넷 연결에 보안을 유지하는 표준 기술이다. 악의적 외부인이 클라이언트와 서버 사이에 전송되는 정보를 확인 및 탈취하는 것을 방지한다.
TLS(Transport Layer Security, 전송 계층 보안)는 현재 더이상 사용되지 않는 SSL을 계승하는 보다 진보된 보안 기술이다. SSL 3.0을 기반으로 만들어졌지만 호환되지는 않는다. 최신 버전은 TLS 1.3이다.
- TLS 1.2의 경우 암호화 방식과 키 교환 통신이 handshake 과정에 포한되어있어 2회 정도의 추가 요청이 있었다.
- TLS 1.3에서는 handshake과정을 최소화해 암호화 통신하는 방안이 추가되어 HTTPS 통신에 속도와 보안이 개선되었다.
- handshake에 0-RTT 모드 추가
- 정적인 RSA와 Diffie-Hellman Cipher Suite 제거
- handshake 최대한 암호화
- 키 교환과 암호화 방식을 Cipher Suite를 통해 묶어서 정하지 않고 개별적 지정
SSL 기술이 TLS로 대체되었다고 하지만 여전히 브라우저 인증서는 SSL 인증서라고 불린다.
sequenceDiagram
participant Client
participant Server
Client->>Server: 1. Client Hello
Server->>Client: 2. Server Hello
Server->>Client: 3. Certificate*
Server->>Client: 4. Server Key Exchange*
Server->>Client: 5. Server Hello Done
Client->>Server: 6. Client Key Exchange
Client->>Server: 7. Change Cipher Spec
Client->>Server: 8. Finished
Server->>Client: 9. Change Cipher Spec
Server->>Client: 10. Finished
Note over Client,Server: TLS Handshake completed
- Client Hello (클라이언트 인사) - TLS 연결을 시작하려는 클라이언트가 서버에게 보내는 첫 번째 메시지로, 이 메시지는 클라이언트가 사용 가능한 암호화 방법, 버전 등의 정보를 서버에게 제공한다.
- Server Hello (서버 인사) - 서버는 클라이언트가 보낸 Client Hello 메시지에 응답하여 서버의 TLS 버전 및 암호화 방법, 그리고 세션 ID를 클라이언트에게 보내는 메시지이다.
- Certificate (인증서) - 선택적인 단계로, 서버는 자신의 인증서를 클라이언트에게 보낸다. 이 인증서는 서버의 공개 키와 함께 클라이언트에게 보내지며, 클라이언트는 이를 사용하여 서버의 신원을 확인할 수 있다.
- Server Key Exchange (서버 키 교환) - 선택적인 단계로, 서버는 이 단계에서 클라이언트와 함께 사용할 대칭키를 생성하고, 이를 암호화하여 클라이언트에게 보낼 수 있다. 이를 통해 클라이언트와 서버는 이후의 암호화된 통신에 사용할 대칭키를 공유할 수 있다.
- Server Hello Done (서버 인사 완료) - 서버는 이 단계에서 Handshake 과정의 마지막 단계를 알리기 위해 이 메시지를 보낸다.
- Client Key Exchange (클라이언트 키 교환) - 클라이언트는 서버가 보낸 인증서와 공개키를 사용하여 서버의 신원을 확인하고, 이후의 통신에 사용할 대칭키를 생성하여 서버에게 보낸다.
- Change Cipher Spec (암호화 변경) - 클라이언트와 서버는 이 메시지를 보내 서로가 이후에 암호화된 메시지를 주고받을 준비가 되었음을 알린다.
- Finished (완료) - 클라이언트와 서버는 각각 이 메시지를 보내 상대방과의 Handshake 과정이 성공적으로 끝났음을 알립니다. 이후부터는 암호화된 데이터를 주고받을 수 있다다.
- TLS Handshake 완료 - 클라이언트와 서버는 Handshake 과정이 완료되었으며, 이후의 암호화된 통신을 시작할 수 있다.
mTLS는 상호 인증방법으로 기존 TLS가 클라이언트가 서버에 대한 인증서를 검증하는 과정이였다면 양쪽의 Key를 확인하여 상호간 인증서를 검증한다는 점에 차이가 있다.
sequenceDiagram
participant Client
participant Server
Client->>Server: Client Hello
Server->>Client: Server Hello
Server->>Client: Certificate*
Server->>Client: Server Key Exchange*
Server->>Client: Certificate Request*
Client->>Server: Certificate*
Client->>Server: Client Key Exchange
Client->>Server: Certificate Verify*
Client->>Server: Change Cipher Spec
Client->>Server: Finished
Server->>Client: Change Cipher Spec
Server->>Client: Finished
Note over Client,Server: mTLS Handshake completed
- Client Hello (클라이언트 인사) - TLS 연결을 시작하려는 클라이언트가 서버에게 보내는 첫 번째 메시지로, 이 메시지는 클라이언트가 사용 가능한 암호화 방법, 버전 등의 정보를 서버에게 제공한다.
- Server Hello (서버 인사) - 서버는 클라이언트가 보낸 Client Hello 메시지에 응답하여 서버의 TLS 버전 및 암호화 방법, 그리고 세션 ID를 클라이언트에게 보내는 메시지이다.
- Certificate (인증서) - 선택적인 단계로, 서버는 자신의 인증서를 클라이언트에게 보낼 수 있다. 이 인증서는 서버의 공개 키와 함께 클라이언트에게 보내지며, 클라이언트는 이를 사용하여 서버의 신원을 확인한다.
- Server Key Exchange (서버 키 교환) - 선택적인 단계로, 서버는 이 단계에서 클라이언트와 함께 사용할 대칭키를 생성하고, 이를 암호화하여 클라이언트에게 보낼 수 있다. 이를 통해 클라이언트와 서버는 이후의 암호화된 통신에 사용할 대칭키를 공유할 수 있다.
- Certificate Request (인증서 요청) - 선택적인 단계로, 서버는 클라이언트에게 자신의 인증서를 제출해야 함을 알리는 요청 메시지를 보낼 수 있다. 이 요청에 대한 응답으로, 클라이언트는 자신의 인증서와 공개키를 서버에게 보낼 수 있다.
- Certificate Verify (인증서 검증) - 상호 인증을 사용하는 경우 포함되는 과정이다. 클라이언트는 이 단계에서 자신이 보낸 인증서가 유효하다는 것을 증명하기 위해 서명을 생성하여 서버에게 보내야 한다.
- Client Key Exchange (클라이언트 키 교환) - 클라이언트는 서버가 보낸 인증서와 공개키를 사용하여 서버의 신원을 확인하고, 이후의 통신에 사용할 대칭키를 생성하여 서버에게 보낸다.
- Change Cipher Spec (암호화 변경) - 클라이언트와 서버는 이 메시지를 보내 서로가 이후에 암호화된 메시지를 주고받을 준비가 되었음을 알린다.
먼저 mTLS의 장점을 살펴보면,
- 서버와 클라이언트 간의 상호 인증을 가능하게 하므로, 서버와 클라이언트 모두 신뢰할 수 있는 대상인지 확인할 수 있다. 이를 통해 중간자 공격 및 위조된 인증서와 같은 보안 문제를 방지할 수 있다.
- mTLS는 암호화된 연결을 통해 전송되는 데이터의 안전성을 보장한다. TLS 프로토콜을 사용하므로, 데이터는 암호화되어 전송되며, 중간자 공격 및 도청과 같은 공격으로부터 안전하게 보호된다.
mTLS의 단점은 다음과 같다.
- 연결을 설정하는 과정에서 추가적인 CPU 리소스와 대역폭이 필요할 수 있습니다. 이는 특히 고사양의 서버에서 큰 부담이 될 수 있습니다.
- 서버와 클라이언트 모두가 인증서를 발급하고 관리해야 한다는 점이 있습니다. 인증서를 발급하는 과정은 복잡할 수 있으며, 이를 관리하는 것도 일정한 노력과 비용이 필요합니다.
- mTLS를 구현하는 것은 애플리케이션과 서버 모두에게 추가적인 복잡성을 요구할 수 있습니다. 이를 위해 애플리케이션과 서버 모두에 대한 추가적인 설정 및 관리가 필요할 수 있습니다.
볼트가 제공하는 PKI 기능과 Agent의 자동 교체 기능을 활용하여 인증서 관리와 발급을 자동화하여 애플리케이션과 서버에 대한 부담을 줄이고 mTLS의 장점을 취할 수 있다.
- 참고 : https://bitgadak.tistory.com/5
- openssl 대신 smallstep 을 사용하면 좀더 간단하다 : https://smallstep.com/hello-mtls/doc/client/requests
- socket example : https://www.electricmonk.nl/log/2018/06/02/ssl-tls-client-certificate-verification-with-python-v3-4-sslcontext/
OpenSSL을 활용하여 볼트를 사용하지 않고 mTLS를 구현하는 과정을 설명한다.
root ca 생성을 위한 root key를 생성한다.
cd cert
openssl genrsa -out root.key 2048
OS에 따라(Linux/MacOS) 권한 변경이 권장된다.
chmod 600 root.key
생성된 root.key
기반의 root ca 인증서 생성을 위한 요청서를 생성한다.
$ openssl req -config ca.conf -extensions usr_cert -new -key root.key -out ca.csr
-config
: 미리 구성해 놓은 ca용 구성 정보를 읽는다.
openssl-xxx.conf
sample
구분 | 작성 예 |
---|---|
Country Name (국가코드) | KR |
State or Province Name (시/도의 전체이름) | Seoul |
Locality Name (시/군/구 등의 이름) | Songpa-gu |
Organization (회사이름) | XXXX |
Organization Unit (부서명) | Server |
Common Name (SSL 인증서를 설치할 서버의 Full Domain) | www.xxxx.com |
$ openssl req -text -in ca.csr
Certificate Request:
Data:
Version: 0 (0x0)
Subject: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=example root
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
<...생략...>
Exponent: 65537 (0x10001)
Attributes:
Requested Extensions:
X509v3 Basic Constraints:
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
<...생략...>
-----BEGIN CERTIFICATE REQUEST-----
<...생략...>
-----END CERTIFICATE REQUEST-----
생성된 요청서에 대해 자체 서명(self-signning)한다.
openssl x509 -req -days 3650 -in ca.csr -signkey root.key -extfile ca.ext -out ca.crt
-days
: 인증서 기간은 10년으로 하였다.-extfile
: 서명시 추가 정보에 대한 내용을 읽는다.
$ openssl x509 -text -noout -in ca.crt
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
ee:38:a2:de:5e:b2:11:c8
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=example root
Validity
Not Before: Mar 15 03:04:58 2023 GMT
Not After : Mar 12 03:04:58 2033 GMT
Subject: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=example root
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
<...생략...>
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
<...생략...>
생성된 root ca 파일을 시스템에 신뢰할 수 있는 인증서로 등록하면, 브라우저 호출시 신뢰할 수 없는 인증서로 인한 경고 창이 뜨지 않는다.
- MacOS의 경우
ca.crt
를 더블클릭하여키체인 접근
앱에인증서
탭에 등록하고, 등록된example.com
인증서를 더블클릭하여신뢰
항목에서이 인증서 사용 시
를항상 신뢰
로 변경한다. - RedHat 계열 OS의 경우
/etc/pki/ca-trust/source/anchors/
에 인증서를 복사 한 후,update-ca-trust
명령을 실행한다. - Windows의 경우
ca.crt
를 더블클릭하여 인증서 창의인증서 설치...
를 클릭,인증서 가져오기 마법사
로 신뢰할 수 있는 인증서로 등록한다.
데모 서비스 A용 인증서를 생성하기 위해 해당 인증서를 위한 key를 생성한다. 생성 시 패스워드를 넣어주며, 패스워드 없는 key를 생성하려는 경우 한번더 풀어주는 과정이 필요하다.
# 패스워드 4자리 이상 입력
openssl genrsa -aes256 -out service-a-with-pw.key 2048
# 패스워드 없는 key
openssl rsa -in service-a-with-pw.key -out service-a.key
서비스 A용 인증서를 위한 요청서를 생성한다.
openssl req -new -key service-a.key -config service-a.conf -out service-a.csr
-config
: 미리 구성해 놓은 서비스 A용 구성 정보를 읽는다.
자체 서명과정에서 앞서 생성한 root ca 인증서와 key를 넣어 서비스 A인증서가 root ca에 종속되도록 구성한다.
openssl x509 -req -days 365 -in service-a.csr -extfile service-a.ext -CA ca.crt -CAkey root.key -CAcreateserial -out service-a.crt
$ openssl x509 -text -in service-a.crt
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
ec:71:b0:dd:72:c2:a2:4a
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=example root
Validity
Not Before: Mar 15 03:36:06 2023 GMT
Not After : Mar 14 03:36:06 2024 GMT
Subject: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=service-a.example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
<..생략..>
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:service-a.example.com
Signature Algorithm: sha256WithRSAEncryption
<..생략..>
-----BEGIN CERTIFICATE-----
<..생략..>
-----END CERTIFICATE-----
-days
: 인증서 기간을 1년으로 하였다.-CA
: root ca 인증서를 지정한다.-CAkey
: root ca의 key를 지정한다.-CAcreateserial
: 서명 작업에 root ca가 인증서에 대한 일련번호 생성-extfile
: 서비스 A를 위한 추가 정보
서비스 B에 대한 인증서도 생성한다. 앞서 설명된 내용을 생략하고 아래 커맨드만 나열한다.
openssl genrsa -aes256 -out service-b-with-pw.key 2048
openssl rsa -in service-b-with-pw.key -out service-b.key
openssl req -new -key service-b.key -config service-b.conf -out service-b.csr
openssl x509 -req -days 365 -in service-b.csr -extfile service-b.ext -CA ca.crt -CAkey root.key -CAcreateserial -out service-b.crt
데모 앱은 Python으로 구성되었다.
$ python --version
Python 3.10.5
$ pip --version
pip 23.0.1
$ pip install requests flask
127.0.0.1 service-a.example.com service-b.example.com
cd python_service_a
python main.py
cd python_service_b
python main.py
Python으로 작성된 flask api server 구성은 다음과 같다.
# main.py
### 생략 ###
if __name__ == "__main__":
app.debug = True
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile='../cert/ca.crt')
ssl_context.load_cert_chain(certfile=f'../cert/{src}.crt', keyfile=f'../cert/{src}.key', password='')
# ssl_context.verify_mode = ssl.CERT_REQUIRED
app.run(host="0.0.0.0", port=src_port, ssl_context=ssl_context, use_reloader=True, extra_files=[f'../cert/{src}.crt'])
ssl.create_default_context
: flask에서 사용할 ssl context를 정의한다. 여기cafile
에 root ca 파일을 지정한다.ssl_context.load_cert_chain
: cert와 key를 지정하여 인증서 체인을 설정한다.ssl_context.verify_mode
: service A는 인증서 검증을 무시할 수 있도록 해당 옵션에 주석처리 한다.app.run(..., extra_files=[f'../cert/{src}.crt'])
: 인증서가 변경되면 flask를 다시 시작하도록 구성한다.
서비스 A의 경우 https로 접근할 수 있고, ssl.CERT_REQUIRED
옵션이 활성화 되어있지 않아 신뢰할 수 없는 인증서라도 curl로 --insecure
옵션을 추가하여 응답을 확인할 수 있다. 브라우저에서도 별도의 신뢰 확인을 통해 접근가능하다.
$ curl https://service-a.example.com:7443
curl: (60) SSL certificate problem: self signed certificate in certificate chain
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.
$ curl --insecure https://service-a.example.com:7443
Hello from "service-a"%
Python으로 작성된 flask api server 구성은 다음과 같다.
# main.py
### 생략 ###
if __name__ == "__main__":
app.debug = True
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile='../cert/ca.crt')
ssl_context.load_cert_chain(certfile=f'../cert/{src}.crt', keyfile=f'../cert/{src}.key', password='')
ssl_context.verify_mode = ssl.CERT_REQUIRED
app.run(host="0.0.0.0", port=src_port, ssl_context=ssl_context, use_reloader=True, extra_files=[f'../cert/{src}.crt'])
ssl_context.verify_mode = ssl.CERT_REQUIRED
설정으로 인해 인증서 검증이 반드시 필요하도록 설정한다.
--insecure
옵션을 추가하더라도 서비스 B는 인증서를 요구한다.
$ curl --insecure https://service-b.example.com:8443
curl: (56) LibreSSL SSL_read: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0
따라서 요청시 root ca, cert(인증서), key를 함께 사용해야 한다.
$ curl --cacert ca.crt --key service-b.key --cert service-b.crt https://service-b.example.com:8443
서비스 A에서 B로 요청할 때 인증서 모두를 설정한 경우이다. 응답이 정상적으로 오는지 확인한다.
https://service-a.example.com:7443/w-mtls
서비스 A에서 B로 요청할 때 A의 인증정보를 담지 않은 경우이다. 서비스 B에서 인증서를 요구하는 메시지가 출력된다.
https://service-a.example.com:7443/wo-cert-mtls
# 응답
SSLError(1, '[SSL: TLSV13_ALERT_CERTIFICATE_REQUIRED] tlsv13 alert certificate required')
서비스 A에서 B로 요청할 때 root ca를 포함하지 않는 경우이다. 인증을 위한 자체 서명 인증서를 요구한다.
https://service-a.example.com:7443/wo-ca-mtls
# 응답
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain')
faketime : https://github.com/wolfcw/libfaketime
faketime
을 사용하여 서비스 A의 인증서 만료 기간을 현재시간 이전으로 만든다.
faketime '2023-01-01 00:00:00' /bin/bash -c 'openssl x509 -req -days 30 -in service-a.csr -extfile service-a.ext -CA ca.crt -CAkey root.key -CAcreateserial -out service-a.crt'
서비스 A가 보유한 인증서가 만료된 경우 인증서 만료됨을 표기한다. (서비스 B 인증서는 정상)
https://service-a.example.com:7443/w-mtls
# 응답
SSLError(SSLError(1, '[SSL: SSLV3_ALERT_CERTIFICATE_EXPIRED] sslv3 alert certificate expired')
faketime : https://github.com/wolfcw/libfaketime
faketime
을 사용하여 서비스 B의 인증서 만료 기간을 현재시간 이전으로 만든다.
faketime '2023-01-01 00:00:00' /bin/bash -c 'openssl x509 -req -days 30 -in service-b.csr -extfile service-b.ext -CA ca.crt -CAkey root.key -CAcreateserial -out service-b.crt'
서비스 B가 보유한 인증서가 만료된 경우 인증서 만료됨을 표기한다. (서비스 A 인증서는 정상)
https://service-a.example.com:7443/w-mtls
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired')
A와 B의 인증서 Root CA가 다른 경우 인증서 서명이 다르므로 요청 실패한다. 아래와 같이 서비스 B를 위한 인증서를 root ca부터 새로 생성한다.
cd cert
openssl genrsa -out root-b.key 2048
chmod 600 root-b.key
openssl req -config ca.conf -extensions usr_cert -new -key root-b.key -out ca-b.csr
openssl x509 -req -days 3650 -in ca-b.csr -signkey root-b.key -extfile ca-b.ext -out ca-b.crt
openssl genrsa -aes256 -out service-b-with-pw.key 2048
openssl rsa -in service-b-with-pw.key -out service-b.key
openssl req -new -key service-b.key -config service-b.conf -out service-b.csr
openssl x509 -req -days 365 -in service-b.csr -extfile service-b.ext -CA ca-b.crt -CAkey root-b.key -CAcreateserial -out service-b.crt
python_service_b
의 main.py
에 서 ca 파일 이름을 변경한다.
if __name__ == "__main__":
app.debug = True
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile='../cert/ca-b.crt')
ssl_context.load_cert_chain(certfile=f'../cert/{src}.crt', keyfile=f'../cert/{src}.key', password='')
ssl_context.verify_mode = ssl.CERT_REQUIRED
app.run(host="0.0.0.0", port=src_port, ssl_context=ssl_context, use_reloader=True, extra_files=[f'../cert/{src}.crt'])
요청 시 서비스 A와 B의 서명이 달라 오류가 발생함을 확인한다.
https://service-a.example.com:7443/w-mtls
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate signature failure')
테스트가 끝났으면 다시 root ca 파일을 원래의 같은 ca.crt
파일로 지정한다.
Vault Download : https://releases.hashicorp.com/vault/
Vault의 인증서 관리 및 자동화 관리 방안을 설명한다.
vault server -dev -dev-root-token-id=root
export VAULT_ADDR='http://127.0.0.1:8200'
$ vault login
Token (will be hidden): root
vault secrets enable pki
Vault 기본 Max TTL
은 32일(786h) 이므로 원하는 TTL
로 변경한다.
vault secrets tune -max-lease-ttl=87600h pki
vault write pki/root/generate/internal \
key_bits=2048 \
private_key_format=pem \
signature_bits=256 \
country=KR \
province=Seoul \
locality=KR \
organization=COMPANY \
ou=DEV \
common_name=example.com \
ttl=87600h
Certificate Revocation List(인증서 해지 목록) 엔드포인트 작성
vault write pki/config/urls \
issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" \
crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl"
미리 Role을 구성해 놓으면 사용자 및 앱은 지정된 규칙에 따라 인증서를 발급받을 수 있다.
vault write pki/roles/example-dot-com \
allowed_domains=example.com \
allow_subdomains=true \
max_ttl=72h
vault write pki/issue/example-dot-com \
common_name=service-a.example.com
vault_agent
디렉토리에서 작업한다.
Vault Agent는 볼트가 가지고 있는 시크릿 정보를 발급 및 TTL
만료 시 자동 갱신해주는 역할을 수행한다.
Vault Agent가 취득할 정책을 추가한다. 앞서 생성한 PKI 시크릿 엔진에 대한 권한이 설정되어있다.
$ vault policy write pki pki_policy.hcl
$ vault auth enable approle
Success! Enabled approle auth method at: approle/
$ vault write auth/approle/role/pki-agent \
secret_id_ttl=120m \
token_ttl=60m \
token_max_tll=120m \
policies="pki"
Success! Data written to: auth/approle/role/pki-agent
$ vault read auth/approle/role/pki-agent/role-id
Key Value
--- -----
role_id dfa2a248-1e1b-e2e9-200c-69c63b9ca447
$ vault write -f auth/approle/role/pki-agent/secret-id
Key Value
--- -----
secret_id 864360c1-c79f-ea7c-727b-7752361fe1ba
secret_id_accessor 3cc068e2-a172-2bb1-c097-b777c3525ba6
Vault Agent 실행 시 approle 인증방식을 사용하도록 구성하는 예제로, role_id
와 secret_id
가 필요하다. Vault Agent 재기동시에는 secret_id
를 재발급 해야 한다.
$ vault read -field=role_id auth/approle/role/pki-agent/role-id > roleid
$ vault write -f -field=secret_id auth/approle/role/pki-agent/secret-id > secretid
Vault Agent는 Template에 따라 시크릿을 특정 파일로 랜더링하는 기능을 갖고 있다.
# ca-a.tpl
{{- /* ca-a.tpl */ -}}
{{ with secret "pki/issue/example-dot-com" "common_name=service-a.example.com" "ttl=2m" }}
{{ .Data.issuing_ca }}{{ end }}
위 구문은 pki/issue/example-dot-com
에서 common_name=service-a.example.com
인 인증서를 발급하는 것으로, 테스트를 위해 ttl=2m
로 짧게 설정하였다. 볼트로 부터 받는 결과 중에서 issuing_ca
값을 랜더링한다.
vault_agent.hcl
에서는 위 Template에 대한 랜더링 결과를 특정 파일로 저장하도록 명시한다.
template {
source = "ca-a.tpl"
destination = "../cert/ca.crt"
}
vault agent -config=vault_agent.hcl -log-level=debug
지정된 TTL
간격마다 템플릿 랜더링 로그 확인한다.
...
2023-03-18T22:29:09.312+0900 [DEBUG] (runner) rendering "ca-a.tpl" => "../cert/ca.crt"
2023-03-18T22:29:09.312+0900 [DEBUG] (runner) checking template a04612e63b9a03a45ef968a8984a23db
2023-03-18T22:29:09.312+0900 [DEBUG] (runner) rendering "cert-a.tpl" => "../cert/service-a.crt"
2023-03-18T22:29:09.312+0900 [DEBUG] (runner) checking template 850589d81f7afe64c7c5a0a8440c8569
2023-03-18T22:29:09.312+0900 [DEBUG] (runner) rendering "key-a.tpl" => "../cert/service-a.key"
2023-03-18T22:29:09.312+0900 [DEBUG] (runner) checking template 60e7f2683d2c76a501eb54879bf89ad2
2023-03-18T22:29:09.312+0900 [DEBUG] (runner) rendering "cert-b.tpl" => "../cert/service-b.crt"
2023-03-18T22:29:09.333+0900 [INFO] (runner) rendered "cert-b.tpl" => "../cert/service-b.crt"
2023-03-18T22:29:09.333+0900 [DEBUG] (runner) checking template 1fb22b9f15857b7eeb0b68a3c9ac6d20
2023-03-18T22:29:09.334+0900 [DEBUG] (runner) rendering "key-b.tpl" => "../cert/service-b.key"
2023-03-18T22:29:09.354+0900 [INFO] (runner) rendered "key-b.tpl" => "../cert/service-b.key"
랜더링이 완료되고, 파일이 갱신되면 Python의 Flask 설정의 extra_files
항목이 변경되므로 재시작되어 인증서를 다시 읽어온다.
* Detected change in '/vault-examples/mtls-pki/cert/130906523', reloading
* Detected change in '/vault-examples/mtls-pki/cert/service-a.crt', reloading
* Restarting with watchdog (fsevents)
* Debugger is active!
* Debugger PIN: 136-647-438
변경된 인증서를 확인해보면 갱신된 유효기간을 확인할 수 있고, 브라우저에서도 인증서 보기를 통해 변경된 인증서의 유효기간을 확인할 수 있다.
인증서 같은 시크릿은 파일 형태로 관리되는데, 이런 파일이 변경되면 애플리케이션 또는 웹서버나 솔루션에서 감지하는 구성이 필요하다. 데모 앱인 Python의 Flask에서는 Debug모드에 extra_files
에 인증서를 지정하여 변경되는 인증서를 감지하도록 하였으나 이는 운영에서는 권장되지 않는 방식이며 인증서 교체와 함께 watch
, reload
, restart
에 대한 동작이 요구된다.
애플리케이션에서 내부적으로 코드 구현을 통해 이를 교체하는 방법도 있으나, mTLS가 적용되는 코드 전반에 변경이 필요하므로 HasihCorp Nomad같은 Vault 연계된 애플리케이션 오케스트레이터를 활용할 수 있다.
Vault의 인증서 관리 및 자동화 관리 방안을 Nomad와 연계하여 설명한다.
Nomad Download : https://releases.hashicorp.com/nomad/
- Vault 서버는 그대로 두고, PKI를 기존것을 사용한다.
- 서비스 A와 B는 종료한다.
- Vault Agent는 종료한다.
준비된 Policy 및 Job은 nomad
디렉토리에 있다.
Nomad 에 부여할 Vault의 정책을 생성한다.
vault policy write nomad-server nomad_policy.hcl
Nomad 에서 사용할 Token Role을 생성한다. Nomad에서 허용되는 정책은 앞서 생성한 pki
이다.
vault write auth/token/roles/nomad-cluster allowed_policies="pki" disallowed_policies=nomad-server token_explicit_max_ttl=0 orphan=true token_period="259200" renewable=true
생성한 Token Role 기반으로 Nomad와의 설정에 사용할 Token을 하나 발급한다.
vault token create -field token -policy nomad-server -period 72h -orphan > /tmp/token.txt
Nomad를 실행한다.
$ nomad agent -dev -vault-enabled=true -vault-address=http://127.0.0.1:8200 -vault-token=$(cat /tmp/token.txt) -vault-tls-skip-verify=true -vault-create-from-role=nomad-cluster
==> No configuration files loaded
==> Starting Nomad agent...
==> Nomad agent configuration:
Advertise Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
Bind Addrs: HTTP: [127.0.0.1:4646]; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
Client: true
Log Level: DEBUG
Region: global (DC: dc1)
Server: true
Version: 1.5.1
==> Nomad agent started! Log data will stream in below:
...
2023-03-19T15:34:30.081+0900 [DEBUG] nomad.vault: starting renewal loop: creation_ttl=72h0m0s
2023-03-19T15:34:30.082+0900 [DEBUG] nomad.vault: successfully renewed server token
2023-03-19T15:34:30.082+0900 [INFO] nomad.vault: successfully renewed token: next_renewal=35h59m59.999944054s
...
export NOMAD_ADDR='http://127.0.0.1:4646'
Nomad Job을 해석하면 다음과 같다.
job "mtls-service-a" {
datacenters = ["dc1"]
type = "service"
group "service" {
count = 1
network {
port "https" {
static = 7433
}
}
# vault에서 할당받을 Polocy를 명시 한다.
# 해당 Policy로 생성되는 Token의 변경시 동작은 change_mode에서 지정한다.
vault {
namespace = ""
policies = ["pki"]
change_mode = "noop"
}
task "python-task" {
driver = "raw_exec"
config {
command = "local/start.sh"
}
template {
data = <<EOH
#!/bin/bash
cp -R /Users/gs/workspaces/hashicorp_example/vault-examples/mtls-pki/python_service_a python_service_a
cd python_service_a
pip install requests flask
python main.py
EOH
destination = "local/start.sh"
}
# Vault Agent에서 구성했던 Template이 Job내에 정의된다.
template {
data = <<EOH
{{- /* ca-a.tpl */ -}}
{{ with secret "pki/issue/example-dot-com" "common_name=service-a.example.com" "ttl=2m" }}
{{ .Data.issuing_ca }}{{ end }}
EOH
destination = "/cert/ca.crt"
change_mode = "noop"
}
# 인증서가 변경되는 경우 change_mode에 지정된 restart를 통해 Job을 재시작한다.
template {
data = <<EOH
{{- /* cert-a.tpl */ -}}
{{ with secret "pki/issue/example-dot-com" "common_name=service-a.example.com" "ttl=2m" }}
{{ .Data.certificate }}{{ end }}
EOH
destination = "/cert/service-a.crt"
change_mode = "restart"
}
template {
data = <<EOH
{{- /* key-a.tpl */ -}}
{{ with secret "pki/issue/example-dot-com" "common_name=service-a.example.com" "ttl=2m" }}
{{ .Data.private_key }}{{ end }}
EOH
destination = "/cert/service-a.key"
change_mode = "noop"
}
}
}
}
change_mode
의 경우 인증서 변경후 동작을 정의하는데,
noop
은 아무 동작도 수행하지 않음을 의미한다.restart
는 Job을 재시작한다.signal
은 system signal을 호출하며, systemctl로 실행되는 프로세스의 경우SIGHUP
을 지정하면 reload 동작이 발생한다.
앞서 Python을 직접 실행했던것과 같이 Nomad 를 통해 Python을 실행하며, 조건은 동일하다. Flask에서 파일 체크를 위해 추가했던 extra_files
는 삭제해도 된다.
nomad job run service_a_job.hcl
nomad job run service_b_job.hcl
Vault 에서 가져온 인증서가 변경되면 change_mode
에 정의된 restart
에 의해 애플리케이션을 자동 재시작 한다.
Consul에서는 mTLS를 위한 인증서를 각 애플리케이션에서 분리하여 envoy로 구현된 proxy에서 이를 대체한다. 따라서 애플리케이션에는 별도 mTLS 구현이 불필요하며, 인증서 교체를 Consul이 제공하는 proxy가 담당하게 된다.
Consul Service Mesh에서 기본 제공하는 mTLS를 사용하는 경우 장점은
- 애플리케이션 개발에 mTLS 및 인증서 관리가 불필요하다.
- Consul 내에서 인증서가 자동 교체된다.
- mTLS의 서비스 간 인증 외에 Intention과 같은 서비스 요청에 대한 방향성을 지정 가능하다.
단점은 Consul의 Control Plane과 Data Plane을 구분하는 동작으로 인해 추가적인 리소스가 발생한다는 점이다.