글 작성자: beaniejoy

이번 게시글은 Spring Boot Applicaiton에 vault secret 데이터들을 적용했던 내용을 정리하는 글입니다. vault 서버가 준비가 안되어있다면 이전에 제가 작성한 글이나 구글링을 통해 vault 설치를 먼저하시는 것을 추천드립니다.
https://beaniejoy.tistory.com/100

 

Vault 서버를 설치해보자(AWS, Lightsail에 vault 서버 구축해보기)

Spring Boot 애플리케이션을 개발하다보면 민감한 정보들을 설정해야할 때가 있습니다. DB 연동시 필수적으로 입력해야 하는 jdbc url, username, password 정보도 있고 Security 인증 관련해서 토큰 발급을

beaniejoy.tistory.com

 

📌 1. Spring Boot 프로젝트에 vault secret 관리 대상이 무엇이 있을까요?

어떻게 보면 Spring Boot 애플리케이션 개발에 있어서 vault를 왜 도입해야하는지에 대한 이유하고 연결될 것 같습니다.

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      jdbc-url: jdbc:mysql://localhost:3306/dongne?autoreconnect=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
      username: root
      password: beaniejoy
      maximum-pool-size: 5
      minimum-idle: 5

jwt:
  secret_key: ZG9uZ25lLWNhZmUtcHJvamVjdC1rZXktZm9yLXRlc3QtY29kZQo
  validity_time_in_sec: 60

secret 관리 대상은 인증 관련 데이터 혹은 DB connection 정보 등 주로 외부 공개에 있어 민감한 데이터들입니다. 이러한 데이터들은 프로젝트 설정파일에 입력을 해두는 것이 아니라 별도로 관리가 되어야 하는데요. 여러 가지 방안이 있을 수 있지만 실무에서도 가장 많이 사용되고 있고 그만큼 안전하고 사용하기 간편한 vault로 관리를 해보고자 합니다.

vault가 왜 안전한지에 대해서는 내부 구조를 알아야하는데 저는 vault document 보고 내부구조 파악에는 실패하였습니다. 혹시 vault를 빠삭하게 아시는분이 계신다면 알려주시거나 vault를 이해하는데 있어 좋은 자료 공유해주시면 정말 감사하겠습니다. (꾸벅)

vault 원리보다 간편하게 적용하는 방법에 대해서 정리해보고자 합니다.

 

📌 2. vault에 해당 데이터들을 저장하자

준비물로 vault 서버가 있어야 합니다. 준비해온 vault 서버에 들어가서 가장 먼저 secret을 설정해야합니다.

vault 인증 후 메인 화면

vault 서버 인증 후 진입하는 메인 화면에서 secret engine 내역을 볼 수 있는데요. Spring Boot application 전용 secret을 위해 새로운 engine을  우측에 사진과 같이 Enable new engine 버튼을 누릅니다.

Generic > KV를 선택하시고 다음으로 넘어갑니다. KV는 vault의 Key-Value 형식 저장소를 의미하고 임의의 secret 정보들을 key value 형태로 저장하게 됩니다.

secret engine의 이름이 되는 path를 지정해야 하는데요. 원하는 이름으로 설정하시면 됩니다.
(저는 이미 secret 이라는 이름의 engine을 생성했는데요. 다른 이름으로 하셔도 됩니다.)

여기까지 되셨다면 secret engine 생성은 완료입니다.

생성된 secret engine 내부 화면

secret engine을 생성했으면 그 안에 들어가서 application에서 사용할 secret 내용을 입력해야 하는데요. 여기서도 경로 설정이 중요합니다.

Spring Boot는 기본적으로 profile로 설정을 환경별로 구분해서 관리하는데요. 각각의 환경에 맞게 secret 정보들을 구성해야 하기 때문에 secret의 path를 잘 만드는 것이 중요합니다.

저는 이미 생성해두었지만 dongne라는 application 이름을 따고 환경별로 local, prod를 구성하고자 합니다. local 환경부터 구성하면 다음과 같습니다.

path에는 dongne/local로 지정하고 secret data에는 key value 형식에 맞춰 application.yml에 적용하고자 하는 설정내용들을 입력하면 됩니다.

특징은 yml 형식이 아닌 properties 파일 형식과 같이 입력하면 됩니다. 다 입력하고 저장하면 됩니다.

local, prod 둘 다 저장하면 위와 같이 환경별로 secret 정보들을 깔끔하게 관리할 수 있습니다.

 

📌 3. Spring Boot 내에 vault 설정

vault에 secret 정보들을 저장했고 이제 Spring Boot application에 vault를 연동해야 합니다.

dependencies {
	//...
    
    implementation("org.springframework.cloud:spring-cloud-starter-vault-config:3.1.3")
}

spring-cloud-starter-vault-config 의존성을 추가해줘야 하는데요. 제 개인 프로젝트의 Spring Boot 버전은 2.7.x 인데요. vault-config 버전을 3.1.3으로 해야 제대로 적용이 되었습니다. Spring Boot 버전하고 vault-config 버전하고 호환성 이슈가 있는지 모르겠지만 버전에 따라 제대로 적용되는지 체크해야 할 것 같습니다.

