Skip to content

Commit

Permalink
No public description
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 574125373
  • Loading branch information
torsm committed Oct 17, 2023
1 parent 3f544e0 commit 3a2d388
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 140 deletions.
25 changes: 5 additions & 20 deletions fleetspeak/src/server/components/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (

"google.golang.org/grpc"

log "github.com/golang/glog"
"github.com/google/fleetspeak/fleetspeak/src/server"
"github.com/google/fleetspeak/fleetspeak/src/server/admin"
"github.com/google/fleetspeak/fleetspeak/src/server/authorizer"
Expand Down Expand Up @@ -96,29 +95,15 @@ func MakeComponents(cfg *cpb.Config) (*server.Components, error) {
l = &chttps.ProxyListener{l}
}
comm, err = https.NewCommunicator(https.Params{
Listener: l,
Cert: []byte(hcfg.Certificates),
ClientCertHeader: hcfg.ClientCertificateHeader,
FrontendMode: hcfg.FrontendMode,
Key: []byte(hcfg.Key),
Streaming: !hcfg.DisableStreaming,
Listener: l,
Cert: []byte(hcfg.Certificates),
FrontendConfig: hcfg.GetFrontendConfig(),
Key: []byte(hcfg.Key),
Streaming: !hcfg.DisableStreaming,
})
if err != nil {
return nil, fmt.Errorf("failed to create communicator: %v", err)
}
if hcfg.FrontendMode != cpb.FrontendMode_MTLS {
log.Warningln("####################################################################")
log.Warningln("# Note: #")
log.Warningln("# Your are running Fleetspeak in a frontend mode other than mTLS. #")
log.Warningln("# This only makes sense if you run Fleetspeak frontends behind a #")
log.Warningln("# TLS-terminating load balancer. #")
log.Warningln("####################################################################")
}
if (hcfg.FrontendMode == cpb.FrontendMode_MTLS && hcfg.ClientCertificateHeader != "") ||
(hcfg.FrontendMode == cpb.FrontendMode_HEADER_TLS && hcfg.ClientCertificateHeader == "") {
return nil, fmt.Errorf("Invalid frontend mode combination for running Fleetspeak: frontendMode=%s, clientCertificateHeader=%s",
hcfg.FrontendMode, hcfg.ClientCertificateHeader)
}
}
// Notification setup.
var nn notifications.Notifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,6 @@ package fleetspeak.components;

option go_package = "github.com/google/fleetspeak/fleetspeak/src/server/components/proto/fleetspeak_components";

// FrontendMode defines the connectivity setup between the Fleetspeak agents
// (clients) and the frontend (server). It is key to Fleetspeak's design that
// agents connect via mTLS. However, the mTLS connection can either be
// terminated by the Fleetspeak frontend (as per the original design) or on an
// intermediate layer 7 load balancer. Depending on the setup choose the
// matching mode below.
enum FrontendMode {
// In this mode Fleetspeak terminates the mTLS connection (as per original
// design). The Fleetspeak frontend can extract the mTLS client certificate
// from the HTTP request to identify client.
MTLS = 0;
// In this mode a layer 7 load balancer is terminating the mTLS connection.
// This requires the that the client certificate is delivered via a HTTP
// header. Use the HttpsConfig.client_certificate_header below to set the
// header's name.
HEADER_TLS = 1;
// HEADER_TLS_CHECKSUM = 2; // reserved for future use
// HEADER_CLEARTEXT = 3; // reserved for future use
// HEADER_CLEARTEXT_CHECKSUM = 4; // reserved for future use
}

