DevOps/Cert manager

[Cert manager] SpringBoot(tomcat) HTTPS 적용하기

ooeunz 2021. 2. 6. 16:37
반응형

🤔 주의

해당 포스팅은 단순히 Spring Boot에 HTTPS를 적용하는 것을 목적으로 하는 포스팅이 아닌 cert manager를 이해하고 kubernetes에서 https 적용을 자동화하는 것을 목적으로 합니다. 아직 cert manager에 대해 제대로 이해하고 있지 않다면 이전 글을 참고해주시기 바랍니다. 해당 포스팅에선 이전 글의 내용을 모두 이해하고 있다는 전제하에 포스팅을 진행합니다.


이번에는 cert-manager를 이용해서 tomcat 통신을 암호화 해보도록 하겠습니다. "이전에 MySQL을 암호화하는 것처럼 쉽게 secret 파일 적용하면 되는 거 아냐?!"라고 생각하실 수도 있지만, 이전과 조금 다른 부분이 있습니다. 이전에 사용했던 Certificate yaml 파일(아래의 코드)을 보면 keyEncoding 값이 pkcs1로 되어져 있는 것을 확인하실 수 있습니다.

 

여기서 PKCS공개키 기반구조에서 인터넷을 이용해 안전하게 정보를 교환하기 위한 제조사간 프로토콜로 RSA가 개발한 암호 작성 시스템입니다. cert-manager에서는 기본적으로 default key encoding 값인 PKCS#1과 PKCS#8만을 지원합니다. 그리고 Spring에선 PKCS#11PKCS#12만을 지원하고 있습니다. 자 이제 무엇이 문제인지 아시겠나요...?

 

우리가 만든 secret 파일은 PKCS#1 인코딩이기 때문에 spring에 적용하기에 알맞지 않습니다. 따라서 추가적으로 변환을 해주거나 cert-manager에서 제공하는 다른 기능을 사용해야 합니다. 해당 포스팅에선 이 두 가지 방법을 모두 살펴보고 왜 cert-manager에선 PKCS#1과 PKCS#8만을 지원하는지 알아보도록 하겠습니다.

 

⚠️ 해당 포스팅의 예제는 이전 포스팅을 참고하여 secret 리소스가 Spring Boot deployment에 주입되어 있다고 가정하고 진행하겠습니다.
# 이전 포스팅에서 사용했던 Certificate yaml파일

apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: selfsigned-cert
  namespace: default
spec:
  secretName: selfsigned-cert-tls
  duration: 2880h # 120d
  renewBefore: 360h # 15d
  commonName: Selfsigned certificate
  isCA: false
  keySize: 4096
  keyAlgorithm: rsa
  keyEncoding: pkcs1
  usages:
    - digital signature
    - key encipherment
    - server auth
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io

 

🏖 애플리케이션 내에서 Encoding 변환하기

첫 번째 방법으론 Spring Boot 애플리케이션 내에서 PKCS#1 인코딩 된 key들을 PKCS#12로 변환해서 사용하도록 하겠습니다. 먼저 이에 필요한 dependency를 추가하도록 하겠습니다. 해당 예시에선 maven 기반으로 예시를 진행하겠습니다.

 

// poam.xml

        <dependency>
            <groupId>de.dentrassi.crypto</groupId>
            <artifactId>pem-keystore</artifactId>
            <version>2.2.0</version> <!-- check for most recent version -->
        </dependency>

 

 

ctron/pem-keystore

A PKCS #1 PEM KeyStore for Java. Contribute to ctron/pem-keystore development by creating an account on GitHub.

github.com

 

dependency를 추가했으면 아래와 같이 PemKeyStoreProvider를 추가해줍니다. 

import de.dentrassi.crypto.pem.PemKeyStoreProvider;

…

public static void main(final String[] args) throws Exception {
  Security.addProvider(new PemKeyStoreProvider());
  SpringApplication.run(Application.class, args);
}

 

그런 다음 keystore.properties에 주입받은 key들을 입력해 준 다음, application.properties에서 아래와 같이 classpath를 지정해주면 SSL 적용이 완료됩니다.

# keystore.properties

alias=keycert
source.key=/etc/tomcat/tls/tls.key
source.cert=/etc/tomcat/tls/tls.crt
# application.properties

