diff --git a/README.md b/README.md index 9665faf3..4f3ecf49 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,9 @@ You can find the complete documentation and examples [here](https://kapow.readth ## Security -Please consider the following security caveats **before** using *Kapow!* - -- [Issue #119](/~https://github.com/BBVA/kapow/issues/119) -- [Security Concerns](https://kapow.readthedocs.io/en/stable/the_project/security.html#security-concerns) +Please consider the following +[Security Concerns](https://kapow.readthedocs.io/en/stable/the_project/security.html#security-concerns) +**before** using *Kapow!* If you are not 100% sure about what you are doing we recommend not using *Kapow!* diff --git a/docs/source/concepts/interfaces.rst b/docs/source/concepts/interfaces.rst index b84eac17..d104b2dc 100644 --- a/docs/source/concepts/interfaces.rst +++ b/docs/source/concepts/interfaces.rst @@ -16,16 +16,20 @@ By default it binds to address ``0.0.0.0`` and port ``8080``, but that can be changed via the ``--bind`` flag. -.. _http-control-interface: +.. _https-control-interface: -HTTP Control Interface ----------------------- +HTTPS Control Interface +----------------------- -The `HTTP Control Interface` is used by the command ``kapow route`` to +The `HTTPS Control Interface` is used by the command ``kapow route`` to administer the list of system routes. +This interface uses mTLS by default (double-pinned autogenerated certs). + By default it binds to address ``127.0.0.1`` and port ``8081``, but that can be -changed via the ``--control-bind`` flag. +changed via the ``--control-bind`` flag. If this is the case, consider +also ``--control-reachable-addr`` which will configure the autogenerated +certificate to match that address. .. _http-data-interface: diff --git a/docs/source/concepts/request_life_cycle.rst b/docs/source/concepts/request_life_cycle.rst index d3729ba0..d2976a95 100644 --- a/docs/source/concepts/request_life_cycle.rst +++ b/docs/source/concepts/request_life_cycle.rst @@ -30,8 +30,8 @@ The spawned entrypoint is run with the following variables added to its environment: - :envvar:`KAPOW_HANDLER_ID`: Containing the `HANDLER_ID` -- :envvar:`KAPOW_DATAAPI_URL`: With the URL of the :ref:`http-data-interface` -- :envvar:`KAPOW_CONTROLAPI_URL`: With the URL of the :ref:`http-control-interface` +- :envvar:`KAPOW_DATA_URL`: With the URL of the :ref:`http-data-interface` +- :envvar:`KAPOW_CONTROL_URL`: With the URL of the :ref:`https-control-interface` 3. ``kapow set /response/body banana`` diff --git a/docs/source/examples/managing_routes.rst b/docs/source/examples/managing_routes.rst index 299273ce..b3d530e9 100644 --- a/docs/source/examples/managing_routes.rst +++ b/docs/source/examples/managing_routes.rst @@ -100,7 +100,7 @@ Or, if you want human-readable output, you can use :program:`jq`: .. note:: - *Kapow!* has a :ref:`http-control-interface`, bound by default to + *Kapow!* has a :ref:`https-control-interface`, bound by default to ``localhost:8081``. diff --git a/internal/certs/certs.go b/internal/certs/certs.go new file mode 100644 index 00000000..24ebc9f1 --- /dev/null +++ b/internal/certs/certs.go @@ -0,0 +1,99 @@ +package certs + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "time" + + "github.com/BBVA/kapow/internal/logger" +) + +type Cert struct { + X509Cert *x509.Certificate + PrivKey crypto.PrivateKey + SignedCert []byte +} + +func (c Cert) SignedCertPEMBytes() []byte { + + PEM := new(bytes.Buffer) + err := pem.Encode(PEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: c.SignedCert, + }) + if err != nil { + logger.L.Fatal(err) + } + + return PEM.Bytes() +} + +func (c Cert) PrivateKeyPEMBytes() []byte { + PEM := new(bytes.Buffer) + err := pem.Encode(PEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(c.PrivKey.(*rsa.PrivateKey)), + }) + if err != nil { + logger.L.Fatal(err) + } + + return PEM.Bytes() +} + +func GenCert(name, altName string, isServer bool) Cert { + + usage := x509.ExtKeyUsageClientAuth + if isServer { + usage = x509.ExtKeyUsageServerAuth + } + + var dnsNames []string + var ipAddresses []net.IP + if altName != "" { + if ipAddr := net.ParseIP(altName); ipAddr != nil { + ipAddresses = []net.IP{ipAddr} + } else { + dnsNames = []string{altName} + } + } + + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + DNSNames: dnsNames, + IPAddresses: ipAddresses, + Subject: pkix.Name{ + CommonName: name, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: false, + BasicConstraintsValid: true, + ExtKeyUsage: []x509.ExtKeyUsage{ + usage, + }, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + logger.L.Fatal(err) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &certPrivKey.PublicKey, certPrivKey) + if err != nil { + logger.L.Fatal(err) + } + + return Cert{ + X509Cert: cert, + PrivKey: certPrivKey, + SignedCert: certBytes, + } +} diff --git a/internal/client/client_test.go b/internal/client/client_test.go new file mode 100644 index 00000000..9bbb6d1d --- /dev/null +++ b/internal/client/client_test.go @@ -0,0 +1,29 @@ +/* + * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package client + +import ( + "os" + "testing" + + "github.com/BBVA/kapow/internal/http" +) + +func TestMain(m *testing.M) { + http.ControlClientGenerator = nil + os.Exit(m.Run()) +} diff --git a/internal/client/get.go b/internal/client/get.go index f0816eed..38cee47f 100644 --- a/internal/client/get.go +++ b/internal/client/get.go @@ -25,5 +25,5 @@ import ( // GetData will perform the request and write the results on the provided writer func GetData(host, id, path string, w io.Writer) error { url := host + "/handlers/" + id + path - return http.Get(url, "", nil, w) + return http.Get(url, nil, w, nil) } diff --git a/internal/client/route_add.go b/internal/client/route_add.go index b79e8a05..bde6c576 100644 --- a/internal/client/route_add.go +++ b/internal/client/route_add.go @@ -36,5 +36,5 @@ func AddRoute(host, path, method, entrypoint, command string, w io.Writer) error payload["entrypoint"] = entrypoint } body, _ := json.Marshal(payload) - return http.Post(url, "application/json", bytes.NewReader(body), w) + return http.Post(url, bytes.NewReader(body), w, http.ControlClientGenerator, http.AsJSON) } diff --git a/internal/client/route_list.go b/internal/client/route_list.go index a22f86fa..d157a559 100644 --- a/internal/client/route_list.go +++ b/internal/client/route_list.go @@ -25,5 +25,5 @@ import ( // ListRoutes queries the kapow! instance for the routes that are registered func ListRoutes(host string, w io.Writer) error { url := host + "/routes" - return http.Get(url, "", nil, w) + return http.Get(url, nil, w, http.ControlClientGenerator) } diff --git a/internal/client/route_remove.go b/internal/client/route_remove.go index 53329ed6..283e7fa1 100644 --- a/internal/client/route_remove.go +++ b/internal/client/route_remove.go @@ -23,5 +23,5 @@ import ( // RemoveRoute removes a registered route in Kapow! server func RemoveRoute(host, id string) error { url := host + "/routes/" + id - return http.Delete(url, "", nil, nil) + return http.Delete(url, nil, nil, http.ControlClientGenerator) } diff --git a/internal/client/set.go b/internal/client/set.go index 745c1aba..33846cb8 100644 --- a/internal/client/set.go +++ b/internal/client/set.go @@ -24,5 +24,5 @@ import ( func SetData(host, handlerID, path string, r io.Reader) error { url := host + "/handlers/" + handlerID + path - return http.Put(url, "", r, nil) + return http.Put(url, r, nil, nil) } diff --git a/internal/cmd/route.go b/internal/cmd/route.go index e6310af6..1fc0e0f4 100644 --- a/internal/cmd/route.go +++ b/internal/cmd/route.go @@ -43,7 +43,7 @@ func init() { } }, } - routeListCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "http://localhost:8081"), "Kapow! control interface URL") + routeListCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL") // TODO: Manage args for url_pattern and command_file (2 exact args) var routeAddCmd = &cobra.Command{ @@ -78,7 +78,7 @@ func init() { }, } // TODO: Add default values for flags and remove path flag - routeAddCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "http://localhost:8081"), "Kapow! control interface URL") + routeAddCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL") routeAddCmd.Flags().StringP("method", "X", "GET", "HTTP method to accept") routeAddCmd.Flags().StringP("entrypoint", "e", "", "Command to execute") routeAddCmd.Flags().StringP("command", "c", "", "Command to pass to the shell") @@ -95,7 +95,7 @@ func init() { } }, } - routeRemoveCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "http://localhost:8081"), "Kapow! control interface URL") + routeRemoveCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL") RouteCmd.AddCommand(routeListCmd) RouteCmd.AddCommand(routeAddCmd) diff --git a/internal/cmd/server.go b/internal/cmd/server.go index df2efcd4..e30963cd 100644 --- a/internal/cmd/server.go +++ b/internal/cmd/server.go @@ -19,16 +19,40 @@ package cmd import ( "bufio" "errors" + "fmt" "io" "os" + "strings" "sync" "github.com/spf13/cobra" + "github.com/BBVA/kapow/internal/certs" "github.com/BBVA/kapow/internal/logger" "github.com/BBVA/kapow/internal/server" ) +func banner() { + fmt.Fprintln(os.Stderr, ` + %% %%%% + %%% %%% + %% %%% %%% + %%%%%%% %%% %%% %%% %%% + *%% %%%%%%%%%%%%%%% %%%% %%% %% + %% %%%%%%%%%. %%% %%%% %%% %%%%%%%% + %%%% %%% %%% %%% %%% %%%%%% %%%% + %%% %%% %%%%%% %%% %%%% %%% %%%% %%%% %%% %%% + %%% %%% %% %%% %%%%% %%%%% %%%% %%% + %%% %%% %% %%%%%%%%% %%%%%%%%%% + %%%%%% %%% %%%%%% %%% + %%% %%%%% %% %%%%%% + %%% %%%%%%% + %%%% + % If you can script it, you can HTTP it. + + `) +} + // ServerCmd is the command line interface for kapow server var ServerCmd = &cobra.Command{ Use: "server [optional flags] [optional init program(s)]", @@ -42,6 +66,8 @@ var ServerCmd = &cobra.Command{ sConf.ControlBindAddr, _ = cmd.Flags().GetString("control-bind") sConf.DataBindAddr, _ = cmd.Flags().GetString("data-bind") + controlReachableAddr, _ := cmd.Flags().GetString("control-reachable-addr") + sConf.CertFile, _ = cmd.Flags().GetString("certfile") sConf.KeyFile, _ = cmd.Flags().GetString("keyfile") @@ -49,29 +75,51 @@ var ServerCmd = &cobra.Command{ sConf.ClientCaFile, _ = cmd.Flags().GetString("clientcafile") sConf.Debug, _ = cmd.Flags().GetBool("debug") + sConf.ControlServerCert = certs.GenCert("control_server", extractHost(controlReachableAddr), true) + sConf.ControlClientCert = certs.GenCert("control_client", "", false) + // Set environment variables KAPOW_DATA_URL and KAPOW_CONTROL_URL only if they aren't set so we don't overwrite user's preferences if _, exist := os.LookupEnv("KAPOW_DATA_URL"); !exist { os.Setenv("KAPOW_DATA_URL", "http://"+sConf.DataBindAddr) } if _, exist := os.LookupEnv("KAPOW_CONTROL_URL"); !exist { - os.Setenv("KAPOW_CONTROL_URL", "http://"+sConf.ControlBindAddr) + os.Setenv("KAPOW_CONTROL_URL", "https://"+controlReachableAddr) } + banner() server.StartServer(sConf) for _, path := range args { - go Run(path, sConf.Debug) + go Run( + path, + sConf.Debug, + sConf.ControlServerCert.SignedCertPEMBytes(), + sConf.ControlClientCert.SignedCertPEMBytes(), + sConf.ControlClientCert.PrivateKeyPEMBytes(), + ) } select {} }, } +func extractHost(s string) string { + i := strings.LastIndex(s, ":") + s = s[:i] + l := len(s) - 1 + if s[0] == '[' && s[l] == ']' { + s = s[1:l] + } + return s +} + func init() { ServerCmd.Flags().String("bind", "0.0.0.0:8080", "IP address and port to bind the user interface to") ServerCmd.Flags().String("control-bind", "localhost:8081", "IP address and port to bind the control interface to") ServerCmd.Flags().String("data-bind", "localhost:8082", "IP address and port to bind the data interface to") + ServerCmd.Flags().String("control-reachable-addr", "localhost:8081", "address (incl. port) through which the control interface can be reached (from the client's point of view)") + ServerCmd.Flags().String("certfile", "", "Cert file to serve thru https") ServerCmd.Flags().String("keyfile", "", "Key file to serve thru https") @@ -100,10 +148,19 @@ func validateServerCommandArguments(cmd *cobra.Command, args []string) error { return nil } -func Run(path string, debug bool) { +func Run( + path string, + debug bool, + controlServerCertPEM, + controlClientCertPEM, + controlClientCertPrivKeyPEM []byte, +) { logger.L.Printf("Running init program %+q", path) cmd := BuildCmd(path) cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_SERVER_CERT=%s", controlServerCertPEM)) + cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_CLIENT_CERT=%s", controlClientCertPEM)) + cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_CLIENT_KEY=%s", controlClientCertPrivKeyPEM)) var wg sync.WaitGroup if debug { diff --git a/internal/http/request.go b/internal/http/request.go index aa4b121d..f38010eb 100644 --- a/internal/http/request.go +++ b/internal/http/request.go @@ -17,30 +17,41 @@ package http import ( + "crypto/tls" + "crypto/x509" "errors" "io" "io/ioutil" "net/http" + "os" + + "github.com/BBVA/kapow/internal/logger" ) +var ControlClientGenerator = GenControlHTTPSClient + +func AsJSON(req *http.Request) { + req.Header.Add("Content-Type", "application/json") +} + // Get perform a request using Request with the GET method -func Get(url string, contentType string, r io.Reader, w io.Writer) error { - return Request("GET", url, contentType, r, w) +func Get(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error { + return Request("GET", url, r, w, clientGenerator, reqTuner...) } // Post perform a request using Request with the POST method -func Post(url string, contentType string, r io.Reader, w io.Writer) error { - return Request("POST", url, contentType, r, w) +func Post(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error { + return Request("POST", url, r, w, clientGenerator, reqTuner...) } // Put perform a request using Request with the PUT method -func Put(url string, contentType string, r io.Reader, w io.Writer) error { - return Request("PUT", url, contentType, r, w) +func Put(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error { + return Request("PUT", url, r, w, clientGenerator, reqTuner...) } // Delete perform a request using Request with the DELETE method -func Delete(url string, contentType string, r io.Reader, w io.Writer) error { - return Request("DELETE", url, contentType, r, w) +func Delete(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error { + return Request("DELETE", url, r, w, clientGenerator, reqTuner...) } var devnull = ioutil.Discard @@ -49,17 +60,24 @@ var devnull = ioutil.Discard // content of the given reader as the body and writing all the contents // of the response to the given writer. The reader and writer are // optional. -func Request(method string, url string, contentType string, r io.Reader, w io.Writer) error { +func Request(method string, url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuners ...func(*http.Request)) error { req, err := http.NewRequest(method, url, r) if err != nil { return err } - if contentType != "" { - req.Header.Add("Content-Type", contentType) + for _, reqTuner := range reqTuners { + reqTuner(req) } - res, err := new(http.Client).Do(req) + var client *http.Client + if clientGenerator == nil { + client = new(http.Client) + } else { + client = clientGenerator() + } + + res, err := client.Do(req) if err != nil { return err } @@ -81,3 +99,43 @@ func Request(method string, url string, contentType string, r io.Reader, w io.Wr return err } + +func GenControlHTTPSClient() *http.Client { + + serverCert, exists := os.LookupEnv("KAPOW_CONTROL_SERVER_CERT") + if !exists { + logger.L.Fatal("KAPOW_CONTROL_SERVER_CERT not in the environment") + } + + clientCert, exists := os.LookupEnv("KAPOW_CONTROL_CLIENT_CERT") + if !exists { + logger.L.Fatal("KAPOW_CONTROL_CLIENT_CERT not in the environment") + } + + clientKey, exists := os.LookupEnv("KAPOW_CONTROL_CLIENT_KEY") + if !exists { + logger.L.Fatal("KAPOW_CONTROL_CLIENT_KEY not in the environment") + } + + // Load client cert + clientTLSCert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) + if err != nil { + logger.L.Fatal(err) + } + + // Load Server cert + serverCertPool := x509.NewCertPool() + serverCertPool.AppendCertsFromPEM([]byte(serverCert)) + + // Setup HTTPS client + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{clientTLSCert}, + RootCAs: serverCertPool, + } + tlsConfig.BuildNameToCertificate() + transport := &http.Transport{TLSClientConfig: tlsConfig} + client := &http.Client{Transport: transport} + + // The client is always right! + return client +} diff --git a/internal/http/request_test.go b/internal/http/request_test.go index bd13c2c5..67198c54 100644 --- a/internal/http/request_test.go +++ b/internal/http/request_test.go @@ -29,7 +29,7 @@ func TestReturnErrorOnInvalidURL(t *testing.T) { defer gock.Off() gock.New("").Reply(200) - err := Request("GET", "://", "", nil, nil) + err := Request("GET", "://", nil, nil, nil) if err == nil { t.Errorf("Expected error not returned") } @@ -45,7 +45,7 @@ func TestRequestGivenMethod(t *testing.T) { mock.Method = "FOO" mock.Reply(200) - err := Request("FOO", "http://localhost", "", nil, nil) + err := Request("FOO", "http://localhost", nil, nil, nil) if err != nil { t.Errorf("Unexpected error on request") } @@ -60,7 +60,7 @@ func TestReturnHTTPErrorAsIs(t *testing.T) { customError := errors.New("FOO") gock.New("http://localhost").ReplyError(customError) - err := Request("GET", "http://localhost", "", nil, nil) + err := Request("GET", "http://localhost", nil, nil, nil) if errors.Unwrap(err) != customError { t.Errorf("Returned error is not the expected error: '%v'", err) } @@ -76,7 +76,7 @@ func TestReturnHTTPReasonAsErrorWhenUnsuccessful(t *testing.T) { Reply(http.StatusTeapot). BodyString(`{"reason": "I'm a teapot"}`) - err := Request("GET", "http://localhost", "", nil, nil) + err := Request("GET", "http://localhost", nil, nil, nil) if err == nil || err.Error() != http.StatusText(http.StatusTeapot) { t.Errorf("Reason should be returned as an error") } @@ -93,7 +93,7 @@ func TestCopyResponseBodyToWriter(t *testing.T) { rw := new(bytes.Buffer) - err := Request("GET", "http://localhost", "", nil, rw) + err := Request("GET", "http://localhost", nil, rw, nil) if err != nil { t.Errorf("Unexpected error %v", err) } @@ -119,7 +119,7 @@ func TestWriteToDevNullWhenNoWriter(t *testing.T) { defer func() { devnull = original }() - err := Request("GET", "http://localhost", "", nil, nil) + err := Request("GET", "http://localhost", nil, nil, nil) if err != nil { t.Errorf("Unexpected error %v", err) } @@ -135,14 +135,13 @@ func TestWriteToDevNullWhenNoWriter(t *testing.T) { } } -func TestSendContentType(t *testing.T) { +func TestSendContentTypeJSON(t *testing.T) { defer gock.Off() gock.New("http://localhost"). - MatchHeader("Content-Type", "foo/bar"). - HeaderPresent("Content-Type"). + MatchHeader("Content-Type", "application/json"). Reply(http.StatusOK) - err := Request("GET", "http://localhost", "foo/bar", nil, nil) + err := Request("GET", "http://localhost", nil, nil, nil, AsJSON) if err != nil { t.Errorf("Unexpected error '%v'", err.Error()) } @@ -158,7 +157,7 @@ func TestGetRequestsWithMethodGet(t *testing.T) { Get("/"). Reply(http.StatusOK) - err := Get("http://localhost/", "", nil, nil) + err := Get("http://localhost/", nil, nil, nil) if err != nil { t.Errorf("Unexpected error %q", err) @@ -175,7 +174,7 @@ func TestPostRequestsWithMethodPost(t *testing.T) { Post("/"). Reply(http.StatusOK) - err := Post("http://localhost/", "", nil, nil) + err := Post("http://localhost/", nil, nil, nil) if err != nil { t.Errorf("Unexpected error %q", err) @@ -192,7 +191,7 @@ func TestPutRequestsWithMethodPut(t *testing.T) { Put("/"). Reply(http.StatusOK) - err := Put("http://localhost/", "", nil, nil) + err := Put("http://localhost/", nil, nil, nil) if err != nil { t.Errorf("Unexpected error %q", err) @@ -209,7 +208,7 @@ func TestDeleteRequestsWithMethodDelete(t *testing.T) { Delete("/"). Reply(http.StatusOK) - err := Delete("http://localhost/", "", nil, nil) + err := Delete("http://localhost/", nil, nil, nil) if err != nil { t.Errorf("Unexpected error %q", err) diff --git a/internal/server/control/control_test.go b/internal/server/control/control_test.go index 52747e18..d93500cd 100644 --- a/internal/server/control/control_test.go +++ b/internal/server/control/control_test.go @@ -58,56 +58,6 @@ func checkErrorResponse(r *http.Response, expectedErrcode int, expectedReason st return errList } -func TestConfigRouterHasRoutesWellConfigured(t *testing.T) { - testCases := []struct { - pattern, method string - handler uintptr - mustMatch bool - vars []string - }{ - {"/routes/FOO", http.MethodGet, reflect.ValueOf(getRoute).Pointer(), true, []string{"id"}}, - {"/routes/FOO", http.MethodPut, reflect.ValueOf(defMethodNotAllowedHandler).Pointer(), true, []string{}}, - {"/routes/FOO", http.MethodPost, reflect.ValueOf(defMethodNotAllowedHandler).Pointer(), true, []string{}}, - {"/routes/FOO", http.MethodDelete, reflect.ValueOf(removeRoute).Pointer(), true, []string{"id"}}, - {"/routes", http.MethodGet, reflect.ValueOf(listRoutes).Pointer(), true, []string{}}, - {"/routes", http.MethodPut, reflect.ValueOf(defMethodNotAllowedHandler).Pointer(), true, []string{}}, - {"/routes", http.MethodPost, reflect.ValueOf(addRoute).Pointer(), true, []string{}}, - {"/routes", http.MethodDelete, reflect.ValueOf(defMethodNotAllowedHandler).Pointer(), true, []string{}}, - {"/", http.MethodGet, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}}, - {"/", http.MethodPut, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}}, - {"/", http.MethodPost, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}}, - {"/", http.MethodDelete, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}}, - {"/FOO", http.MethodGet, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}}, - {"/FOO", http.MethodPut, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}}, - {"/FOO", http.MethodPost, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}}, - {"/FOO", http.MethodDelete, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}}, - } - r := configRouter() - - for _, tc := range testCases { - rm := mux.RouteMatch{} - rq, _ := http.NewRequest(tc.method, tc.pattern, nil) - if matched := r.Match(rq, &rm); tc.mustMatch == matched { - if tc.mustMatch { - // Check for Handler match. - realHandler := reflect.ValueOf(rm.Handler).Pointer() - if realHandler != tc.handler { - t.Errorf("Handler mismatch. Expected: %X, got: %X", tc.handler, realHandler) - } - - // Check for variables - for _, vn := range tc.vars { - if _, exists := rm.Vars[vn]; !exists { - t.Errorf("Variable not present: %s", vn) - } - } - } - } else { - t.Errorf("Route mismatch: %+v", tc) - } - } -} - func TestPathValidatorNoErrorWhenCorrectPath(t *testing.T) { err := pathValidator("/routes/{routeID}") diff --git a/internal/server/control/server.go b/internal/server/control/server.go index 82ce965b..c4ba626f 100644 --- a/internal/server/control/server.go +++ b/internal/server/control/server.go @@ -17,24 +17,47 @@ package control import ( + "crypto/tls" + "crypto/x509" "net" "net/http" "sync" + "github.com/BBVA/kapow/internal/certs" "github.com/BBVA/kapow/internal/logger" ) // Run Starts the control server listening in bindAddr -func Run(bindAddr string, wg *sync.WaitGroup) { +func Run(bindAddr string, wg *sync.WaitGroup, serverCert, clientCert certs.Cert) { - listener, err := net.Listen("tcp", bindAddr) + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(clientCert.SignedCertPEMBytes()) + + ln, err := net.Listen("tcp", bindAddr) if err != nil { logger.L.Fatal(err) } + server := &http.Server{ + Addr: bindAddr, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{ + tls.Certificate{ + Certificate: [][]byte{serverCert.SignedCert}, + PrivateKey: serverCert.PrivKey, + Leaf: serverCert.X509Cert, + }, + }, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: caCertPool, + }, + Handler: configRouter(), + } + // Signal startup logger.L.Printf("ControlServer listening at %s\n", bindAddr) wg.Done() - logger.L.Fatal(http.Serve(listener, configRouter())) + // Listen to HTTPS connections with the server certificate and wait + logger.L.Fatal(server.ServeTLS(ln, "", "")) } diff --git a/internal/server/server.go b/internal/server/server.go index ea0d6caa..08a2c2a1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -19,6 +19,7 @@ package server import ( "sync" + "github.com/BBVA/kapow/internal/certs" "github.com/BBVA/kapow/internal/server/control" "github.com/BBVA/kapow/internal/server/data" "github.com/BBVA/kapow/internal/server/user" @@ -34,13 +35,16 @@ type ServerConfig struct { ClientAuth, Debug bool + + ControlServerCert certs.Cert + ControlClientCert certs.Cert } // StartServer Starts one instance of each server in a goroutine and remains listening on a channel for trace events generated by them func StartServer(config ServerConfig) { var wg = sync.WaitGroup{} wg.Add(3) - go control.Run(config.ControlBindAddr, &wg) + go control.Run(config.ControlBindAddr, &wg, config.ControlServerCert, config.ControlClientCert) go data.Run(config.DataBindAddr, &wg) go user.Run(config.UserBindAddr, &wg, config.CertFile, config.KeyFile, config.ClientCaFile, config.ClientAuth, config.Debug)