springboot 프로젝트를 진행할 때 각종 설정정보가 저장되는 곳이 application.yml 이다. spring initilizr를 통해서 기본 프로젝트를 생성하면 application.properties로 되어있는 그 파일이다. application.yml을 사용하던 application.properties를 사용하던 상관은 없다. 데이터 형식만 약간 달라질 뿐 근본은 똑같기 때문이다.
이 파일에는 db정보 같은 민감한 내용들이 기입되는데 내용이 db환경과 일치하지 않으면 springboot프로젝트가 구동되지 않는다. 그만큼 중요한 역할을 하는 파일이다.
종종 git을 통해 협업을 하다보면 이 파일로 인한 문제가 발생한다. github에 프로젝트를 push 하게 되면 이 파일도 함께 원격저장소에 업로드 되는데 이 것은 공개적으로 민감정보가 웹상에 노출된다는 의미이다. 때문에 이를 해결하기 위한 가장 간단한 방법은 git ignore파일에 해당 application.yml파일을 등록하여 git이 push 할 때 이 파일을 제외하도록 하는 방법이다.
이 방법은 협업자들이 각자 application.yml파일의 사본을 가지고 있고 나머지 코드만 push하여 application.yml파일은 원격레포지토리에 업로드 되지 않도록 하는 방식이다. 원천적으로 파일을 올리지 않기 때문에 보안상 강력한 방법이다. 또한 설정파일은 프로젝트 초반 세팅시 건드리고 크게 수정할 일이 없을 뿐더러 혹여나 변경사항이 있더라도 그 정도는 감수할 만 하다.
최근 팀 프로젝트를 하나 시작했는데 이번에도 gitignore방식을 채택해서 프로젝트 관리를 하려고 하였으나 생각지도 못한 문제가 발생한다.
이번 프로젝트에서는 CI/CD를 도입하여 자동화까지 하려는 것이 목적이었다. jenkins로 github에서 소스코드를 받아와 자동으로 빌드하도록 구성하려니 application.yml파일이 존재하지 않기 때문에 빌드와 배포가 정상적으로 수행되지 않았다. 때문에 이를 해결하기 위해서 application.yml파일을 빌드 시에 소스코드에 삽입해주는 등의 과정이 필요해졌다.
이 문제를 해결하기 위해 떠올린 방법은 아래 세가지이다.
-첫번째, jenkins컨테이너 내부에 application.yml파일을 저장해놓고 빌드시에 넣어준다.
-두번째, ssh 접속을 통해 서버 pc에 접근하고 이 곳에서 git pull을 받아서 빌드, 도커이미지화, 배포한다.
-세번째, application.yml파일을 애초에 소스코드에 포함시킨다.
첫번째 방식은 jenkins컨테이너가 자체적으로 application.yml파일을 가지고 있다가 빌드시에 넣어주는 방식이다. jenkins는 강력한 도구들을 제공하고 있기 때문에 이 방식이 크게 어렵진 않으나 컨테이너가 어플리케이션의 정보를 가지고 있는다는 것이 마음에 안들었다. jenkins는 CI/CD역할만 해주면 되는데 어플리케이션과 의존관계가 생기기 때문이다. java코드에서만 의존성을 느슨하게 하는것이 중요한 것이 아니다. 컨테이너 간에도 의존성이 강해지게 되면 도커를 사용해 서비스를 분리하는 것이 큰 의미가 없기 때문이다. 그래서 이 방법은 패스했다.
두번째 방식은 jenkins파이프라인에서 내 로컬 pc에 접근해서 git pull을 받는 방식이다. 기존 로컬 프로젝트에는 application.yml 파일이 존재할테니 ssh접속으로 로컬 pc에 접근하여 git pull부터 빌드, 도커이미지화, 배포를 차례차례진행하는 방식이다. 이 방법도 github의 소스코드를 받아와서 빌드해주는 jenkins의 기능을 사용하지 않고 로컬 pc에서 모든것을 진행하는 방식이라 마음에 들지 않았다.
마지막으로 세번 째 방식은 "그렇다면 아예 application.yml파일을 빼지 않는 방법은 없을까?" 라는 고민을 하다가 찾게된 방식이다. 파일은 github에 공개되지만 그 내용들을 암호화하게 되면 파일이 올라가도 크게 상관이 없기 때문에다. 게다가 application.yml파일이 없어서 빌드가 정상적으로 되지 않던 기존의 문제도 해결할 수 있다. 또한 설정파일을 하나의 저장소에서 관리하면 변경이 있을때마다 협업자들이 내용을 수동으로 각각수정할 필요도 없다.
그래서 이번 포스팅에서는 springboot 프로젝트에 적용할 수 있는 `jasypt` 라이브러리를 이용해서 민감 정보를 암호화하는 방법을 다뤄보겠다.
JASYPT
Jasypt라이브러리란?
https://github.com/ulisesbocchio/jasypt-spring-boot
jasypt라이브러리는 민감정보의 암호화를 해주는 라이브러리로 springboot환경에서 매우 편리하게 적용할 수 있다. 위 프로젝트의 README에 사용법이 매우 쉽게 설명되어있다.
jasypt로 검색하면 많은 블로그 포스팅이 나오는데 복사 붙여넣기 한 것처럼 내용이 똑같고 틀린 부분도 똑같이 공유하고 있기 때문에 왠만하면 공식문서를 참조하는 것이 좋을 것이다. 나도 블로그를 보며 실행해보는데 암만해도 복호화가 되지 않아 몇시간 동안 헤메다가 공식문서를 보고 1분만에 문제를 해결했다.
의존성 추가
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'
나는 빌드방식을 gradle로 선택했기 때문에 위 코드를 build.gradle파일에 붙여넣어준다. 의존성이 추가되면 항상 gradle새로고침을 통해 의존성을 반영하도록 해준다.
Config파일 생성, application.yml에 설정추가
그 다음 프로젝트 적당한 위치에 JasyptConfig라는 파일을 하나 생성해준다. gradle에 의존성이 잘 추가되었다면 import가 정상적으로 잘 될것이다.
#JasyptConfig.java
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JasyptConfig {
@Value("${jasypt.encryptor.password}")
private String KEY;
@Bean(name = "jasyptStringEncryptor")
public StringEncryptor stringEncryptor(){
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(KEY);
config.setPoolSize("1");
config.setAlgorithm("PBEWithMD5AndDES");
config.setStringOutputType("base64");
config.setKeyObtentionIterations("1000");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
encryptor.setConfig(config);
return encryptor;
}
}
@Value("$jasypt.encryptor.password"): application.yml파일에서 해당하는 값을 가져온다. 이 값이 암호화와 복호화를 위해 사용되는 password이고 개발자가 잘 관리해야 하는 값이다.
stringEncryptor 메서드내부에서는 암호화방식을 설정해준다. 알고리즘 방식이나 출력형식등이 세팅된다. 세부적인 세팅을 하려면 공식문서를 참조하도록 하자.
#application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/highgarden_db?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: highgarden
password: 1234
#jasypt 설정
jasypt:
encryptor:
bean: jasyptStringEncryptor
password: your_password
기본적으로 위처럼 db정보가 기입되어 있을텐데 아래에 jqsypt관련 설정을 추가해준다. bean에서 `jasyptStringEncryptor`라고 되어있는 부분은 위에서 JasyptConfig에서 등록한 bean의 이름이다. 두 이름이 일치해야 한다. password는 Config에서 @Value 어노테이션으로 가져와서 암호화와 복호화에 사용하게 될 비밀번호이다.
#SpringbootBoardApplication.java
import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableEncryptableProperties //꼭 추가해주도록 한다.
public class SpringbootBoardApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootBoardApplication.class, args);
}
}
블로그만 보다가 한참 헤메었던 부분이 바로 이곳인데 @EnableEncryptableProperties라는 어노테이션을 프로젝트 main메서드가 있는 클래스에 추가해주어야 한다. 이 어노테이션을 추가해주지 않으면 런타임 상태에서 암호화된 설정값들을 자동으로 스프링이 변환해주지 않아서 복호화가 되지 않는다.
인텔리제이 addVM options에 비밀번호 추가
앞서 jasypt를 사용하는 이유가 민감정보를 암호화시키려는 목적이라고 했다. 하지만 현재 application.yml파일에는 암복호화를 위한 password가 노출되어 있는 상태이다. 마치 자물쇠와 열쇠를 함께 두는 꼴이나 마찬가지다. password와 암호화 방식만 알면 문자열을 복호화하는 것은 식은죽 먹기이기 때문에 password는 소중하게 보관해야 하는 중요한 정보이다.
인텔리제이에서는 설정파일의 값을 동적으로 넣어주는 옵션이 있다. 이를 이용해 application.yml파일에 비밀번호가 노출되지 않은 상태에서도 어플리케이션을 구동할 수 있다.
-Run -> Edid Configurations -> SpringBoot 프로젝트 -> Modify Options -> Add VM options
위와같이 VM 옵션을 사용하도록 설정해주고 다음과 같이 값을 추가해준다.
-Djasypt.encryptor.password=your_password
-D를 쓴다음 property이름과 값을 적어준다. 나는 앞서 application.yml파일의 encryptor password를 `your_password`로 했기 때문에 같은 값을 적어준다.
*만약 이 설정값을 test에서도 사용하고 싶다면 Gradle의 Test에서 사용할 수 있도록 추가해주어야 한다. Test코드에서는 본 프로젝트의 설정값을 가져오려면 시점문제를 고려하는 등의 귀찮은 작업이 필요하기 때문에 여기서도 그냥 추가해준다.
#build.gradle
마지막으로 다음과 같은 설정도 build.gradle에 추가해주면 테스트코드에서 System.getProperty()메서드로 IDE에 저장된 설정값을 가져올 수 있다.
tasks.named('test') {
useJUnitPlatform()
systemProperty "jasypt.encryptor.password", System.getProperty("jasypt.encryptor.password")
}
암호화 문자열생성하기
복잡한 설정이 끝났으니 이제 테스트코드로 암복호화를 테스트해보자.
import org.assertj.core.api.Assertions;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class JasyptTest {
String encryptKey;
@Test
void jasypt(){
encryptKey = System.getProperty("jasypt.encryptor.password");
String url = "jdbc:mysql://localhost:3306/highgarden_db?serverTimezone=Asia/Seoul&characterEncoding=UTF-8";
String username = "highgarden";
String password = "1234";
String encryptUrl = jasyptEncrypt(url);
String encryptUsername = jasyptEncrypt(username);
String encryptPassword = jasyptEncrypt(password);
String decryptUrl = jasyptDecryt(encryptUrl);
String decryptUsername = jasyptDecryt(encryptUsername);
String decryptPassword = jasyptDecryt(encryptPassword);
System.out.println("encryptUrl : " + encryptUrl);
System.out.println("encryptUsername : " + encryptUsername);
System.out.println("encryptPassword : " + encryptPassword);
System.out.println("decryptUrl : " + decryptUrl);
System.out.println("decryptUserName : " + decryptUsername);
System.out.println("decryptPassword : " + decryptPassword);
Assertions.assertThat(url).isEqualTo(jasyptDecryt(encryptUrl));
}
private String jasyptEncrypt(String input) {
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
encryptor.setAlgorithm("PBEWithMD5AndDES");
encryptor.setPassword(encryptKey);
return encryptor.encrypt(input);
}
private String jasyptDecryt(String input){
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
encryptor.setAlgorithm("PBEWithMD5AndDES");
encryptor.setPassword(encryptKey);
return encryptor.decrypt(input);
}
}
System.getProperty("jasypt.encryptor.password") 메서드로 인텔리제이 IDE에 저장한 비밀번호 설정값(`your_password`)을 가져온다. 암호화 하려는 문자열들을 선언하고 암호화해서 콘솔에 출력하고 다시 복호화 해서 출력하는 코드이다.
앞선 설정이 잘 수행되었다면 다음과 같이 콘솔에 문자열이 출력될 것이다. 우리가 사용할 것은 암호화 된 문자열들이다. 이 문자열을 드래그 해서 application.yml에 복사하도록 하자.
#application.yml
암호화된 문자열을 스프링이 인지하려면 특별한 형식으로 적어주어야 한다. `ENC(암호화된문자열)` 의 형태로 적어주면 런타임시에 자동으로 스프링이 암호화된 문자열을 복호화하여 설정값으로 사용한다.
spring:
datasource:
hikari:
maximum-pool-size: 4
url: ENC(LPpesE/vPteqNciOcNNKXDsPvpHXGOy7UMWM7kIobDaWrXj6Pd44ruX34rw0lee3TnVXy2yDSQXuLIac4Eu3cggKfGtw7DO0+UVDtGSZvVBpiOOERwvf0ggpVHPFsRT8POlgn+N0WtsD9eOlluSf+A==)
username: ENC(cIRCqV5zMSICi6JjCPvwZzF0tpN05mbe)
password: ENC(M8tjFMWjs3f/E6nJFBQRTPRi2zAqtZ8096fI8RV/ldc=)
#jasypt 설정
jasypt:
encryptor:
bean: jasyptStringEncryptor
jasypt라이브러리를 이용해 암호화와 복호화가 작동하는지 테스트 해보았다. 하지만 위 테스트코드로는 런타임시에 db와의 연동이 잘 이루어지는지 테스트할 수 없다. 실제로 스프링이 복호화를 잘 수행하여 연동을 잘 마치는지 테스트 해보려면 실제 어플리케이션을 구동해보아야 한다.
다음 시간에는 암호화된 설정파일로 db와 연동을 해보고 docker환경에서는 동적으로 설정값을 어떻게 넣어주는지 알아보겠다.
'공부하자 > Springboot' 카테고리의 다른 글
[springboot]jasypt라이브러리로 민감정보 암호화 하기(빌드, 배포, 도커)-2 (0) | 2024.07.24 |
---|---|
[C++]누적 합 구하는 공식 (0) | 2024.04.11 |