基于Vault的敏感信息保护

基于Vault的敏感信息保护

背景

在应用程序的配置中,有一类信息比较敏感,比如数据库的用户名/密码、云平台的 AK/SK、各种 API keys、各类账号/密码等,这些信息的泄露会带来严重的安全问题。

然而在实际生产活动中,这些敏感信息的管理有很大的漏洞,存在很大的泄露风险:

  1. 代码或配置以明文形式记录敏感信息,存放在代码仓库中,甚至误上传到 GitHub;
  2. 敏感信息的生成、分发、保管、部署全流程经多人之手,缺乏有效的管控手段;
  3. 敏感信息生成之后长期有效,没有自动轮转机制,加大了泄露风险及影响程度。

敏感信息保护是网络安全工作的一个重要部分。

敏感信息保护

敏感信息保护是一个比较复杂的系统性工作,主要包括以下几个部分:

  • 要有一个专门的平台来托管敏感信息,本文采用 HashiCorp 公司开源的 Vault 工具
  • 应用程序要与该平台集成,从平台获取敏感信息,并完成续租和轮转等操作
  • 部署工具要与该平台集成,为应用程序注入登录平台所需的身份凭据

Vault 是一个强大的敏感信息管理工具,自带了多种认证引擎和密码引擎,并通过插件机制允许自定义引擎,可应用于多种常见的敏感信息保护场景,具体用法本文不做介绍,请参考Vault官方文档。至于部署发布工具与 Vault 的集成,与所使用的部署工具及发布流水线有关,每个公司不尽相同,本文也不做详细展开。

本文主要探讨应用程序与 Vault 的集成,以数据库凭据为例,介绍应用程序如何安全地从 Vault 获取敏感信息,并进一步实现自动轮转。

应用集成方案

应用程序与 Vault 的集成可以采用直接方式,即开发者自行编写代码实现登录认证、Token 续租、过期再登录以及敏感信息的获取、续租和轮转等逻辑,这种集成方式对应用程序有较多的代码侵入,实现成本较高。

Vault 官方提供了一种对应用程序代码低侵入甚至无侵入的集成方案,即 Vault Agent,它实现了与 Vault Server 的所有交互逻辑,并且还可以通过模板功能将获取的敏感信息渲染成本地配置文件,应用程序只需要读取该配置文件即可。

本文采用基于 Agent 的间接集成方案:Agent 负责登录 Vault 并管理 Token 续租及过期再登录,根据配置模板文件从 Vault 获取所需的敏感信息,渲染成本地配置文件,管理敏感信息的续租及轮转,并更新本地配置文件;应用程序只需读取本地配置文件获取敏感信息,并持续监听该文件,当文件变化时进行动态更新。这是一种完全解耦的间接集成方式,如下图所示。

34E5F7F3-6059-4653-8E70-9814DB8E4D2C.png

准备工作

1. 创建具有 CRUD 权限的数据库角色

# 首先启用数据库密码引擎
$ vault secrets enable database
# 创建 MySQL 数据库配置
$ export MYSQL_URL=x.x.x.x:3306
$ vault write database/config/mydb \
    plugin_name=mysql-database-plugin \
    connection_url="{{username}}:{{password}}@tcp($MYSQL_URL)/" \
    allowed_roles="mydb-crud" \
    username="root" \
    password="******"
# 说明:该用户需要具有用户管理权限,此处直接使用 root
# 创建 mydb-crud 角色(具有增删改查完整权限)
$ vault write database/roles/mydb-crud \
    db_name=mydb \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, DELETE, UPDATE ON mydb.* TO '{{name}}'@'%';" \
    default_ttl="2m" \
    max_ttl="10m"
# 说明:为方便测试,此处 TTL 设置较小,实际使用时需要评估合理的值
# 测试获取 mydb-crud 角色的凭据,并查看验证
$ vault read database/creds/mydb-crud
$ vault list sys/leases/lookup/database/creds/mydb-crud
# 首先启用数据库密码引擎
$ vault secrets enable database




# 创建 MySQL 数据库配置
$ export MYSQL_URL=x.x.x.x:3306
$ vault write database/config/mydb \
    plugin_name=mysql-database-plugin \
    connection_url="{{username}}:{{password}}@tcp($MYSQL_URL)/" \
    allowed_roles="mydb-crud" \
    username="root" \
    password="******"
# 说明:该用户需要具有用户管理权限,此处直接使用 root