message Config {
// Mysql connection string. Required.
//
Expand Down Expand Up @@ -85,7 +64,41 @@ message Config {
bool notification_use_http_notifier = 10;
}

// In this mode Fleetspeak accepts a mTLS connection directly from the client.
// The Fleetspeak frontend uses the client certificate from the HTTPS request
// to identify the client.
// This is the default operating mode of the frontend.
message MTlsConfig {}

// In this mode Fleetspeak accepts a TLS connection from an intermediate actor
// which terminates the TLS protocol (typically a layer 7 load balancer).
// The intermediate actor passes the client certificate it receives from the
// original TLS connection to the frontend via an HTTP header.
// The Fleetspeak frontend uses the certificate passed in this header to
// identify the client.
message HttpsHeaderConfig {
// The name of the HTTP header set by the intermediary that contains the
// forwarded client certificate. Required.
string client_certificate_header = 1;
}

// The fronted config determines how the Fleetspeak frontend communicates with
// clients and how it identifies them.
message FrontendConfig {
// The mode in which the frontend should operate. Defaults to MTlsConfig.
//
// Note: Typically MTlsConfig should be used. The other options are only used
// in scenarios where a direct TLS connection between client and server is not
// possible.
oneof frontend_mode {
MTlsConfig mtls_config = 7;
HttpsHeaderConfig https_header_config = 8;
}
}

message HttpsConfig {
reserved 5, 6;

// The bind address to listen on for client connections, e.g. ":443" or
// "localhost:1234". Required.
string listen_address = 1;
Expand All @@ -103,24 +116,9 @@ message HttpsConfig {
// server->client communications latency.
bool disable_streaming = 4;

// If set, the server will validate the client certificate from the request
// header. This should be used if TLS is terminated at the load balancer and
// client certificates can be passed upstream to the fleetspeak server as an
// http header.
// Note for this parameter to take effect you also need to set the
// frontend_mode parameter below accordingly. This is a safety net mechanism
// to avoid scenarios where client_certificate_header is set to non-empty
// unintentionally.
string client_certificate_header = 5;

// The frontend_mode parameter serves as a safety net to avoid scenarios where
// the client_certificate_header is set accidentially.
// frontend_mode defaults to mTLS where the client certificate is delivered
// in Fleetspeaks' original design.
// In case certificate delivery is desired through a HTTP header you need set
// both the frontend_mode and the client_certificate_header parameters
// accordingly.
FrontendMode frontend_mode = 6;
// The frontend config.
// Optional; If not set, Fleetspeak will default to using MTlsConfig.
FrontendConfig frontend_config = 7;
}

message AdminConfig {
Expand Down
46 changes: 25 additions & 21 deletions fleetspeak/src/server/https/client_certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,44 @@ import (
)

// GetClientCert returns the client certificate from either the request header or TLS connection state.
func GetClientCert(req *http.Request, hn string, frontendMode cpb.FrontendMode) (*x509.Certificate, error) {
switch frontendMode {
case cpb.FrontendMode_MTLS:
if hn == "" {
return getCertFromTLS(req)
}
case cpb.FrontendMode_HEADER_TLS:
if hn != "" {
return getCertFromHeader(hn, req.Header)
}
}
return nil, fmt.Errorf("received invalid frontend mode combination: frontendMode=%s, clientCertHeader=%s", frontendMode, hn)
func GetClientCert(req *http.Request, frontendConfig *cpb.FrontendConfig) (*x509.Certificate, error) {

Check failure on line 15 in fleetspeak/src/server/https/client_certificate.go

View workflow job for this annotation

GitHub Actions / test-osx

undefined: cpb.FrontendConfig

Check failure on line 15 in fleetspeak/src/server/https/client_certificate.go

View workflow job for this annotation

GitHub Actions / build-test-linux

undefined: cpb.FrontendConfig

Check failure on line 15 in fleetspeak/src/server/https/client_certificate.go

View workflow job for this annotation

GitHub Actions / build-test-linux

undefined: cpb.FrontendConfig

Check failure on line 15 in fleetspeak/src/server/https/client_certificate.go

View workflow job for this annotation

GitHub Actions / test-osx

undefined: cpb.FrontendConfig
// Default to using mTLS if frontend_config or frontend_mode have not been set
if frontendConfig.GetFrontendMode() == nil {
return getCertFromTLS(req)
}

if frontendConfig.GetMtlsConfig() != nil {
return getCertFromTLS(req)
} else if frontendConfig.GetHttpsHeaderConfig() != nil {
return getCertFromHeader(frontendConfig.GetHttpsHeaderConfig().GetClientCertificateHeader(), req.Header)
}

// Given the above if statements are exhaustive this error should never be reached
return nil, errors.New("invalid frontend_config")
}

func getCertFromHeader(hn string, rh http.Header) (*x509.Certificate, error) {
headerCert := rh.Get(hn)
if headerCert == "" {
return nil, errors.New("no certificate found in header")
return nil, fmt.Errorf("no certificate found in header with name %q", hn)
}
// Most certificates are URL PEM encoded
if decodedCert, err := url.PathUnescape(headerCert); err != nil {
decodedCert, err := url.PathUnescape(headerCert)
if err != nil {
return nil, err
} else {
headerCert = decodedCert
}
block, rest := pem.Decode([]byte(headerCert))
if block == nil || block.Type != "CERTIFICATE" {
return nil, errors.New("failed to decode PEM block containing certificate")

block, rest := pem.Decode([]byte(decodedCert))
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
if block.Type != "CERTIFICATE" {
return nil, errors.New("PEM block is not a certificate")
}
if len(rest) != 0 {
return nil, errors.New("received more than 1 client cert")
}
cert, err := x509.ParseCertificate(block.Bytes)
return cert, err
return x509.ParseCertificate(block.Bytes)
}

func getCertFromTLS(req *http.Request) (*x509.Certificate, error) {
Expand Down
90 changes: 49 additions & 41 deletions fleetspeak/src/server/https/client_certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,62 +97,72 @@ func makeTestClient(t *testing.T) (common.ClientID, *http.Client, []byte) {
}

func TestFrontendMode_MTLS(t *testing.T) {
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// test the valid frontend mode combination of receiving the client cert in the req
cert, err := GetClientCert(req, "", cpb.FrontendMode_MTLS)
if err != nil {
t.Fatal(err)
}
// make sure we received the client cert in the req
if cert == nil {
t.Error("Expected client certificate but received none")
}
// test the invalid frontend mode combination
_, err = GetClientCert(req, "", cpb.FrontendMode_HEADER_TLS)
if err == nil {
t.Error("Expected error for invalid frontend mode combination but received none")
// All these configs should end up using mTLS
configs := []*cpb.FrontendConfig{
&cpb.FrontendConfig{
FrontendMode: &cpb.FrontendConfig_MtlsConfig{
MtlsConfig: &cpb.MTlsConfig{},
},
},
&cpb.FrontendConfig{
FrontendMode: nil,
},
nil,
}

for _, frontendConfig := range configs {
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// test the valid frontend mode combination of receiving the client cert in the req
cert, err := GetClientCert(req, frontendConfig)
if err != nil {
t.Fatal(err)
}
// make sure we received the client cert in the req
if cert == nil {
t.Error("Expected client certificate but received none")
}
fmt.Fprintln(w, "Testing Frontend Mode: MTLS")
}))
ts.TLS = &tls.Config{
ClientAuth: tls.RequireAnyClientCert,
}
fmt.Fprintln(w, "Testing Frontend Mode: MTLS")
}))
ts.TLS = &tls.Config{
ClientAuth: tls.RequireAnyClientCert,
}
ts.StartTLS()
defer ts.Close()
ts.StartTLS()
defer ts.Close()

_, client, _ := makeTestClient(t)
_, client, _ := makeTestClient(t)

res, err := client.Get(ts.URL)
if err != nil {
t.Fatal(err)
}
res, err := client.Get(ts.URL)
if err != nil {
t.Fatal(err)
}

body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
_, err = io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
}
}

fmt.Printf("%s", body)
}

func TestFrontendMode_HEADER_TLS(t *testing.T) {
clientCertHeader := "ssl-client-cert"
frontendConfig := &cpb.FrontendConfig{
FrontendMode: &cpb.FrontendConfig_HttpsHeaderConfig{
HttpsHeaderConfig: &cpb.HttpsHeaderConfig{
ClientCertificateHeader: clientCertHeader,
},
},
}
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// test the valid frontend mode combination of receiving the client cert in the header
cert, err := GetClientCert(req, clientCertHeader, cpb.FrontendMode_HEADER_TLS)
cert, err := GetClientCert(req, frontendConfig)
if err != nil {
t.Fatal(err)
}
// make sure we received the client cert in the header
if cert == nil {
t.Error("Expected client certificate but received none")
}
// test the invalid frontend mode combination
_, err = GetClientCert(req, clientCertHeader, cpb.FrontendMode_MTLS)
if err == nil {
t.Error("Expected error for invalid frontend mode combination but received none")
}
fmt.Fprintln(w, "Testing Frontend Mode: HEADER_TLS")
}))
ts.TLS = &tls.Config{
Expand All @@ -175,11 +185,9 @@ func TestFrontendMode_HEADER_TLS(t *testing.T) {
t.Fatal(err)
}

body, err := io.ReadAll(res.Body)
_, err = io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
}

