From 2aeb1649a15b085ab25eb80bf26d06c55d2427d6 Mon Sep 17 00:00:00 2001 From: Ilmar Kerm Date: Sun, 19 Oct 2025 21:42:45 +0200 Subject: [PATCH 1/4] Signed-off-by: Ilmar Kerm Add support for "database" secret engine, also all other secret engine types by using Logical() backend type. --- collector/config.go | 12 ++++++++--- hashivault/hashivault.go | 25 +++++++++++++++++++--- site/docs/configuration/hashicorp-vault.md | 8 +++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/collector/config.go b/collector/config.go index 9ecd2dee..60af7c05 100644 --- a/collector/config.go +++ b/collector/config.go @@ -81,6 +81,7 @@ type HashiCorpVault struct { SecretPath string `yaml:"secretPath"` UsernameAttr string `yaml:"usernameAttribute"` PasswordAttr string `yaml:"passwordAttribute"` + AsProxy string `yaml:"useAsProxyFor"` // Private to avoid making multiple calls fetchedSecert map[string]string } @@ -160,14 +161,14 @@ func (c ConnectConfig) GetQueryTimeout() int { } func (h HashiCorpVault) GetUsernameAttr() string { - if h.UsernameAttr == "" { + if h.UsernameAttr == "" || h.MountType == "database" { return "username" } return h.UsernameAttr } func (h HashiCorpVault) GetPasswordAttr() string { - if h.PasswordAttr == "" { + if h.PasswordAttr == "" || h.MountType == "database" { return "password" } return h.PasswordAttr @@ -193,7 +194,12 @@ func (d DatabaseConfig) GetUsername() string { } if d.isHashiCorpVault() && d.Vault.HashiCorp.MountType != "" && d.Vault.HashiCorp.MountName != "" && d.Vault.HashiCorp.SecretPath != "" { d.fetchHashiCorpVaultSecret() - return d.Vault.HashiCorp.fetchedSecert[d.Vault.HashiCorp.GetUsernameAttr()] + userName := d.Vault.HashiCorp.fetchedSecert[d.Vault.HashiCorp.GetUsernameAttr()] + if d.Vault.HashiCorp.AsProxy == "" { + return userName + } else { + return fmt.Sprintf("%s[%s]", userName, d.Vault.HashiCorp.AsProxy) + } } return d.Username } diff --git a/hashivault/hashivault.go b/hashivault/hashivault.go index e7dbf8c4..4a3a22d9 100644 --- a/hashivault/hashivault.go +++ b/hashivault/hashivault.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "time" + "fmt" "github.com/oracle/oci-go-sdk/v65/example/helpers" "log/slog" @@ -75,6 +76,7 @@ func CreateVaultClient(logger *slog.Logger, socketPath string) HashicorpVaultCli func (c HashicorpVaultClient) getVaultSecret(mountType string, mount string, path string, requiredKeys []string) (map[string]string,error) { result := map[string]string{} var err error + var secretData map[string]interface{} if mountType == "kvv2" || mountType == "kvv1" { // Handle simple key-value secrets var secret *vault.KVSecret @@ -88,14 +90,31 @@ func (c HashicorpVaultClient) getVaultSecret(mountType string, mount string, pat c.logger.Error("Failed to fetch secret from HashiCorp Vault", "err", err) return result, err } - // Expect simple one-level JSON, remap interface{} straight to string - for key,val := range secret.Data { - result[key] = strings.TrimRight(val.(string), "\r\n") // make sure a \r and/or \n didn't make it into the secret + secretData = secret.Data + } else if mountType == "database" || mountType == "logical" { + // Handle other types of secrets, for example database roles, just using the Logical() backend + var secret *vault.Secret + var secretPath string + if mountType == "database" { + secretPath = fmt.Sprintf("%s/creds/%s", mount, path) + } else { + secretPath = fmt.Sprintf("%s/%s", mount, path) + } + c.logger.Info("Making logical call to HashiCorp Vault", "mountType", mountType, "mountName", mount, "secretPath", path, "expectedKeys", requiredKeys) + secret, err = c.client.Logical().Read(secretPath) + if err != nil { + c.logger.Error("Failed to fetch secret from HashiCorp Vault", "err", err) + return result, err } + secretData = secret.Data } else { c.logger.Error(UnsupportedMountType.Error()) return result, UnsupportedMountType } + // Expect simple one-level JSON, remap interface{} straight to string + for key,val := range secretData { + result[key] = strings.TrimRight(val.(string), "\r\n") // make sure a \r and/or \n didn't make it into the secret + } // Check that we have all required keys present for _, key := range requiredKeys { val, keyExists := result[key] diff --git a/site/docs/configuration/hashicorp-vault.md b/site/docs/configuration/hashicorp-vault.md index 4939927b..26e77919 100644 --- a/site/docs/configuration/hashicorp-vault.md +++ b/site/docs/configuration/hashicorp-vault.md @@ -15,9 +15,9 @@ databases: vault: hashicorp: proxySocket: /var/run/vault/vault.sock - mountType: secret engine type, currently either "kvv1" or "kvv2" + mountType: "kvv1", "kvv2", "database" or "logical" mountName: secret engine mount path - secretPath: path of the secret + secretPath: path of the secret or database role name usernameAttribute: name of the JSON attribute, where to read the database username, if ommitted defaults to "username" passwordAttribute: name of the JSON attribute, where to read the database password, if ommitted defaults to "password" ``` @@ -35,6 +35,10 @@ databases: secretPath: oracle/mydb/monitoring ``` +### Dynamic database credentials + + + ### Authentication In this first version it currently only supports queries via HashiCorp Vault Proxy configured to run on the local host and listening on a Unix socket. Currently also required use_auto_auth_token option to be set. From decd61213c9f5f919eed8c6b814ae7085ab79132 Mon Sep 17 00:00:00 2001 From: Ilmar Kerm Date: Mon, 20 Oct 2025 17:09:20 +0200 Subject: [PATCH 2/4] Signed-off-by: Ilmar Kerm Added documentation about using HashiCorp Vault dynamic database credentials. --- site/docs/configuration/hashicorp-vault.md | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/site/docs/configuration/hashicorp-vault.md b/site/docs/configuration/hashicorp-vault.md index 26e77919..649c966f 100644 --- a/site/docs/configuration/hashicorp-vault.md +++ b/site/docs/configuration/hashicorp-vault.md @@ -37,7 +37,69 @@ databases: ### Dynamic database credentials +Instead of fixed database credentials Vault also supports dynamic credentials that are created every time application requests them. This +makes sure the credentials always have a short time-to-live and even if they leak, they quickly become invalid. +Follow [Vault documentation on how to set up Oracle database plugin for Vault](https://developer.hashicorp.com/vault/docs/secrets/databases/oracle). + +A few additional notes about connecting exporter to CDB. NB! Below are just example commands, adjust them to fit your environment. + +When setting up connection to CDB, then also need to edit "username_template" parameter, so Vault would create a C## common user for exporter. + +```sh +vault write database/config/mydb \ + plugin_name=vault-plugin-database-oracle \ + allowed_roles="mydb_exporter" \ + connection_url='{{username}}/{{password}}@//172.17.0.3:1521/FREE' \ + username_template='{{ printf "C##V_%s_%s_%s_%s" (.DisplayName | truncate 8) (.RoleName | truncate 8) (random 20) (unix_time) | truncate 30 | uppercase | replace "-" "_" | replace "." "_" }}' \ + username='c##vaultadmin' \ + password='vaultadmin' +``` + +Since Vault is creating common users in CDB, it needs to have CREATE/ALTER/DROP USER privileges on all containers. Here is a modification of the documented Vault Oracle plugin admin user privileges. + +```sql +GRANT CREATE USER to c##vaultadmin WITH ADMIN OPTION container=all; +GRANT ALTER USER to c##vaultadmin WITH ADMIN OPTION container=all; +GRANT DROP USER to c##vaultadmin WITH ADMIN OPTION container=all; +GRANT CREATE SESSION to c##vaultadmin WITH ADMIN OPTION; +GRANT SELECT on gv_$session to c##vaultadmin; +GRANT SELECT on v_$sql to c##vaultadmin; +GRANT ALTER SYSTEM to c##vaultadmin WITH ADMIN OPTION; +``` + +Create no authentication user in Oracle database, that has actual monitoring privileges. + +```sql +CREATE USER c##exporter NO AUTHENTICATION; +GRANT create session TO c##exporter; +GRANT all necessary privileges that Exporter needs TO c##exporter; +``` + +Create database role in Vault: + +```sh +vault write database/roles/mydb_exporter \ + db_name=mydb \ + creation_statements='CREATE USER {{username}} IDENTIFIED BY "{{password}}"; GRANT CREATE SESSION TO {{username}}; ALTER USER c##exporter GRANT CONNECT THROUGH {{username}};' \ + default_ttl="7d" \ + max_ttl="10d" +``` + +NB! Make sure to restart Exporter before TTL above expires, this will fetch new database credentials. When TTL expires, Vault will drop the dynamically created database users. + +And create database config in Exporter: + +```yaml +databases: + mydb: + vault: + hashicorp: + proxySocket: /var/run/vault/vault.sock + mountType: database + mountName: database + secretPath: mydb_exporter +``` ### Authentication From 52be46e1144d34b71982abb612ab51eb94b926bf Mon Sep 17 00:00:00 2001 From: Ilmar Kerm Date: Mon, 20 Oct 2025 17:14:52 +0200 Subject: [PATCH 3/4] Forgot to add "useAsPorxyFor" line. --- site/docs/configuration/hashicorp-vault.md | 1 + 1 file changed, 1 insertion(+) diff --git a/site/docs/configuration/hashicorp-vault.md b/site/docs/configuration/hashicorp-vault.md index 649c966f..3dd38dd3 100644 --- a/site/docs/configuration/hashicorp-vault.md +++ b/site/docs/configuration/hashicorp-vault.md @@ -99,6 +99,7 @@ databases: mountType: database mountName: database secretPath: mydb_exporter + useAsProxyFor: c##exporter ``` ### Authentication From 9bcd300a0eed09bb2a7835803d2215ec125a4079 Mon Sep 17 00:00:00 2001 From: Ilmar Kerm Date: Tue, 21 Oct 2025 17:41:35 +0200 Subject: [PATCH 4/4] Changed Vault secret engine types to constants --- collector/config.go | 4 ++-- hashivault/hashivault.go | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/collector/config.go b/collector/config.go index 60af7c05..ae5f46c1 100644 --- a/collector/config.go +++ b/collector/config.go @@ -161,14 +161,14 @@ func (c ConnectConfig) GetQueryTimeout() int { } func (h HashiCorpVault) GetUsernameAttr() string { - if h.UsernameAttr == "" || h.MountType == "database" { + if h.UsernameAttr == "" || h.MountType == hashivault.MountTypeDatabase { return "username" } return h.UsernameAttr } func (h HashiCorpVault) GetPasswordAttr() string { - if h.PasswordAttr == "" || h.MountType == "database" { + if h.PasswordAttr == "" || h.MountType == hashivault.MountTypeDatabase { return "password" } return h.PasswordAttr diff --git a/hashivault/hashivault.go b/hashivault/hashivault.go index 4a3a22d9..93dc0b3a 100644 --- a/hashivault/hashivault.go +++ b/hashivault/hashivault.go @@ -17,6 +17,13 @@ import ( vault "github.com/hashicorp/vault/api" ) +const ( + MountTypeKVv1 = "kvv1" + MountTypeKVv2 = "kvv2" + MountTypeDatabase = "database" + MountTypeLogical = "logical" +) + var UnsupportedMountType = errors.New("Unsupported HashiCorp Vault mount type") var RequiredKeyMissing = errors.New("Required key missing from HashiCorp Vault secret") @@ -77,11 +84,11 @@ func (c HashicorpVaultClient) getVaultSecret(mountType string, mount string, pat result := map[string]string{} var err error var secretData map[string]interface{} - if mountType == "kvv2" || mountType == "kvv1" { + if mountType == MountTypeKVv1 || mountType == MountTypeKVv2 { // Handle simple key-value secrets var secret *vault.KVSecret c.logger.Info("Making call to HashiCorp Vault", "mountType", mountType, "mountName", mount, "secretPath", path, "expectedKeys", requiredKeys) - if mountType == "kvv2" { + if mountType == MountTypeKVv2 { secret, err = c.client.KVv2(mount).Get(context.TODO(), path) } else { secret, err = c.client.KVv1(mount).Get(context.TODO(), path) @@ -91,11 +98,11 @@ func (c HashicorpVaultClient) getVaultSecret(mountType string, mount string, pat return result, err } secretData = secret.Data - } else if mountType == "database" || mountType == "logical" { + } else if mountType == MountTypeDatabase || mountType == MountTypeLogical { // Handle other types of secrets, for example database roles, just using the Logical() backend var secret *vault.Secret var secretPath string - if mountType == "database" { + if mountType == MountTypeDatabase { secretPath = fmt.Sprintf("%s/creds/%s", mount, path) } else { secretPath = fmt.Sprintf("%s/%s", mount, path)