# 创建 mydb-crud 角色(具有增删改查完整权限)
$ vault write database/roles/mydb-crud \
    db_name=mydb \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, DELETE, UPDATE ON mydb.* TO '{{name}}'@'%';" \
    default_ttl="2m" \
    max_ttl="10m"
# 说明:为方便测试,此处 TTL 设置较小,实际使用时需要评估合理的值
    
# 测试获取 mydb-crud 角色的凭据,并查看验证
$ vault read database/creds/mydb-crud
$ vault list sys/leases/lookup/database/creds/mydb-crud
# 首先启用数据库密码引擎 $ vault secrets enable database # 创建 MySQL 数据库配置 $ export MYSQL_URL=x.x.x.x:3306 $ vault write database/config/mydb \     plugin_name=mysql-database-plugin \     connection_url="{{username}}:{{password}}@tcp($MYSQL_URL)/" \     allowed_roles="mydb-crud" \     username="root" \     password="******" # 说明:该用户需要具有用户管理权限,此处直接使用 root # 创建 mydb-crud 角色(具有增删改查完整权限) $ vault write database/roles/mydb-crud \     db_name=mydb \     creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, DELETE, UPDATE ON mydb.* TO '{{name}}'@'%';" \     default_ttl="2m" \     max_ttl="10m" # 说明:为方便测试,此处 TTL 设置较小,实际使用时需要评估合理的值 # 测试获取 mydb-crud 角色的凭据,并查看验证 $ vault read database/creds/mydb-crud $ vault list sys/leases/lookup/database/creds/mydb-crud

2. 创建具有上述数据库权限的 AppRole

# 启用 Approle 认证引擎
$ vault auth enable approle
# 创建权限策略 mydb-policy
$ vault policy write mydb-policy -<<EOF
#获取凭据的权限
path "database/creds/mydb-crud" {
  capabilities = [ "read" ]
}
#续租凭据的权限
path "sys/leases/+/database/creds/mydb-crud/*" {
 capabilities = [ "update" ]
}
EOF
# 创建具有 mydb-policy 权限的 AppRole
$ vault write auth/approle/role/myapp token_policies="mydb-policy" \
    token_ttl=2m token_max_ttl=10m
# 查看创建的 AppRole
$ vault read auth/approle/role/myapp
# 启用 Approle 认证引擎
$ vault auth enable approle




# 创建权限策略 mydb-policy
$ vault policy write mydb-policy -<<EOF
#获取凭据的权限
path "database/creds/mydb-crud" {
  capabilities = [ "read" ]
}
#续租凭据的权限
path "sys/leases/+/database/creds/mydb-crud/*" {
  capabilities = [ "update" ]
}

EOF

# 创建具有 mydb-policy 权限的 AppRole
$ vault write auth/approle/role/myapp token_policies="mydb-policy" \
    token_ttl=2m token_max_ttl=10m

# 查看创建的 AppRole
$ vault read auth/approle/role/myapp
# 启用 Approle 认证引擎 $ vault auth enable approle # 创建权限策略 mydb-policy $ vault policy write mydb-policy -<<EOF #获取凭据的权限 path "database/creds/mydb-crud" {   capabilities = [ "read" ] } #续租凭据的权限 path "sys/leases/+/database/creds/mydb-crud/*" {   capabilities = [ "update" ] } EOF # 创建具有 mydb-policy 权限的 AppRole $ vault write auth/approle/role/myapp token_policies="mydb-policy" \     token_ttl=2m token_max_ttl=10m # 查看创建的 AppRole $ vault read auth/approle/role/myapp

3. 获取 AppRole 身份凭据( RoleID 和 SecretID )

# 获取 RoleID
$ vault read -field=role_id auth/approle/role/myapp/role-id >~/.roleid
# 获取 SecretID
$vault write -f -field=secret_id auth/approle/role/myapp/secret-id >~/.secretid
# 获取 RoleID
$ vault read -field=role_id auth/approle/role/myapp/role-id >~/.roleid




# 获取 SecretID
$vault write -f -field=secret_id auth/approle/role/myapp/secret-id >~/.secretid
# 获取 RoleID $ vault read -field=role_id auth/approle/role/myapp/role-id >~/.roleid # 获取 SecretID $vault write -f -field=secret_id auth/approle/role/myapp/secret-id >~/.secretid

然后由部署发布工具将 RoleID 和 SecretID 注入到应用程序所在服务器的约定位置文件中。

登录认证