fmt.Printf("%s", body)
}
15 changes: 7 additions & 8 deletions fleetspeak/src/server/https/https.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,13 @@ func (l listener) Accept() (net.Conn, error) {

// Params wraps the parameters required to create an https communicator.
type Params struct {
Listener net.Listener // Where to listen for connections, required.
Cert, Key []byte // x509 encoded certificate and matching private key, required.
Streaming bool // Whether to enable streaming communications.
ClientCertHeader string // Where to locate the client certificate from the request header, if not provided use TLS request.
FrontendMode cpb.FrontendMode // Safety net to explicitly select the mode how client certificates are delivered (MTLS or HEADER_TLS)
StreamingLifespan time.Duration // Maximum time to keep a streaming connection open, defaults to 10 min.
StreamingCloseTime time.Duration // How much of StreamingLifespan to allocate to an orderly stream close, defaults to 30 sec.
StreamingJitter time.Duration // Maximum amount of jitter to add to StreamingLifespan.
Listener net.Listener // Where to listen for connections, required.
Cert, Key []byte // x509 encoded certificate and matching private key, required.
Streaming bool // Whether to enable streaming communications.
FrontendConfig *cpb.FrontendConfig // Configure how the frontend identifies and communicates with clients

Check failure on line 90 in fleetspeak/src/server/https/https.go

View workflow job for this annotation

GitHub Actions / test-osx

undefined: cpb.FrontendConfig

Check failure on line 90 in fleetspeak/src/server/https/https.go

View workflow job for this annotation

GitHub Actions / build-test-linux

undefined: cpb.FrontendConfig

Check failure on line 90 in fleetspeak/src/server/https/https.go

View workflow job for this annotation

GitHub Actions / build-test-linux

undefined: cpb.FrontendConfig

Check failure on line 90 in fleetspeak/src/server/https/https.go

View workflow job for this annotation

GitHub Actions / test-osx

undefined: cpb.FrontendConfig
StreamingLifespan time.Duration // Maximum time to keep a streaming connection open, defaults to 10 min.
StreamingCloseTime time.Duration // How much of StreamingLifespan to allocate to an orderly stream close, defaults to 30 sec.
StreamingJitter time.Duration // Maximum amount of jitter to add to StreamingLifespan.
}

// NewCommunicator creates a Communicator, which listens through l and identifies
Expand Down
Loading

0 comments on commit 3a2d388

Please sign in to comment.