diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 9479f8fc9..5d1184390 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -263,7 +263,7 @@ func buildGRPCExporter(cfg *config.Agent, m *metrics.Metrics) (node.TerminalFunc return nil, fmt.Errorf("missing target host or port: %s:%d", cfg.TargetHost, cfg.TargetPort) } - grpcExporter, err := exporter.StartGRPCProto(cfg.TargetHost, cfg.TargetPort, cfg.GRPCMessageMaxFlows, m) + grpcExporter, err := exporter.StartGRPCProto(cfg.TargetHost, cfg.TargetPort, cfg.TargetTLSCACertPath, cfg.TargetTLSUserCertPath, cfg.TargetTLSUserKeyPath, cfg.GRPCMessageMaxFlows, m) if err != nil { return nil, err } diff --git a/pkg/config/config.go b/pkg/config/config.go index 079b67801..92ba40dd8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -101,6 +101,12 @@ type Agent struct { TargetHost string `env:"TARGET_HOST"` // Port is the port the flow or packet collector, when the EXPORT variable is set to "grpc" TargetPort int `env:"TARGET_PORT"` + // CA certificate path of the target, when TLS is used. Empty by default (no TLS). + TargetTLSCACertPath string `env:"TARGET_TLS_CA_CERT_PATH"` + // User certificate path, when mTLS is used. Empty by default (no mTLS). + TargetTLSUserCertPath string `env:"TARGET_TLS_USER_CERT_PATH"` + // User certificate key path, when mTLS is used. Empty by default (no mTLS). + TargetTLSUserKeyPath string `env:"TARGET_TLS_USER_KEY_PATH"` // GRPCMessageMaxFlows specifies the limit, in number of flows, of each GRPC message. Messages // larger than that number will be split and submitted sequentially. GRPCMessageMaxFlows int `env:"GRPC_MESSAGE_MAX_FLOWS" envDefault:"10000"` diff --git a/pkg/exporter/grpc_proto.go b/pkg/exporter/grpc_proto.go index cec5e2a11..dacc53de6 100644 --- a/pkg/exporter/grpc_proto.go +++ b/pkg/exporter/grpc_proto.go @@ -32,8 +32,8 @@ type GRPCProto struct { batchCounter prometheus.Counter } -func StartGRPCProto(hostIP string, hostPort int, maxFlowsPerMessage int, m *metrics.Metrics) (*GRPCProto, error) { - clientConn, err := grpc.ConnectClient(hostIP, hostPort) +func StartGRPCProto(hostIP string, hostPort int, caPath, userCertPath, userKeyPath string, maxFlowsPerMessage int, m *metrics.Metrics) (*GRPCProto, error) { + clientConn, err := grpc.ConnectClient(hostIP, hostPort, caPath, userCertPath, userKeyPath) if err != nil { return nil, err } diff --git a/pkg/exporter/grpc_proto_test.go b/pkg/exporter/grpc_proto_test.go index 27d3759c2..c9127fcee 100644 --- a/pkg/exporter/grpc_proto_test.go +++ b/pkg/exporter/grpc_proto_test.go @@ -29,7 +29,7 @@ func TestIPv4GRPCProto_ExportFlows_AgentIP(t *testing.T) { defer coll.Close() // Start GRPCProto exporter stage - exporter, err := StartGRPCProto("127.0.0.1", port, 1000, metrics.NoOp()) + exporter, err := StartGRPCProto("127.0.0.1", port, "", "", "", 1000, metrics.NoOp()) require.NoError(t, err) // Send some flows to the input of the exporter stage @@ -71,7 +71,7 @@ func TestIPv6GRPCProto_ExportFlows_AgentIP(t *testing.T) { defer coll.Close() // Start GRPCProto exporter stage - exporter, err := StartGRPCProto("::1", port, 1000, metrics.NoOp()) + exporter, err := StartGRPCProto("::1", port, "", "", "", 1000, metrics.NoOp()) require.NoError(t, err) // Send some flows to the input of the exporter stage @@ -114,7 +114,7 @@ func TestGRPCProto_SplitLargeMessages(t *testing.T) { const msgMaxLen = 10000 // Start GRPCProto exporter stage - exporter, err := StartGRPCProto("127.0.0.1", port, msgMaxLen, metrics.NoOp()) + exporter, err := StartGRPCProto("127.0.0.1", port, "", "", "", msgMaxLen, metrics.NoOp()) require.NoError(t, err) // Send a message much longer than the limit length diff --git a/pkg/grpc/flow/client.go b/pkg/grpc/flow/client.go index 350bc619b..ca7a4d083 100644 --- a/pkg/grpc/flow/client.go +++ b/pkg/grpc/flow/client.go @@ -2,23 +2,60 @@ package flowgrpc import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" + "github.com/netobserv/netobserv-ebpf-agent/pkg/pbflow" "github.com/netobserv/netobserv-ebpf-agent/pkg/utils" + "github.com/sirupsen/logrus" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" ) +var clog = logrus.WithField("component", "grpc.Client") + // ClientConnection wraps a gRPC+protobuf connection type ClientConnection struct { client pbflow.CollectorClient conn *grpc.ClientConn } -func ConnectClient(hostIP string, hostPort int) (*ClientConnection, error) { - // TODO: allow configuring some options (keepalive, backoff...) +func ConnectClient(hostIP string, hostPort int, caPath, userCertPath, userKeyPath string) (*ClientConnection, error) { + // TODO: allow configuring more options (keepalive, backoff...) + var opts []grpc.DialOption + if caPath == "" { + clog.Info("Starting GRPC client - no TLS") + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + // Configure TLS (server CA) + caCert, err := os.ReadFile(caPath) + if err != nil { + return nil, fmt.Errorf("cannot load CA certificate: %w", err) + } + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(caCert) + tlsConfig := &tls.Config{ + RootCAs: pool, + } + if userCertPath != "" && userKeyPath != "" { + clog.Info("Starting GRPC client with mTLS") + // Configure mTLS (client certificates) + cert, err := tls.LoadX509KeyPair(userCertPath, userKeyPath) + if err != nil { + return nil, fmt.Errorf("cannot load client certificate: %w", err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } else { + clog.Info("Starting GRPC client with TLS") + } + + opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) + } socket := utils.GetSocket(hostIP, hostPort) - conn, err := grpc.NewClient(socket, - grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient(socket, opts...) if err != nil { return nil, err } diff --git a/pkg/grpc/flow/grpc_test.go b/pkg/grpc/flow/grpc_test.go index 39e933422..f5683bd8e 100644 --- a/pkg/grpc/flow/grpc_test.go +++ b/pkg/grpc/flow/grpc_test.go @@ -2,26 +2,32 @@ package flowgrpc import ( "context" + "crypto/tls" + "crypto/x509" + "fmt" + "os" "testing" "time" - "github.com/mariomac/guara/pkg/test" + test2 "github.com/mariomac/guara/pkg/test" "github.com/netobserv/netobserv-ebpf-agent/pkg/pbflow" + "github.com/netobserv/netobserv-ebpf-agent/pkg/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "google.golang.org/protobuf/types/known/timestamppb" ) const timeout = 5 * time.Second func TestGRPCCommunication(t *testing.T) { - port, err := test.FreeTCPPort() + port, err := test2.FreeTCPPort() require.NoError(t, err) serverOut := make(chan *pbflow.Records) _, err = StartCollector(port, serverOut) require.NoError(t, err) - cc, err := ConnectClient("127.0.0.1", port) + cc, err := ConnectClient("127.0.0.1", port, "", "", "") require.NoError(t, err) client := cc.Client() @@ -93,7 +99,7 @@ func TestGRPCCommunication(t *testing.T) { } func TestConstructorOptions(t *testing.T) { - port, err := test.FreeTCPPort() + port, err := test2.FreeTCPPort() require.NoError(t, err) intercepted := make(chan struct{}) // Override the default GRPC collector to verify that StartCollector is applying the @@ -109,7 +115,7 @@ func TestConstructorOptions(t *testing.T) { return handler(ctx, req) }))) require.NoError(t, err) - cc, err := ConnectClient("127.0.0.1", port) + cc, err := ConnectClient("127.0.0.1", port, "", "", "") require.NoError(t, err) client := cc.Client() @@ -127,13 +133,13 @@ func TestConstructorOptions(t *testing.T) { } func BenchmarkIPv4GRPCCommunication(b *testing.B) { - port, err := test.FreeTCPPort() + port, err := test2.FreeTCPPort() require.NoError(b, err) serverOut := make(chan *pbflow.Records, 1000) collector, err := StartCollector(port, serverOut) require.NoError(b, err) defer collector.Close() - cc, err := ConnectClient("127.0.0.1", port) + cc, err := ConnectClient("127.0.0.1", port, "", "", "") require.NoError(b, err) defer cc.Close() client := cc.Client() @@ -188,13 +194,13 @@ func BenchmarkIPv4GRPCCommunication(b *testing.B) { } func BenchmarkIPv6GRPCCommunication(b *testing.B) { - port, err := test.FreeTCPPort() + port, err := test2.FreeTCPPort() require.NoError(b, err) serverOut := make(chan *pbflow.Records, 1000) collector, err := StartCollector(port, serverOut) require.NoError(b, err) defer collector.Close() - cc, err := ConnectClient("::1", port) + cc, err := ConnectClient("::1", port, "", "", "") require.NoError(b, err) defer cc.Close() client := cc.Client() @@ -249,3 +255,173 @@ func BenchmarkIPv6GRPCCommunication(b *testing.B) { <-serverOut } } + +// Note: there's more tests focused on TLS in FLP, that also cover agent's functions +func TestGRPCCommunication_TLS(t *testing.T) { + _, _, _, ca, cert, key, cleanup := test.CreateAllCerts(t) + defer cleanup() + opts, err := buildTLSServerOptions(cert, key, "") + require.NoError(t, err) + + port, err := test2.FreeTCPPort() + require.NoError(t, err) + serverOut := make(chan *pbflow.Records) + _, err = StartCollector(port, serverOut, WithGRPCServerOptions(opts...)) + require.NoError(t, err) + cc, err := ConnectClient("127.0.0.1", port, ca, "", "") + require.NoError(t, err) + client := cc.Client() + + go func() { + _, err = client.Send(context.Background(), + &pbflow.Records{Entries: []*pbflow.Record{{ + EthProtocol: 123, Network: &pbflow.Network{ + SrcAddr: &pbflow.IP{ + IpFamily: &pbflow.IP_Ipv4{Ipv4: 0x11223344}, + }, + DstAddr: &pbflow.IP{ + IpFamily: &pbflow.IP_Ipv4{Ipv4: 0x55667788}, + }, + }}}, + }) + require.NoError(t, err) + }() + + var rs *pbflow.Records + select { + case rs = <-serverOut: + case <-time.After(timeout): + require.Fail(t, "timeout waiting for flows") + } + assert.Len(t, rs.Entries, 1) + r := rs.Entries[0] + assert.EqualValues(t, 123, r.EthProtocol) + assert.EqualValues(t, 0x11223344, r.GetNetwork().GetSrcAddr().GetIpv4()) + assert.EqualValues(t, 0x55667788, r.GetNetwork().GetDstAddr().GetIpv4()) + + select { + case rs = <-serverOut: + assert.Failf(t, "shouldn't have received any flow", "Got: %#v", rs) + default: + // ok! + } +} + +// Note: there's more tests focused on TLS in FLP, that also cover agent's functions +func TestGRPCCommunication_MutualTLS(t *testing.T) { + clCA, clCert, clKey, ca, cert, key, cleanup := test.CreateAllCerts(t) + defer cleanup() + opts, err := buildTLSServerOptions(cert, key, clCA) + require.NoError(t, err) + + port, err := test2.FreeTCPPort() + require.NoError(t, err) + serverOut := make(chan *pbflow.Records) + _, err = StartCollector(port, serverOut, WithGRPCServerOptions(opts...)) + require.NoError(t, err) + cc, err := ConnectClient("127.0.0.1", port, ca, clCert, clKey) + require.NoError(t, err) + client := cc.Client() + + go func() { + _, err = client.Send(context.Background(), + &pbflow.Records{Entries: []*pbflow.Record{{ + EthProtocol: 123, Network: &pbflow.Network{ + SrcAddr: &pbflow.IP{ + IpFamily: &pbflow.IP_Ipv4{Ipv4: 0x11223344}, + }, + DstAddr: &pbflow.IP{ + IpFamily: &pbflow.IP_Ipv4{Ipv4: 0x55667788}, + }, + }}}, + }) + require.NoError(t, err) + }() + + var rs *pbflow.Records + select { + case rs = <-serverOut: + case <-time.After(timeout): + require.Fail(t, "timeout waiting for flows") + } + assert.Len(t, rs.Entries, 1) + r := rs.Entries[0] + assert.EqualValues(t, 123, r.EthProtocol) + assert.EqualValues(t, 0x11223344, r.GetNetwork().GetSrcAddr().GetIpv4()) + assert.EqualValues(t, 0x55667788, r.GetNetwork().GetDstAddr().GetIpv4()) + + select { + case rs = <-serverOut: + assert.Failf(t, "shouldn't have received any flow", "Got: %#v", rs) + default: + // ok! + } +} + +func TestGRPCCommunication_MutualTLS_InvalidCert(t *testing.T) { + _, clCert, clKey, ca, cert, key, cleanup := test.CreateAllCerts(t) + defer cleanup() + + // Here we pass the server CA, which was NOT used to generate the client cert, which means that the client cert should be rejected upon connecting + opts, err := buildTLSServerOptions(cert, key, ca) + require.NoError(t, err) + + port, err := test2.FreeTCPPort() + require.NoError(t, err) + serverOut := make(chan *pbflow.Records) + _, err = StartCollector(port, serverOut, WithGRPCServerOptions(opts...)) + require.NoError(t, err) + cc, err := ConnectClient("127.0.0.1", port, ca, clCert, clKey) + require.NoError(t, err) + client := cc.Client() + + go func() { + _, err = client.Send(context.Background(), + &pbflow.Records{Entries: []*pbflow.Record{{ + EthProtocol: 123, Network: &pbflow.Network{ + SrcAddr: &pbflow.IP{ + IpFamily: &pbflow.IP_Ipv4{Ipv4: 0x11223344}, + }, + DstAddr: &pbflow.IP{ + IpFamily: &pbflow.IP_Ipv4{Ipv4: 0x55667788}, + }, + }}}, + }) + require.ErrorContains(t, err, "tls: unknown certificate authority") + }() + + select { + case rs := <-serverOut: + assert.Failf(t, "shouldn't have received any flow", "Got: %#v", rs) + default: + // ok! + } +} + +func buildTLSServerOptions(certPath, keyPath, clientCAPath string) ([]grpc.ServerOption, error) { + var opts []grpc.ServerOption + if certPath != "" && keyPath != "" { + // TLS + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("cannot load configured certificate: %w", err) + } + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.NoClientCert, + } + if clientCAPath != "" { + // mTLS + caCert, err := os.ReadFile(clientCAPath) + if err != nil { + return nil, fmt.Errorf("cannot load configured client CA certificate: %w", err) + } + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(caCert) + tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert + tlsCfg.ClientCAs = pool + } + opts = append(opts, grpc.Creds(credentials.NewTLS(tlsCfg))) + } + return opts, nil +} diff --git a/pkg/test/tls.go b/pkg/test/tls.go new file mode 100644 index 000000000..b4bf69450 --- /dev/null +++ b/pkg/test/tls.go @@ -0,0 +1,234 @@ +package test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// Fake certificates / private keys for test + +// CA generated with: +// openssl req -newkey rsa:2048 -nodes -days 3650 -x509 -keyout ca.key -out ca.crt -subj "/CN=*" +const caCert string = `-----BEGIN CERTIFICATE----- +MIIC+TCCAeGgAwIBAgIULGvHF3aRgJryhvb/9lQMR8TPpIYwDQYJKoZIhvcNAQEL +BQAwDDEKMAgGA1UEAwwBKjAeFw0yMzA4MDIwODUzMTdaFw0zMzA3MzAwODUzMTda +MAwxCjAIBgNVBAMMASowggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK +NcqKT0leAUzpkmp0x7PYGXvGSXviN7zbo415he1mIYWvuGBhB2J3aUlafABJ2wxD +tdjXFDUI2T9BjRDrbsha4LzhzeBFc3xorlp/KDVZnhgbbHeCL8bfgQrfjsFzAXNa +QEdoTwBRs8fznXVzQ7ecWhobyT9M84v2Mlh93YQFEueiHx0Z8jFUESn6vcOXWXqF +8VZWlPPsRauy79zFkCmr09UKxyOWGtImM+9Sgvda7oZGkJBZ1gvhBULOG72ekhsH +RtlT4Xmf4irINm4vRnZcFRJgwaOsCvX/9gyDCfoJ0ioUZ5ZmhYNGJeNSi63LnAZm +1Zsa4ZOGvtdsdAgaZN1jAgMBAAGjUzBRMB0GA1UdDgQWBBQ34SoDX/LC+i2h57cI +aOGmGZTgBzAfBgNVHSMEGDAWgBQ34SoDX/LC+i2h57cIaOGmGZTgBzAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBfFSuPYJK0Gt8psgARSHLJUzSB +X9XmcIYpeFIZk5GqmGnj/0op4w3R/T/TwYTf7+FvqGIKaMyXSgeJJu1uC1M/AI11 +nQmv9XtLmX2BJtKWORoBOPYKnoGSaljoQJZzJJ09lzasHLy68cYezbqb+3+EIGEa +vBKdFgbDyYQpSIs3oAIW9drcEywFf8s5ZSewPhaz1byDlvTHJjKNGoWwm/tlXhv/ +GXHWiYftbJRGHDiA9BqZT2g/vMz/1e9k5wSek+fqaBQNS7nEijUz+Qk0LlmagZV3 +kom8Fkz5HTYkmZVzXPW8spFEuIibCgRK1qA1RuDsyNxMnk3c1jcR8B5AJ/VI +-----END CERTIFICATE-----` + +// CA generated with: +// openssl req -newkey rsa:2048 -nodes -days 3650 -x509 -keyout ca.key -out ca.crt -subj "/CN=*" +const clientCA string = `-----BEGIN CERTIFICATE----- +MIIC+TCCAeGgAwIBAgIUCStrU+idWDMcp01lWVF2cQhp0wIwDQYJKoZIhvcNAQEL +BQAwDDEKMAgGA1UEAwwBKjAeFw0yNTEyMDUxNTM1MjlaFw0zNTEyMDMxNTM1Mjla +MAwxCjAIBgNVBAMMASowggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0 +ldnq3a7RXZ3tyO5OmSP2Q+z1vE/IyqSPZtGopxf8vupcklClWjrJs/rojVWMjAgE +YH4EPNOQgWhZjJQaaWaaP9HuENwkkflneQ39zSKfU6hU3jV/gYjED5QNCTsbrV1J +m/y6FzdGumwCvoztXTcg6exRWkifrJkcO9Fg0CpQ3hFuJw81G18W9yFPvHAf1XPJ +8Lglbg4zFcMOMBp8Ob5L8UITV7BGaKFVmBQM8/F4sIBYDx6ACktwxpk52pLIJF1z +95dixTZjwBN9meji8hyV/IuJ5UvwwtADhRtLhB+CWcDPuu3fVPVodRoeONSg0q/1 +lfaTT4fkQCz1MaZ4kochAgMBAAGjUzBRMB0GA1UdDgQWBBSdDuiQNR5nJrRm5xNV +QabgI9/xIzAfBgNVHSMEGDAWgBSdDuiQNR5nJrRm5xNVQabgI9/xIzAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvBl5AZIpq1zZDQy9QGc6SznHT +R6bGmFPZRFEtCK00NrQT1n5fHB3dWtkUqLuIcxy6tzRf+yn9rKeqASKcc/adxrMg +Gnp5hIHN94P2LdTyHhwbGTPE+zDZyDSBfhuKmGBWMqXVkjWaxdU9/YwSe+tODvSQ +BDOqYTeCOUBHbP2oESqr66dx8DYqJieaIBiH1XCaRAhk3/DcqRzgdc24Cv+zL6gn +xhdEapxmUK2mGxiOkchn0Qy8xrT5sRZRSpXbLhia1NsK6qieC0INkkvW3lpgB30p +WDFE2110hEj7aOzf4UajFrCJIONiXqbwrANp0ILtannrKuvF40ghH8ZV5+Q5 +-----END CERTIFICATE-----` + +// Client key (for mTLS) generated with: +// openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/C=GB/ST=London/L=London/O=libvault consultants/OU=IT Department/CN=*" +const clientKey string = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8EZEm3cXCZZ65 +UXQC0iV4VA51hM0BcXKiCetNWMvSaeNwANtEQkAQ5SztfVaZkgPpSsZjzYVmFvKc +CyVTxct4vZ7Fn8Vyl2QWNXzzHcssSck7edCMOVWoITgGvPJTyrj8Q0meZ7tJ5IGk +uzdkeEPy4DD22ObGUjgScntJO65kq0xJ+Ku0Mw3s4RfHi+fsDbvRjvuQcu0y+L3t +6sJKsM0vyuWLqQKDN8SUAG3gJFLtNVK5W4H8eG72Y68vgBRXHBCTt2hesOsCAgW3 +djZrDx6vFnj5fsHLEUA6T1iXcl8yL0+ALShtEjBgaOu9RlbFY3Pei7uV2fujjRSk +ihKMbQsZAgMBAAECggEAAQI5HJPA7Ud9P/IzZJZ68/fDchbpwJG6syrJc8s/oJvH +yACBLI8MZ+rKwGVVMxKo6bXodX2TMxZ5a6PVqercKgQeV2IBfZlZRJM53dXxkoW8 +yhBfsXjXQEUZV1PpGtDyCAxWVz6oLv/GQDtu0x+gAav5J0HHjxW+zj6F8cEbsNeG +AL4QjIxO9RYXA+KLIu7Nx6XSsiZFyDJCM31xT5WtS6jzzMJ3wdbTotI4TQzim3au +Exh03aFnksKSmvF+txQncZeHSmJw90T6nEvUjW6dLKI3XKgTVPQVfrDh1h+qLLQA +9tuZEZgRHUCwlCtljYiS67RhBhU9cZ8IqNOpJsFQmQKBgQDj4wvVtmE/ybiDm9j5 +hrw8AR4hAA2OdCmGJgRfKhCJzgd2wg/+sIFYjNadD6ULgadjaiDoaHNKCh6frGAF +PMkFk4U5jwK9QCtls/24xo8HzEFadfJEgv50iTJNAl57Z/chNhhAAaVAZlWqRTN+ +D/FYMozX+bs0r37MSeGiIcSgjQKBgQDTRQJ5DNqKez/Pi8rzJaKM9YJyb6UlmIsy +IzKj7UoLyQCjzK4fo6z42QVPtQzR6J0zkdiruPeFqvrfEW+xdi+0Jy5DRGekUVrm +dX5tbcGPPZYfsu8u96gi43TJTVatQ1pKEN0+8sq8wzsSO0cfZ8nnB+iCaJUbib08 +cEYiL1NPvQKBgQCBZsWrnxptvD/YC8ETP9zXPdM77enEwFVr5V6KIzqs5Z77Yoru +lo98Fs0u9llDxWWlX/g7wEPnAQQOqzUDBFcpoXD/FCP8DtoVsDUcnTNOvD9H/L2L +Bc8zoUw8ymGYNZrw8uSmQ8jwXqu6Of1ZUfg7msi7QwV4j0ay/ijvhbk/aQKBgBeA +I6hHb7/budtiV274jr5TSPFlzd8CuukW1Tk62fO5piKSUAQg9sqviVG2d/iZgXMN +FCb16kKqJEHP9LauyNunSBQfdc/nZM8h3rBZdyBx31MjWkvFLKTE3GbP/YZEabS3 +b4TjCP46UUXT5jNuHh1e2dQ3we5QQgaJDqQa04+ZAoGBALTIJW/uq7y+3Dxq4oLC +mato2oHmf/8TCruwAXljdiUPgH2SCDlH/62oBes/zokS1cLk+N/5n25EiffcyviT +heaRcSqLl5Q3opyt37Jk/nDjB3+P+sCAX4EsRJl3U4M0r4ickKan+g856Tt+AQl0 +NHdueneZkKeVhhtQrGu9WzM3 +-----END PRIVATE KEY-----` + +// Client cert (for mTLS) generated with: +// openssl x509 -req -days 3650 -sha256 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -extfile <(echo subjectAltName = IP:127.0.0.1) +const clientCert string = `-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgIUcOdT2T03jT2H/W78F/usXc4zv78wDQYJKoZIhvcNAQEL +BQAwDDEKMAgGA1UEAwwBKjAeFw0yNTEyMDUxNTM4MDhaFw0zNTEyMDMxNTM4MDha +MHIxCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRv +bjEdMBsGA1UECgwUbGlidmF1bHQgY29uc3VsdGFudHMxFjAUBgNVBAsMDUlUIERl +cGFydG1lbnQxCjAIBgNVBAMMASowggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC8EZEm3cXCZZ65UXQC0iV4VA51hM0BcXKiCetNWMvSaeNwANtEQkAQ5Szt +fVaZkgPpSsZjzYVmFvKcCyVTxct4vZ7Fn8Vyl2QWNXzzHcssSck7edCMOVWoITgG +vPJTyrj8Q0meZ7tJ5IGkuzdkeEPy4DD22ObGUjgScntJO65kq0xJ+Ku0Mw3s4RfH +i+fsDbvRjvuQcu0y+L3t6sJKsM0vyuWLqQKDN8SUAG3gJFLtNVK5W4H8eG72Y68v +gBRXHBCTt2hesOsCAgW3djZrDx6vFnj5fsHLEUA6T1iXcl8yL0+ALShtEjBgaOu9 +RlbFY3Pei7uV2fujjRSkihKMbQsZAgMBAAGjUzBRMA8GA1UdEQQIMAaHBH8AAAEw +HQYDVR0OBBYEFKR8VXncy1LIMzMm8Js5TM75gk+lMB8GA1UdIwQYMBaAFJ0O6JA1 +HmcmtGbnE1VBpuAj3/EjMA0GCSqGSIb3DQEBCwUAA4IBAQCh7v5ZoKQCqZ+Jfo+E +RXM8Yc0bLgwon0VuAsfVhACulRhEh+raloC4tC/+gaH2hhZo1cMsDH0sPw4/2ird +7pblFIRa10hQ5lRuz59+bO5OZwnZpXPYYCso9KfY5I5xoNDC1UC6T22ZwxLFiCxL +W7/IbRLN/BmMGYHwXg2H9LLeb/3n1UJDSNox9bHxM4OxjJw5mZwNg9qy5uSZYOd6 +9e/zHl7dIq6MX618Wbk+oXCpi3RVaoJFeHlBrIdJK/hgbmaDJOgJ+lMEOrHWyjPg +LK2QDUoqRp31VeRk0UvBnwBQfgGYjvvb7qBWtPRwvGSsmhxxdaD4qUHi22athUAF +HqR9 +-----END CERTIFICATE-----` + +// Server key generated with: +// openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/C=GB/ST=London/L=London/O=libvault consultants/OU=IT Department/CN=*" +const serverKey string = `-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClYidsRfhbhwxx +GUZ7X7/CwltfZCof/FmrYqzsWzimr8tUWPy/AHJHo+A08ixuZMBuCn2Uh22gFaEZ +Kn3Uk8ZmGXb9IhYZxTJrYx9+ZUvBaN5g8072bbF2TCCDYhX1w9o488mSVe3vP/I/ +tINuOy990fr1BkhzSoFD/xVhQMqoDN00+B8mIWJ9pstknDcAM+6Z+FfUuwzLcxx5 +LogpOIGhYt+03ywXDxfuvXfRFXFGCwifcLx8XyiOchBckxLtfXioC4Q61gWE63VN +J7qyPDrdUZ/CCY5Vz+qRqsF2rlwOWPluXrWzaKzlvmCm5iQ8J58c6dHhyY8Yob8K +xHo9F56bAgMBAAECggEARgN6Hg+3FwRio4SoPnWoCErghM8yOC1MRs5013C82HAm +m5Q6l5+YQbTiJ3f4kFmNz2gYhucYZUOS2kUPVQ2kWbfhFEO4aHt/n0+s1wUKH5yG +PDP000VX8fVDdGtzUYJy4VZvmMhQ/M6s/wQr+eALeHALFmztAgXiGIemJPBZeu+c +rbAFowzdYsHpTSrh2nm0HYSoyZr87wQoQjLo0c6FqLLxYcnCvMemcg8HK4iw6I0a +vbbG61Pdg4CNy6ZgSS96+WwiixrcWoQ7CN339giRmFT2e6vulbreQtD4QzlmqtWW +su+Du3kc/9XqtJwOpyOkHyXrALMnpPC+c0WnVp76xQKBgQDa/kd79kOWzQXTKmqn +heuaKq/UTMYlGhc13KhEpXeX/r0skWMKjuvxgVacNxv4F4d2axCLmADh1UotZsDz +9Hlvq5aPn8f/VYv0CCwOrigSyv25AEAZCnk84igbiSq1TfXppJzyqYoLue2HrW4k +CHL9KviBwauePNRDv+KNZJGALQKBgQDBVLHMDF6xjsy27T4toqLz1thdiBjd/GR+ +SE6+j18z/CKJVCWlRBykmJ3vxW9qAptdb86ZxmfIFnkWJTt/yEthvyfqHe6ksXED +SElOKuReUYAPJazaGS5F9TywGnsVxhwn9BtqPMy/b7WRQJowcG9zSlntjT+cn7sg +GfSB6ASO5wKBgDaiRXc5ovcWQyPBa0ZL9NFLYP5YAP70mWHIoPovRbzXwp5BzzGt +IlPn7pGedg3Y4OS8JS6OR3oP2ielgPHbxggECNXgCOc8kmPZPhSTgk/d8Jqc42Db +6g80ZMkp2UvOHVGizb0Eavot8oJs1BONQBLFC6ZjiMs7ZcFZN84KjvopAoGAYPAz +ummVbZh5o1tv6vf6lyNqF/Pu7Bfq17sv6LMA/JL3Sj6sJaLybcGsp5Yq2E/4UTCH +umlWjmheTLFclST8T0XHIMfjaici0I+FWjF9kqFxAadVdYJcxm1CAdc1UmSkp4/p +0yorS+4ab3uiFJm7+GYWk1tYwxMAhAcfp6eL6Y8CgYAMNptHbD/fL0wrXDBVWRYP +LL55dHKf6Op+c2N9/82WsaMpHQ/A8MiIn2iwQReEIjl0zk+OfvTgQEz1FEw+zZk/ +kFWRLF8Hel5mv0JKz8ExZ4LHTTE8OpxvEbvP3j6A/MngpsuN73LA4i/GqrktGtiy +UYF0gdXCSiPfvXct5lwU7g== +-----END PRIVATE KEY-----` + +// Server cert generated with: +// openssl x509 -req -days 3650 -sha256 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extfile <(echo subjectAltName = IP:127.0.0.1) +const serverCert string = `-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgIUZGbRxIm/HK0OiVdTzIFWmnPqttUwDQYJKoZIhvcNAQEL +BQAwDDEKMAgGA1UEAwwBKjAeFw0yMzA4MDIwODU0NTlaFw0zMzA3MzAwODU0NTla +MHIxCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRv +bjEdMBsGA1UECgwUbGlidmF1bHQgY29uc3VsdGFudHMxFjAUBgNVBAsMDUlUIERl +cGFydG1lbnQxCjAIBgNVBAMMASowggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQClYidsRfhbhwxxGUZ7X7/CwltfZCof/FmrYqzsWzimr8tUWPy/AHJHo+A0 +8ixuZMBuCn2Uh22gFaEZKn3Uk8ZmGXb9IhYZxTJrYx9+ZUvBaN5g8072bbF2TCCD +YhX1w9o488mSVe3vP/I/tINuOy990fr1BkhzSoFD/xVhQMqoDN00+B8mIWJ9pstk +nDcAM+6Z+FfUuwzLcxx5LogpOIGhYt+03ywXDxfuvXfRFXFGCwifcLx8XyiOchBc +kxLtfXioC4Q61gWE63VNJ7qyPDrdUZ/CCY5Vz+qRqsF2rlwOWPluXrWzaKzlvmCm +5iQ8J58c6dHhyY8Yob8KxHo9F56bAgMBAAGjUzBRMA8GA1UdEQQIMAaHBH8AAAEw +HQYDVR0OBBYEFOdbqIlktmQGpRr9ydKkACwx/OnhMB8GA1UdIwQYMBaAFDfhKgNf +8sL6LaHntwho4aYZlOAHMA0GCSqGSIb3DQEBCwUAA4IBAQAMKfFetofRb9dFInU5 +KpF3+IVwrR53UbUbNF0mnQ7aNRE7YfLPRTOV2Dp5zeOlUiO6FhK1AkCcs1RILzUM +bUwolEbgQRmMV8NPyY+0vkBQDJQYfw3bHm2NCWRKd2A0KI9rX1VpWvY3Z300zmLM +TPgRGwN4oZbQLpbI6iZ+MuaBw9c3xOuVKGI0OQybl7MM49Uk/QAf+Ltb+VD/b+NR +QtOnsqqqb3s8LlqTbYn1zM9FSX2YNRljDkElTVfzhlD2qpMvy8Ep8qrAlFcI8yZ8 +HKUIvMe6pjPWHHGVkKBldRqQIOH5WoUSKjrC8koV+Kqj6PMXKquyZdvdC3bhgj4l +Pnib +-----END CERTIFICATE-----` + +// CreateCACert returns paths to CA cert and the cleanup function to defer +func CreateCACert(t *testing.T) (string, func()) { + name, cleanup, err := DumpToTemp(caCert) + require.NoError(t, err) + return name, cleanup +} + +// CreateClientCerts returns paths to: +// - ca +// - user cert +// - user key +// and the cleanup function to defer +func CreateClientCerts(t *testing.T) (string, string, string, func()) { + ca, cleanupCA, err := DumpToTemp(caCert) + require.NoError(t, err) + uc, cleanupUC, err := DumpToTemp(clientCert) + require.NoError(t, err) + uk, cleanupUK, err := DumpToTemp(clientKey) + require.NoError(t, err) + return ca, uc, uk, func() { + cleanupCA() + cleanupUC() + cleanupUK() + } +} + +// CreateAllCerts returns paths to: +// - user ca +// - user cert +// - user key +// - server ca +// - server cert +// - server key +// and the cleanup function to defer +func CreateAllCerts(t *testing.T) (string, string, string, string, string, string, func()) { + cc, cleanupCC, err := DumpToTemp(clientCA) + require.NoError(t, err) + ca, cleanupCA, err := DumpToTemp(caCert) + require.NoError(t, err) + uc, cleanupUC, err := DumpToTemp(clientCert) + require.NoError(t, err) + uk, cleanupUK, err := DumpToTemp(clientKey) + require.NoError(t, err) + sc, cleanupSC, err := DumpToTemp(serverCert) + require.NoError(t, err) + sk, cleanupSK, err := DumpToTemp(serverKey) + require.NoError(t, err) + return cc, uc, uk, ca, sc, sk, func() { + cleanupCC() + cleanupCA() + cleanupUC() + cleanupUK() + cleanupSC() + cleanupSK() + } +} + +func DumpToTemp(content string) (string, func(), error) { + file, err := os.CreateTemp("", "agent-tests-") + if err != nil { + return "", nil, err + } + err = os.WriteFile(file.Name(), []byte(content), 0644) + if err != nil { + defer os.Remove(file.Name()) + return "", nil, err + } + return file.Name(), func() { + os.Remove(file.Name()) + }, nil +}