Agent 的 Auto_Auth 功能实现了登录认证、Token 续租和过期再登录等逻辑,允许指定认证方法和 Token 保存位置。这里采用 AppRole 认证,需要指定 RoleID 和 SecretID 两个文件的位置(由部署发布工具注入)。

auto-auth 配置块如下所示:

auto_auth {
method {
type = "approle"
config = {
role_id_file_path = "/vault/config/approle/roleid"
secret_id_file_path = "/vault/config/approle/secretid"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/tmp/.vault-token-via-agent"
}
}
}
auto_auth {
  method {
    type   = "approle"
    config = {
      role_id_file_path = "/vault/config/approle/roleid"
      secret_id_file_path = "/vault/config/approle/secretid"
      remove_secret_id_file_after_reading = false
    }
  }

  sink "file" {
      config = {
          path = "/tmp/.vault-token-via-agent"
      }
  }
}
auto_auth { method { type = "approle" config = { role_id_file_path = "/vault/config/approle/roleid" secret_id_file_path = "/vault/config/approle/secretid" remove_secret_id_file_after_reading = false } } sink "file" { config = { path = "/tmp/.vault-token-via-agent" } } }

获取数据库凭据

Agent 的 Template 功能可以根据指定位置的模板文件获取所需的敏感信息,填充、渲染成配置文件,保存在指定位置,当渲染出的结果文件发生变化时还可以执行给定的命令。

template 相关的配置块如下所示:

template_config {
exit_on_retry_failure = true
}
template {
error_on_missing_key = true
source = "/vault/config/appconf/config.ctmpl"
destination = "/vault/config/appconf/config.yaml.tmp"
exec {
command = ["dd", "if=/vault/config/appconf/config.yaml.tmp", "of=/vault/config/appconf/config.yaml" ]
timeout = "5s"
}
}
template_config {
  exit_on_retry_failure = true
}

template {
  error_on_missing_key = true
  source = "/vault/config/appconf/config.ctmpl"
  destination = "/vault/config/appconf/config.yaml.tmp"
  exec {
    command = ["dd", "if=/vault/config/appconf/config.yaml.tmp", "of=/vault/config/appconf/config.yaml" ]
    timeout = "5s"
  }
}
template_config { exit_on_retry_failure = true } template { error_on_missing_key = true source = "/vault/config/appconf/config.ctmpl" destination = "/vault/config/appconf/config.yaml.tmp" exec { command = ["dd", "if=/vault/config/appconf/config.yaml.tmp", "of=/vault/config/appconf/config.yaml" ] timeout = "5s" } }

配置模板文件config.ctmpl通过模板语言指定敏感信息的占位符及获取路径,经 Agent 渲染后生成应用程序能识别的配置文件config.yaml。配置模板文件的相关片段如下所示:

# config.ctmpl
database:
mysql:
{{- with secret "database/creds/mydb-crud" }}
username: {{ .Data.username }}
password: {{ .Data.password }}
{{- end }}
address : x.x.x.x:3306
dbname : mydb
options : charset=utf8mb4&parseTime=True&loc=Local
# config.ctmpl
database:
  mysql:
    {{- with secret "database/creds/mydb-crud" }}
    username: {{ .Data.username }}
    password: {{ .Data.password }}
    {{- end }}
    address : x.x.x.x:3306
    dbname  : mydb
    options : charset=utf8mb4&parseTime=True&loc=Local
# config.ctmpl database: mysql: {{- with secret "database/creds/mydb-crud" }} username: {{ .Data.username }} password: {{ .Data.password }} {{- end }} address : x.x.x.x:3306 dbname : mydb options : charset=utf8mb4&parseTime=True&loc=Local

应用程序读取本地配置文件config.yaml即可获取数据库凭据,不需要与 Vault 进行交互,实现了与 Vault 的完全解耦。

使用数据库凭据

关于 Golang 应用程序如何读取配置文件可以参考《浅谈Golang配置管理》这篇文章。

这里仅给出使用数据库凭据相关的代码片段,如下:

var (
mysqlUsername string
mysqlPassword string
mysqlAddress string
mysqlDBname string
mysqlOptions string
)
func initConfig() {
mysqlUsername = Config.Database.MySQL.Username
mysqlPassword = Config.Database.MySQL.Password
mysqlAddress = Config.Database.MySQL.Address
mysqlDBname = Config.Database.MySQL.DBname
mysqlOptions = Config.Database.MySQL.Options
}
func connectMySQL() (*gorm.DB, error) {
msyqlDSN := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", mysqlUsername,
mysqlPassword, mysqlAddress, mysqlDBname, mysqlOptions)
return gorm.Open(mysql.Open(msyqlDSN), &gorm.Config{})
}
var (
    mysqlUsername string
    mysqlPassword string
    mysqlAddress  string
    mysqlDBname   string
    mysqlOptions  string
)

