6 분 소요


application.yml 혹은 bootstrap.yml에 있는 plain text 값들을 암호화처리하는 방법에 대해 알아보자.

개요

대칭키 (Shared)

  • 암/복호화 시에 같은 키를 사용한다.

비대칭키 (RSA Keypair)

  • 암호화 할 때 사용되는 키와 복호화 할 때 사용되는 키가 다르다.
  • 공개키와 사설키를 사용한다.
  • 키 값을 생성하기 위해 Java keytool을 사용한다.

흐름

스크린샷 2022-10-06 오전 9 44 10
민감한 환경 변수가 담긴 application.yml 파일을 그대로 git repository에 올리게되면, 보안적인 이슈가 발생할 수 있으므로 암호화를 진행할 것이다.

이 때 텍스트가 암호화 되어있다는 표시로, 텍스트 앞에 {cipher}를 붙인다.
암호화 된 값이 Spring Cloud Coonfig Server에서 읽혀지고,
각각의 마이크로 서비스들에게 전달되는 시점에, key를 이용해 cipher text를 복호화 한 뒤에 사용한다.

이렇게 repository 혹은 파일시스템에 민감한 데이터 혹은 secret 값을 저장시켜 놓는다고 하더라도 외부에 노출될 수 있는 위험을 최소화 할 수 있다.

JCE (Java Cryptography Extension)

java8(jdk8)을 사용한다면 JCE를 추가적으로 사용해야한다.

🚨 주의

  • java8(jdk8)을 사용한다면, encryption 혹은 decryption 할 때 아래와 같은 문제가 발생할 수 있다.
    - Illegal key size or default parameters
    - Unsupported keysize of algorithm parameters
  • 이 경우에는 아래 사이트에서 필요한 파일을 추가로 설치해야 한다.
    - https://www.oracle.com/java/technologies/javase-jce-all-downloads.html
    스크린샷 2022-10-06 오전 9 56 52
  • 이 과정은 java11(jdk11) 이상을 사용할 때에는 생략할 수 있다.

JCE를 사용해야한다고 하면, 위에서 말한 파일을 다운로드 받은 후에 아래 과정을 따라하자.
스크린샷 2022-10-06 오전 9 59 47

대칭키를 이용한 암호화

🍊 config-service

pom.xml

bootstrap이 이미 추가되어있다면 추가하지 않아도 된다.

<!-- Bootstrap -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

bootstrap.yml

기존에 bootstrap.yml 파일이 없었기 때문에 새로 생성한다.
대칭키 방식은 암/복호화 시 같은 키를 사용한다.
이때 사용될 키를 bootstrap.yml에 정의하자. (키는 마음대로 정의하면 된다.)

encrypt:
  key: abcdefghijklmnopqrstuvwxyz0123456789

테스트
암호화가 잘 되는지 테스트를 해보자.
service-discovery와 방금 설정한 config-service 두 서버를 기동한 뒤에,
포스트맨에서 http://localhost:8888/encrypt에 다음 요청을 해보자.
스크린샷 2022-10-06 오전 10 28 58
암호화가 되었다.

이번에는 cipher text를 가지고 복호화 테스트를 해보자.
http://localhost:8888/decrypt에 다음 요청을 해보자.
스크린샷 2022-10-06 오전 10 31 29
복호화도 잘 되는 것을 확인할 수 있다.

🍊 user-service

application.yml

application.yml에 있던 db 연동 시 필요한 설정 정보를 별도의 파일로 따로 분리시키자.
이는 config-serviceuser-service.yml로 이동시킬 것이다.
아래에서는 어떤 정보가 사리졌는지 나타내기 위해 주석으로 표시했지만, 실제로는 삭제한다.

# datasource:
#  driver-class-name: org.h2.Driver
#  url: jdbc:h2:mem:testdb
#  username: sa
#  password: 1234

bootstrap.yml

읽어오고자 하는 설정 파일의 정보를 명시한다.
이 때 nameconfig-service라는 Configuration Server의 이름을 적게되면 application.yml 파일을 읽어오고,
user-service와 같은 특정 이름을 명시하면 user-service.yml을 읽어온다.

