Files
threshold/internal/tests/cluster/x509_verification_test.go
T
Jeff MitchellandJohan Brandhorst-Satzkorn fff5f9506f Migrate proxy logic to the API package (#4216)
This allows arbitrary programs to have an easy mechanism to make
connections through Boundary using Go. Internal cluster tests and test
helpers have been migrated to use this package, as well as the `connect`
command.

---------

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2024-01-21 22:34:33 -05:00

291 lines
8.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cluster
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"sync"
"testing"
"time"
apiproxy "github.com/hashicorp/boundary/api/proxy"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/daemon/controller"
tg "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets"
"github.com/hashicorp/boundary/internal/daemon/worker"
"github.com/hashicorp/boundary/internal/event"
"github.com/hashicorp/boundary/internal/tests/helper"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
"nhooyr.io/websocket"
)
func TestCustomX509Verification_Client(t *testing.T) {
req := require.New(t)
ctx := context.Background()
ec := event.TestEventerConfig(t, "TestWorkerReplay", event.TestWithObservationSink(t), event.TestWithSysSink(t))
testLock := &sync.Mutex{}
// This prevents us from running tests in parallel.
tg.SetupSuiteTargetFilters(t)
logger := hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
})
req.NoError(event.InitSysEventer(logger, testLock, "use-TestCustomX509Verification", event.WithEventerConfig(&ec.EventerConfig)))
conf, err := config.DevController()
conf.Eventing = &ec.EventerConfig
req.NoError(err)
c1 := controller.NewTestController(t, &controller.TestControllerOpts{
Config: conf,
InitialResourcesSuffix: "1234567890",
Logger: logger.Named("c1"),
})
conf, err = config.DevWorker()
conf.Eventing = &ec.EventerConfig
req.NoError(err)
w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{
WorkerAuthKms: c1.Config().WorkerAuthKms,
InitialUpstreams: c1.ClusterAddrs(),
Logger: logger.Named("w1"),
})
// Give time for it to connect
time.Sleep(10 * time.Second)
err = w1.Worker().WaitForNextSuccessfulStatusUpdate()
req.NoError(err)
helper.ExpectWorkers(t, c1, w1)
// Connect target
client := c1.Client()
client.SetToken(c1.Token().Token)
tcl := targets.NewClient(client)
tgt, err := tcl.Read(ctx, "ttcp_1234567890")
req.NoError(err)
req.NotNil(tgt)
// Create test server, update default port on target
ts := helper.NewTestTcpServer(t)
require.NotNil(t, ts)
t.Cleanup(ts.Close)
tgt, err = tcl.Update(ctx, tgt.Item.Id, tgt.Item.Version, targets.WithTcpTargetDefaultPort(ts.Port()), targets.WithSessionConnectionLimit(-1))
req.NoError(err)
req.NotNil(tgt)
tests := []struct {
name string
certPool *x509.CertPool
dnsName *bytes.Buffer
wantErrContains string
}{
{
name: "base",
},
{
name: "modified cert pool",
certPool: x509.NewCertPool(),
wantErrContains: "signed by unknown authority",
},
{
name: "modified dns name",
dnsName: bytes.NewBuffer([]byte("foobar")),
wantErrContains: "not foobar",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
sar, err := tcl.AuthorizeSession(ctx, "ttcp_1234567890")
require.NoError(err)
require.NotNil(sar)
sessAuth, err := sar.GetSessionAuthorization()
require.NoError(err)
sessAuthzData, err := sessAuth.GetSessionAuthorizationData()
require.NoError(err)
require.LessOrEqual(1, len(sessAuthzData.WorkerInfo))
tlsConf := apiproxy.TestClientTlsConfig(t, sessAuth.AuthorizationToken, apiproxy.WithWorkerHost(sessAuth.SessionId))
certPool := tc.certPool
if certPool == nil {
parsedCert, err := x509.ParseCertificate(sessAuthzData.Certificate)
require.NoError(err)
certPool = x509.NewCertPool()
certPool.AddCert(parsedCert)
}
dnsName := sessAuth.SessionId
if tc.dnsName != nil {
dnsName = tc.dnsName.String()
}
verifyOpts := x509.VerifyOptions{
DNSName: dnsName,
Roots: certPool,
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
}
tlsConf.VerifyConnection = func(cs tls.ConnectionState) error {
// Go will not run this without at least one peer certificate, but
// doesn't hurt to check
if len(cs.PeerCertificates) == 0 {
return errors.New("no peer certificates provided")
}
_, err := cs.PeerCertificates[0].Verify(verifyOpts)
return err
}
transport := cleanhttp.DefaultTransport()
transport.DisableKeepAlives = false
// This isn't/shouldn't used anyways really because the connection is
// hijacked, just setting for completeness
transport.IdleConnTimeout = 0
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
// time.Sleep(1000 * time.Second)
dialer := &tls.Dialer{Config: tlsConf}
return dialer.DialContext(ctx, network, addr)
}
conn, _, err := websocket.Dial(
ctx,
fmt.Sprintf("ws://%s/v1/proxy", sessAuthzData.WorkerInfo[0].Address),
&websocket.DialOptions{
HTTPClient: &http.Client{
Transport: transport,
},
Subprotocols: []string{globals.TcpProxyV1},
},
)
if conn != nil {
defer conn.Close(websocket.StatusNormalClosure, "done")
}
switch tc.wantErrContains != "" {
case true:
require.Error(err)
require.Contains(err.Error(), tc.wantErrContains)
default:
require.NoError(err)
}
})
}
}
func TestCustomX509Verification_Server(t *testing.T) {
ec := event.TestEventerConfig(t, "TestCustomX509Verification_Server", event.TestWithObservationSink(t), event.TestWithSysSink(t))
testLock := &sync.Mutex{}
logger := hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
})
require.NoError(t, event.InitSysEventer(logger, testLock, "use-TestCustomX509Verification_Server", event.WithEventerConfig(&ec.EventerConfig)))
t.Run("bad cert pool", testCustomX509Verification_Server(ec, x509.NewCertPool(), "", "bad certificate"))
t.Run("bad dns name", testCustomX509Verification_Server(ec, nil, "foobar", "bad certificate"))
}
func testCustomX509Verification_Server(ec event.TestConfig, certPool *x509.CertPool, dnsName, wantErrContains string) func(t *testing.T) {
return func(t *testing.T) {
req := require.New(t)
ctx := context.Background()
// This prevents us from running tests in parallel.
tg.SetupSuiteTargetFilters(t)
conf, err := config.DevController()
conf.Eventing = &ec.EventerConfig
req.NoError(err)
c1 := controller.NewTestController(t, &controller.TestControllerOpts{
Config: conf,
InitialResourcesSuffix: "1234567890",
})
conf, err = config.DevWorker()
conf.Eventing = &ec.EventerConfig
req.NoError(err)
w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{
WorkerAuthKms: c1.Config().WorkerAuthKms,
InitialUpstreams: c1.ClusterAddrs(),
})
w1.Worker().TestOverrideX509VerifyCertPool = certPool
w1.Worker().TestOverrideX509VerifyDnsName = dnsName
// Give time for it to connect
time.Sleep(10 * time.Second)
err = w1.Worker().WaitForNextSuccessfulStatusUpdate()
req.NoError(err)
// Connect target
client := c1.Client()
client.SetToken(c1.Token().Token)
tcl := targets.NewClient(client)
tgt, err := tcl.Read(ctx, "ttcp_1234567890")
req.NoError(err)
req.NotNil(tgt)
// Create test server, update default port on target
ts := helper.NewTestTcpServer(t)
require.NotNil(t, ts)
t.Cleanup(ts.Close)
tgt, err = tcl.Update(ctx, tgt.Item.Id, tgt.Item.Version, targets.WithTcpTargetDefaultPort(ts.Port()), targets.WithSessionConnectionLimit(-1))
req.NoError(err)
req.NotNil(tgt)
sar, err := tcl.AuthorizeSession(ctx, "ttcp_1234567890")
req.NoError(err)
req.NotNil(sar)
sessAuth, err := sar.GetSessionAuthorization()
req.NoError(err)
sessAuthzData, err := sessAuth.GetSessionAuthorizationData()
req.NoError(err)
req.LessOrEqual(1, len(sessAuthzData.WorkerInfo))
tlsConf := apiproxy.TestClientTlsConfig(t, sessAuth.AuthorizationToken, apiproxy.WithWorkerHost(sessAuth.SessionId))
transport := cleanhttp.DefaultTransport()
transport.DisableKeepAlives = false
// This isn't/shouldn't used anyways really because the connection is
// hijacked, just setting for completeness
transport.IdleConnTimeout = 0
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
// time.Sleep(1000 * time.Second)
dialer := &tls.Dialer{Config: tlsConf}
return dialer.DialContext(ctx, network, addr)
}
conn, _, err := websocket.Dial(
ctx,
fmt.Sprintf("wss://%s/v1/proxy", sessAuthzData.WorkerInfo[0].Address),
&websocket.DialOptions{
HTTPClient: &http.Client{
Transport: transport,
},
Subprotocols: []string{globals.TcpProxyV1},
},
)
if conn != nil {
defer conn.Close(websocket.StatusNormalClosure, "done")
}
switch wantErrContains != "" {
case true:
req.Error(err)
req.Contains(err.Error(), wantErrContains)
default:
req.NoError(err)
}
}
}