func initConfig() {
    mysqlUsername = Config.Database.MySQL.Username
    mysqlPassword = Config.Database.MySQL.Password
    mysqlAddress = Config.Database.MySQL.Address
    mysqlDBname = Config.Database.MySQL.DBname
    mysqlOptions = Config.Database.MySQL.Options
}

func connectMySQL() (*gorm.DB, error) {
    msyqlDSN := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", mysqlUsername,
        mysqlPassword, mysqlAddress, mysqlDBname, mysqlOptions)
        
    return gorm.Open(mysql.Open(msyqlDSN), &gorm.Config{})
}
var ( mysqlUsername string mysqlPassword string mysqlAddress string mysqlDBname string mysqlOptions string ) func initConfig() { mysqlUsername = Config.Database.MySQL.Username mysqlPassword = Config.Database.MySQL.Password mysqlAddress = Config.Database.MySQL.Address mysqlDBname = Config.Database.MySQL.DBname mysqlOptions = Config.Database.MySQL.Options } func connectMySQL() (*gorm.DB, error) { msyqlDSN := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", mysqlUsername, mysqlPassword, mysqlAddress, mysqlDBname, mysqlOptions) return gorm.Open(mysql.Open(msyqlDSN), &gorm.Config{}) }

数据库凭据自动轮转

Agent 从 Vault 获取数据库凭据后,会在其TTL到期前进行续租,当因Max-TTL限制无法续租时,会自动轮转,重新获取一组新的凭据,并更新在本地配置文件中。

应用程序监听到本地配置文件的变化时,需要读取新的凭据,并进行动态加载。配置动态更新的具体方法可以参考《浅谈Golang配置管理》这篇文章。

这里仅给出配置动态加载相关的示例代码,如下:

var db *gorm.DB
var dbLocker sync.Mutex
func reconnectMySQL() {
// get new mysql creds
creds := getNewMySQLCreds()
if creds.Username == mysqlUsername && creds.Password == mysqlPassword {
log.Println("MySQL creds not changed, skip mysql reconnection.")
return
}
dbLocker.Lock()
defer dbLocker.Unlock()
// re-connect mysql with new creds
mysqlUsername = creds.Username
mysqlPassword = creds.Password
d, err := connectMySQL()
if err != nil {
log.Println("MySQL connect failed:", err)
return
}
// setupDatabase(d)
db = d
}
var db *gorm.DB
var dbLocker sync.Mutex




func reconnectMySQL() {
    // get new mysql creds
    creds := getNewMySQLCreds()
    if creds.Username == mysqlUsername && creds.Password == mysqlPassword {
        log.Println("MySQL creds not changed, skip mysql reconnection.")
        return
    }

    dbLocker.Lock()
    defer dbLocker.Unlock()

    // re-connect mysql with new creds
    mysqlUsername = creds.Username
    mysqlPassword = creds.Password
    d, err := connectMySQL()
    if err != nil {
        log.Println("MySQL connect failed:", err)
        return
    }
    
    // setupDatabase(d)

    db = d
}
var db *gorm.DB var dbLocker sync.Mutex func reconnectMySQL() { // get new mysql creds creds := getNewMySQLCreds() if creds.Username == mysqlUsername && creds.Password == mysqlPassword { log.Println("MySQL creds not changed, skip mysql reconnection.") return } dbLocker.Lock() defer dbLocker.Unlock() // re-connect mysql with new creds mysqlUsername = creds.Username mysqlPassword = creds.Password d, err := connectMySQL() if err != nil { log.Println("MySQL connect failed:", err) return } // setupDatabase(d) db = d }

说明:上述示例代码通过重建gorm.DB对象来更新数据库凭据,是一种可行的方式,但是比较粗暴,会导致连接重建,在业务高峰期时可能会影响服务性能,在生产上建议寻求更优雅、平滑的实现方式。大家有好的实现或思路可以在评论区留言分享。

总结

本文探讨了基于 Vault 的敏感信息保护方案,重点介绍了应用程序通过 Agent 与 Vault 间接集成的方法,以数据库凭据为例,具体说明了应用程序如何安全地从 Vault 获取敏感信息,并进一步实现自动轮转。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYFQ6brU' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片