spring:
  cloud:
    config:
      uri: http://127.0.0.1:8888
      name: user-service # 🌟 native-file-repo 안에 있는 user-service.yml 파일을 읽어온다.

🍊 native-file-repo

user-service.yml

아까 user-service에서 잘라낸 db 연결 정보를 user-service.yml로 붙여넣는다.
이 때, h2 database의 password(= 1234)를 암호화해서 넣어준다.

spring:
  datasource:
  driver-class-name: org.h2.Driver
  url: jdbc:h2:mem:testdb
  username: sa
  password: '{cipher}c0c61dbe3eb9ef876f5b1810bd510a49d2ee8aa679956f68d9d1d4ff3b661108'
...

파일을 저장한 뒤에 config-service를 재기동해준다.

참고
암호화는 위에서 했던 것처럼 포스트맨을 이용하면 된다.
스크린샷 2022-10-06 오전 10 44 16

테스트

테스트를 위해 gateway-service도 기동해준다. (현재 service-discovery -> config-service -> gateway-service -> user-service가 기동되어있다.)
http://localhost:8888/user-service/default에 접속해보자.
아래 사진에서 웹 브라우저를 통해 보이는 단계가 user라는 마이크로 서비스가 사용하는 시점이라고 볼 수 있다.
스크린샷 2022-10-06 오전 10 51 40
spring.password를 보면, 정상적으로 1234라는 plain text를 읽어오는 것을 확인할 수 있다.
따라서 저장할 때에는 암호화된 cypher text를 저장하고, 사용할 때에는 위 사진에서 처럼 decrypted 된 plain text를 사용함을 알 수 있다.

만약 패스워드에 잘못된 암호문을 넣는다면?

native-file-repo/user-service.yml에서 password에
'{cipher}c0c61dbe3eb9ef876f5b1810bd510a49d2ee8aa679956f68d9d1d4ff3b661108_wrong'과 같이 일부러 잘못된 암호문을 넣어보자.

http://localhost:8888/user-service/default 페이지를 새로고침 하게 되면, 아래와 같이 <n/a>로 표시됨을 알 수 있다.
스크린샷 2022-10-06 오전 10 58 56

h2-console 접속 테스트

localhost:8761으로 유레카 홈에 들어가서 user-service의 포트번호를 확인한 뒤, 주소 마지막을 /h2-console로 변경해 h2 콘솔로 접속한다.
이전에 했던 것처럼 패스워드 없이 test connection을 시도해보자.
스크린샷 2022-10-06 오전 11 12 02
패스워드 없이는 접속되지 않는 것을 확인했다.
이번에는 패스워드에 1234를 입력해보자.
스크린샷 2022-10-06 오전 11 12 31
우리가 암호문으로 설정한대로 1234를 입력해야 접속이 되는 것을 확인할 수 있다.

비대칭키를 이용한 암호화

키 생성

  • public, private key 생성 -> JDK keytool 이용
  • 보통은 암호화에 private key를 사용하고 복호화에 public key를 사용하지만 반대가 되어도 상관없다. (암/복호화 키가 서로 다르기만 하면)
# key가 저장될 디렉토리 생성
$ mkdir ${user.home}/study/msa/keystore

# keystore 디렉토리로 이동
$ cd keystore

# key 생성
$ keytool -genkeypair -alias apiEncryptionKey -keyalg RSA \
-dname "CN=Ann, OU=API Development, O=test.co.kr, L=Seoul, C=KR" \
-keypass "test1234" -keystore apiEncryptionKey.jks -storepass "test1234"

스크린샷 2022-10-06 오전 11 43 50

  • -alias: 생성된 키에 대한 별칭, alias를 통해 호출하고 사용 (여기서는 생성하고자 하는 키 파일과 같은 형식으로 지정)
  • -keyalg: 키 생성 시, 사용되는 알고리즘
  • -dname: 키 생성 시, 셀프 인증에 사용되는 부가 정보
    - CN: Company Name
    - OU: Organization Unit
    - O: Organization
    - L: Location
    - C: Country
  • -keypass: 키 생성 시, 패스워드 (임의로 생성)
  • -keystore: 키의 실제 파일 이름 (.jks)
  • -storepass: 저장되는 키 파일의 패스워드 (임의로 생성)

