Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions collector/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -160,14 +161,14 @@ func (c ConnectConfig) GetQueryTimeout() int {
}

func (h HashiCorpVault) GetUsernameAttr() string {
if h.UsernameAttr == "" {
if h.UsernameAttr == "" || h.MountType == hashivault.MountTypeDatabase {
return "username"
}
return h.UsernameAttr
}

func (h HashiCorpVault) GetPasswordAttr() string {
if h.PasswordAttr == "" {
if h.PasswordAttr == "" || h.MountType == hashivault.MountTypeDatabase {
return "password"
}
return h.PasswordAttr
Expand All @@ -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
}
Expand Down
36 changes: 31 additions & 5 deletions hashivault/hashivault.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ import (
"net"
"net/http"
"time"
"fmt"
"github.com/oracle/oci-go-sdk/v65/example/helpers"

"log/slog"
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")

Expand Down Expand Up @@ -75,11 +83,12 @@ 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
if mountType == "kvv2" || mountType == "kvv1" {
var secretData map[string]interface{}
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)
Expand All @@ -88,14 +97,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 == 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 == MountTypeDatabase {
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]
Expand Down
71 changes: 69 additions & 2 deletions site/docs/configuration/hashicorp-vault.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
Expand All @@ -35,6 +35,73 @@ databases:
secretPath: oracle/mydb/monitoring
```

### 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
useAsProxyFor: c##exporter
```

### 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.
Expand Down