diff --git a/hack/docgen/main.go b/hack/docgen/main.go index a8e269c8eb..fbc186bb50 100644 --- a/hack/docgen/main.go +++ b/hack/docgen/main.go @@ -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) } diff --git a/internal/app/machined/pkg/controllers/runtime/maintenance_service.go b/internal/app/machined/pkg/controllers/runtime/maintenance_service.go index 40a76dfa9c..7a0f7fcb84 100644 --- a/internal/app/machined/pkg/controllers/runtime/maintenance_service.go +++ b/internal/app/machined/pkg/controllers/runtime/maintenance_service.go @@ -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 { diff --git a/internal/app/machined/pkg/controllers/runtime/maintenance_service_test.go b/internal/app/machined/pkg/controllers/runtime/maintenance_service_test.go index f4045576e0..a6aab9de44 100644 --- a/internal/app/machined/pkg/controllers/runtime/maintenance_service_test.go +++ b/internal/app/machined/pkg/controllers/runtime/maintenance_service_test.go @@ -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) diff --git a/internal/app/maintenance/peer.go b/internal/app/maintenance/peer.go deleted file mode 100644 index 94a0dc9319..0000000000 --- a/internal/app/maintenance/peer.go +++ /dev/null @@ -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 -} diff --git a/internal/app/maintenance/server.go b/internal/app/maintenance/server.go index c4f98a8ff9..77061ae9ed 100644 --- a/internal/app/maintenance/server.go +++ b/internal/app/maintenance/server.go @@ -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 +} diff --git a/pkg/grpc/middleware/authz/context.go b/pkg/grpc/middleware/authz/context.go index 31cf0b6a25..e00b5a9b2a 100644 --- a/pkg/grpc/middleware/authz/context.go +++ b/pkg/grpc/middleware/authz/context.go @@ -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) diff --git a/pkg/grpc/middleware/authz/injector.go b/pkg/grpc/middleware/authz/injector.go index 286a16286a..31055bd1da 100644 --- a/pkg/grpc/middleware/authz/injector.go +++ b/pkg/grpc/middleware/authz/injector.go @@ -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 +}