생성된 키 정보를 자세히 확인해보고 싶다면, 아래 커맨드를 입력한다.

$ keytool -list -keystore apiEncryptionKey.jks -v

패스워드를 물어보면, 아까 지정한 test1234를 입력한다.
스크린샷 2022-10-06 오전 11 47 43
그럼 위 사진과 같은 결과가 나오는데, PrivateKeyEntry를 보면, 이 키는 private key 임을 알 수 있다.

그렇다면 이 private key에서 public key를 끄집어내보자.

$ keytool -export -alias apiEncryptionKey -keystore apiEncryptionKey.jks -rfc -file trustServer.cer

패스워드를 물어보면, 마찬가지로 test1234를 입력한다.
스크린샷 2022-10-06 오전 11 52 00
성공적으로 공개키도 발급받았다.

그런데 .cer 확장자는 인증서 파일이다.
인증서 파일을 jks 파일로 변환하여 사용할 수도 있다.

$ keytool -import -alias trustServer -file trustServer.cer -keystore publicKey.jks

스크린샷 2022-10-06 오전 11 56 10
위 박스에는 키의 정보가 표시되고, 마지막에 를 입력하면 jks 파일로 변환할 수 있다.

이 키 정보를 아까처럼 자세히 확인해보자.

$ keytool -list -keystore publicKey.jks -v

스크린샷 2022-10-06 오후 12 00 19
trustedCertEntry를 보면, 이 키는 public key 임을 알 수 있다.

참고
사실 Spring Cloud Config Server에서는 비밀키(apiEncryptionKey.jks)만 사용할 것이다.
나머지는 나중에 공개키가 필요한 상황이 생긴다면, 사용해볼 수 있을 것이다.

🍊 config-service

이제 생성한 비밀키의 위치와 파일명을 Configuration Server에 명시하자.

bootstrap.yml

encrypt:
  key-store:
    location: file://${user.home}/study/msa/keystore/apiEncryptionKey.jks
    password: test1234
    alias: apiEncryptionKey

이제 config-service를 재기동하자.

참고
어디까지가 ${user.home} 일까?
keystore 디렉토리에서 pwd를 입력하게되면 아래와 같이 나오는데,
스크린샷 2022-10-06 오후 12 10 15
여기서 /Users/사용자명까지가 ${user.home} 이다.

포스트맨 암호화 테스트
대칭키 때와 같은 방법으로 테스트한다.
스크린샷 2022-10-06 오후 12 13 33
대칭키 때보다 더 복잡하게 암호화가 되었다.

이번에는 cipher text를 가지고 복호화 테스트를 해보자.
스크린샷 2022-10-06 오후 12 16 34
복호화도 잘 되는 것을 확인할 수 있다.

🍊 native-file-repo

user-service.yml

아까 user-service에서 잘라낸 db 연결 정보를 user-service.yml로 붙여넣는다.
이 때, h2 database의 password(= 1234)를 암호화해서 넣어준다.

spring:
  datasource:
  driver-class-name: org.h2.Driver
  url: jdbc:h2:mem:testdb
  username: sa
  password: '{cipher}AAQCgo+KT8kG9ISz3Rr0FEG2uoOR9jJXhjMzbTWgcDQtTh4rShUrj3KtiOfFaN8Gk/MoJeyszPKouvCrQihy1SyVjNiZuGaIXkk2bf4HTMayE4IP1xvLF7nOEHbvxuZR6dv2UUVK2eJcQ+bskIk9+fBjVL9C4t1Gw/VDA9rMVzu55VYcsdsRSEnIfSCUlOzaWaH0XsuiDbZ5UI1LxqEB3hXLkazCWkahj2jFoKElzZ1kZ/QRnOFQTtXrv3jtf0P0ytwTVBqEa5Gjz4JJLr3tybc7i0avs+tqXjWKj35C/3Aq+sDH1L3sl+JNns4QiDclTpHTt+wDg/pqlg2MhZ0HAOIJNBtuP5P/qk6Pwk0fCUyWhuifmOMo9OuBBZowXsMk0Hqg=' # 🌟 비대칭키 암호화 방식 사용
