Skip to content

Commit 2ff1261

Browse files
authored
Adding support for HashiCorp Vault dynamic database credentials (#401)
* Signed-off-by: Ilmar Kerm <[email protected]> Add support for "database" secret engine, also all other secret engine types by using Logical() backend type.
1 parent d103b23 commit 2ff1261

File tree

3 files changed

+109
-10
lines changed

3 files changed

+109
-10
lines changed

collector/config.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type HashiCorpVault struct {
8181
SecretPath string `yaml:"secretPath"`
8282
UsernameAttr string `yaml:"usernameAttribute"`
8383
PasswordAttr string `yaml:"passwordAttribute"`
84+
AsProxy string `yaml:"useAsProxyFor"`
8485
// Private to avoid making multiple calls
8586
fetchedSecert map[string]string
8687
}
@@ -160,14 +161,14 @@ func (c ConnectConfig) GetQueryTimeout() int {
160161
}
161162

162163
func (h HashiCorpVault) GetUsernameAttr() string {
163-
if h.UsernameAttr == "" {
164+
if h.UsernameAttr == "" || h.MountType == hashivault.MountTypeDatabase {
164165
return "username"
165166
}
166167
return h.UsernameAttr
167168
}
168169

169170
func (h HashiCorpVault) GetPasswordAttr() string {
170-
if h.PasswordAttr == "" {
171+
if h.PasswordAttr == "" || h.MountType == hashivault.MountTypeDatabase {
171172
return "password"
172173
}
173174
return h.PasswordAttr
@@ -193,7 +194,12 @@ func (d DatabaseConfig) GetUsername() string {
193194
}
194195
if d.isHashiCorpVault() && d.Vault.HashiCorp.MountType != "" && d.Vault.HashiCorp.MountName != "" && d.Vault.HashiCorp.SecretPath != "" {
195196
d.fetchHashiCorpVaultSecret()
196-
return d.Vault.HashiCorp.fetchedSecert[d.Vault.HashiCorp.GetUsernameAttr()]
197+
userName := d.Vault.HashiCorp.fetchedSecert[d.Vault.HashiCorp.GetUsernameAttr()]
198+
if d.Vault.HashiCorp.AsProxy == "" {
199+
return userName
200+
} else {
201+
return fmt.Sprintf("%s[%s]", userName, d.Vault.HashiCorp.AsProxy)
202+
}
197203
}
198204
return d.Username
199205
}

hashivault/hashivault.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ import (
1010
"net"
1111
"net/http"
1212
"time"
13+
"fmt"
1314
"github.com/oracle/oci-go-sdk/v65/example/helpers"
1415

1516
"log/slog"
1617
vault "github.com/hashicorp/vault/api"
1718
)
1819

20+
const (
21+
MountTypeKVv1 = "kvv1"
22+
MountTypeKVv2 = "kvv2"
23+
MountTypeDatabase = "database"
24+
MountTypeLogical = "logical"
25+
)
26+
1927
var UnsupportedMountType = errors.New("Unsupported HashiCorp Vault mount type")
2028
var RequiredKeyMissing = errors.New("Required key missing from HashiCorp Vault secret")
2129

@@ -75,11 +83,12 @@ func CreateVaultClient(logger *slog.Logger, socketPath string) HashicorpVaultCli
7583
func (c HashicorpVaultClient) getVaultSecret(mountType string, mount string, path string, requiredKeys []string) (map[string]string,error) {
7684
result := map[string]string{}
7785
var err error
78-
if mountType == "kvv2" || mountType == "kvv1" {
86+
var secretData map[string]interface{}
87+
if mountType == MountTypeKVv1 || mountType == MountTypeKVv2 {
7988
// Handle simple key-value secrets
8089
var secret *vault.KVSecret
8190
c.logger.Info("Making call to HashiCorp Vault", "mountType", mountType, "mountName", mount, "secretPath", path, "expectedKeys", requiredKeys)
82-
if mountType == "kvv2" {
91+
if mountType == MountTypeKVv2 {
8392
secret, err = c.client.KVv2(mount).Get(context.TODO(), path)
8493
} else {
8594
secret, err = c.client.KVv1(mount).Get(context.TODO(), path)
@@ -88,14 +97,31 @@ func (c HashicorpVaultClient) getVaultSecret(mountType string, mount string, pat
8897
c.logger.Error("Failed to fetch secret from HashiCorp Vault", "err", err)
8998
return result, err
9099
}
91-
// Expect simple one-level JSON, remap interface{} straight to string
92-
for key,val := range secret.Data {
93-
result[key] = strings.TrimRight(val.(string), "\r\n") // make sure a \r and/or \n didn't make it into the secret
100+
secretData = secret.Data
101+
} else if mountType == MountTypeDatabase || mountType == MountTypeLogical {
102+
// Handle other types of secrets, for example database roles, just using the Logical() backend
103+
var secret *vault.Secret
104+
var secretPath string
105+
if mountType == MountTypeDatabase {
106+
secretPath = fmt.Sprintf("%s/creds/%s", mount, path)
107+
} else {
108+
secretPath = fmt.Sprintf("%s/%s", mount, path)
109+
}
110+
c.logger.Info("Making logical call to HashiCorp Vault", "mountType", mountType, "mountName", mount, "secretPath", path, "expectedKeys", requiredKeys)
111+
secret, err = c.client.Logical().Read(secretPath)
112+
if err != nil {
113+
c.logger.Error("Failed to fetch secret from HashiCorp Vault", "err", err)
114+
return result, err
94115
}
116+
secretData = secret.Data
95117
} else {
96118
c.logger.Error(UnsupportedMountType.Error())
97119
return result, UnsupportedMountType
98120
}
121+
// Expect simple one-level JSON, remap interface{} straight to string
122+
for key,val := range secretData {
123+
result[key] = strings.TrimRight(val.(string), "\r\n") // make sure a \r and/or \n didn't make it into the secret
124+
}
99125
// Check that we have all required keys present
100126
for _, key := range requiredKeys {
101127
val, keyExists := result[key]

site/docs/configuration/hashicorp-vault.md

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ databases:
1515
vault:
1616
hashicorp:
1717
proxySocket: /var/run/vault/vault.sock
18-
mountType: secret engine type, currently either "kvv1" or "kvv2"
18+
mountType: "kvv1", "kvv2", "database" or "logical"
1919
mountName: secret engine mount path
20-
secretPath: path of the secret
20+
secretPath: path of the secret or database role name
2121
usernameAttribute: name of the JSON attribute, where to read the database username, if ommitted defaults to "username"
2222
passwordAttribute: name of the JSON attribute, where to read the database password, if ommitted defaults to "password"
2323
```
@@ -35,6 +35,73 @@ databases:
3535
secretPath: oracle/mydb/monitoring
3636
```
3737
38+
### Dynamic database credentials
39+
40+
Instead of fixed database credentials Vault also supports dynamic credentials that are created every time application requests them. This
41+
makes sure the credentials always have a short time-to-live and even if they leak, they quickly become invalid.
42+
43+
Follow [Vault documentation on how to set up Oracle database plugin for Vault](https://developer.hashicorp.com/vault/docs/secrets/databases/oracle).
44+
45+
A few additional notes about connecting exporter to CDB. NB! Below are just example commands, adjust them to fit your environment.
46+
47+
When setting up connection to CDB, then also need to edit "username_template" parameter, so Vault would create a C## common user for exporter.
48+
49+
```sh
50+
vault write database/config/mydb \
51+
plugin_name=vault-plugin-database-oracle \
52+
allowed_roles="mydb_exporter" \
53+
connection_url='{{username}}/{{password}}@//172.17.0.3:1521/FREE' \
54+
username_template='{{ printf "C##V_%s_%s_%s_%s" (.DisplayName | truncate 8) (.RoleName | truncate 8) (random 20) (unix_time) | truncate 30 | uppercase | replace "-" "_" | replace "." "_" }}' \
55+
username='c##vaultadmin' \
56+
password='vaultadmin'
57+
```
58+
59+
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.
60+
61+
```sql
62+
GRANT CREATE USER to c##vaultadmin WITH ADMIN OPTION container=all;
63+
GRANT ALTER USER to c##vaultadmin WITH ADMIN OPTION container=all;
64+
GRANT DROP USER to c##vaultadmin WITH ADMIN OPTION container=all;
65+
GRANT CREATE SESSION to c##vaultadmin WITH ADMIN OPTION;
66+
GRANT SELECT on gv_$session to c##vaultadmin;
67+
GRANT SELECT on v_$sql to c##vaultadmin;
68+
GRANT ALTER SYSTEM to c##vaultadmin WITH ADMIN OPTION;
69+
```
70+
71+
Create no authentication user in Oracle database, that has actual monitoring privileges.
72+
73+
```sql
74+
CREATE USER c##exporter NO AUTHENTICATION;
75+
GRANT create session TO c##exporter;
76+
GRANT all necessary privileges that Exporter needs TO c##exporter;
77+
```
78+
79+
Create database role in Vault:
80+
81+
```sh
82+
vault write database/roles/mydb_exporter \
83+
db_name=mydb \
84+
creation_statements='CREATE USER {{username}} IDENTIFIED BY "{{password}}"; GRANT CREATE SESSION TO {{username}}; ALTER USER c##exporter GRANT CONNECT THROUGH {{username}};' \
85+
default_ttl="7d" \
86+
max_ttl="10d"
87+
```
88+
89+
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.
90+
91+
And create database config in Exporter:
92+
93+
```yaml
94+
databases:
95+
mydb:
96+
vault:
97+
hashicorp:
98+
proxySocket: /var/run/vault/vault.sock
99+
mountType: database
100+
mountName: database
101+
secretPath: mydb_exporter
102+
useAsProxyFor: c##exporter
103+
```
104+
38105
### Authentication
39106
40107
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.

0 commit comments

Comments
 (0)