Skip to content
This repository has been archived by the owner on Apr 19, 2024. It is now read-only.

Commit

Permalink
Merge pull request #124 from denkyl08/add-http-status-listener
Browse files Browse the repository at this point in the history
Enable running gubernator in Kubernetes while enforcing mTLS
  • Loading branch information
thrawn01 authored Jan 20, 2022
2 parents 2df3fd2 + a4336b5 commit 3081495
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 16 deletions.
8 changes: 8 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ type DaemonConfig struct {
// (Required) The `address:port` that will accept HTTP requests
HTTPListenAddress string

// (Optional) The `address:port` that will accept HTTP requests for /v1/HealthCheck
// without verifying client certificates. Only starts listener when TLS config is provided.
// TLS config is identical to what is applied on HTTPListenAddress, except that server
// Does not attempt to verify client certificate. Useful when your health probes cannot
// provide client certificate but you want to enforce mTLS in other RPCs (like in K8s)
HTTPStatusListenAddress string

// (Optional) Defines the max age connection from client in seconds.
// Default is infinity
GRPCMaxConnectionAgeSeconds int
Expand Down Expand Up @@ -244,6 +251,7 @@ func SetupDaemonConfig(logger *logrus.Logger, configFile string) (DaemonConfig,
// Main config
setter.SetDefault(&conf.GRPCListenAddress, os.Getenv("GUBER_GRPC_ADDRESS"), "localhost:81")
setter.SetDefault(&conf.HTTPListenAddress, os.Getenv("GUBER_HTTP_ADDRESS"), "localhost:80")
setter.SetDefault(&conf.HTTPStatusListenAddress, os.Getenv("GUBER_STATUS_HTTP_ADDRESS"), "")
setter.SetDefault(&conf.GRPCMaxConnectionAgeSeconds, getEnvInteger(log, "GUBER_GRPC_MAX_CONN_AGE_SEC"), 0)
setter.SetDefault(&conf.CacheSize, getEnvInteger(log, "GUBER_CACHE_SIZE"), 50_000)
setter.SetDefault(&conf.AdvertiseAddress, os.Getenv("GUBER_ADVERTISE_ADDRESS"), conf.GRPCListenAddress)
Expand Down
58 changes: 47 additions & 11 deletions daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package gubernator

import (
"context"
"crypto/tls"
"log"
"net"
"net/http"
Expand All @@ -44,15 +45,16 @@ type Daemon struct {
HTTPListener net.Listener
V1Server *V1Instance

log logrus.FieldLogger
pool PoolInterface
conf DaemonConfig
httpSrv *http.Server
grpcSrvs []*grpc.Server
wg syncutil.WaitGroup
statsHandler *GRPCStatsHandler
promRegister *prometheus.Registry
gwCancel context.CancelFunc
log logrus.FieldLogger
pool PoolInterface
conf DaemonConfig
httpSrv *http.Server
httpSrvNoMTLS *http.Server
grpcSrvs []*grpc.Server
wg syncutil.WaitGroup
statsHandler *GRPCStatsHandler
promRegister *prometheus.Registry
gwCancel context.CancelFunc
}

// SpawnDaemon starts a new gubernator daemon according to the provided DaemonConfig.
Expand Down Expand Up @@ -255,7 +257,37 @@ func (s *Daemon) Start(ctx context.Context) error {
return errors.Wrap(err, "while starting HTTP listener")
}

addrs := []string{s.conf.HTTPListenAddress}

if s.conf.ServerTLS() != nil {

// If configured, start another listener at configured address and server only
// /v1/HealthCheck while not requesting or verifying client certificate.
if s.conf.HTTPStatusListenAddress != "" {
addrs = append(addrs, s.conf.HTTPStatusListenAddress)
muxNoMTLS := http.NewServeMux()
muxNoMTLS.Handle("/v1/HealthCheck", gateway)
s.httpSrvNoMTLS = &http.Server{
Addr: s.conf.HTTPStatusListenAddress,
Handler: muxNoMTLS,
ErrorLog: log,
TLSConfig: s.conf.ServerTLS().Clone(),
}
s.httpSrvNoMTLS.TLSConfig.ClientAuth = tls.NoClientCert
httpListener, err := net.Listen("tcp", s.conf.HTTPStatusListenAddress)
if err != nil {
return errors.Wrap(err, "while starting HTTP listener for health metric")
}
s.wg.Go(func() {
s.log.Infof("HTTPS Status Handler Listening on %s ...", s.conf.HTTPStatusListenAddress)
if err := s.httpSrvNoMTLS.ServeTLS(httpListener, "", ""); err != nil {
if err != http.ErrServerClosed {
s.log.WithError(err).Error("while starting TLS Status HTTP server")
}
}
})
}

// This is to avoid any race conditions that might occur
// since the tls config is a shared pointer.
s.httpSrv.TLSConfig = s.conf.ServerTLS().Clone()
Expand All @@ -279,7 +311,6 @@ func (s *Daemon) Start(ctx context.Context) error {
}

// Validate we can reach the GRPC and HTTP endpoints before returning
addrs := []string{s.conf.HTTPListenAddress}
for _, l := range s.GRPCListeners {
addrs = append(addrs, l.Addr().String())
}
Expand All @@ -292,7 +323,7 @@ func (s *Daemon) Start(ctx context.Context) error {

// Close gracefully closes all server connections and listening sockets
func (s *Daemon) Close() {
if s.httpSrv == nil {
if s.httpSrv == nil && s.httpSrvNoMTLS == nil {
return
}

Expand All @@ -302,6 +333,10 @@ func (s *Daemon) Close() {

s.log.Infof("HTTP Gateway close for %s ...", s.conf.HTTPListenAddress)
s.httpSrv.Shutdown(context.Background())
if s.httpSrvNoMTLS != nil {
s.log.Infof("HTTP Status Gateway close for %s ...", s.conf.HTTPStatusListenAddress)
s.httpSrvNoMTLS.Shutdown(context.Background())
}
for i, srv := range s.grpcSrvs {
s.log.Infof("GRPC close for %s ...", s.GRPCListeners[i].Addr())
srv.GracefulStop()
Expand All @@ -310,6 +345,7 @@ func (s *Daemon) Close() {
s.statsHandler.Close()
s.gwCancel()
s.httpSrv = nil
s.httpSrvNoMTLS = nil
s.grpcSrvs = nil
}

Expand Down
34 changes: 29 additions & 5 deletions tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,9 @@ func TestTLSClusterWithClientAuthentication(t *testing.T) {

func TestHTTPSClientAuth(t *testing.T) {
conf := gubernator.DaemonConfig{
GRPCListenAddress: "127.0.0.1:9695",
HTTPListenAddress: "127.0.0.1:9685",
GRPCListenAddress: "127.0.0.1:9695",
HTTPListenAddress: "127.0.0.1:9685",
HTTPStatusListenAddress: "127.0.0.1:9686",
TLS: &gubernator.TLSConfig{
CaFile: "certs/ca.pem",
CertFile: "certs/gubernator.pem",
Expand All @@ -275,17 +276,40 @@ func TestHTTPSClientAuth(t *testing.T) {
d := spawnDaemon(t, conf)
defer d.Close()

client := &http.Client{
clientWithCert := &http.Client{
Transport: &http.Transport{
TLSClientConfig: conf.TLS.ClientTLS,
},
}

req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/v1/HealthCheck", conf.HTTPListenAddress), nil)
clientWithoutCert := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: conf.TLS.ServerTLS.RootCAs,
},
},
}

reqCertRequired, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/v1/HealthCheck", conf.HTTPListenAddress), nil)
require.NoError(t, err)
resp, err := client.Do(req)
reqNoClientCertRequired, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/v1/HealthCheck", conf.HTTPStatusListenAddress), nil)
require.NoError(t, err)

// Test that a client without a cert can access /v1/HealthCheck at status address
resp, err := clientWithoutCert.Do(reqNoClientCertRequired)
require.NoError(t, err)
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, `{"status":"healthy","message":"","peer_count":1}`, strings.ReplaceAll(string(b), " ", ""))

// Verify we get an error when we try to access existing HTTPListenAddress without cert
_, err = clientWithoutCert.Do(reqCertRequired)
assert.Error(t, err)

// Check that with a valid client cert we can access /v1/HealthCheck at existing HTTPListenAddress
resp, err = clientWithCert.Do(reqCertRequired)
require.NoError(t, err)
b, err = ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, `{"status":"healthy","message":"","peer_count":1}`, strings.ReplaceAll(string(b), " ", ""))
}

0 comments on commit 3081495

Please sign in to comment.