...

위 암호문은 기존 비밀번호인 1234를 비대칭키 암호화 한 것이다.
파일을 저장한 뒤에 웹 브라어저에서 값을 확인해보자.

테스트

http://localhost:8888/user-service/default에 접속해보자.
스크린샷 2022-10-06 오후 12 36 51
아까와 마찬가지로 정상적으로 1234라는 plain text를 읽어오는 것을 확인할 수 있다.

🍊 gateway-service

지금까지는 db 정보를 암호화해서 사용했다면, 이번에는 토큰의 secret 정보를 암호화해서 사용해보자.

bootstrap.yml

현재 gateway-service는 native-file-repo 안에 있는 ecommerce.yml 파일에서 구성 정보를 읽어오는 것을 확인할 수 있다.

spring:
  cloud:
    config:
      uri: http://127.0.0.1:8888
      name: ecommerce # 🌟 native-file-repo 안에 있는 ecommerce.yml 파일을 읽어온다.

🍊 native-file-repo

ecommerce.yml

기존 token.secret에는 아래와 같이 plain text가 노출되어 있었다.

token:
  expiration_time: 864000000
  secret: user_token_native_ecommerce

gateway:
  ip: 127.0.0.1

이를 아래처럼 암호문으로 변경하자.

token:
  expiration_time: 864000000
  secret: '{cipher}AQA4EotojUaSDNpQ5raB7YyPaZ+kOFdojsDz4UCo/cjHnAjQgZDrZFFvJxFPvYgNAUzQb2RJENdTsMai3WOhWxSJPWaWG/0l4qD3El0/eslslIfuvGMyrwTBaE97M05q/wRmjfBL/uW0pKmjAzNXX3NkGlVezq5HoNr+k9f4FEhz6CEdSnRsOndLkMr9AwOZFFzp1E/jCHdrQ80AZKdMPpCZLR8sSU/NZa28jL9odkKKGggJ6IFKTEBAIRbUwuHZX1jS+j9ugv+U6sD+QtzGEiNl0yoqoAeC52kfLJRsWS9JpA1HEJ0m+J2Bk4v7M37TtOXtPXJmsqEPuBNhLLuK48zN1nz9HKIqlMCc6J6Lr4v479l9c/73dtXNZNCgV609uzwWqTqa6/p1TiuH4NJszJyp'

gateway:
  ip: 127.0.0.1

참고
암호화는 위에서 했던 것처럼 포스트맨을 이용하면 된다.
스크린샷 2022-10-06 오후 12 44 27

테스트

암/복호화 테스트

http://localhost:8888/ecommerce/default에 접속해보자.
스크린샷 2022-10-06 오후 12 46 52
정상적으로 user_token_native_ecommerce라는 plain text를 읽어오는 것을 확인할 수 있다.

gateway-service 기동 후 받아오는 토큰 값 확인

기존 application.yml 파일에 있던 토큰 정보를 주석처리 한다.

#token:
#  secret: user_token


그리고 토큰 정보를 받아오는 곳에 break point를 찍고 디버깅 모드로 실행한다.
어떠한 user-service를 호출해도 게이트웨이를 거치기 때문에, http://localhost:8000/user-service/health-check를 호출해보자.
스크린샷 2022-10-06 오후 12 56 31
암호문으로 등록한 token.secret의 정보를 정상적으로 복호화하여 사용하는 것을 확인할 수 있다.

그런데, 중요한 것은 user-service에서 사용하는 구성파일(native-repo-repository/user-service.yml)에 있는 토큰 값과 gateway-service에서 사용하는 구성파일(native-repo-repository/ecommerce.yml)에 있는 토큰 값이 같아야 한다는 것이다.
따라서 user-service.yml에 있는 토큰 정보를 ecommerce.yml에 있는 토큰 정보와 일치시킨다.

