Skip to content

Commit

Permalink
Merge pull request #2914 from hashicorp/tls-client-options
Browse files Browse the repository at this point in the history
Add tls client options to api/cli
  • Loading branch information
kyhavlov authored Apr 14, 2017
2 parents f960ab8 + b1d9852 commit 43818f5
Show file tree
Hide file tree
Showing 28 changed files with 1,404 additions and 41 deletions.
116 changes: 86 additions & 30 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
Expand All @@ -19,6 +17,7 @@ import (
"time"

"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-rootcerts"
)

const (
Expand All @@ -38,6 +37,26 @@ const (
// whether or not to use HTTPS.
HTTPSSLEnvName = "CONSUL_HTTP_SSL"

// HTTPCAFile defines an environment variable name which sets the
// CA file to use for talking to Consul over TLS.
HTTPCAFile = "CONSUL_CACERT"

// HTTPCAPath defines an environment variable name which sets the
// path to a directory of CA certs to use for talking to Consul over TLS.
HTTPCAPath = "CONSUL_CAPATH"

// HTTPClientCert defines an environment variable name which sets the
// client cert file to use for talking to Consul over TLS.
HTTPClientCert = "CONSUL_CLIENT_CERT"

// HTTPClientKey defines an environment variable name which sets the
// client key file to use for talking to Consul over TLS.
HTTPClientKey = "CONSUL_CLIENT_KEY"

// HTTPTLSServerName defines an environment variable name which sets the
// server name to use as the SNI host when connecting via TLS
HTTPTLSServerName = "CONSUL_TLS_SERVER_NAME"

// HTTPSSLVerifyEnvName defines an environment variable name which sets
// whether or not to disable certificate checking.
HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY"
Expand Down Expand Up @@ -163,6 +182,8 @@ type Config struct {
// Token is used to provide a per-request ACL token
// which overrides the agent's default token.
Token string

TLSConfig TLSConfig
}

// TLSConfig is used to generate a TLSClientConfig that's useful for talking to
Expand All @@ -177,6 +198,10 @@ type TLSConfig struct {
// communication, defaults to the system bundle if not specified.
CAFile string

// CAPath is the optional path to a directory of CA certificates to use for
// Consul communication, defaults to the system bundle if not specified.
CAPath string

// CertFile is the optional path to the certificate for Consul
// communication. If this is set then you need to also set KeyFile.
CertFile string
Expand Down Expand Up @@ -254,27 +279,28 @@ func defaultConfig(transportFn func() *http.Transport) *Config {
}
}

if verify := os.Getenv(HTTPSSLVerifyEnvName); verify != "" {
doVerify, err := strconv.ParseBool(verify)
if v := os.Getenv(HTTPTLSServerName); v != "" {
config.TLSConfig.Address = v
}
if v := os.Getenv(HTTPCAFile); v != "" {
config.TLSConfig.CAFile = v
}
if v := os.Getenv(HTTPCAPath); v != "" {
config.TLSConfig.CAPath = v
}
if v := os.Getenv(HTTPClientCert); v != "" {
config.TLSConfig.CertFile = v
}
if v := os.Getenv(HTTPClientKey); v != "" {
config.TLSConfig.KeyFile = v
}
if v := os.Getenv(HTTPSSLVerifyEnvName); v != "" {
doVerify, err := strconv.ParseBool(v)
if err != nil {
log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLVerifyEnvName, err)
}

if !doVerify {
tlsClientConfig, err := SetupTLSConfig(&TLSConfig{
InsecureSkipVerify: true,
})

// We don't expect this to fail given that we aren't
// parsing any of the input, but we panic just in case
// since this doesn't have an error return.
if err != nil {
panic(err)
}

transport := transportFn()
transport.TLSClientConfig = tlsClientConfig
config.HttpClient.Transport = transport
config.TLSConfig.InsecureSkipVerify = true
}
}

Expand Down Expand Up @@ -309,17 +335,12 @@ func SetupTLSConfig(tlsConfig *TLSConfig) (*tls.Config, error) {
tlsClientConfig.Certificates = []tls.Certificate{tlsCert}
}

if tlsConfig.CAFile != "" {
data, err := ioutil.ReadFile(tlsConfig.CAFile)
if err != nil {
return nil, fmt.Errorf("failed to read CA file: %v", err)
}

caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(data) {
return nil, fmt.Errorf("failed to parse CA certificate")
}
tlsClientConfig.RootCAs = caPool
rootConfig := &rootcerts.Config{
CAFile: tlsConfig.CAFile,
CAPath: tlsConfig.CAPath,
}
if err := rootcerts.ConfigureTLS(tlsClientConfig, rootConfig); err != nil {
return nil, err
}

return tlsClientConfig, nil
Expand Down Expand Up @@ -347,6 +368,41 @@ func NewClient(config *Config) (*Client, error) {
config.HttpClient = defConfig.HttpClient
}

if config.TLSConfig.Address == "" {
config.TLSConfig.Address = defConfig.TLSConfig.Address
}

if config.TLSConfig.CAFile == "" {
config.TLSConfig.CAFile = defConfig.TLSConfig.CAFile
}

if config.TLSConfig.CAPath == "" {
config.TLSConfig.CAPath = defConfig.TLSConfig.CAPath
}

if config.TLSConfig.CertFile == "" {
config.TLSConfig.CertFile = defConfig.TLSConfig.CertFile
}

if config.TLSConfig.KeyFile == "" {
config.TLSConfig.KeyFile = defConfig.TLSConfig.KeyFile
}

if !config.TLSConfig.InsecureSkipVerify {
config.TLSConfig.InsecureSkipVerify = defConfig.TLSConfig.InsecureSkipVerify
}

tlsClientConfig, err := SetupTLSConfig(&config.TLSConfig)

// We don't expect this to fail given that we aren't
// parsing any of the input, but we panic just in case
// since this doesn't have an error return.
if err != nil {
return nil, err
}

config.HttpClient.Transport.(*http.Transport).TLSClientConfig = tlsClientConfig

parts := strings.SplitN(config.Address, "://", 2)
if len(parts) == 2 {
switch parts[0] {
Expand Down
94 changes: 91 additions & 3 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/hashicorp/consul/testutil"
"strings"
)

type configCallback func(c *Config)
Expand Down Expand Up @@ -86,6 +87,16 @@ func TestDefaultConfig_env(t *testing.T) {
defer os.Setenv(HTTPAuthEnvName, "")
os.Setenv(HTTPSSLEnvName, "1")
defer os.Setenv(HTTPSSLEnvName, "")
os.Setenv(HTTPCAFile, "ca.pem")
defer os.Setenv(HTTPCAFile, "")
os.Setenv(HTTPCAPath, "certs/")
defer os.Setenv(HTTPCAPath, "")
os.Setenv(HTTPClientCert, "client.crt")
defer os.Setenv(HTTPClientCert, "")
os.Setenv(HTTPClientKey, "client.key")
defer os.Setenv(HTTPClientKey, "")
os.Setenv(HTTPTLSServerName, "consul.test")
defer os.Setenv(HTTPTLSServerName, "")
os.Setenv(HTTPSSLVerifyEnvName, "0")
defer os.Setenv(HTTPSSLVerifyEnvName, "")

Expand All @@ -108,7 +119,22 @@ func TestDefaultConfig_env(t *testing.T) {
if config.Scheme != "https" {
t.Errorf("expected %q to be %q", config.Scheme, "https")
}
if !config.HttpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify {
if config.TLSConfig.CAFile != "ca.pem" {
t.Errorf("expected %q to be %q", config.TLSConfig.CAFile, "ca.pem")
}
if config.TLSConfig.CAPath != "certs/" {
t.Errorf("expected %q to be %q", config.TLSConfig.CAPath, "certs/")
}
if config.TLSConfig.CertFile != "client.crt" {
t.Errorf("expected %q to be %q", config.TLSConfig.CertFile, "client.crt")
}
if config.TLSConfig.KeyFile != "client.key" {
t.Errorf("expected %q to be %q", config.TLSConfig.KeyFile, "client.key")
}
if config.TLSConfig.Address != "consul.test" {
t.Errorf("expected %q to be %q", config.TLSConfig.Address, "consul.test")
}
if !config.TLSConfig.InsecureSkipVerify {
t.Errorf("expected SSL verification to be off")
}

Expand All @@ -132,9 +158,9 @@ func TestSetupTLSConfig(t *testing.T) {
if err != nil {
t.Fatalf("err: %v", err)
}
expected := &tls.Config{}
expected := &tls.Config{RootCAs: cc.RootCAs}
if !reflect.DeepEqual(cc, expected) {
t.Fatalf("bad: %v", cc)
t.Fatalf("bad: \n%v, \n%v", cc, expected)
}

// Try some address variations with and without ports.
Expand Down Expand Up @@ -215,6 +241,68 @@ func TestSetupTLSConfig(t *testing.T) {
if cc.RootCAs == nil {
t.Fatalf("didn't load root CAs")
}

// Use a directory to load the certs instead
cc, err = SetupTLSConfig(&TLSConfig{
CAPath: "../test/ca_path",
})
if err != nil {
t.Fatalf("err: %v", err)
}
if len(cc.RootCAs.Subjects()) != 2 {
t.Fatalf("didn't load root CAs")
}
}

func TestClientTLSOptions(t *testing.T) {
t.Parallel()
_, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.CAFile = "../test/client_certs/rootca.crt"
conf.CertFile = "../test/client_certs/server.crt"
conf.KeyFile = "../test/client_certs/server.key"
conf.VerifyIncoming = true
})
defer s.Stop()

// Set up a client without certs
clientWithoutCerts, err := NewClient(&Config{
Address: s.HTTPSAddr,
Scheme: "https",
TLSConfig: TLSConfig{
Address: "consul.test",
CAFile: "../test/client_certs/rootca.crt",
},
})
if err != nil {
t.Fatal(err)
}

// Should fail
_, err = clientWithoutCerts.Agent().Self()
if err == nil || !strings.Contains(err.Error(), "bad certificate") {
t.Fatal(err)
}

// Set up a client with valid certs
clientWithCerts, err := NewClient(&Config{
Address: s.HTTPSAddr,
Scheme: "https",
TLSConfig: TLSConfig{
Address: "consul.test",
CAFile: "../test/client_certs/rootca.crt",
CertFile: "../test/client_certs/client.crt",
KeyFile: "../test/client_certs/client.key",
},
})
if err != nil {
t.Fatal(err)
}

// Should succeed
_, err = clientWithCerts.Agent().Self()
if err != nil {
t.Fatal(err)
}
}

func TestSetQueryOptions(t *testing.T) {
Expand Down
30 changes: 28 additions & 2 deletions command/base/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,14 @@ type Command struct {
hidden *flag.FlagSet

// These are the options which correspond to the HTTP API options
httpAddr StringValue
token StringValue
httpAddr StringValue
token StringValue
caFile StringValue
caPath StringValue
certFile StringValue
keyFile StringValue
tlsServerName StringValue

datacenter StringValue
stale BoolValue
}
Expand All @@ -55,6 +61,11 @@ func (c *Command) HTTPClient() (*api.Client, error) {
config := api.DefaultConfig()
c.httpAddr.Merge(&config.Address)
c.token.Merge(&config.Token)
c.caFile.Merge(&config.TLSConfig.CAFile)
c.caPath.Merge(&config.TLSConfig.CAPath)
c.certFile.Merge(&config.TLSConfig.CertFile)
c.keyFile.Merge(&config.TLSConfig.KeyFile)
c.tlsServerName.Merge(&config.TLSConfig.Address)
c.datacenter.Merge(&config.Datacenter)
return api.NewClient(config)
}
Expand Down Expand Up @@ -83,6 +94,18 @@ func (c *Command) httpFlagsClient(f *flag.FlagSet) *flag.FlagSet {
f = flag.NewFlagSet("", flag.ContinueOnError)
}

f.Var(&c.caFile, "ca-file",
"Path to a CA file to use for TLS when communicating with Consul. This "+
"can also be specified via the CONSUL_CACERT environment variable.")
f.Var(&c.caPath, "ca-path",
"Path to a directory of CA certificates to use for TLS when communicating "+
"with Consul. This can also be specified via the CONSUL_CAPATH environment variable.")
f.Var(&c.certFile, "client-cert",
"Path to a client cert file to use for TLS when `verify_incoming` is enabled. This "+
"can also be specified via the CONSUL_CLIENT_CERT environment variable.")
f.Var(&c.keyFile, "client-key",
"Path to a client key file to use for TLS when `verify_incoming` is enabled. This "+
"can also be specified via the CONSUL_CLIENT_KEY environment variable.")
f.Var(&c.httpAddr, "http-addr",
"The `address` and port of the Consul HTTP agent. The value can be an IP "+
"address or DNS address, but it must also include the port. This can "+
Expand All @@ -93,6 +116,9 @@ func (c *Command) httpFlagsClient(f *flag.FlagSet) *flag.FlagSet {
"ACL token to use in the request. This can also be specified via the "+
"CONSUL_HTTP_TOKEN environment variable. If unspecified, the query will "+
"default to the token of the Consul agent at the HTTP address.")
f.Var(&c.tlsServerName, "tls-server-name",
"The server name to use as the SNI host when connecting via TLS. This "+
"can also be specified via the CONSUL_TLS_SERVER_NAME environment variable.")

return f
}
Expand Down
29 changes: 29 additions & 0 deletions test/ca_path/cert1.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFADCCAuqgAwIBAgIBATALBgkqhkiG9w0BAQswEzERMA8GA1UEAxMIQ2VydEF1
dGgwHhcNMTUwNTExMjI0NjQzWhcNMjUwNTExMjI0NjU0WjATMREwDwYDVQQDEwhD
ZXJ0QXV0aDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALcMByyynHsA
+K4PJwo5+XHygaEZAhPGvHiKQK2Cbc9NDm0ZTzx0rA/dRTZlvouhDyzcJHm+6R1F
j6zQv7iaSC3qQtJiPnPsfZ+/0XhFZ3fQWMnfDiGbZpF1kJF01ofB6vnsuocFC0zG
aGC+SZiLAzs+QMP3Bebw1elCBIeoN+8NWnRYmLsYIaYGJGBSbNo/lCpLTuinofUn
L3ehWEGv1INwpHnSVeN0Ml2GFe23d7PUlj/wNIHgUdpUR+KEJxIP3klwtsI3QpSH
c4VjWdf4aIcka6K3IFuw+K0PUh3xAAPnMpAQOtCZk0AhF5rlvUbevC6jADxpKxLp
OONmvCTer4LtyNURAoBH52vbK0r/DNcTpPEFV0IP66nXUFgkk0mRKsu8HTb4IOkC
X3K4mp18EiWUUtrHZAnNct0iIniDBqKK0yhSNhztG6VakVt/1WdQY9Ey3mNtxN1O
thqWFKdpKUzPKYC3P6PfVpiE7+VbWTLLXba+8BPe8BxWPsVkjJqGSGnCte4COusz
M8/7bbTgifwJfsepwFtZG53tvwjWlO46Exl30VoDNTaIGvs1fO0GqJlh2A7FN5F2
S1rS5VYHtPK8QdmUSvyq+7JDBc1HNT5I2zsIQbNcLwDTZ5EsbU6QR7NHDJKxjv/w
bs3eTXJSSNcFD74wRU10pXjgE5wOFu9TAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIA
BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQHazgZ3Puiuc6K2LzgcX5b6fAC
PzAfBgNVHSMEGDAWgBQHazgZ3Puiuc6K2LzgcX5b6fACPzALBgkqhkiG9w0BAQsD
ggIBAEmeNrSUhpHg1I8dtfqu9hCU/6IZThjtcFA+QcPkkMa+Z1k0SOtsgW8MdlcA
gCf5g5yQZ0DdpWM9nDB6xDIhQdccm91idHgf8wmpEHUj0an4uyn2ESCt8eqrAWf7
AClYORCASTYfguJCxcfvwtI1uqaOeCxSOdmFay79UVitVsWeonbCRGsVgBDifJxw
G2oCQqoYAmXPM4J6syk5GHhB1O9MMq+g1+hOx9s+XHyTui9FL4V+IUO1ygVqEQB5
PSiRBvcIsajSGVao+vK0gf2XfcXzqr3y3NhBky9rFMp1g+ykb2yWekV4WiROJlCj
TsWwWZDRyjiGahDbho/XW8JciouHZhJdjhmO31rqW3HdFviCTdXMiGk3GQIzz/Jg
P+enOaHXoY9lcxzDvY9z1BysWBgNvNrMnVge/fLP9o+a0a0PRIIVl8T0Ef3zeg1O
CLCSy/1Vae5Tx63ZTFvGFdOSusYkG9rlAUHXZE364JRCKzM9Bz0bM+t+LaO0MaEb
YoxcXEPU+gB2IvmARpInN3oHexR6ekuYHVTRGdWrdmuHFzc7eFwygRqTFdoCCU+G
QZEkd+lOEyv0zvQqYg+Jp0AEGz2B2zB53uBVECtn0EqrSdPtRzUBSByXVs6QhSXn
eVmy+z3U3MecP63X6oSPXekqSyZFuegXpNNuHkjNoL4ep2ix
-----END CERTIFICATE-----
Loading

0 comments on commit 43818f5

Please sign in to comment.