spring:
    config:
        import:
          - optional:file:./env/.env.${spring.profiles.active}[.properties] # local
          - vault://
    cloud:
        vault:
            uri: ${VAULT_DOMAIN_ADDR} # vault domain uri (port: 8200)
            authentication: TOKEN
            token: ${VAULT_AUTH_TOKEN} # vault root token
            connection-timeout: 3000
            read-timeout: 10000
            kv:
                backend: secret
                application-name: dongne
                default-context:
                profiles: ${spring.profiles.active}

위의 설정내용을 vault를 적용하고자 하는 모듈이나 프로젝트의 application.yml에 작성합니다.
위에서 중요한 항목은 uri, token 정보입니다. 해당 정보들도 외부에 노출되면 안되는 내용이기 때문에 저같은 경우 환경 변수를 통해 주입받도록 했습니다. (uri, token을 가져오는 방식은 여러가지라서 원하는 방식으로 하셔도 상관없습니다.)

그리고 kv 필드에 backend, application-name, profiles 내용들이 있는데요. Spring Boot 애플리케이션을 실행하게되면 vault-config가 해당 항목들을 참조해서 secret path를 조합하고 그 path에 있는 secret 데이터들을 가져오게 됩니다.

예를 들어 backend: secret, application-name: hello, profiles: local로 한다면 위와 같이 애플리케이션 실행시점에 secret/hello/localsecret/hello path에 있는 secret 정보들을 가져오게 됩니다.

위에서 vault 설정할 때 vault에 저장해두었던 secret 데이터의 path는 secret/dongne/local 이었습니다.
그렇기 때문에 application-name: dongne라고 설정해두어야 하고, profiles: ${spring.profiles.active} 통해 현재 실행되고 있는 profile 환경에 따라 vault의 secret 데이터를 가져오도록 설정해두면 됩니다.

설정을 마치고 애플리케이션을 실행하면 프로젝트에 따로 jdbc-url을 설정한 적이 없는데 vault에 있는 secret 내용을 가져와서 설정이 된 것을 확인할 수 있습니다.

이렇게 vault를 사용하게 되면 local 환경에서 실행할 때 뿐만 아니라 어떤 spring boot profile 환경에서든, 관리하기 어려운 민감데이터들을 vault 하나로 쉽게 관리할 수 있다는 점에서 실무에서 많이 사용되고 있는 것 같습니다.

 

📌 (번외) 적용해보면서 직면했던 또 다른 애로사항(테스트 실행시 vault 비활성화)

vault를 사용하는 것은 좋은데 테스트 실행시 vault를 사용하지 않고 테스트용 secret 데이터들은 application.yml 파일에다가 설정해두고 싶은 경우가 있을 수 있는데요. 이 때 조금 애를 먹었던 기억이 있습니다.

예를 들어 local profile에서는 vault를 활성화하고 test profile에서는 vault 비활성화해두고 싶은 경우 아래와 같이 설정하면 에러가 발생합니다.

# application-test.yml
spring:
    cloud:
    	vault:
            enabled: false
            
# application-local.yml
spring:
    config:
        import:
          - optional:file:./env/.env.${spring.profiles.active}[.properties] # local
          - vault://
    cloud:
        vault:
            uri: ${VAULT_DOMAIN_ADDR} # vault domain uri (port: 8200)
            authentication: TOKEN
            token: ${VAULT_AUTH_TOKEN} # vault root token
            connection-timeout: 3000
            read-timeout: 10000
            kv:
                backend: secret
                application-name: dongne
                default-context:
                profiles: ${spring.profiles.active}

각 profile에 해당하는 yml 설정파일에 위와 같이 설정하고 local 환경에서 애플리케이션을 실행하면 다음과 같은 에러가 발생합니다.

Caused by: java.net.URISyntaxException: Illegal character in path at index 1: ${VAULT_DOMAIN_ADDR}

env 파일이 분명 있음에도 해당 파일을 가져오지 못해서 그런 것인지 모르겠지만 정확한 원인은 아직 파악을 못했습니다. 특이한 것은 application-local.yml 이 아닌 application.yml에 설정하면 env 파일을 잘 가져온다는 것입니다.

그럼 기존대로 application.yml에 vault 관련 설정을 하고 test profile로 @SpringBootTest가 있는 테스트코드를 실행해보면 위와 똑같은 에러가 발생하게 됩니다.

# application.yml
spring:
  config:
    import:
      - optional:file:./env/.env.${spring.profiles.active}[.properties] # local
      - optional:vault://
  cloud:
    vault:
      uri: ${VAULT_DOMAIN_ADDR} # vault domain uri (port: 8200)
      authentication: TOKEN
      token: ${VAULT_AUTH_TOKEN} # vault root token
      connection-timeout: 3000
      read-timeout: 10000
      kv:
        backend: secret
        application-name: dongne
        default-context:
        profiles: ${spring.profiles.active}

---
spring:
  config:
    activate:
      on-profile: test

  cloud:
    vault:
      enabled: false

@SpringBootTest를 통한 테스트에서는 vault 비활성화, 애플리케이션 실행시에는 vault 적용하기 위해 결국 위와 같이 수정을 했습니다.

기존에 spring.config.import 부분에 vault:// 적용했던 부분을 optional로 바꾸고 같은 application.yml 파일에 test profile을 따로 만들어서 거기에 vault를 비활성화하였습니다. 이렇게 하니 테스트도, 애플리케이션 실행도 잘 이루어 지긴 했습니다.
(이런 방식 말고 테스트와 애플리케이션 실행시 vault 설정을 격리해서 관리하는 더 좋은 방법이 있을 것 같네요)