feat: allow access to all resources over siderolink in maintenance mode
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 <utku.ozdemir@siderolabs.com>
This commit is contained in:
parent
53721883d5
commit
0b7a27e6a1
@ -444,7 +444,6 @@ func renderDoc(doc *Doc, dest string) {
|
||||
defer out.Close()
|
||||
|
||||
_, err = out.Write(formatted)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to write output file: %v", err)
|
||||
}
|
||||
|
@ -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 {
|
||||
@ -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 {
|
||||
|
@ -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"
|
||||
@ -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{
|
||||
@ -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
|
||||
},
|
||||
}))
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -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)
|
||||
|
@ -1,50 +0,0 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/resources/network"
|
||||
)
|
||||
|
||||
func verifyPeer(ctx context.Context, condition func(netip.Addr) bool) bool {
|
||||
remotePeer, ok := peer.FromContext(ctx)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if remotePeer.Addr.Network() != "tcp" {
|
||||
return false
|
||||
}
|
||||
|
||||
ip, _, err := net.SplitHostPort(remotePeer.Addr.String())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return condition(addr)
|
||||
}
|
||||
|
||||
func assertPeerSideroLink(ctx context.Context) error {
|
||||
if !verifyPeer(ctx, func(addr netip.Addr) bool {
|
||||
return network.IsULA(addr, network.ULASideroLink)
|
||||
}) {
|
||||
return status.Error(codes.Unimplemented, "API is not implemented in maintenance mode")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
||||
@ -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:
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
@ -23,9 +26,13 @@ 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
|
||||
|
||||
@ -33,11 +40,23 @@ const (
|
||||
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{})
|
||||
}
|
||||
@ -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)
|
||||
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user