Skip to content

vault

The vault driver reads values from the KV secrets engine of HashiCorp Vault.

import "github.com/insei/tinyconf/drivers/vault"

vaultDriver, err := vault.New("app/config")

The driver talks to the HTTP API of Vault directly and adds no dependencies.

Keys

A secret in Vault is a JSON object. The vault tag is a key in it, tags of nested structs are keys of nested objects:

type Config struct {
    Database struct {
        Host     string `vault:"host"`
        Password string `vault:"password" hidden:"true"`
    } `vault:"database"`
    Hosts []string `vault:"hosts"`
}
vault kv put -mount=secret app/config - <<'EOF'
{"database": {"host": "db.internal", "password": "secret"}, "hosts": ["a", "b"]}
EOF

Values of the secret can be JSON values or strings: "port": 5432 and "port": "5432" are the same for an int field, "timeout": "1m30s" is a time.Duration, see Supported types.

Set the tags of the parents

The path of the nested field is built from the vault tags of its parents. If the Database field has no vault tag, the Password field is read from the password key in the root of the secret, and the manager logs a warning. Embedded structs without the tag are the exception.

Several secrets

vaultDriver, err := vault.New("app/config", vault.WithPaths("app/database", "shared/certificates"))

Secrets are merged in the order of addition, like several files: values from the next secret override values from the previous one, nested objects are merged by keys. It is a common layout: the settings and the credentials in different secrets with different access policies.

Options

vaultDriver, err := vault.New("app/config",
    vault.WithAddress("https://vault.internal:8200"),
    vault.WithTokenFile("/vault/secrets/token"),
    vault.WithMount("kv"),
)
Option Default Description
WithAddress(address) VAULT_ADDR or https://127.0.0.1:8200 URL of Vault
WithToken(token) VAULT_TOKEN the token
WithTokenFile(path) the file with the token, it is read before every request
WithNamespace(namespace) VAULT_NAMESPACE the namespace of Vault Enterprise
WithMount(mount) secret the mount of the KV secrets engine
WithKVv1() KV version 2 read from the KV secrets engine version 1
WithHTTPClient(client) http.DefaultClient the client with TLS settings, for example
WithPaths(paths...) additional secrets
WithOptional() secrets can be absent, by default an absent secret is an error
WithWatchInterval(interval) 30 seconds the interval of checking the secrets for changes

The first argument of New can be an empty string, if the secrets are set by WithPaths.

Authentication

The driver uses a token. Other methods — Kubernetes, AppRole, cloud IAM — are handled by Vault Agent: it logs in, renews the token and writes it to the file. Point the driver to this file:

vaultDriver, err := vault.New("app/config", vault.WithTokenFile("/vault/secrets/token"))

The file is read before every request, so the renewed token is used without restart of the application.

Errors

Case Result
the secret is absent error of Parse, unless WithOptional()
the token can not read the secret error of Parse that wraps tinyconf.ErrAccessDenied, logged as access denied
Vault is not available or sealed error of Parse

The driver is skipped in the Parse call with the error, see Missing files and values.

Reloading

Vault does not notify about changes, so the driver checks the versions of the secrets every 30 seconds while there are subscriptions. The interval is set with WithWatchInterval. A secret that appears, disappears or becomes available again after an error is a change too.

For KV version 2 the version of the secret is checked, for KV version 1 — the hash of its data.

Documentation

GenDoc("vault") generates the command of the Vault CLI that puts the current values to the first secret, values of hidden:"true" fields are empty:

vault kv put -mount=secret app/config - <<'EOF'
{
  "database": {
    "host": "db.internal",
    "password": ""
  },
  "hosts": [
    "a",
    "b"
  ]
}
EOF