user-service.yml 변경 전

spring:
  datasource:
  driver-class-name: org.h2.Driver
  url: jdbc:h2:mem:testdb
  username: sa
  password: '{cipher}AQCgo+KT8kG9ISz3Rr0FEG2uoOR9jJXhjMzbTWgcDQtTh4rShUrj3KtiOfFaN8Gk/MoJeyszPKouvCrQihy1SyVjNiZuGaIXkk2bf4HTMayE4IP1xvLF7nOEHbvxuZR6dv2UUVK2eJcQ+bskIk9+fBjVL9C4t1Gw/VDA9rMVzu55VYcsdsRSEnIfSCUlOzaWaH0XsuiDbZ5UI1LxqEB3hXLkazCWkahj2jFoKElzZ1kZ/QRnOFQTtXrv3jtf0P0ytwTVBqEa5Gjz4JJLr3tybc7i0avs+tqXjWKj35C/3Aq+sDH1L3sl+JNns4QiDclTpHTt+wDg/pqlg2MhZ0HAOIJNBtuP5P/qk6Pwk0fCUyWhuifmOMo9OuBBZowXsMk0Hqg='

token:
  expiration_time: 864000000
  secret: user_token_native_user_service # 토큰 정보 평문

gateway:
  ip: 127.0.0.1


user-service.yml 변경 후

spring:
  datasource:
  driver-class-name: org.h2.Driver
  url: jdbc:h2:mem:testdb
  username: sa
  password: '{cipher}AQCgo+KT8kG9ISz3Rr0FEG2uoOR9jJXhjMzbTWgcDQtTh4rShUrj3KtiOfFaN8Gk/MoJeyszPKouvCrQihy1SyVjNiZuGaIXkk2bf4HTMayE4IP1xvLF7nOEHbvxuZR6dv2UUVK2eJcQ+bskIk9+fBjVL9C4t1Gw/VDA9rMVzu55VYcsdsRSEnIfSCUlOzaWaH0XsuiDbZ5UI1LxqEB3hXLkazCWkahj2jFoKElzZ1kZ/QRnOFQTtXrv3jtf0P0ytwTVBqEa5Gjz4JJLr3tybc7i0avs+tqXjWKj35C/3Aq+sDH1L3sl+JNns4QiDclTpHTt+wDg/pqlg2MhZ0HAOIJNBtuP5P/qk6Pwk0fCUyWhuifmOMo9OuBBZowXsMk0Hqg='

token:
  expiration_time: 864000000
  secret: '{cipher}AQA4EotojUaSDNpQ5raB7YyPaZ+kOFdojsDz4UCo/cjHnAjQgZDrZFFvJxFPvYgNAUzQb2RJENdTsMai3WOhWxSJPWaWG/0l4qD3El0/eslslIfuvGMyrwTBaE97M05q/wRmjfBL/uW0pKmjAzNXX3NkGlVezq5HoNr+k9f4FEhz6CEdSnRsOndLkMr9AwOZFFzp1E/jCHdrQ80AZKdMPpCZLR8sSU/NZa28jL9odkKKGggJ6IFKTEBAIRbUwuHZX1jS+j9ugv+U6sD+QtzGEiNl0yoqoAeC52kfLJRsWS9JpA1HEJ0m+J2Bk4v7M37TtOXtPXJmsqEPuBNhLLuK48zN1nz9HKIqlMCc6J6Lr4v479l9c/73dtXNZNCgV609uzwWqTqa6/p1TiuH4NJszJyp' # 🌟 토큰 정보 암호문

gateway:
  ip: 127.0.0.1

🌟 참고
이렇게 두 토큰 정보를 양쪽에 똑같이 맞추는 방법도 있지만, 하나로 모을 수도 있다.

하나로 모은다는 것은,
ecommerce.yml이든 user-service.yml이든 상위 개념으로 application.yml을 가지기 때문에,
ecommerce.ymluser-service.yml 모두 token.secret 정보를 지우고, application.yml에 정의하게 되면,
두 서비스 모두 application.yml에 있는 token.secret 정보를 공통으로 가지고 온다.



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기