diff --git a/.gitignore b/.gitignore index 59e82d1..c1c1bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ vendor/ .idea bin/ + +test/integration_test/**/*.* +!test/integration_test/**/*.sh diff --git a/cmd/server/server.go b/cmd/server/server.go index 99d8494..cdb8f1f 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -32,6 +32,9 @@ func NewServerCommand() *cobra.Command { cmd.Flags().IntVarP(&conf.ListenPort, "port", "p", 31767, "listen port of the Chaosd Server") cmd.Flags().StringVarP(&conf.ListenHost, "host", "a", "0.0.0.0", "listen host of the Chaosd Server") + cmd.Flags().StringVar(&conf.SSLCertFile, "cert", "", "path to a PEM encoded certificate file") + cmd.Flags().StringVar(&conf.SSLKeyFile, "key", "", "path to a PEM encoded private key file") + cmd.Flags().StringVar(&conf.SSLClientCAFile, "CA", "", "path to a PEM encoded CA's certificate file") cmd.Flags().StringVarP(&conf.Runtime, "runtime", "r", "docker", "current container runtime") cmd.Flags().BoolVar(&conf.EnablePprof, "enable-pprof", true, "enable pprof") cmd.Flags().IntVar(&conf.PprofPort, "pprof-port", 31766, "listen port of the pprof server") diff --git a/pkg/config/config.go b/pkg/config/config.go index 43ba2f7..8deea43 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,12 +26,15 @@ type Config struct { Version bool - ListenPort int - ListenHost string - Runtime string - EnablePprof bool - PprofPort int - Platform string + ListenPort int + ListenHost string + SSLCertFile string + SSLKeyFile string + SSLClientCAFile string + Runtime string + EnablePprof bool + PprofPort int + Platform string } // Parse parses flag definitions from the argument list. @@ -58,6 +61,14 @@ func (c *Config) Validate() error { return errors.Errorf("container runtime %s is not supported", c.Runtime) } + if (len(c.SSLCertFile) > 0 || len(c.SSLKeyFile) > 0) && (len(c.SSLCertFile) == 0 || len(c.SSLKeyFile) == 0) { + return errors.New("provide both certificate and private key") + } + + if len(c.SSLClientCAFile) > 0 && len(c.SSLCertFile) == 0 { + return errors.New("attempting to use client CA without providing server certificate") + } + return nil } diff --git a/pkg/server/httpserver/auth.go b/pkg/server/httpserver/auth.go new file mode 100644 index 0000000..cf010d0 --- /dev/null +++ b/pkg/server/httpserver/auth.go @@ -0,0 +1,129 @@ +// Copyright 2021 Chaos Mesh Authors. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpserver + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/pingcap/log" + "go.uber.org/zap" + + "github.com/chaos-mesh/chaosd/pkg/server/utils" +) + +const ( + MTLSServer = "mTLS" + TLSServer = "tls" + HTTPServer = "http" +) + +var ( + errMissingClientCert = utils.ErrAuth.New("Sorry, but you need to provide a client certificate to continue") +) + +func (s *httpServer) serverMode() string { + if len(s.conf.SSLCertFile) > 0 { + if len(s.conf.SSLClientCAFile) > 0 { + return MTLSServer + } + return TLSServer + } + return HTTPServer +} + +func (s *httpServer) Run(handler func()) (err error) { + addr := s.conf.Address() + mode := s.serverMode() + + if mode == MTLSServer { + log.Info("starting HTTPS server with Client Auth", zap.String("address", addr)) + + caCert, ioErr := ioutil.ReadFile(s.conf.SSLClientCAFile) + if ioErr != nil { + err = ioErr + return + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + tlsConfig := &tls.Config{ + ClientCAs: caCertPool, + ClientAuth: tls.RequestClientCert, + } + + s.engine.Use(authenticateClientCert(tlsConfig)) + server := &http.Server{ + Addr: addr, + TLSConfig: tlsConfig, + Handler: s.engine, + } + + handler() + err = server.ListenAndServeTLS(s.conf.SSLCertFile, s.conf.SSLKeyFile) + } else if mode == TLSServer { + log.Info("starting HTTPS server", zap.String("address", addr)) + + handler() + err = s.engine.RunTLS(addr, s.conf.SSLCertFile, s.conf.SSLKeyFile) + } else { + log.Info("starting HTTP server", zap.String("address", addr)) + + handler() + err = s.engine.Run(addr) + } + return +} + +func verifyCertificates(config *tls.Config, certs []*x509.Certificate) error { + t := config.Time + if t == nil { + t = time.Now + } + opts := x509.VerifyOptions{ + Roots: config.ClientCAs, + CurrentTime: t(), + Intermediates: x509.NewCertPool(), + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + + _, err := certs[0].Verify(opts) + if err != nil { + return err + } + return nil +} + +func authenticateClientCert(config *tls.Config) gin.HandlerFunc { + return func(ctx *gin.Context) { + clientTLS := ctx.Request.TLS + if len(clientTLS.PeerCertificates) > 0 { + if err := verifyCertificates(config, clientTLS.PeerCertificates); err != nil { + _ = ctx.AbortWithError(http.StatusForbidden, utils.ErrAuth.Wrap(err, "Unauthorized certificate credentials")) + } else { + ctx.Next() + } + } else { + _ = ctx.AbortWithError(http.StatusUnauthorized, errMissingClientCert) + } + } +} diff --git a/pkg/server/httpserver/server.go b/pkg/server/httpserver/server.go index 4c87edb..df84792 100644 --- a/pkg/server/httpserver/server.go +++ b/pkg/server/httpserver/server.go @@ -42,6 +42,7 @@ func NewServer( exp core.ExperimentStore, ) *httpServer { e := gin.Default() + e.Use(utils.MWHandleErrors()) return &httpServer{ @@ -57,21 +58,15 @@ func Register(s *httpServer, scheduler scheduler.Scheduler) { return } - handler(s) - go func() { - addr := s.conf.Address() - log.Debug("starting HTTP server", zap.String("address", addr)) - - if err := s.engine.Run(addr); err != nil { + if err := s.Run(s.handler); err != nil { log.Fatal("failed to start HTTP server", zap.Error(err)) } }() - scheduler.Start() } -func handler(s *httpServer) { +func (s *httpServer) handler() { api := s.engine.Group("/api") { api.GET("/swagger/*any", swaggerserver.Handler()) diff --git a/pkg/server/utils/error.go b/pkg/server/utils/error.go index edb3892..74f485b 100644 --- a/pkg/server/utils/error.go +++ b/pkg/server/utils/error.go @@ -23,6 +23,7 @@ import ( var ( ErrNS = errorx.NewNamespace("error.api") + ErrAuth = ErrNS.NewType("auth") ErrOther = ErrNS.NewType("other") ErrInvalidRequest = ErrNS.NewType("invalid_request") ErrInternalServer = ErrNS.NewType("internal_server_error") diff --git a/test/integration_test/mtls_server/gen_certs.sh b/test/integration_test/mtls_server/gen_certs.sh new file mode 100644 index 0000000..880ad48 --- /dev/null +++ b/test/integration_test/mtls_server/gen_certs.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +# Copyright 2021 Chaos Mesh Authors. +# +# 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, +# See the License for the specific language governing permissions and +# limitations under the License. + +# Generate script because certs expire in 1 year (365 days) + +mkdir -p client server + +# generate server certificate +openssl req \ + -x509 \ + -newkey rsa:4096 \ + -keyout server/server_key.pem \ + -out server/server_cert.pem \ + -nodes \ + -days 365 \ + -subj "/CN=localhost/O=Client\ Certificate\ Demo" + +# generate server-signed (valid) certifcate +openssl req \ + -newkey rsa:4096 \ + -keyout client/valid_key.pem \ + -out client/valid_csr.pem \ + -nodes \ + -days 365 \ + -subj "/CN=Valid" + +# sign with server_cert.pem +openssl x509 \ + -req \ + -in client/valid_csr.pem \ + -CA server/server_cert.pem \ + -CAkey server/server_key.pem \ + -out client/valid_cert.pem \ + -set_serial 01 \ + -days 365 + +# generate self-signed (invalid) certifcate +openssl req \ + -newkey rsa:4096 \ + -keyout client/invalid_key.pem \ + -out client/invalid_csr.pem \ + -nodes \ + -days 365 \ + -subj "/CN=Invalid" + +# sign with invalid_csr.pem +openssl x509 \ + -req \ + -in client/invalid_csr.pem \ + -signkey client/invalid_key.pem \ + -out client/invalid_cert.pem \ + -days 365 diff --git a/test/integration_test/mtls_server/run.sh b/test/integration_test/mtls_server/run.sh new file mode 100644 index 0000000..0e5b84a --- /dev/null +++ b/test/integration_test/mtls_server/run.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +# Copyright 2021 Chaos Mesh Authors. +# +# 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, +# See the License for the specific language governing permissions and +# limitations under the License. + +set -u + +cur=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +cd $cur + +bin_path=../../../bin + +RED='\033[0;31m' +NC='\033[0m' # No Color +PORT=31767 +ENDPOINT="https://localhost:$PORT/api/system/health" + +function failtest() { + msg="$1" + kill $server_pid + echo -e "${RED}FAIL: $msg$NC" + exit 1 +} + +function request() { + cert_prefix=$1 + result_status_code=$2 + cmd="curl -skw \n%{http_code} $ENDPOINT" + if [[ -n "$cert_prefix" ]]; then + cmd="$cmd --cert client/${cert_prefix}_cert.pem --key client/${cert_prefix}_key.pem" + fi + response=$($cmd) + body=$(echo "$response" | head -n1) + status=$(echo "$response" | tail -n1) + if [[ "$status" != "$result_status_code" ]]; then + failtest "expected $result_status_code, got $status" + fi +} + +echo "Generating certificates" +bash +ex ./gen_certs.sh + +echo "Starting Server in mTLS mode" +${bin_path}/chaosd server \ + --port $PORT \ + --cert ./server/server_cert.pem \ + --key ./server/server_key.pem \ + --CA ./server/server_cert.pem \ +> server.out & + +server_pid=$! + +sleep 2 + +if ! kill -0 $server_pid > /dev/null 2>&1; then + echo -e "${RED}ERROR: Couldn't start server$NC" + cat server.out + exit 1 +fi + +echo -n "Test with no certificate... " +request '' 401 +echo "Passed" + +echo -n "Test with invalid certificate... " +request 'invalid' 403 +echo "Passed" + +echo -n "Test with valid certificate... " +request 'valid' 200 +echo "Passed" + +kill $server_pid