Skip to content

Commit

Permalink
feat: allow access to all resources over siderolink in maintenance mode
Browse files Browse the repository at this point in the history
SideroLink is a secure channel, so we can allow read access to the resources. This will give us more control of the node via Omni and/or other systems using SideroLink.

Signed-off-by: Utku Ozdemir <[email protected]>
  • Loading branch information
utkuozdemir committed Feb 16, 2024
1 parent 5372188 commit 0b7a27e
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 64 deletions.
1 change: 0 additions & 1 deletion hack/docgen/main.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import (
)

// MaintenanceServiceController runs the maintenance service based on the configuration.
type MaintenanceServiceController struct{}
type MaintenanceServiceController struct {
SiderolinkPeerCheckFunc authz.SideroLinkPeerCheckFunc
}

// Name implements controller.Controller interface.
func (ctrl *MaintenanceServiceController) Name() string {
Expand Down Expand Up @@ -117,7 +119,8 @@ func (ctrl *MaintenanceServiceController) Run(ctx context.Context, r controller.
srv := maintenance.New(cfgCh)

injector := &authz.Injector{
Mode: authz.ReadOnly,
Mode: authz.ReadOnlyWithAdminOnSiderolink,
SideroLinkPeerCheckFunc: ctrl.SiderolinkPeerCheckFunc,
}

if debug.Enabled {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/siderolabs/go-retry/retry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc/metadata"

"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
runtimectrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/runtime"
Expand All @@ -32,6 +33,8 @@ import (
"github.com/siderolabs/talos/pkg/machinery/resources/runtime"
)

const isSiderolinkPeerHeaderKey = "is-siderolink-peer"

func TestMaintenanceServiceSuite(t *testing.T) {
suite.Run(t, &MaintenanceServiceSuite{
DefaultSuite: ctest.DefaultSuite{
Expand All @@ -42,7 +45,16 @@ func TestMaintenanceServiceSuite(t *testing.T) {
suite.Require().NoError(suite.Runtime().RegisterController(&secrets.MaintenanceRootController{}))
suite.Require().NoError(suite.Runtime().RegisterController(&secrets.MaintenanceCertSANsController{}))
suite.Require().NoError(suite.Runtime().RegisterController(&secrets.MaintenanceController{}))
suite.Require().NoError(suite.Runtime().RegisterController(&runtimectrl.MaintenanceServiceController{}))
suite.Require().NoError(suite.Runtime().RegisterController(&runtimectrl.MaintenanceServiceController{
SiderolinkPeerCheckFunc: func(ctx context.Context) (netip.Addr, bool) {
isSiderolinkPeer := len(metadata.ValueFromIncomingContext(ctx, isSiderolinkPeerHeaderKey)) > 0
if isSiderolinkPeer {
return netip.MustParseAddr("127.0.0.42"), true
}

return netip.Addr{}, false
},
}))
},
},
})
Expand Down Expand Up @@ -130,6 +142,19 @@ func (suite *MaintenanceServiceSuite) TestRunService() {
_, err = net.Dial("tcp", oldListenAddress)
suite.Require().ErrorContains(err, "connection refused")

// test the API again over SideroLink - the Admin role must be injected to the call
mc, err = client.New(suite.Ctx(),
client.WithTLSConfig(&tls.Config{
InsecureSkipVerify: true,
}), client.WithEndpoints(maintenanceConfig.TypedSpec().ListenAddress),
)
suite.Require().NoError(err)

siderolinkCtx := metadata.AppendToOutgoingContext(suite.Ctx(), isSiderolinkPeerHeaderKey, "yep")

_, err = mc.Version(siderolinkCtx)
suite.Require().NoError(err)

// teardown the maintenance service
_, err = suite.State().Teardown(suite.Ctx(), maintenanceRequest.Metadata())
suite.Require().NoError(err)
Expand Down
50 changes: 0 additions & 50 deletions internal/app/maintenance/peer.go

This file was deleted.

26 changes: 18 additions & 8 deletions internal/app/maintenance/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ import (
"github.com/siderolabs/talos/internal/app/resources"
storaged "github.com/siderolabs/talos/internal/app/storaged"
"github.com/siderolabs/talos/internal/pkg/configuration"
"github.com/siderolabs/talos/pkg/grpc/middleware/authz"
"github.com/siderolabs/talos/pkg/machinery/api/machine"
"github.com/siderolabs/talos/pkg/machinery/api/storage"
"github.com/siderolabs/talos/pkg/machinery/config"
"github.com/siderolabs/talos/pkg/machinery/config/configloader"
v1alpha1machine "github.com/siderolabs/talos/pkg/machinery/config/machine"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/role"
"github.com/siderolabs/talos/pkg/version"
)

Expand Down Expand Up @@ -71,7 +73,7 @@ func (s *Server) Register(obj *grpc.Server) {
}

// ApplyConfiguration implements [machine.MachineServiceServer].
func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfigurationRequest) (*machine.ApplyConfigurationResponse, error) {
func (s *Server) ApplyConfiguration(_ context.Context, in *machine.ApplyConfigurationRequest) (*machine.ApplyConfigurationResponse, error) {
//nolint:exhaustive
switch in.Mode {
case machine.ApplyConfigurationRequest_TRY:
Expand Down Expand Up @@ -130,13 +132,13 @@ func (s *Server) GenerateConfiguration(ctx context.Context, in *machine.Generate
}

// GenerateClientConfiguration implements the [machine.MachineServiceServer] interface.
func (s *Server) GenerateClientConfiguration(ctx context.Context, in *machine.GenerateClientConfigurationRequest) (*machine.GenerateClientConfigurationResponse, error) {
func (s *Server) GenerateClientConfiguration(context.Context, *machine.GenerateClientConfigurationRequest) (*machine.GenerateClientConfigurationResponse, error) {
return nil, status.Error(codes.Unimplemented, "client configuration (talosconfig) can't be generated in the maintenance mode")
}

// Version implements the machine.MachineServer interface.
func (s *Server) Version(ctx context.Context, in *emptypb.Empty) (*machine.VersionResponse, error) {
if err := assertPeerSideroLink(ctx); err != nil {
func (s *Server) Version(ctx context.Context, _ *emptypb.Empty) (*machine.VersionResponse, error) {
if err := s.assertAdminRole(ctx); err != nil {
return nil, err
}

Expand All @@ -161,7 +163,7 @@ func (s *Server) Version(ctx context.Context, in *emptypb.Empty) (*machine.Versi

// Upgrade initiates an upgrade.
func (s *Server) Upgrade(ctx context.Context, in *machine.UpgradeRequest) (reply *machine.UpgradeResponse, err error) {
if err = assertPeerSideroLink(ctx); err != nil {
if err = s.assertAdminRole(ctx); err != nil {
return nil, err
}

Expand Down Expand Up @@ -210,7 +212,7 @@ func (s *Server) Upgrade(ctx context.Context, in *machine.UpgradeRequest) (reply
//
//nolint:gocyclo
func (s *Server) Reset(ctx context.Context, in *machine.ResetRequest) (reply *machine.ResetResponse, err error) {
if err = assertPeerSideroLink(ctx); err != nil {
if err = s.assertAdminRole(ctx); err != nil {
return nil, err
}

Expand Down Expand Up @@ -293,7 +295,7 @@ func (s *Server) Reset(ctx context.Context, in *machine.ResetRequest) (reply *ma

// MetaWrite implements the [machine.MachineServiceServer] interface.
func (s *Server) MetaWrite(ctx context.Context, req *machine.MetaWriteRequest) (*machine.MetaWriteResponse, error) {
if err := assertPeerSideroLink(ctx); err != nil {
if err := s.assertAdminRole(ctx); err != nil {
return nil, err
}

Expand Down Expand Up @@ -324,7 +326,7 @@ func (s *Server) MetaWrite(ctx context.Context, req *machine.MetaWriteRequest) (

// MetaDelete implements the [machine.MachineServiceServer] interface.
func (s *Server) MetaDelete(ctx context.Context, req *machine.MetaDeleteRequest) (*machine.MetaDeleteResponse, error) {
if err := assertPeerSideroLink(ctx); err != nil {
if err := s.assertAdminRole(ctx); err != nil {
return nil, err
}

Expand Down Expand Up @@ -352,3 +354,11 @@ func (s *Server) MetaDelete(ctx context.Context, req *machine.MetaDeleteRequest)
Messages: []*machine.MetaDelete{{}},
}, nil
}

func (s *Server) assertAdminRole(ctx context.Context) error {
if !authz.HasRole(ctx, role.Admin) {
return status.Error(codes.Unimplemented, "API is not implemented in maintenance mode")
}

return nil
}
5 changes: 5 additions & 0 deletions pkg/grpc/middleware/authz/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func GetRoles(ctx context.Context) role.Set {
return set
}

// HasRole returns true if the context includes the given role.
func HasRole(ctx context.Context, r role.Role) bool {
return GetRoles(ctx).Includes(r)
}

// getFromContext returns roles stored in the context.
func getFromContext(ctx context.Context) (role.Set, bool) {
set, ok := ctx.Value(ctxKey{}).(role.Set)
Expand Down
65 changes: 63 additions & 2 deletions pkg/grpc/middleware/authz/injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ package authz
import (
"context"
"fmt"
"net"
"net/netip"

grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"

"github.com/siderolabs/talos/pkg/machinery/resources/network"
"github.com/siderolabs/talos/pkg/machinery/role"
)

Expand All @@ -23,21 +26,37 @@ const (
// Disabled is used when RBAC is disabled in the machine configuration. All roles are assumed.
Disabled InjectorMode = iota

// ReadOnly is used to inject only Reader role.
// ReadOnly is used to inject only the Reader role.
ReadOnly

// ReadOnlyWithAdminOnSiderolink is used to inject the Admin role if the peer is a SideroLink peer.
// Otherwise, the Reader role is injected.
ReadOnlyWithAdminOnSiderolink

// MetadataOnly is used internally. Checks only metadata.
MetadataOnly

// Enabled is used when RBAC is enabled in the machine configuration. Roles are extracted normally.
Enabled
)

var (
adminRoleSet = role.MakeSet(role.Admin)
readerRoleSet = role.MakeSet(role.Reader)
)

// SideroLinkPeerCheckFunc checks if the peer is a SideroLink peer.
type SideroLinkPeerCheckFunc func(ctx context.Context) (netip.Addr, bool)

// Injector sets roles to the context.
type Injector struct {
// Mode.
Mode InjectorMode

// SideroLinkPeerCheckFunc checks if the peer is a SideroLink peer.
// When not specified, it defaults to isSideroLinkPeer.
SideroLinkPeerCheckFunc SideroLinkPeerCheckFunc

// Logger.
Logger func(format string, v ...interface{})
}
Expand Down Expand Up @@ -65,7 +84,21 @@ func (i *Injector) extractRoles(ctx context.Context) role.Set {
return role.All

case ReadOnly:
return role.MakeSet(role.Reader)
return readerRoleSet

case ReadOnlyWithAdminOnSiderolink:
check := i.SideroLinkPeerCheckFunc
if check == nil {
check = isSideroLinkPeer
}

if siderolinkPeerAddr, siderolinkPeer := check(ctx); siderolinkPeer {
i.logf("inject admin role for SideroLink peer %q", siderolinkPeerAddr)

return adminRoleSet
}

return readerRoleSet

case MetadataOnly:
roles, _ := getFromMetadata(ctx, i.logf)
Expand Down Expand Up @@ -135,3 +168,31 @@ func (i *Injector) StreamInterceptor() grpc.StreamServerInterceptor {
return handler(srv, wrapped)
}
}

func isSideroLinkPeer(ctx context.Context) (netip.Addr, bool) {
addr, ok := peerAddress(ctx)
if !ok {
return netip.Addr{}, false
}

return addr, network.IsULA(addr, network.ULASideroLink)
}

func peerAddress(ctx context.Context) (netip.Addr, bool) {
remotePeer, ok := peer.FromContext(ctx)
if !ok {
return netip.Addr{}, false
}

ip, _, err := net.SplitHostPort(remotePeer.Addr.String())
if err != nil {
return netip.Addr{}, false
}

addr, err := netip.ParseAddr(ip)
if err != nil {
return netip.Addr{}, false
}

return addr, true
}

0 comments on commit 0b7a27e

Please sign in to comment.