Skip to content 它致力于提供企业级敏感数据管理的全套解决方案,包括丰富的功能、易用的客户端和安全的存储方案,是该领域的不二之选,它的工作流程如下图所示
客户端:Vault 提供了丰富的使用方式,有 Web UI、命令行工具 Cli、HTTP API,也有基于 API 开发的各种编程语言的 SDK,这些使用者被成为客户端
身份验证:客户端提供自身的认证信息,Vault 使用这些信息来确定他们是否具有合法的访问权限,一旦通过验证,Vault 就会生成一个令牌(Token)给客户端并将其与设置好的访问策略关联起来。Vault 会将身份验证管理和决策委托给配置好的外部身份验证方法,比如 Amazon Web Services、GitHub、Google Cloud Platform、Kubernetes、Microsoft Azure、Okta 等,也支持使用内置的 Token 验证方式。不同的组织和场景,可以选用不同的验证方法。
数据访问:在 Vault 中,敏感数据逻辑上是存放在不同的 Secret Engines 模型中,实际存储后端支持不同的实现方案,默认情况下使用内置的基于 Raft 协议开发的本地存储。客户端通过验证之后取得相应的令牌,则可以通过此令牌访问自己权限内的敏感数据和资源。
启用 Secret、Transit、Database 引擎# ⚠️ 本文所有的 Vault 设置请勿直接在生产环境套用,请根据实际情况设置
因为启用过程比较简单就不过多介绍了,参考官方文档即可顺利启用。
KV - 该引擎是一种通用的键值存储,用于将任意机密信息存储在已配置的 Vault 物理存储中,此后端可以为一个键存储单个值,也可以启用版本控制,为每个键存储可设置数量的版本值
Transit - 该引擎是在数据传输过程中的加密解密功能,Vault 不会存储发送到此引擎的数据,它也可以被视为“密码学即服务”或“加密即服务”,该密钥引擎还可以对数据进行签名和验证;生成数据的哈希和 HMAC;以及充当随机字节的数据源。
Database - 该引擎可以通过配置的角色为数据库连接动态生成凭证,支持多种数据库并提供扩展框架,这意味着需要访问数据库的服务不再需要硬编码凭证,它们可以从 Vault 请求凭证,并利用 Vault 的租赁机制更轻松地轮换密钥,保证安全。
从 Vault KV Engine 读取敏感数据# 如果我们有一些敏感数据已经存放到 Vault 中,比如一个秘密的字符串,那么可以使用如下的代码将此字符串从 KV 中读取到程序中使用
java @RefreshScope
@RestController
@ConfigurationProperties
public class SecretController {
@Value("${secret:n/a}")
String secret;
@GetMapping("secret")
public String secret() {
return secret;
}
}配置文件中需要设置读取的 KV 的应用名,也就是在 Vault 中的路径
plain #默认情况下,Spring cloud vault 会读取路径 /secret engine/{application}/{profile 如果有的话}
#如果不设置 kv.application 则会读取 spring.application.name
#如果这个也没有,会读取默认生成的应用名称
spring.cloud.vault.kv.application-name=super-secret
spring.cloud.vault.kv.backend=kv
spring.cloud.vault.kv.enabled=true #默认值可不设置对应的我们需要在 Vault 中添加一个这样配置的 Key 并设置好我们的秘密字符串,如图
访问我们的应用,这可以得到 Vault 中存放的秘密
bash $ curl http://127.0.0.1:8080/secret
KEEP THIS IN VAULT我们将 Vault 中的数据更新一下,并通知 Spring boot,然后就能得到新的值
bash #通知 Spring 刷新
#需要依赖 org.springframework.boot:spring-boot-starter-actuator
#并在配置文件中暴漏 refresh 端口 management.endpoints.web.exposure.include=refresh
$ curl --request POST 'http://127.0.0.1:8080/actuator/refresh'
#获取最新值
$ curl http://127.0.0.1:8080/secret
Now you see the new value使用 Vault Transit 引擎作为加密服务# 假设在应用中,我们的 User 实体中有一个字段 password 需要加密存储到数据库中
java @Data
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Convert(converter = PasswordConverter.class)
private String password;
private String idCode;
}那么我们可以在 Converter 中使用 Vault 作为我们的加密服务
java @Service
public class PasswordConverter implements AttributeConverter<String, String> {
public static final String KEY_NAME = "password";
private final VaultOperations vaultOperations;
public PasswordConverter(VaultOperations vaultOperations) {
this.vaultOperations = vaultOperations;
}
@Override
public String convertToDatabaseColumn(String password) {
Plaintext plaintext = Plaintext.of(password);
return vaultOperations.opsForTransit().encrypt(KEY_NAME, plaintext).getCiphertext();
}
@Override
public String convertToEntityAttribute(String password) {
return vaultOperations.opsForTransit().decrypt(KEY_NAME, password);
}
}此时需要设置一下 Vault 的 Transit 引擎,添加我们需要的路径
使用 Vault Database 引擎来管理我们的数据库凭证并定时轮换# 现在公司要求数据库用户名和密码不能存放在配置文件中,那么我们就需要修改我们的配置文件
java spring.datasource.url=jdbc:mysql://127.0.0.1:3306/vault-your-data?createDatabaseIfNotExist=true
#不能再直接配置了
#spring.datasource.username=vault-data
#spring.datasource.password=123456
#需要从 Vault 中获取凭证,默认情况下会直接设置为上面注释的两个属性 spring.datasource.username & spring.datasource.password
spring.config.import=vault://
spring.cloud.vault.database.enabled=true
spring.cloud.vault.database.role=vault-root
spring.cloud.vault.database.backend=database
spring.cloud.vault.database.static-role=false对应的,我们需要设置 Vault 的 Database engine 来满足我们的需求
⚠️ 我们另外创建一个 Root 权限的账号来专门给 Vault 使用
创建 Role,就是客户端可以用的凭证,需要是动态的,具体 Dynamic 和 Static 的差别,请参考官方文档
设置完成之后,我们应用启动的时候就会从 Vault 获取到数据库的凭证并顺利初始化数据库连接
那么数据库密钥轮换之后,如何才能及时拿到新的凭证,并且保证应用不会下线呢
java @Configuration
@ConditionalOnBean(SecretLeaseContainer.class)
public class DatabaseLeaseEventHandler {
private final Logger log = LoggerFactory.getLogger(DatabaseLeaseEventHandler.class);
private final ConfigurableApplicationContext applicationContext;
private final HikariDataSource hikariDataSource;
private final SecretLeaseContainer secretLeaseContainer;
@Value("${spring.cloud.vault.database.role}")
private String datasourceRole;
public DatabaseLeaseEventHandler(ConfigurableApplicationContext applicationContext, HikariDataSource hikariDataSource,
SecretLeaseContainer secretLeaseContainer) {
this.applicationContext = applicationContext;
this.hikariDataSource = hikariDataSource;
this.secretLeaseContainer = secretLeaseContainer;
}
@PostConstruct
public void afterInit() {
var path = "database/creds/%s".formatted(datasourceRole);
secretLeaseContainer.addLeaseListener(leaseEvent -> {
RequestedSecret source = leaseEvent.getSource();
if (path.equals(source.getPath())) {
Mode mode = source.getMode();
if (leaseEvent instanceof SecretLeaseExpiredEvent && Mode.RENEW.equals(mode)) {
log.info("Database lease is expired, request new database credentials");
secretLeaseContainer.requestRotatingSecret(path);
} else if (leaseEvent instanceof SecretLeaseCreatedEvent secretLeaseCreatedEvent && Mode.ROTATE.equals(mode)) {
log.info("Database lease is created, update to new database credentials");
Map<String, Object> secrets = secretLeaseCreatedEvent.getSecrets();
var username = secrets.get("username");
var password = secrets.get("password");
Credential credential = new Credential(username, password);
if (!credential.valid()) {
log.error("Cannot get updated DB credentials. Shutting down.");
applicationContext.close();
return;
}
refreshDatabaseCredentials(credential);
}
}
});
}
private void refreshDatabaseCredentials(Credential credential) {
updateProperties(credential.stringify());
updateDataSource(credential.stringify());
}
private void updateProperties(StringCredential credential) {
System.setProperty("spring.datasource.username", credential.username());
System.setProperty("spring.datasource.password", credential.password());
}
private void updateDataSource(StringCredential credential) {
hikariDataSource.getHikariConfigMXBean().setUsername(credential.username());
hikariDataSource.getHikariConfigMXBean().setPassword(credential.password());
hikariDataSource.getHikariPoolMXBean().softEvictConnections();
log.info("Database credentials are updated");
}
}
record Credential(Object username, Object password) {
public boolean valid() {
return username != null && password != null;
}
public StringCredential stringify() {
return new StringCredential(username.toString(), password.toString());
}
}
record StringCredential(String username, String password) {}Vault 还有很多适用于不同场景的模型文中没有介绍,大家可以参考官方文档进一步了解选用。
试想如果将 Vault 和基础设施流水线结合起来,在不同的环境或者阶段,给应用提供不同的凭证和敏感数据,那么我们整个应用就是脱敏的,可以无压力分发,还具有足够的灵活性以在不同阶段具有不同的表现,保持健壮。
本文所涉及到的代码及配置已经提交至 Github,供参考