diff --git a/agent/agent.go b/agent/agent.go index c0d840947..32ba092fc 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -380,6 +380,13 @@ func (a *Agent) Start() error { a.logger.Printf("[WARN] agent: error restoring proxy state: %s", err) } } + + acfg, err := a.config.APIConfig(true) + if err != nil { + return err + } + a.proxyManager.ProxyEnv = acfg.GenerateEnv() + go a.proxyManager.Run() } diff --git a/agent/config/runtime.go b/agent/config/runtime.go index 05ad8796c..2914c38bf 100644 --- a/agent/config/runtime.go +++ b/agent/config/runtime.go @@ -9,6 +9,7 @@ import ( "time" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/consul/types" @@ -1189,6 +1190,70 @@ func (c *RuntimeConfig) IncomingHTTPSConfig() (*tls.Config, error) { return tc.IncomingTLSConfig() } +func (c *RuntimeConfig) apiAddresses(maxPerType int) (unixAddrs, httpAddrs, httpsAddrs []string) { + if len(c.HTTPSAddrs) > 0 { + for i, addr := range c.HTTPSAddrs { + if i < maxPerType { + httpsAddrs = append(httpsAddrs, addr.String()) + } else { + break + } + } + } + if len(c.HTTPAddrs) > 0 { + unix_count := 0 + http_count := 0 + for _, addr := range c.HTTPAddrs { + switch addr.(type) { + case *net.UnixAddr: + if unix_count < maxPerType { + unixAddrs = append(unixAddrs, addr.String()) + unix_count += 1 + } + default: + if http_count < maxPerType { + httpAddrs = append(httpAddrs, addr.String()) + http_count += 1 + } + } + } + } + + return +} + +func (c *RuntimeConfig) APIConfig(includeClientCerts bool) (*api.Config, error) { + cfg := &api.Config{ + Datacenter: c.Datacenter, + TLSConfig: api.TLSConfig{InsecureSkipVerify: !c.VerifyOutgoing}, + } + + unixAddrs, httpAddrs, httpsAddrs := c.apiAddresses(1) + + if len(httpsAddrs) > 0 { + cfg.Address = httpsAddrs[0] + cfg.Scheme = "https" + cfg.TLSConfig.CAFile = c.CAFile + cfg.TLSConfig.CAPath = c.CAPath + if includeClientCerts { + cfg.TLSConfig.CertFile = c.CertFile + cfg.TLSConfig.KeyFile = c.KeyFile + } + } else if len(httpAddrs) > 0 { + cfg.Address = httpAddrs[0] + cfg.Scheme = "http" + } else if len(unixAddrs) > 0 { + cfg.Address = "unix://" + unixAddrs[0] + // this should be ignored - however we are still talking http over a unix socket + // so it makes sense to set it like this + cfg.Scheme = "http" + } else { + return nil, fmt.Errorf("No suitable client address can be found") + } + + return cfg, nil +} + // Sanitized returns a JSON/HCL compatible representation of the runtime // configuration where all fields with potential secrets had their // values replaced by 'hidden'. In addition, network addresses and diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index 9592b4967..4df0bd5a0 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -4507,6 +4507,107 @@ func TestSanitize(t *testing.T) { require.JSONEq(t, rtJSON, string(b)) } +func TestRuntime_apiAddresses(t *testing.T) { + rt := RuntimeConfig{ + HTTPAddrs: []net.Addr{ + &net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678}, + &net.UnixAddr{Name: "/var/run/foo"}, + }, + HTTPSAddrs: []net.Addr{ + &net.TCPAddr{IP: net.ParseIP("198.18.0.2"), Port: 5678}, + }} + + unixAddrs, httpAddrs, httpsAddrs := rt.apiAddresses(1) + + require.Len(t, unixAddrs, 1) + require.Len(t, httpAddrs, 1) + require.Len(t, httpsAddrs, 1) + + require.Equal(t, "/var/run/foo", unixAddrs[0]) + require.Equal(t, "198.18.0.1:5678", httpAddrs[0]) + require.Equal(t, "198.18.0.2:5678", httpsAddrs[0]) +} + +func TestRuntime_APIConfigHTTPS(t *testing.T) { + rt := RuntimeConfig{ + HTTPAddrs: []net.Addr{ + &net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678}, + &net.UnixAddr{Name: "/var/run/foo"}, + }, + HTTPSAddrs: []net.Addr{ + &net.TCPAddr{IP: net.ParseIP("198.18.0.2"), Port: 5678}, + }, + Datacenter: "dc-test", + CAFile: "/etc/consul/ca.crt", + CAPath: "/etc/consul/ca.dir", + CertFile: "/etc/consul/server.crt", + KeyFile: "/etc/consul/ssl/server.key", + VerifyOutgoing: false, + } + + cfg, err := rt.APIConfig(false) + require.NoError(t, err) + require.Equal(t, "198.18.0.2:5678", cfg.Address) + require.Equal(t, "https", cfg.Scheme) + require.Equal(t, rt.CAFile, cfg.TLSConfig.CAFile) + require.Equal(t, rt.CAPath, cfg.TLSConfig.CAPath) + require.Equal(t, "", cfg.TLSConfig.CertFile) + require.Equal(t, "", cfg.TLSConfig.KeyFile) + require.Equal(t, rt.Datacenter, cfg.Datacenter) + require.Equal(t, true, cfg.TLSConfig.InsecureSkipVerify) + + rt.VerifyOutgoing = true + cfg, err = rt.APIConfig(true) + require.NoError(t, err) + require.Equal(t, "198.18.0.2:5678", cfg.Address) + require.Equal(t, "https", cfg.Scheme) + require.Equal(t, rt.CAFile, cfg.TLSConfig.CAFile) + require.Equal(t, rt.CAPath, cfg.TLSConfig.CAPath) + require.Equal(t, rt.CertFile, cfg.TLSConfig.CertFile) + require.Equal(t, rt.KeyFile, cfg.TLSConfig.KeyFile) + require.Equal(t, rt.Datacenter, cfg.Datacenter) + require.Equal(t, false, cfg.TLSConfig.InsecureSkipVerify) +} + +func TestRuntime_APIConfigHTTP(t *testing.T) { + rt := RuntimeConfig{ + HTTPAddrs: []net.Addr{ + &net.UnixAddr{Name: "/var/run/foo"}, + &net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678}, + }, + Datacenter: "dc-test", + } + + cfg, err := rt.APIConfig(false) + require.NoError(t, err) + require.Equal(t, rt.Datacenter, cfg.Datacenter) + require.Equal(t, "198.18.0.1:5678", cfg.Address) + require.Equal(t, "http", cfg.Scheme) + require.Equal(t, "", cfg.TLSConfig.CAFile) + require.Equal(t, "", cfg.TLSConfig.CAPath) + require.Equal(t, "", cfg.TLSConfig.CertFile) + require.Equal(t, "", cfg.TLSConfig.KeyFile) +} + +func TestRuntime_APIConfigUNIX(t *testing.T) { + rt := RuntimeConfig{ + HTTPAddrs: []net.Addr{ + &net.UnixAddr{Name: "/var/run/foo"}, + }, + Datacenter: "dc-test", + } + + cfg, err := rt.APIConfig(false) + require.NoError(t, err) + require.Equal(t, rt.Datacenter, cfg.Datacenter) + require.Equal(t, "unix:///var/run/foo", cfg.Address) + require.Equal(t, "http", cfg.Scheme) + require.Equal(t, "", cfg.TLSConfig.CAFile) + require.Equal(t, "", cfg.TLSConfig.CAPath) + require.Equal(t, "", cfg.TLSConfig.CertFile) + require.Equal(t, "", cfg.TLSConfig.KeyFile) +} + func splitIPPort(hostport string) (net.IP, int) { h, p, err := net.SplitHostPort(hostport) if err != nil { diff --git a/agent/proxy/manager.go b/agent/proxy/manager.go index 65ffff738..d0b59fb0b 100644 --- a/agent/proxy/manager.go +++ b/agent/proxy/manager.go @@ -69,6 +69,9 @@ type Manager struct { // DataDir string + // Extra environment variables to set for the proxies + ProxyEnv []string + // SnapshotPeriod is the duration between snapshots. This can be set // relatively low to ensure accuracy, because if the new snapshot matches // the last snapshot taken, no file will be written. Therefore, setting @@ -434,7 +437,7 @@ func (m *Manager) newProxy(mp *local.ManagedProxy) (Proxy, error) { } // Pass in the environmental variables for the proxy process - cmd.Env = os.Environ() + cmd.Env = append(m.ProxyEnv, os.Environ()...) // Build the daemon structure proxy.Command = &cmd diff --git a/agent/proxy/manager_test.go b/agent/proxy/manager_test.go index d9e63b6c6..c1d2f66a0 100644 --- a/agent/proxy/manager_test.go +++ b/agent/proxy/manager_test.go @@ -6,6 +6,7 @@ import ( "os/exec" "path/filepath" "sort" + "strings" "testing" "time" @@ -291,6 +292,9 @@ func TestManagerPassesEnvironment(t *testing.T) { envData := os.Environ() sort.Strings(envData) for _, envVariable := range envData { + if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") { + continue + } data = append(data, envVariable...) data = append(data, "\n"...) } @@ -303,7 +307,60 @@ func TestManagerPassesEnvironment(t *testing.T) { } }) - require.Equal(fileContent, data) + require.Equal(data, fileContent) +} + +// Test to check if the parent and the child processes +// have the same environmental variables +func TestManagerPassesProxyEnv(t *testing.T) { + t.Parallel() + + require := require.New(t) + state := local.TestState(t) + m, closer := testManager(t) + defer closer() + m.State = state + defer m.Kill() + + penv := make([]string, 0, 2) + penv = append(penv, "HTTP_ADDR=127.0.0.1:8500") + penv = append(penv, "HTTP_SSL=false") + m.ProxyEnv = penv + + // Add Proxy for the test + td, closer := testTempDir(t) + defer closer() + path := filepath.Join(td, "env-variables") + testStateProxy(t, state, "environTest", helperProcess("environ", path)) + + //Run the manager + go m.Run() + + //Get the environmental variables from the OS + var fileContent []byte + var err error + var data []byte + envData := os.Environ() + envData = append(envData, "HTTP_ADDR=127.0.0.1:8500") + envData = append(envData, "HTTP_SSL=false") + sort.Strings(envData) + for _, envVariable := range envData { + if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") { + continue + } + data = append(data, envVariable...) + data = append(data, "\n"...) + } + + // Check if the file written to from the spawned process + // has the necessary environmental variable data + retry.Run(t, func(r *retry.R) { + if fileContent, err = ioutil.ReadFile(path); err != nil { + r.Fatalf("No file ya dummy") + } + }) + + require.Equal(data, fileContent) } // Test the Snapshot/Restore works. diff --git a/api/api.go b/api/api.go index 6b359fef2..649238302 100644 --- a/api/api.go +++ b/api/api.go @@ -405,6 +405,29 @@ func SetupTLSConfig(tlsConfig *TLSConfig) (*tls.Config, error) { return tlsClientConfig, nil } +func (c *Config) GenerateEnv() []string { + env := make([]string, 0, 10) + + env = append(env, + fmt.Sprintf("%s=%s", HTTPAddrEnvName, c.Address), + fmt.Sprintf("%s=%s", HTTPTokenEnvName, c.Token), + fmt.Sprintf("%s=%t", HTTPSSLEnvName, c.Scheme == "https"), + fmt.Sprintf("%s=%s", HTTPCAFile, c.TLSConfig.CAFile), + fmt.Sprintf("%s=%s", HTTPCAPath, c.TLSConfig.CAPath), + fmt.Sprintf("%s=%s", HTTPClientCert, c.TLSConfig.CertFile), + fmt.Sprintf("%s=%s", HTTPClientKey, c.TLSConfig.KeyFile), + fmt.Sprintf("%s=%s", HTTPTLSServerName, c.TLSConfig.Address), + fmt.Sprintf("%s=%t", HTTPSSLVerifyEnvName, !c.TLSConfig.InsecureSkipVerify)) + + if c.HttpAuth != nil { + env = append(env, fmt.Sprintf("%s=%s:%s", HTTPAuthEnvName, c.HttpAuth.Username, c.HttpAuth.Password)) + } else { + env = append(env, fmt.Sprintf("%s=", HTTPAuthEnvName)) + } + + return env +} + // Client provides a client to the Consul API type Client struct { config Config diff --git a/api/api_test.go b/api/api_test.go index f06bc1b30..407d9994a 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/hashicorp/consul/testutil" + "github.com/stretchr/testify/require" ) type configCallback func(c *Config) @@ -548,3 +549,73 @@ func TestAPI_IsRetryableError(t *testing.T) { t.Fatal("should be a retryable error") } } + +func TestAPI_GenerateEnv(t *testing.T) { + t.Parallel() + + c := &Config{ + Address: "127.0.0.1:8500", + Token: "test", + Scheme: "http", + TLSConfig: TLSConfig{ + CAFile: "", + CAPath: "", + CertFile: "", + KeyFile: "", + Address: "", + InsecureSkipVerify: true, + }, + } + + expected := []string{ + "CONSUL_HTTP_ADDR=127.0.0.1:8500", + "CONSUL_HTTP_TOKEN=test", + "CONSUL_HTTP_SSL=false", + "CONSUL_CACERT=", + "CONSUL_CAPATH=", + "CONSUL_CLIENT_CERT=", + "CONSUL_CLIENT_KEY=", + "CONSUL_TLS_SERVER_NAME=", + "CONSUL_HTTP_SSL_VERIFY=false", + "CONSUL_HTTP_AUTH=", + } + + require.Equal(t, expected, c.GenerateEnv()) +} + +func TestAPI_GenerateEnvHTTPS(t *testing.T) { + t.Parallel() + + c := &Config{ + Address: "127.0.0.1:8500", + Token: "test", + Scheme: "https", + TLSConfig: TLSConfig{ + CAFile: "/var/consul/ca.crt", + CAPath: "/var/consul/ca.dir", + CertFile: "/var/consul/server.crt", + KeyFile: "/var/consul/ssl/server.key", + Address: "127.0.0.1:8500", + InsecureSkipVerify: false, + }, + HttpAuth: &HttpBasicAuth{ + Username: "user", + Password: "password", + }, + } + + expected := []string{ + "CONSUL_HTTP_ADDR=127.0.0.1:8500", + "CONSUL_HTTP_TOKEN=test", + "CONSUL_HTTP_SSL=true", + "CONSUL_CACERT=/var/consul/ca.crt", + "CONSUL_CAPATH=/var/consul/ca.dir", + "CONSUL_CLIENT_CERT=/var/consul/server.crt", + "CONSUL_CLIENT_KEY=/var/consul/ssl/server.key", + "CONSUL_TLS_SERVER_NAME=127.0.0.1:8500", + "CONSUL_HTTP_SSL_VERIFY=true", + "CONSUL_HTTP_AUTH=user:password", + } + + require.Equal(t, expected, c.GenerateEnv()) +}