Development/Spring & Springboot

[개발 Tip] Yaml, Properties 안에 중요한 정보를 암호화하기 feat. Jasypt 사용 방법

bbubbush 2022. 5. 18. 09:20

들어가며

며칠 전, Github에 토이 프로젝트 소스를 올렸더니, GitGuardian에서 *.yaml 파일에 계정 정보가 들어가 있다고 알려주었다.

단순히 파일만 삭제하고 커밋하면 git log를 통해 다시 확인할 수 있는 여지가 있어서 reset 후 gitignore에 *.yaml파일을 추가하여 재 커밋하였다.

 

이런 문제를 더 쉽게 해결할 수 있는 방법은 없을까 고민하다 프로퍼티 정보 암호화 방법을 찾게 되어 이렇게 공유한다.

 

Jasypt란 무엇??

Java Simplified Encryption의 약자로, 자바로 간단히 암호화할 수 있게 도와주는 라이브러리다. 읽을 때는 '자시프트' 라고 발음하는 것 같다(유튜브 참고)

자세한 정보는 이곳을 참고하여 확인하면 된다. 그럼 바로 예제 코드를 살펴보자. 이번 예제는 Gradle 기준으로 설명되어 있다.

 

진행 순서

1. Gradle에 'jasypt-spring-boot-starter' 추가

2. Jasypt 설정 정보 추가(yaml 파일 사용)

3. Jasypt Config 파일 생성

4. 로컬 환경에서 암호화할 프로퍼티 정보를 암호화된 값으로 확인

5. 4번에서 확인된 값을 yaml 혹은 properties 파일에 적용

 

 

1. Gradle에 'jasypt-spring-boot-starter' 추가

 

build.gradle에 아래 의존성을 추가한다.

... 생략

implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'

... 생략

 

반응형

 

2. Jasypt 설정 정보 추가(yaml 파일 사용)

 

application.yaml에 아래와 같이 정보를 추가해준다. 각 설정의 정보는 아래에 적어두었다.

 

... 생략

# Jasypt
jasypt:
  encryptor:
    bean: jasyptStringEncryptor
    algorithm: PBEWithMD5AndDES
    pool-size: 2
    string-output-type: base64
    key-obtention-iterations: 100000
    password: password
    
... 생략

bean: 3번 항목에서 만드는 Jasypt Config 파일에서 등록하는 빈의 이름을 적는다.

algorithm: 암/복호화에 사용되는 알고리즘을 말한다. 알고리즘을 선택하는 상황은 많지 않아 예시로 가장 많이 언급된 알고리즘을 적었다.

pool-size: 암호화 요청을 담고 있는 pool의 크기이다. 2를 기본값으로 권장하고 있다.

string-output-type: 암호화 이후에 어떤 형태로 값을 받을지 설정한다. base64 / hexadecimal을 선택할 수 있다.

key-obtention-iterations: 암호화 키를 얻기 위해 반복해야 하는 해시 횟수이다. 클수록 암호화는 오래 걸리지만 보안 강도는 높아진다.

password: 암호화 키이다. 비밀키이므로 노출되지 않도록 주의한다.

 

 

3. Jasypt Config 파일 생성

 

@Configuration
@EnableEncryptableProperties
public class JasyptConfig {
  @Value("${jasypt.encryptor.algorithm}")
  private String algorithm;
  @Value("${jasypt.encryptor.pool-size}")
  private int poolSize;
  @Value("${jasypt.encryptor.string-output-type}")
  private String stringOutputType;
  @Value("${jasypt.encryptor.key-obtention-iterations}")
  private int keyObtentionIterations;
  @Value("${jasypt.encryptor.password}")
  private String password;
  
  @Bean
  public StringEncryptor jasyptStringEncryptor() {
    PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
    encryptor.setPoolSize(poolSize);
    encryptor.setAlgorithm(algorithm);
    encryptor.setPassword(password);
    encryptor.setStringOutputType(stringOutputType);
    encryptor.setKeyObtentionIterations(keyObtentionIterations);
    return encryptor;
  }
}

 

