💡 Vault - HashCorp 사에서 만든 Vault는 다양한 환경에서 애플리케이션의 외부 비밀 속성(예: 데이터베이스 비밀번호, API 키 등)을 외부화된 구성으로 중앙에서 관리할 수 있습니다. - Spring Boot 환경에서 Vault로부터 시크릿 정보를 읽어오며 Valut에 시크릿 정보를 쓰는 것도 가능합니다. 이러한 방식으로 애플리케이션의 중요한 정보는 코드에서 분리되어 보안이 보장됩니다. - 기밀정보의 동적인 제공, 중앙 집중식 시크릿 관리, 즉각적인 액세스 제어, 감사 추적 기능 등을 제공하여, 기업의 보안 정책을 준수하는 데 도움이 됩니다.
1. Vault의 특징
특징
설명
기밀정보의 동적 제공
애플리케이션이 필요에 따라 비밀정보를 요청하고, Vault는 요청된 정보를 제공합니다.
중앙 집중식 시크릿 관리
모든 애플리케이션의 시크릿 정보를 중앙에서 관리할 수 있습니다.
즉각적인 액세스 제어
애플리케이션이 Vault에 접근 할 수 있는 ‘특정 토큰 아이디’를 기반으로 Vault 서비스를 호출하고, Vault는 해당 '특정 토큰 아이디'를 수신한 뒤 요청된 '비밀정보'를 제공합니다.
감사 추적 기능
Vault는 모든 액세스 요청을 로깅하여 감사를 위한 추적이 가능합니다.
2. Vault 인증 방식
💡 Vault 인증 방식
- Vault에 접근하기 위한 인증 방식을 의미합니다. Root Token, 정책 기반 토큰 (Policy-based Token), AppRole 등의 인증 방식을 통해서 Vault를 접근합니다.
인증 방식
설명
Root Token
모든 권한을 가진 ‘루트 권한 토큰’을 이용하는 인증 방식이며, 정책, 인증 방식, 시크릿 엔진, 모든 데이터 접근 가능한 인증 방식입니다. 절대적인 권한을 가지고 있기에, 직접적으로 애플리케이션 레벨에서 인증방식으로 사용되지 않습니다.
정책 기반 토큰 (Policy-based Token)
정책(policy)에 의해 접근 범위가 제한된 ‘토큰’을 이용하는 인증 방식이며, 정책 범위 별로 강제 제한된 권한을 부여하고 접근하는 인증방식 입니다.
AppRole
Role 별로 지정된 정책(Policy)에 따라서 접근 범위가 제한되며, ‘RoleID, Secret ID’에 따라 ‘토큰’을 발급받아서 접근 가능한 인증 방식입니다. 해당 Role은 운영환경 혹은 팀별로 접근을 제어할 수 있으며, 사용자는 Role Id, Secret Id를 통해 인증만 수행하고, 이후 토큰에 대해서는 머신이 이를 갱신/관리하기에 별도의 Token 재발급 과정을 수행하지 않습니다.
1. Spring Boot → Hashicorp Vault: Secret Request(비밀정보 요청) - Spring Boot 애플리케이션에서는 서버를 실행할 때, Valut에 접근할 수 있는 ‘특정 토큰 아이디’(Secret Request)’ 를 기반으로 Vault 서비스를 호출하여 ‘비밀 정보(Secret)’를 검색합니다.
2. Hashicorp Vault → Spring Boot: Secret Response(비밀정보 응답) - Valut 서비스는 Spring Boot 애플리케이션의 ‘특정 토큰 아이디’(Secret Request)’를 수신하고 요청된 ‘비밀 정보(Secret)’를 전달합니다. - 이 단계를 통해서 애플리케이션은 필요한 시크릿 정보를 안전하게 받아올 수 있습니다. 3. Spring Boot → Remote Service : Connection Request(커넥션 요청) - Spring Boot 애플리케이션이 원격 서비스와 연결을 시도하는 과정을 의미합니다. - 이 단계에서 애플리케이션은 Vault에서 안전하게 가져온 비밀 정보(예: 데이터베이스 비밀번호, API 키 등)를 사용하여 원격 서비스에 연결 요청을 보냅니다. - 이렇게 함으로써, 중요한 정보를 코드 내에 직접 작성하거나 노출시키지 않아도 되어 보안을 더 효과적으로 관리할 수 있습니다. 4. Remote Service → Spring Boot : Connection Response(커넥션 연결) - Vault 서비스가 Spring Boot 애플리케이션의 Secret Request를 수신하고, 요청된 '비밀 정보'를 애플리케이션으로 전송하는 단계입니다. - 이 단계를 통해 애플리케이션은 필요한 시크릿 정보를 안전하게 받아올 수 있습니다.
2) Database Secret Engine
💡 Database Secret Engine
- Database Secret Engine은 Vault가 DB 관리자 권한으로 접속하여 Role에 정의된 SQL 정책을 기반으로 요청 시마다 TTL(Time To Live)이 있는 임시 DB 계정을 생성하고, 만료 시 자동으로 폐기하는 동적 자격 증명 시스템을 의미합니다. - Vault는 데이터베이스에 관리자 권한을 가진 계정으로 접속하여 Role에 정의된 SQL 정책에 따라 사용자 생성 및 권한을 부여하며, TTL 만료 시 해당 계정을 자동으로 폐기한다.
1. 설정 과정
💡 설정 과정 1. Vaul내에 'DB 관리자 계정 설정'합니다. - Vault 내에서 '데이터베이스 관리자 계정(Admin)'을 설정합니다. - 해당 설정 과정을 통해서 추후에 관리자 계정을 통하여 Vault에서 동적인 임의의 계정을 생성하는 데 사용됩니다.
2. Vault에 'DB Role'을 생성하고 정의합니다. - Vault 내에서 '데이터베이스 접근 권한(Role)'을 설정합니다. - 이 접근 권한 과정에서 특정 권한에 대해 읽기만(read-only) 혹은 쓰기(Write) 권한 등.. 생성되는 사용자에 대한 데이터베이스 권한에 대해 제한을 둘 수 있습니다.
3. 클라이언트에서는 Vault로 자격증명(credential) 요청합니다. - 클라이언트(애플리케이션)에서 Vault 접속을 위한 자격증명을 수행을 합니다. - 클라이언트는 Vault로 접속을 하기 위해 사전에 구현된 인증 방식(Policy Token, AppRole)을 통해 인증을 수행합니다.
4. Vault에서 DB 사용자 생성합니다. - 자격 증명이 성공하면, Vault는 Role에 맞는 권한 및 TTL(Time To Live)을 가진 DB 사용자를 생성합니다.
5. Vault가 관리자 권한으로 DB에 접속합니다. - 생성된 동적인 사용자 정보(username, password)를 기반으로 데이터베이스에 접속을 합니다.
7. TTL 만료 시 자동 폐기 - 지정된 시간이 지난 애플리케이션의 사용자를 DB 접속 계정을 자동 폐기합니다.
1. 새 데이터베이스 생성 2. 새 사용자(Role) 생성 3. 다른 사용자에게 권한 부여 4. 복제 스트림 사용 5. 애플리케이션 DB 운영 대부분 수행
SELECT
rolname,
rolsuper,
rolcreatedb,
rolcreaterole,
rolcanlogin,
rolreplication,
rolbypassrls
FROM pg_roles
WHERE rolname = 'localmaster'
rolname | rolsuper | rolcreatedb | rolcreaterole | rolcanlogin | rolreplication | rolbypassrls
-------------+----------+-------------+---------------+-------------+----------------+--------------
localmaster | f | t | t | t | t | f
- 아래와 같이 각각에 추가 권한들을 추가해 주고, 중요한 Database Dynamic Credentials 부분을 추가합니다.
# ===============================
# 🔐 AppRole 로그인 권한
# ===============================
# AppRole 방식으로 로그인하기 위해 반드시 필요
# Spring Boot / CLI / Agent가 role_id + secret_id로 토큰을 발급받을 때 사용
path "auth/approle/login" {
capabilities = ["create"] # 로그인 요청은 create 권한
}
# ===============================
# 🗄 KV v2 (필수)
# ===============================
# ✅ loc 경로 자체에 대한 접근
# Spring Cloud Vault는 먼저 "루트 경로"를 조회함
path "kv2/data/loc" {
capabilities = ["read"]
}
# loc 메타데이터 조회 권한
path "kv2/metadata/loc" {
capabilities = ["list"]
}
# ===============================
# 📁 loc 하위 Secret 접근
# ===============================
# 실제 애플리케이션 설정 값 접근
# 예: kv2/loc/db_url, kv2/loc/db_name 등
path "kv2/data/loc/*" {
capabilities = ["read"]
}
# 하위 secret 목록 조회
# 일부 Vault 클라이언트/라이브러리에서 필요
path "kv2/metadata/loc/*" {
capabilities = ["list"]
}
# ===============================
# 🗄 Database Dynamic Credentials
# ===============================
# database secret engine에서
# testRole 기반 동적 DB 계정 발급 권한
#
# 이 경로가 없으면:
path "database/creds/testRole" {
capabilities = ["read"]
}
# ===============================
# 🔄 Token Self Management
# ===============================
# 현재 토큰 정보 조회
# Spring Cloud Vault가 토큰 TTL, renewable 여부 확인 시 사용
path "auth/token/lookup-self" {
capabilities = ["read"]
}
# 토큰 갱신 권한
# renewable 토큰일 경우 TTL 자동 연장 가능
path "auth/token/renew-self" {
capabilities = ["update"]
}
# 자기 자신 토큰 폐기 권한
# 정상 종료 시 Vault Agent / App에서 토큰 정리 가능
path "auth/token/revoke-self" {
capabilities = ["update"]
}
# 현재 토큰이 어떤 권한을 갖고 있는지 확인
# Spring Cloud Vault 내부 검증용
path "sys/capabilities-self" {
capabilities = ["update"]
}
2. 터미널에서 생성한 approle, ACL Policy를 기반으로 Role ID를 생성합니다.
- 기본 기능과 주요 라이브러리인 spring-cloud-dependencies, spring-cloud-starter-bootstrap, spring-cloud-starter-vault-config, spring-cloud-vault-config-databases를 설정하였습니다. - 추가로 데이터베이스 연결을 위해 PostgreSQL Driver, JDBC, MyBatis를 추가하였습니다.
- 사전 단계에서 발급받은 VAULT_APP_ROLE_ID, VAULT_APP_ROLE_SECRET 값을 환경 변수로 추가하였고 이를 불러옵니다. - 기존의 KV Engine의 경우는 이전과 동일한 구성입니다. - 이번 과정에서는 ‘Vault Database Engine 접속 정보’ 부분이 추가되었습니다. - 해당 내용은 아래와 같습니다.
- @Getter, @Setter 통해서 멤버 변수에 접근할 수 있도록 어노테이션을 지정하였습니다. - @Component를 빈으로 등록하여서, 다른 서비스나 컴포넌트에 주입하여 사용이 가능하도록 하였습니다. - @ConfigurationProperties(prefix = "vault") : 상위 prefix 내에 하위에 동일한 이름으로 각각 자동 바인딩이 됩니다.
💡 DBConfig - 서버가 실행이 되고 설정되는 Configuartion 파일입니다. - 해당 페이지에서는 Vault 접속 이후 KV Secret Engine에서 조회된 값을 기반으로 주요 정보를 가져옵니다. - 이를 토대로 JDBC URL을 연결하고 DataBase Secret Engine에서 동적으로 생성한 username, password를 기반으로 데이터베이스 연결을 수행합니다.
package com.adjh.springboot3vault.config;
import com.adjh.springboot3vault.properties.VaultDbProperties;
import com.adjh.springboot3vault.properties.VaultKVProperties;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 최초 데이터 베이스 연결 설정을 구성하는 설정 클래스입니다.
*/
@Configuration
public class DBConfig {
private final ApplicationContext applicationContext;
private final VaultKVProperties kvProperties;
private final VaultDbProperties dbProperties;
public DBConfig(ApplicationContext applicationContext, VaultKVProperties kvProperties, VaultDbProperties dbProperties) {
this.applicationContext = applicationContext;
this.kvProperties = kvProperties;
this.dbProperties = dbProperties;
}
/**
* DataSource 구성
*
* @return
*/
@Bean
public DataSource dataSource() {
HikariConfig hikariConfig = new HikariConfig();
// 1. KV Secret Engine에서 조회된 값
System.out.println("kvProperties = " + kvProperties.getDbUrl());
System.out.println("kvProperties = " + kvProperties.getDbPort());
System.out.println("kvProperties = " + kvProperties.getDbName());
// 2. KV Secret Engine 내에 조회한 속성을 조회하여 세팅함.
// [구조예시] jdbc:postgresql://localhost:5432/testdb
hikariConfig.setJdbcUrl(
String.format("jdbc:postgresql://%s:%s/%s",
kvProperties.getDbUrl(),
kvProperties.getDbPort(),
kvProperties.getDbName())
);
// 3. DB Secret Engine 내에서 조회한 속성을 조회하여 HikariCP에 세팅함.(동적 사용자, 비밀번호)
hikariConfig.setUsername(dbProperties.getUsername());
hikariConfig.setPassword(dbProperties.getPassword());
System.out.println("dbProperties = " + dbProperties.getUsername());
System.out.println("dbProperties = " + dbProperties.getPassword());
// 4. 기타 HikariCP 설정
hikariConfig.setDriverClassName("org.postgresql.Driver");
hikariConfig.setConnectionTimeout(10000); //커넥션 풀에서 새로운 커넥션을 가져올 때 최대 몇 ms까지 기다릴지를 설정 (단위: 밀리초)
hikariConfig.setValidationTimeout(13000); //커넥션이 유효한지 테스트할 때 (connection.isValid()) 검증 쿼리가 완료될 때까지 기다릴 최대 시간 (단위: 밀리초)
hikariConfig.setIdleTimeout(300000); //풀에 있는 커넥션이 아무 작업도 하지 않고 대기(idle) 상태로 유지될 수 있는 최대 시간 (단위: 밀리초)
hikariConfig.setMaxLifetime(600000); // 커넥션이 생성된 후 존재할 수 있는 최대 시간 (단위: 밀리초) - 10분
hikariConfig.setMaximumPoolSize(5);
hikariConfig.setMinimumIdle(1);
hikariConfig.setConnectionTestQuery("SELECT 1");
hikariConfig.setInitializationFailTimeout(-1);
return new HikariDataSource(hikariConfig);
}
/**
* MyBatis 구성
*
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean session = new SqlSessionFactoryBean();
session.setDataSource(dataSource);
session.setMapperLocations(applicationContext.getResources("classpath:mapper/*.xml"));
session.setTypeAliasesPackage("com.adjh.springboot3vault.model");
session.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:config/common-mybatis-config.xml"));
return session.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
💡 아래와 같이 KV Secret Engine 값과 DB Secret Engine 값이 정상적으로 불러와졌습니다.
💡 아래와 같이 서버를 재 실행할 경우 아래와 같이 동적으로 사용자와 비밀번호가 바뀜을 확인하였습니다.
9. 데이터베이스 연결 확인
💡 데이터베이스 연결 확인
- 첫 번째 줄에서 KV Secret Engine과 Database Secret Engine이 연결이 됨을 확인하였습니다. - HikariCP Connection Pool이 연결이 됨을 확인하였습니다.
10. 실제 데이터베이스에 접근하여 데이터를 조회합니다.
💡 실제 데이터베이스에 접근하여 데이터를 조회합니다.
- TB_USER 테이블 내의 사용자 정보를 가져오는 비즈니스 로직을 구성합니다.
💡 아래와 같이 MyBatis를 통해서 SQL문을 구성했고 이를 구성하는 비즈니스 로직은 제외하였습니다.
11. Controller에 Endpoint로 호출하여서 데이터 조회를 확인합니다.
💡 Controller에 Endpoint로 호출하여서 데이터 조회를 확인합니다
- 아래와 같은 API를 구성하였고 이를 호출합니다.
package com.adjh.springboot3vault.controller;
import com.adjh.springboot3vault.dto.UserDto;
import com.adjh.springboot3vault.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 사용자 PK(userSq)로 조회
*/
@PostMapping("/{userSq}")
public ResponseEntity<UserDto> findByUserSq(@PathVariable Long userSq) {
UserDto userDto = userService.findByUserSq(userSq);
return ResponseEntity.ok(userDto);
}
/**
* 사용자 ID로 조회
*/
@PostMapping("/userId")
public ResponseEntity<UserDto> findByUserId(@RequestParam String userId) {
UserDto userDto = userService.findByUserId(userId);
return ResponseEntity.ok(userDto);
}
/**
* 전체 사용자 조회
*/
@GetMapping("/user")
public ResponseEntity<List<UserDto>> findAll() {
return ResponseEntity.ok(userService.selectUser());
}
}
12. 실제 결과를 확인합니다
💡 실제 결과를 확인합니다
- v1/user/user를 통해서 TB_USER 테이블 내의 모든 사용자를 조회하는 비즈니스 로직을 수행했을 때, 모두 잘 가져옴을 확인하였습니다.
💡 [참고] SQL에서 아래와 같이 조회를 하였을 때, 연결된 사용자를 확인할 수도 있습니다
SELECT * FROM pg_stat_activity;
💡 [참고] 아래와 같이 Configuration 파일을 통해서 Datasouce로 연결된 사용자를 확인할 수도 있습니다.