server.ssl.key-store-type=PEMCFG.MOD
server.ssl.key-store=classpath:keystore.properties
server.ssl.key-password=
server.ssl.key-alias=keycert

 

🏖 Cert-manager로 jks 또는 PKCS#12 keystore 만들기

다음은 cert-manager 단에서 Spring에서 사용할 수 있도록 jks나 PKCS#12로 이루어진 keystore를 만드는 방법입니다.

 

"조금 전에 PKCS#12 인코딩은 지원하지 않는다고 했으면서 무슨 소리지?" 라는 생각이 드실 수 있는데, 정확히 말씀드리면 PKCS#12 인코딩은 지원하지 않는데 PKCS#12로 인코딩된 keystore 파일은 만들 수 있습니다.

 

왜 굳이 이렇게 지원 하느냐라는 의문이 드실 수 있는데 그건 cert manager가 내부적으로 golang을 이용하여 인증서를 생성하고 있기 때문에 그렇습니다.

 

우선 JKS와 Keystore에 대해 모르는 독자를 위해 JKS와 Keystore에 대해서 살펴보고 이 문제에 대해 이야기를 이어가도록 하겠습니다.

 

🤔 JKS와 Keystore의 차이점이 뭔가요?

JKS는 Java Key Store의 약자이고 자바 내에서 사용하는 keystore입니다. 그리고 keystore란 인증서, 비밀 키 등의 컨테이너입니다. 즉 우리가 이전 포스팅에서 만들었던 secret 파일의 데이터들이 컨테이너 혹은 객체처럼 생성된 파일이라고 생각하시면 편할 것 같습니다.

이 둘은 단순히 키 저장소의 유형의 차이일 뿐 큰 차이는 없습니다.

 

좀 더 자세한 내용은 아래의 URL에서 확인합니다.

 

Difference between .keystore file and .jks file

I have tried to find the difference between .keystore files and .jks files, yet I could not find it. I know jks is for "Java keystore" and both are a way to store key/value pairs. Is there any

stackoverflow.com


그럼 계속해서 어째서 KeyEncoding은 PKCS#1과 PKCS#8은 지원하면서 왜 PKCS#12는 jks나 keystore만을 지원하는지 알아보도록 하겠습니다. 해당 문제에 관해서는 cert-manager의 내부 구현 코드를 확인하면 알 수 있습니다.

 

아래의 코드는 cert manager의 코드(114번째 라인)를 발췌한 것입니다.

// the type of key encoding and then inspecting the type of key provided.
// It only supports encoding RSA or ECDSA keys.
func EncodePrivateKey(pk crypto.PrivateKey, keyEncoding v1.PrivateKeyEncoding) ([]byte, error) {
	switch keyEncoding {
	case v1.PrivateKeyEncoding(""), v1.PKCS1:
		switch k := pk.(type) {
		case *rsa.PrivateKey:
			return EncodePKCS1PrivateKey(k), nil
		case *ecdsa.PrivateKey:
			return EncodeECPrivateKey(k)
		default:
			return nil, fmt.Errorf("error encoding private key: unknown key type: %T", pk)
		}
	case v1.PKCS8:
		return EncodePKCS8PrivateKey(pk)
	default:
		return nil, fmt.Errorf("error encoding private key: unknown key encoding: %s", keyEncoding)
	}
}

// EncodePKCS1PrivateKey will marshal a RSA private key into x509 PEM format.
func EncodePKCS1PrivateKey(pk *rsa.PrivateKey) []byte {
	block := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)}

	return pem.EncodeToMemory(block)
}

// EncodePKCS8PrivateKey will marshal a private key into x509 PEM format.
func EncodePKCS8PrivateKey(pk interface{}) ([]byte, error) {
	keyBytes, err := x509.MarshalPKCS8PrivateKey(pk)
	if err != nil {
		return nil, err
	}
	block := &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}

	return pem.EncodeToMemory(block), nil
}

해당 코드를 살펴보면 x509.MarshalPKCS1PrivateKey() 와 같은 라이브러리(go언어 자체로 내장하고 있는 라이브러리입니다.)를 호출해서 PKCS#1과 PKCS#8을 키를 생성하는 것을 볼 수 있습니다.

 

반면 PKCS#12에 관해서는 위 라이브러리에서 지원해주지 않기 때문에 cert manager는 pkcs12 라이브러리를 사용합니다.