4. 로컬 환경에서 암호화할 프로퍼티 정보를 암호화된 값으로 확인

 

빈으로 등록한 객체를 통해 데이터를 암호화한다. 별도로 파일을 만들기 번거로워서 필자는 JasyptConfig에서 그냥 출력해서 확인했다.

... 생략

  @Bean
  public StringEncryptor jasyptStringEncryptor() {
	... 생략

	String source = "변환할 데이터";
	log.info("plane :: {}, encrypt :: {}", source, encryptor.encrypt(source));
    return encryptor;
  }

... 생략

어떻게 암호화된 데이터를 확인하든 상관은 없지만 어떤 데이터를 암호화해서 어떤 데이터가 나온 건지는 외부에서 알면 안 된다.

 

 

5. 4번에서 확인된 값을 yaml 혹은 properties 파일에 적용

 

 

마지막으로 암호화 값으로 설정 파일을 변경한다. 예를 들어 spring.datasource.username을 암호화했다면 아래처럼 적용하면 된다.

spring:
  datasource:
    username: ENC(암호화값)

이렇게 자시프트를 사용하여 설정값을 암호화했다.

 

 

추가 Tip

이렇게 적용하고 나니깐 무언가 이상하다.

중요한 설정 정보들을 암호화 하긴 했는데 정장 암호화할 때 사용된 비밀키가 고스란히 보이고 있다. 이건 공격자에게 한 번의 번거로움을 줄 뿐이지 동일한 설정 정보와 동일한 키를 갖고 있으면 복호화하는 건 그리 어렵지 않다.

 

그래서 비밀키를 별도의 txt 파일로 분리하고, 이를. gitignore를 통해 제외시킨다면, 정말로 안전하게 설정 정보를 커밋할 수 있다.

 

아래는 변경된 JasyptConfi 파일 소스다. 암호화 키 파일을 읽어서 설정하는 방식이다. 꼭 암호화 키를 담고 있는 파일은 gitignore를 통해 제외시키자!

package com.bb.common.config;

import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
import org.jasypt.encryption.StringEncryptor;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;

@Configuration
@EnableEncryptableProperties
public class JasyptConfig {
  @Value("${jasypt.encryptor.algorithm}")
  private String algorithm;
  @Value("${jasypt.encryptor.pool-size}")
  private int poolSize;
  @Value("${jasypt.encryptor.string-output-type}")
  private String stringOutputType;
  @Value("${jasypt.encryptor.key-obtention-iterations}")
  private int keyObtentionIterations;

  @Bean
  public StringEncryptor jasyptStringEncryptor() {
    PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
    encryptor.setPoolSize(poolSize);
    encryptor.setAlgorithm(algorithm);
    encryptor.setPassword(getJasyptEncryptorPassword());
    encryptor.setStringOutputType(stringOutputType);
    encryptor.setKeyObtentionIterations(keyObtentionIterations);
    return encryptor;
  }

  private String getJasyptEncryptorPassword() {
    try {
      ClassPathResource resource = new ClassPathResource("jasypt-encryptor-password.txt");
      return Files.readAllLines(Paths.get(resource.getURI())).stream()
          .collect(Collectors.joining(""));
    } catch (IOException e) {
      throw new RuntimeException("Not found Jasypt password file.");
    }
  }
}

 

마치며

최근에 어떤 개발자가 AWS 계정을 해킹당해 엄청난 비용이 발생하게 되는 안타까운 글을 본 적이 있다. 남의 이야기가 아니라 내 이야기가 될 수도 있다. 

 

시간 날 때마다 GitGuardian을 통해 본인이 인터넷에 공개하고 있는 중요한 정보가 얼마나 있는지 한 번씩 확인해보자.