feat: support ssl + client ssl authentication (#69)

Signed-off-by: Shivansh Saini <shivanshs9@gmail.com>
This commit is contained in:
Shivansh Saini 2021-09-30 14:38:45 +05:30 committed by GitHub
parent c1b722e87e
commit d795cf65e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 303 additions and 14 deletions

3
.gitignore vendored
View File

@ -18,3 +18,6 @@ vendor/
.idea
bin/
test/integration_test/**/*.*
!test/integration_test/**/*.sh

View File

@ -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")

View File

@ -28,6 +28,9 @@ type Config struct {
ListenPort int
ListenHost string
SSLCertFile string
SSLKeyFile string
SSLClientCAFile string
Runtime string
EnablePprof bool
PprofPort int
@ -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
}

View File

@ -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)
}
}
}

View File

@ -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())

View File

@ -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")

View File

@ -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

View File

@ -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