Initial BYOW auth PoC (#29)
This commit is contained in:
1 parent
8ed560fc3e
commit
f748af7b02
21 files changed
+707
-404
No files matched your search
@@ -36,6 +36,9 @@ import (
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-secure-stdlib/mlock"
|
||||
"github.com/hashicorp/go-secure-stdlib/pluginutil/v2"
|
||||
"github.com/hashicorp/nodeenrollment/noderegistration"
|
||||
nodeefile "github.com/hashicorp/nodeenrollment/nodestorage/file"
|
||||
"github.com/hashicorp/nodeenrollment/nodetypes"
|
||||
ua "go.uber.org/atomic"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@@ -86,6 +89,9 @@ type Controller struct {
|
||||
// Used to signal the Health Service to start
|
||||
// replying to queries with "503 Service Unavailable".
|
||||
HealthService *health.Service
|
||||
|
||||
// PoC: Testing bits for BYOW
|
||||
NodeeFileStorage *nodeefile.FileStorage
|
||||
}
|
||||
|
||||
func New(ctx context.Context, conf *Config) (*Controller, error) {
|
||||
@@ -299,6 +305,7 @@ func New(ctx context.Context, conf *Config) (*Controller, error) {
|
||||
c.ConnectionRepoFn = func() (*session.ConnectionRepository, error) {
|
||||
return session.NewConnectionRepository(ctx, dbase, dbase, c.kms)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -309,6 +316,17 @@ func (c *Controller) Start() error {
|
||||
return nil
|
||||
}
|
||||
c.baseContext, c.baseCancel = context.WithCancel(context.Background())
|
||||
|
||||
var err error
|
||||
c.NodeeFileStorage, err = nodeefile.NewFileStorage(c.baseContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = nodetypes.RotateRootCertificates(c.baseContext, c.NodeeFileStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.registerJobs(); err != nil {
|
||||
return fmt.Errorf("error registering jobs: %w", err)
|
||||
}
|
||||
@@ -391,3 +409,7 @@ func (c *Controller) Shutdown() error {
|
||||
func (c *Controller) WorkerStatusUpdateTimes() *sync.Map {
|
||||
return c.workerStatusUpdateTimes
|
||||
}
|
||||
|
||||
func (c *Controller) AuthorizeNodeeWorker(keyId string) error {
|
||||
return noderegistration.AuthorizeNode(c.baseContext, c.NodeeFileStorage, keyId)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
@@ -42,6 +43,7 @@ import (
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/go-secure-stdlib/listenerutil"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"github.com/hashicorp/nodeenrollment/nodetypes"
|
||||
"github.com/mr-tron/base58"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -65,6 +67,7 @@ func (c *Controller) apiHandler(props HandlerProperties) (http.Handler, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mux.Handle("/v1/nodes", handleNodes(c))
|
||||
mux.Handle("/v1/", grpcGwMux)
|
||||
mux.Handle("/", handleUi(c))
|
||||
|
||||
@@ -561,6 +564,68 @@ func wrapHandlerWithCallbackInterceptor(h http.Handler, c *Controller) http.Hand
|
||||
})
|
||||
}
|
||||
|
||||
func handleNodes(c *Controller) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
type vals struct {
|
||||
WaitingNodes []string `json:"waiting_nodes"`
|
||||
}
|
||||
var err error
|
||||
var currVals vals
|
||||
currVals.WaitingNodes, err = c.NodeeFileStorage.List(c.baseContext, (*nodetypes.NodeInformation)(nil))
|
||||
if err != nil {
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
ret, err := json.Marshal(currVals)
|
||||
if err != nil {
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
_, err = w.Write(ret)
|
||||
if err != nil {
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
|
||||
case http.MethodPost:
|
||||
body, err := io.ReadAll(req.Body)
|
||||
req.Body.Close()
|
||||
if err != nil {
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
type val struct {
|
||||
KeyId string `json:"key_id"`
|
||||
}
|
||||
var currVal val
|
||||
if err := json.Unmarshal(body, &currVal); err != nil {
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
if err := c.AuthorizeNodeeWorker(currVal.KeyId); err != nil {
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(204)
|
||||
return
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
func WrapForwardedForHandler(h http.Handler, authorizedAddrs []*sockaddr.SockAddrMarshaler, rejectNotPresent, rejectNonAuthz bool, hopSkips int) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package workers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
pbs "github.com/hashicorp/nodeenrollment/multihop"
|
||||
"github.com/hashicorp/nodeenrollment/nodeauth"
|
||||
"github.com/hashicorp/nodeenrollment/noderegistration"
|
||||
"github.com/hashicorp/nodeenrollment/nodetypes"
|
||||
)
|
||||
|
||||
type multihopServiceServer struct {
|
||||
pbs.UnimplementedMultihopServiceServer
|
||||
|
||||
currentParams nodeauth.CurrentParameterFactory
|
||||
}
|
||||
|
||||
func NewMultihopServiceServer(
|
||||
currentParams nodeauth.CurrentParameterFactory,
|
||||
) *multihopServiceServer {
|
||||
return &multihopServiceServer{
|
||||
currentParams: currentParams,
|
||||
}
|
||||
}
|
||||
|
||||
var _ pbs.MultihopServiceServer = (*multihopServiceServer)(nil)
|
||||
|
||||
func (m *multihopServiceServer) FetchNodeCredentials(ctx context.Context, req *nodetypes.FetchNodeCredentialsRequest) (*nodetypes.FetchNodeCredentialsResponse, error) {
|
||||
const op = "workers.(multihopServiceServer).FetchNodeCredentials"
|
||||
_, storage, opt, err := m.currentParams()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: error getting current parameters: %w", op, err)
|
||||
}
|
||||
return noderegistration.FetchNodeCredentials(ctx, storage, req, opt...)
|
||||
}
|
||||
|
||||
func (m *multihopServiceServer) GenerateServerCertificates(ctx context.Context, req *nodetypes.GenerateServerCertificatesRequest) (*nodetypes.GenerateServerCertificatesResponse, error) {
|
||||
const op = "workers.(multihopServiceServer).GenerateServerCertificates"
|
||||
_, storage, opt, err := m.currentParams()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: error getting current parameters: %w", op, err)
|
||||
}
|
||||
return noderegistration.GenerateServerCertificates(ctx, storage, req, opt...)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/observability/event"
|
||||
nodee "github.com/hashicorp/nodeenrollment"
|
||||
"github.com/hashicorp/nodeenrollment/nodeauth"
|
||||
)
|
||||
|
||||
// interceptingListener allows us to validate the nonce from a connection before
|
||||
@@ -45,42 +48,54 @@ func newInterceptingListener(c *Controller, baseLn net.Listener) *interceptingLi
|
||||
|
||||
func (m *interceptingListener) Accept() (net.Conn, error) {
|
||||
const op = "controller.(interceptingListener).Accept"
|
||||
ctx := context.TODO()
|
||||
ctx := m.c.baseContext
|
||||
conn, err := m.baseLn.Accept()
|
||||
if err != nil {
|
||||
// We may already be closed but can't be sure, so just try anyways
|
||||
if conn != nil {
|
||||
if err := conn.Close(); err != nil {
|
||||
event.WriteError(context.TODO(), op, err, event.WithInfoMsg("error closing worker connection"))
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection"))
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, 20)
|
||||
read, err := conn.Read(nonce)
|
||||
if err != nil {
|
||||
if err := conn.Close(); err != nil {
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection"))
|
||||
tlsConn := conn.(*tls.Conn)
|
||||
switch {
|
||||
case nodeauth.ContainsNodeAuthAlpnProto(tlsConn.ConnectionState().NegotiatedProtocol):
|
||||
event.WriteSysEvent(ctx, op, "worker successfully authed", "key_id", nodee.KeyIdFromPkix(tlsConn.ConnectionState().PeerCertificates[0].SubjectKeyId))
|
||||
return conn, nil
|
||||
|
||||
case strings.HasPrefix(tlsConn.ConnectionState().NegotiatedProtocol, "v1workerauth"):
|
||||
nonce := make([]byte, 20)
|
||||
read, err := conn.Read(nonce)
|
||||
if err != nil {
|
||||
if err := conn.Close(); err != nil {
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection"))
|
||||
}
|
||||
return nil, fmt.Errorf("error reading nonce from connection: %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("error reading nonce from connection: %w", err)
|
||||
}
|
||||
if read != len(nonce) {
|
||||
if err := conn.Close(); err != nil {
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection"))
|
||||
if read != len(nonce) {
|
||||
if err := conn.Close(); err != nil {
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection"))
|
||||
}
|
||||
return nil, fmt.Errorf("error reading nonce from worker, expected %d bytes, got %d", 20, read)
|
||||
}
|
||||
return nil, fmt.Errorf("error reading nonce from worker, expected %d bytes, got %d", 20, read)
|
||||
}
|
||||
workerInfoRaw, found := m.c.workerAuthCache.Load(string(nonce))
|
||||
if !found {
|
||||
if err := conn.Close(); err != nil {
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection"))
|
||||
workerInfoRaw, found := m.c.workerAuthCache.Load(string(nonce))
|
||||
if !found {
|
||||
if err := conn.Close(); err != nil {
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing worker connection"))
|
||||
}
|
||||
return nil, errors.New("did not find valid nonce for incoming worker")
|
||||
}
|
||||
return nil, errors.New("did not find valid nonce for incoming worker")
|
||||
workerInfo := workerInfoRaw.(*workerAuthEntry)
|
||||
workerInfo.conn = tlsConn
|
||||
event.WriteSysEvent(ctx, op, "worker successfully authed", "name", workerInfo.Name)
|
||||
return tlsConn, nil
|
||||
|
||||
default:
|
||||
return nil, nodeauth.NewTempError(errors.New("unable to authenticate incoming connection"))
|
||||
}
|
||||
workerInfo := workerInfoRaw.(*workerAuthEntry)
|
||||
workerInfo.conn = conn
|
||||
event.WriteSysEvent(ctx, op, "worker successfully authed", "name", workerInfo.Name)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (m *interceptingListener) Close() error {
|
||||
|
||||
@@ -17,6 +17,9 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/handlers/workers"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/internal/metric"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
nodee "github.com/hashicorp/nodeenrollment"
|
||||
"github.com/hashicorp/nodeenrollment/multihop"
|
||||
"github.com/hashicorp/nodeenrollment/nodeauth"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -103,9 +106,21 @@ func (c *Controller) configureForApi(ln *base.ServerListener) ([]func(), error)
|
||||
}
|
||||
|
||||
func (c *Controller) configureForCluster(ln *base.ServerListener) (func(), error) {
|
||||
l := tls.NewListener(ln.ClusterListener, &tls.Config{
|
||||
GetConfigForClient: c.validateWorkerTls,
|
||||
})
|
||||
l, err := nodeauth.NewInterceptingListener(
|
||||
ln.ClusterListener,
|
||||
&tls.Config{
|
||||
GetConfigForClient: c.validateWorkerTls,
|
||||
},
|
||||
nodeauth.MakeCurrentParametersFactory(
|
||||
c.baseContext,
|
||||
nodee.NopTransactionStorage(c.NodeeFileStorage),
|
||||
),
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error instantiating node auth listener: %w", err)
|
||||
}
|
||||
|
||||
workerReqInterceptor, err := workerRequestInfoInterceptor(c.baseContext, c.conf.Eventer)
|
||||
if err != nil {
|
||||
@@ -129,6 +144,12 @@ func (c *Controller) configureForCluster(ln *base.ServerListener) (func(), error
|
||||
c.workerStatusUpdateTimes, c.kms)
|
||||
pbs.RegisterServerCoordinationServiceServer(workerServer, workerService)
|
||||
pbs.RegisterSessionServiceServer(workerServer, workerService)
|
||||
|
||||
multihopService := workers.NewMultihopServiceServer(
|
||||
nodeauth.MakeCurrentParametersFactory(c.baseContext, nodee.NopTransactionStorage(c.NodeeFileStorage)),
|
||||
)
|
||||
multihop.RegisterMultihopServiceServer(workerServer, multihopService)
|
||||
|
||||
metric.InitializeClusterCollectors(c.conf.PrometheusRegisterer, workerServer)
|
||||
|
||||
ln.GrpcServer = workerServer
|
||||
|
||||
@@ -27,6 +27,7 @@ type workerAuthEntry struct {
|
||||
// been replayed, then stores connection information into the auth cache, which
|
||||
// is used by the intercepting listener to log information.
|
||||
func (c Controller) validateWorkerTls(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
const op = "controller.(Controller).validateWorkerTls"
|
||||
for _, p := range hello.SupportedProtos {
|
||||
switch {
|
||||
case strings.HasPrefix(p, "v1workerauth-"):
|
||||
@@ -50,7 +51,7 @@ func (c Controller) validateWorkerTls(hello *tls.ClientHelloInfo) (*tls.Config,
|
||||
return tlsConf, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
return nil, errors.New("no supported protos found in hello")
|
||||
}
|
||||
|
||||
// v1WorkerAuthConfig:
|
||||
|
||||
Reference in new issue
Block a user