diff --git a/CHANGELOG.md b/CHANGELOG.md index ee1c2cd46..6326d9eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ ## Changelog +### Unreleased + +#### Improvements + +* [PR #620](https://github.com/fabiolb/fabio/pull/620): Read Vault token from file + + The new `vaultfetchtoken` option for the vault and vault-pki certificate + sources can be used to load Vault tokens from environment variables other + than `VAULT_TOKEN` and from files on disk. fabio will automatically notice + when file contents change and start using new tokens. + + This improves integration with [Nomad](https://www.nomadproject.io/docs/job-specification/vault.html) + and the [Vault Agent](https://www.vaultproject.io/docs/agent/). + + Thanks to [@murphymj25](https://github.com/murphymj25) for the patch. + ### [v1.5.11](https://github.com/fabiolb/fabio/releases/tag/v1.5.11) - 25 Feb 2019 #### Breaking Changes diff --git a/cert/source.go b/cert/source.go index 6ce1dd3c3..7c5378769 100644 --- a/cert/source.go +++ b/cert/source.go @@ -70,7 +70,7 @@ func NewSource(cfg config.CertSource) (Source, error) { ClientCAPath: cfg.ClientCAPath, CAUpgradeCN: cfg.CAUpgradeCN, Refresh: cfg.Refresh, - Client: DefaultVaultClient, + Client: NewVaultClient(cfg.VaultFetchToken), }, nil case "vault-pki": src := NewVaultPKISource() @@ -78,7 +78,7 @@ func NewSource(cfg config.CertSource) (Source, error) { src.ClientCAPath = cfg.ClientCAPath src.CAUpgradeCN = cfg.CAUpgradeCN src.Refresh = cfg.Refresh - src.Client = DefaultVaultClient + src.Client = NewVaultClient(cfg.VaultFetchToken) return src, nil default: diff --git a/cert/vault_client.go b/cert/vault_client.go index e41afe79f..fe2e0618c 100644 --- a/cert/vault_client.go +++ b/cert/vault_client.go @@ -3,7 +3,9 @@ package cert import ( "encoding/json" "errors" + "io/ioutil" "log" + "os" "strings" "sync" "time" @@ -14,20 +16,45 @@ import ( // vaultClient wraps an *api.Client and takes care of token renewal // automatically. type vaultClient struct { - addr string // overrides the default config - token string // overrides the VAULT_TOKEN environment variable + addr string // overrides the default config + token string // overrides the VAULT_TOKEN environment variable + fetchVaultToken string + prevFetchedToken string client *api.Client mu sync.Mutex } +func NewVaultClient(fetchVaultToken string) *vaultClient { + return &vaultClient{ + fetchVaultToken: fetchVaultToken, + } +} + var DefaultVaultClient = &vaultClient{} func (c *vaultClient) Get() (*api.Client, error) { c.mu.Lock() defer c.mu.Unlock() - if c.client != nil { + if c.fetchVaultToken != "" { + token := strings.TrimSpace(getVaultToken(c.fetchVaultToken)) + if token != c.prevFetchedToken { + log.Printf("[DEBUG] vault: token has changed, setting new token") + // did we get a wrapped token? + resp, err := c.client.Logical().Unwrap(token) + switch { + case err == nil: + log.Printf("[INFO] vault: Unwrapped token %s", token) + c.client.SetToken(resp.Auth.ClientToken) + case strings.HasPrefix(err.Error(), "no value found at"): + // not a wrapped token + default: + return nil, err + } + c.prevFetchedToken = token + } + } return c.client, nil } @@ -39,16 +66,22 @@ func (c *vaultClient) Get() (*api.Client, error) { if c.addr != "" { conf.Address = c.addr } - client, err := api.NewClient(conf) if err != nil { return nil, err } + if c.fetchVaultToken != "" { + token := strings.TrimSpace(getVaultToken(c.fetchVaultToken)) + log.Printf("[DEBUG] vault: fetching initial token") + if token != c.prevFetchedToken { + c.token = token + c.prevFetchedToken = token + } + } if c.token != "" { client.SetToken(c.token) } - token := client.Token() if token == "" { return nil, errors.New("vault: no token") @@ -132,3 +165,34 @@ func (c *vaultClient) keepTokenAlive() { timer.Reset(ttl / 2) } } + +func getVaultToken(c string) string { + var token string + c = strings.TrimSpace(c) + cArray := strings.SplitN(c, ":", 2) + if len(cArray) < 2 { + log.Printf("[WARN] vault: vaultfetchtoken not properly set") + return token + } + if cArray[0] == "file" { + b, err := ioutil.ReadFile(cArray[1]) // just pass the file name + if err != nil { + log.Printf("[WARN] vault: Failed to fetch token from %s", c) + } else { + token = string(b) + log.Printf("[DEBUG] vault: Successfully fetched token from %s", c) + return token + } + } else if cArray[0] == "env" { + token = os.Getenv(cArray[1]) + if len(token) == 0 { + log.Printf("[WARN] vault: Failed to fetch token from %s", c) + } else { + log.Printf("[DEBUG] vault: Successfully fetched token from %s", c) + return token + } + } else { + log.Printf("[WARN] vault: vaultfetchtoken not properly set") + } + return token +} diff --git a/config/config.go b/config/config.go index 59fce39c0..d04cd3530 100644 --- a/config/config.go +++ b/config/config.go @@ -22,14 +22,15 @@ type Config struct { } type CertSource struct { - Name string - Type string - CertPath string - KeyPath string - ClientCAPath string - CAUpgradeCN string - Refresh time.Duration - Header http.Header + Name string + Type string + CertPath string + KeyPath string + ClientCAPath string + CAUpgradeCN string + Refresh time.Duration + Header http.Header + VaultFetchToken string } type Listen struct { diff --git a/config/load.go b/config/load.go index 5578f69e0..be3102bcb 100644 --- a/config/load.go +++ b/config/load.go @@ -577,6 +577,8 @@ func parseCertSource(cfg map[string]string) (c CertSource, err error) { return CertSource{}, err } c.Refresh = d + case "vaultfetchtoken": + c.VaultFetchToken = v case "hdr": p := strings.SplitN(v, ": ", 2) if len(p) != 2 { diff --git a/fabio.properties b/fabio.properties index e007b15e4..6b9c43df7 100644 --- a/fabio.properties +++ b/fabio.properties @@ -114,8 +114,9 @@ # automatic refreshing set 'refresh' to zero. # # The path to vault must be provided in the VAULT_ADDR environment -# variable. The token must be provided in the VAULT_TOKEN environment -# variable. +# variable. The token can be provided in the VAULT_TOKEN environment +# variable, or provided by using the Vault fetch token option. By default the +# token is loaded once from the VAULT_TOKEN environment variable. See Vault PKI for details. # # cs=;type=vault;cert=secret/fabio/certs # @@ -137,6 +138,12 @@ # and re-issue them 24 hours before they expire. The CA for client # authentication is expected to be stored at secret/fabio/client-certs. # +# 'vaultfetchtoken' enables fetching the vault token from a file on the filesystem or an environment +# variable at the Vault refresh interval. If fetching the token from a file the 'file:[path]' syntax should be used, +# if fetching the token from an env variable, the 'env:[ENV]' syntax should be used. +# +# cs=;type=vault;cert=secret/fabio/certs;vaultfetchtoken=env:VAULT_TOKEN +# # Common options # # All certificate stores support the following options: