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

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

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