mirror of https://github.com/hashicorp/boundary
Use custom X.509 verification logic for proxy connections (#2008)
This reduces the chance that we'll hit a failure due to a mismatched worker address, on the certificate, especially if a worker has multiple addresses. We aren't relying on IP/DNS for security anyways, so let's not have it be a hindrance.pull/2021/head
parent
631de192d3
commit
bf9e7874b1
@ -0,0 +1,69 @@
|
||||
package connect
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
targetspb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets"
|
||||
)
|
||||
|
||||
// ClientTlsConfig creates a TLS configuration from the session authorization
|
||||
// data and host
|
||||
func ClientTlsConfig(sessionAuthzData *targetspb.SessionAuthorizationData, host string) (*tls.Config, error) {
|
||||
const op = "connect.ClientTlsConfig"
|
||||
if sessionAuthzData == nil {
|
||||
return nil, fmt.Errorf("%s: nil session authorization data", op)
|
||||
}
|
||||
parsedCert, err := x509.ParseCertificate(sessionAuthzData.Certificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode mTLS certificate: %w", err)
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AddCert(parsedCert)
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
Certificates: []tls.Certificate{
|
||||
{
|
||||
Certificate: [][]byte{sessionAuthzData.Certificate},
|
||||
PrivateKey: ed25519.PrivateKey(sessionAuthzData.PrivateKey),
|
||||
Leaf: parsedCert,
|
||||
},
|
||||
},
|
||||
ServerName: host,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
NextProtos: []string{"http/1.1", sessionAuthzData.SessionId},
|
||||
|
||||
// This is set this way so we can make use of VerifyConnection, which we
|
||||
// set on this TLS config below. We are not skipping verification!
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
if host == "" {
|
||||
tlsConf.ServerName = parsedCert.DNSNames[0]
|
||||
}
|
||||
|
||||
// We disable normal DNS SAN behavior as we don't rely on DNS or IP
|
||||
// addresses for security and want to avoid issues with including localhost
|
||||
// etc.
|
||||
verifyOpts := x509.VerifyOptions{
|
||||
DNSName: sessionAuthzData.SessionId,
|
||||
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 fmt.Errorf("%s: no peer certificates provided", op)
|
||||
}
|
||||
_, err := cs.PeerCertificates[0].Verify(verifyOpts)
|
||||
return err
|
||||
}
|
||||
|
||||
return tlsConf, nil
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/cmd/config"
|
||||
"github.com/hashicorp/boundary/internal/observability/event"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller"
|
||||
"github.com/hashicorp/boundary/internal/servers/worker"
|
||||
"github.com/hashicorp/boundary/internal/tests/helper"
|
||||
"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{}
|
||||
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"),
|
||||
})
|
||||
t.Cleanup(c1.Shutdown)
|
||||
|
||||
conf, err = config.DevWorker()
|
||||
conf.Eventing = &ec.EventerConfig
|
||||
req.NoError(err)
|
||||
w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{
|
||||
WorkerAuthKms: c1.Config().WorkerAuthKms,
|
||||
InitialControllers: c1.ClusterAddrs(),
|
||||
Logger: logger.Named("w1"),
|
||||
})
|
||||
t.Cleanup(w1.Shutdown)
|
||||
|
||||
// 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)
|
||||
|
||||
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)
|
||||
sess := helper.NewTestSession(ctx, t, tcl, "ttcp_1234567890")
|
||||
|
||||
certPool := tc.certPool
|
||||
if certPool == nil {
|
||||
parsedCert, err := x509.ParseCertificate(sess.SessionAuthzData.Certificate)
|
||||
require.NoError(err)
|
||||
certPool = x509.NewCertPool()
|
||||
certPool.AddCert(parsedCert)
|
||||
}
|
||||
|
||||
dnsName := sess.SessionId
|
||||
if tc.dnsName != nil {
|
||||
dnsName = tc.dnsName.String()
|
||||
}
|
||||
|
||||
verifyOpts := x509.VerifyOptions{
|
||||
DNSName: dnsName,
|
||||
Roots: certPool,
|
||||
KeyUsages: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
},
|
||||
}
|
||||
sess.Transport.TLSClientConfig.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
|
||||
}
|
||||
conn, _, err := websocket.Dial(
|
||||
ctx,
|
||||
fmt.Sprintf("wss://%s/v1/proxy", sess.WorkerAddr),
|
||||
&websocket.DialOptions{
|
||||
HTTPClient: &http.Client{
|
||||
Transport: sess.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) {
|
||||
t.Parallel()
|
||||
req := require.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
conf, err := config.DevController()
|
||||
conf.Eventing = &ec.EventerConfig
|
||||
req.NoError(err)
|
||||
c1 := controller.NewTestController(t, &controller.TestControllerOpts{
|
||||
Config: conf,
|
||||
InitialResourcesSuffix: "1234567890",
|
||||
})
|
||||
t.Cleanup(c1.Shutdown)
|
||||
|
||||
conf, err = config.DevWorker()
|
||||
conf.Eventing = &ec.EventerConfig
|
||||
req.NoError(err)
|
||||
w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{
|
||||
WorkerAuthKms: c1.Config().WorkerAuthKms,
|
||||
InitialControllers: c1.ClusterAddrs(),
|
||||
})
|
||||
w1.Worker().TestOverrideX509VerifyCertPool = certPool
|
||||
w1.Worker().TestOverrideX509VerifyDnsName = dnsName
|
||||
t.Cleanup(w1.Shutdown)
|
||||
|
||||
// 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)
|
||||
|
||||
sess := helper.NewTestSession(ctx, t, tcl, "ttcp_1234567890")
|
||||
|
||||
conn, _, err := websocket.Dial(
|
||||
ctx,
|
||||
fmt.Sprintf("wss://%s/v1/proxy", sess.WorkerAddr),
|
||||
&websocket.DialOptions{
|
||||
HTTPClient: &http.Client{
|
||||
Transport: sess.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue