Move environment variable reading logic to API.

This allows the same environment variables to be read, parsed, and used
from any API client as was previously handled in the CLI. The CLI now
uses the API environment variable reading capability, then overrides any
values from command line flags, if necessary.

Fixes #618
This commit is contained in:
Jeff Mitchell 2015-11-03 14:21:14 -05:00
parent f8c13ed69f
commit 32e23bea71
6 changed files with 229 additions and 165 deletions

View File

@ -18,6 +18,8 @@ IMPROVEMENTS:
* api: API client now uses a 60 second timeout instead of indefinite [GH-681]
* api: Implement LookupSelf, RenewSelf, and RevokeSelf functions for auth
tokens [GH-739]
* api: Standardize environment variable reading logic inside the API; the CLI
now uses this but can still override via command-line parameters [GH-618]
* audit: HMAC-SHA256'd client tokens are now stored with each request entry.
Previously they were only displayed at creation time; this allows much
better traceability of client actions. [GH-713]

View File

@ -1,11 +1,17 @@
package api
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@ -13,6 +19,13 @@ import (
"github.com/hashicorp/go-cleanhttp"
)
const EnvVaultAddress = "VAULT_ADDR"
const EnvVaultCACert = "VAULT_CACERT"
const EnvVaultCAPath = "VAULT_CAPATH"
const EnvVaultClientCert = "VAULT_CLIENT_CERT"
const EnvVaultClientKey = "VAULT_CLIENT_KEY"
const EnvVaultInsecure = "VAULT_SKIP_VERIFY"
var (
errRedirect = errors.New("redirect")
)
@ -44,14 +57,99 @@ func DefaultConfig() *Config {
HttpClient: cleanhttp.DefaultClient(),
}
config.HttpClient.Timeout = time.Second * 60
transport := config.HttpClient.Transport.(*http.Transport)
transport.TLSHandshakeTimeout = 10 * time.Second
transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
}
if addr := os.Getenv("VAULT_ADDR"); addr != "" {
config.Address = addr
if v := os.Getenv(EnvVaultAddress); v != "" {
config.Address = v
}
return config
}
// ReadEnvironment reads configuration information from the
// environment. If there is an error, no configuration value
// is updated.
func (c *Config) ReadEnvironment() error {
var envAddress string
var envCACert string
var envCAPath string
var envClientCert string
var envClientKey string
var envInsecure bool
var foundInsecure bool
var newCertPool *x509.CertPool
var clientCert tls.Certificate
var foundClientCert bool
if v := os.Getenv(EnvVaultAddress); v != "" {
envAddress = v
}
if v := os.Getenv(EnvVaultCACert); v != "" {
envCACert = v
}
if v := os.Getenv(EnvVaultCAPath); v != "" {
envCAPath = v
}
if v := os.Getenv(EnvVaultClientCert); v != "" {
envClientCert = v
}
if v := os.Getenv(EnvVaultClientKey); v != "" {
envClientKey = v
}
if v := os.Getenv(EnvVaultInsecure); v != "" {
var err error
envInsecure, err = strconv.ParseBool(v)
if err != nil {
return fmt.Errorf("Could not parse VAULT_SKIP_VERIFY")
}
foundInsecure = true
}
// If we need custom TLS configuration, then set it
if envCACert != "" || envCAPath != "" || envClientCert != "" || envClientKey != "" || envInsecure {
var err error
if envCACert != "" {
newCertPool, err = LoadCACert(envCACert)
} else if envCAPath != "" {
newCertPool, err = LoadCAPath(envCAPath)
}
if err != nil {
return fmt.Errorf("Error setting up CA path: %s", err)
}
if envClientCert != "" && envClientKey != "" {
clientCert, err = tls.LoadX509KeyPair(envClientCert, envClientKey)
if err != nil {
return err
}
foundClientCert = true
} else if envClientCert != "" || envClientKey != "" {
return fmt.Errorf("Both client cert and client key must be provided")
}
}
if envAddress != "" {
c.Address = envAddress
}
clientTLSConfig := c.HttpClient.Transport.(*http.Transport).TLSClientConfig
if foundInsecure {
clientTLSConfig.InsecureSkipVerify = envInsecure
}
if newCertPool != nil {
clientTLSConfig.RootCAs = newCertPool
}
if foundClientCert {
clientTLSConfig.Certificates = []tls.Certificate{clientCert}
}
return nil
}
// Client is the client to the Vault API. Create a client with
// NewClient.
type Client struct {
@ -66,6 +164,7 @@ type Client struct {
// automatically added to the client. Otherwise, you must manually call
// `SetToken()`.
func NewClient(c *Config) (*Client, error) {
u, err := url.Parse(c.Address)
if err != nil {
return nil, err
@ -203,3 +302,74 @@ START:
return result, nil
}
// Loads the certificate from given path and creates a certificate pool from it.
func LoadCACert(path string) (*x509.CertPool, error) {
certs, err := loadCertFromPEM(path)
if err != nil {
return nil, err
}
result := x509.NewCertPool()
for _, cert := range certs {
result.AddCert(cert)
}
return result, nil
}
// Loads the certificates present in the given directory and creates a
// certificate pool from it.
func LoadCAPath(path string) (*x509.CertPool, error) {
result := x509.NewCertPool()
fn := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
certs, err := loadCertFromPEM(path)
if err != nil {
return err
}
for _, cert := range certs {
result.AddCert(cert)
}
return nil
}
return result, filepath.Walk(path, fn)
}
// Creates a certificate from the given path
func loadCertFromPEM(path string) ([]*x509.Certificate, error) {
pemCerts, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
certs := make([]*x509.Certificate, 0, 5)
for len(pemCerts) > 0 {
var block *pem.Block
block, pemCerts = pem.Decode(pemCerts)
if block == nil {
break
}
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
certs = append(certs, cert)
}
return certs, nil
}

View File

@ -99,3 +99,38 @@ func TestClientRedirect(t *testing.T) {
t.Fatalf("Bad: %s", buf.String())
}
}
func TestClientEnvSettings(t *testing.T) {
cwd, _ := os.Getwd()
oldCACert := os.Getenv(EnvVaultCACert)
oldCAPath := os.Getenv(EnvVaultCAPath)
oldClientCert := os.Getenv(EnvVaultClientCert)
oldClientKey := os.Getenv(EnvVaultClientKey)
oldSkipVerify := os.Getenv(EnvVaultInsecure)
os.Setenv("VAULT_CACERT", cwd+"/../test/key/ourdomain.cer")
os.Setenv("VAULT_CAPATH", cwd+"/../test/key")
os.Setenv("VAULT_CLIENT_CERT", cwd+"/../test/key/ourdomain.cer")
os.Setenv("VAULT_CLIENT_KEY", cwd+"/../test/key/ourdomain.key")
os.Setenv("VAULT_SKIP_VERIFY", "true")
defer os.Setenv("VAULT_CACERT", oldCACert)
defer os.Setenv("VAULT_CAPATH", oldCAPath)
defer os.Setenv("VAULT_CLIENT_CERT", oldClientCert)
defer os.Setenv("VAULT_CLIENT_KEY", oldClientKey)
defer os.Setenv("VAULT_SKIP_VERIFY", oldSkipVerify)
config := DefaultConfig()
if err := config.ReadEnvironment(); err != nil {
t.Fatalf("error reading environment: %v", err)
}
tlsConfig := config.HttpClient.Transport.(*http.Transport).TLSClientConfig
if len(tlsConfig.RootCAs.Subjects()) == 0 {
t.Fatalf("bad: expected a cert pool with at least one subject")
}
if len(tlsConfig.Certificates) != 1 {
t.Fatalf("bad: expected client tls config to have a client certificate")
}
if tlsConfig.InsecureSkipVerify != true {
t.Fatalf("bad: %s", tlsConfig.InsecureSkipVerify)
}
}

View File

@ -3,13 +3,11 @@ package api
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/hashicorp/hcl"
@ -95,9 +93,9 @@ func (c *SSHAgentConfig) NewClient() (*Client, error) {
var certPool *x509.CertPool
var err error
if c.CACert != "" {
certPool, err = loadCACert(c.CACert)
certPool, err = LoadCACert(c.CACert)
} else if c.CAPath != "" {
certPool, err = loadCAPath(c.CAPath)
certPool, err = LoadCAPath(c.CAPath)
}
if err != nil {
return nil, err
@ -199,74 +197,3 @@ func (c *SSHAgent) Verify(otp string) (*SSHVerifyResponse, error) {
}
return &verifyResp, nil
}
// Loads the certificate from given path and creates a certificate pool from it.
func loadCACert(path string) (*x509.CertPool, error) {
certs, err := loadCertFromPEM(path)
if err != nil {
return nil, err
}
result := x509.NewCertPool()
for _, cert := range certs {
result.AddCert(cert)
}
return result, nil
}
// Loads the certificates present in the given directory and creates a
// certificate pool from it.
func loadCAPath(path string) (*x509.CertPool, error) {
result := x509.NewCertPool()
fn := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
certs, err := loadCertFromPEM(path)
if err != nil {
return err
}
for _, cert := range certs {
result.AddCert(cert)
}
return nil
}
return result, filepath.Walk(path, fn)
}
// Creates a certificate from the given path
func loadCertFromPEM(path string) ([]*x509.Certificate, error) {
pemCerts, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
certs := make([]*x509.Certificate, 0, 5)
for len(pemCerts) > 0 {
var block *pem.Block
block, pemCerts = pem.Decode(pemCerts)
if block == nil {
break
}
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
certs = append(certs, cert)
}
return certs, nil
}

View File

@ -9,27 +9,17 @@ import (
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/token"
"github.com/mitchellh/cli"
)
// EnvVaultAddress can be used to set the address of Vault
const EnvVaultAddress = "VAULT_ADDR"
const EnvVaultCACert = "VAULT_CACERT"
const EnvVaultCAPath = "VAULT_CAPATH"
const EnvVaultClientCert = "VAULT_CLIENT_CERT"
const EnvVaultClientKey = "VAULT_CLIENT_KEY"
const EnvVaultInsecure = "VAULT_SKIP_VERIFY"
// FlagSetFlags is an enum to define what flags are present in the
// default FlagSet returned by Meta.FlagSet.
type FlagSetFlags uint
@ -67,51 +57,40 @@ type Meta struct {
// flag settings for this command.
func (m *Meta) Client() (*api.Client, error) {
config := api.DefaultConfig()
if v := os.Getenv(EnvVaultAddress); v != "" {
config.Address = v
err := config.ReadEnvironment()
if err != nil {
return nil, errwrap.Wrapf("error reading environment: {{err}}", err)
}
if m.flagAddress != "" {
config.Address = m.flagAddress
}
if m.ForceAddress != "" {
config.Address = m.ForceAddress
}
if v := os.Getenv(EnvVaultCACert); v != "" {
m.flagCACert = v
}
if v := os.Getenv(EnvVaultCAPath); v != "" {
m.flagCAPath = v
}
if v := os.Getenv(EnvVaultClientCert); v != "" {
m.flagClientCert = v
}
if v := os.Getenv(EnvVaultClientKey); v != "" {
m.flagClientKey = v
}
if v := os.Getenv(EnvVaultInsecure); v != "" {
var err error
m.flagInsecure, err = strconv.ParseBool(v)
if err != nil {
return nil, fmt.Errorf("Invalid value passed in for -insecure flag: %s", err)
}
}
// If we need custom TLS configuration, then set it
if m.flagCACert != "" || m.flagCAPath != "" || m.flagClientCert != "" || m.flagClientKey != "" || m.flagInsecure {
// We may have set items from the environment so start with the
// existing TLS config
tlsConfig := config.HttpClient.Transport.(*http.Transport).TLSClientConfig
var certPool *x509.CertPool
var err error
if m.flagCACert != "" {
certPool, err = m.loadCACert(m.flagCACert)
certPool, err = api.LoadCACert(m.flagCACert)
} else if m.flagCAPath != "" {
certPool, err = m.loadCAPath(m.flagCAPath)
certPool, err = api.LoadCAPath(m.flagCAPath)
}
if err != nil {
return nil, fmt.Errorf("Error setting up CA path: %s", err)
return nil, errwrap.Wrapf("Error setting up CA path: {{err}}", err)
}
tlsConfig := &tls.Config{
InsecureSkipVerify: m.flagInsecure,
MinVersion: tls.VersionTLS12,
RootCAs: certPool,
if certPool != nil {
tlsConfig.RootCAs = certPool
}
if m.flagInsecure {
tlsConfig.InsecureSkipVerify = true
}
if m.flagClientCert != "" && m.flagClientKey != "" {
@ -123,20 +102,6 @@ func (m *Meta) Client() (*api.Client, error) {
} else if m.flagClientCert != "" || m.flagClientKey != "" {
return nil, fmt.Errorf("Both client cert and client key must be provided")
}
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSClientConfig: tlsConfig,
TLSHandshakeTimeout: 10 * time.Second,
},
}
config.HttpClient = client
}
// Build the client

View File

@ -2,7 +2,6 @@ package command
import (
"flag"
"os"
"reflect"
"sort"
"testing"
@ -40,37 +39,3 @@ func TestFlagSet(t *testing.T) {
}
}
}
func TestEnvSettings(t *testing.T) {
os.Setenv("VAULT_CACERT", "/path/to/fake/cert.crt")
os.Setenv("VAULT_CAPATH", "/path/to/fake/certs")
os.Setenv("VAULT_CLIENT_CERT", "/path/to/fake/client.crt")
os.Setenv("VAULT_CLIENT_KEY", "/path/to/fake/client.key")
os.Setenv("VAULT_SKIP_VERIFY", "true")
defer os.Setenv("VAULT_CACERT", "")
defer os.Setenv("VAULT_CAPATH", "")
defer os.Setenv("VAULT_CLIENT_CERT", "")
defer os.Setenv("VAULT_CLIENT_KEY", "")
defer os.Setenv("VAULT_SKIP_VERIFY", "")
var m Meta
// Err is ignored as it is expected that the test settings
// will cause errors; just check the flag settings
m.Client()
if m.flagCACert != "/path/to/fake/cert.crt" {
t.Fatalf("bad: %s", m.flagAddress)
}
if m.flagCAPath != "/path/to/fake/certs" {
t.Fatalf("bad: %s", m.flagAddress)
}
if m.flagClientCert != "/path/to/fake/client.crt" {
t.Fatalf("bad: %s", m.flagAddress)
}
if m.flagClientKey != "/path/to/fake/client.key" {
t.Fatalf("bad: %s", m.flagAddress)
}
if m.flagInsecure != true {
t.Fatalf("bad: %s", m.flagAddress)
}
}