왜 keyEncoding 값에 따라 라이브러리르 구분해둔지는 잘 모르겠지만... (혹시 안다면 알려주세요 😭) cert manager가 key encoding 값에 따라 certificate를 다르게 생성하는 이유에 대해서는 알 수 있었습니다.

 

 

🧑🏻‍💻JKS를 이용해서 Spring에 HTTPS 적용하기

서론이 길었습니다만... 어찌어찌 cert-manager에서 JKS와 Keystore를 왜 따로 지원하는지를 알았으니 해당 기능을 사용해서 tomcat에 HTTPS를 적용해보도록 하겠습니다. 해당 포스팅에선 JKS를 이용하도록 하겠습니다. (방법이 크게 다르지 않습니다. 옵션 하나 차이.)

 

그러기 위해서 먼저 JKS 파일에 비밀번호를 부여해주기 위한 secret 파일 하나를 생성하도록 하겠습니다. 이 파일을 이용해서 우리는 JKS에 비밀번호를 부여하게 됩니다.

 

secret은 안에 포함하고 있는 데이터를 base64로 인코딩해서 넣어야 합니다. (자세한 내용은 여기를 참조하세요.) 해당 예시에선 password를 편의를 위해 "1234"로 지정하도록 하겠습니다. 그리고 이 password를 이용해서 secret을 만들기 위해서 먼저 base64 값으로 변환하도록 하겠습니다.

이제 변환한 값을 이용해서 secret 파일을 만듭니다.

# jks-password-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: jks-password-secret
  namespace: default
type: Opaque
data:
  password-key: MTIzNA==  # 1234

그런 다음 해당 키 값을 이용해서 Certificate를 만들어보도록 하겠습니다.

 

⚠️해당 포스팅에선 이전 포스팅을 통해서 ClusterIssuer가 생성되어져 있다고 가정합니다.

# selfsigned-jks.yaml

apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: selfsigned-jks
  namespace: noah-test
spec:
  secretName: selfsigned-cert-jks
  duration: 2880h # 120d
  renewBefore: 360h # 15d
  commonName: ooeunz.tistory.com
  isCA: false
  keySize: 2048
  keyAlgorithm: rsa
  keyEncoding: pkcs1
  keystores:
    jks:
      create: true
      passwordSecretRef: # Password used to encrypt the keystore
        key: password-key
        name: jks-password-secret
  usages:
    - digital signature
    - key encipherment
    - server auth
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io

spec.keystores.passwordSecretRef를 보시면 name에 방금 생성한 secret의 이름을 지정하고 key에 jks-password-secret.yaml에서 넣은 key값이 들어간 것을 확인할 수 있습니다. 해당 데이터를 jks에 password로 사용하겠다는 뜻으로, spring에서 해당 jks를 사용할 때 아까 입력해둔 패스워드를 사용하게 됩니다.

 

⚠️만약 jks가 아니라 keystore를 사용하고 싶다면 jks가 아닌 pkcs12를 넣어주시면 됩니다.

 

이제 모든 준비가 끝났습니다. 아까와 같이 새로 만든 secret 파일을 deployment에 주입시켜 줍니다. 그런 다음, 이번에는 application.properties만 아래와 같이 변경해줍니다.

# application.properties

server.ssl.enabled=true
server.ssl.key-store=/etc/tomcat/tls/keystore.jks
server.ssl.key-store-password=1234

조금 눈 여겨 볼 부분은, server.ssl.key-store에는 mount 해준 seccret의 경로를 잡아주고 server.ssl.key-store-password에 jks-password-secret에 넣어준 passrod를 넣어준다는 점입니다.


여기까지 Spring에서도 cert-manager를 이용해서 HTTPS를 적용해보았습니다. 이외에도 이전 포스팅에서 잠깐 언급했던 isCa 옵션을 이용해서 namespace별로 CA를 별도로 관리하는 등 다양한 활용 방법이 있으니 참고하시면 좋을 것 같습니다.

 

마지막으로 아래의 URL에서 해당 포스팅에서 사용한 코드를 확인하실 수 있습니다. 😊

 

ooeunz/blog-code

Contribute to ooeunz/blog-code development by creating an account on GitHub.

github.com

 

반응형