mirror of https://github.com/chaos-mesh/chaosd.git
feat: support ssl + client ssl authentication (#69)
Signed-off-by: Shivansh Saini <shivanshs9@gmail.com>
This commit is contained in:
parent
c1b722e87e
commit
d795cf65e5
|
|
@ -18,3 +18,6 @@ vendor/
|
|||
.idea
|
||||
|
||||
bin/
|
||||
|
||||
test/integration_test/**/*.*
|
||||
!test/integration_test/**/*.sh
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue