mirror of
https://github.com/siderolabs/talos.git
synced 2025-11-30 23:21:14 +01:00
- replace `interface{}` with `any` using `gofmt -r 'interface{} -> any -w'`
- replace `a = []T{}` with `var a []T` where possible.
- replace `a = []T{}` with `a = make([]T, 0, len(b))` where possible.
Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
199 lines
5.3 KiB
Go
199 lines
5.3 KiB
Go
// 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 authz
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
|
|
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2"
|
|
"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"
|
|
)
|
|
|
|
// InjectorMode specifies how roles are extracted.
|
|
type InjectorMode int
|
|
|
|
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 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 ...any)
|
|
}
|
|
|
|
func (i *Injector) logf(format string, v ...any) {
|
|
if i.Logger != nil {
|
|
i.Logger(format, v...)
|
|
}
|
|
}
|
|
|
|
// extractRoles returns roles extracted from the user's certificate (in case of the first apid instance),
|
|
// or from gRPC metadata (in case of subsequent apid instances, machined, or user with impersonator role).
|
|
//
|
|
//nolint:gocyclo
|
|
func (i *Injector) extractRoles(ctx context.Context) role.Set {
|
|
// sanity check
|
|
if _, ok := getFromContext(ctx); ok {
|
|
panic("roles should not be present in the context at this point")
|
|
}
|
|
|
|
switch i.Mode {
|
|
case Disabled:
|
|
i.logf("RBAC is disabled, injecting all roles")
|
|
|
|
return role.All
|
|
|
|
case ReadOnly:
|
|
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)
|
|
|
|
return roles
|
|
|
|
case Enabled:
|
|
p, ok := peer.FromContext(ctx)
|
|
if !ok {
|
|
panic("can't get peer information")
|
|
}
|
|
|
|
tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
|
|
if !ok {
|
|
panic(fmt.Sprintf("expected credentials.TLSInfo, got %T", p.AuthInfo))
|
|
}
|
|
|
|
if len(tlsInfo.State.PeerCertificates) == 0 {
|
|
panic("expected at least one certificate")
|
|
}
|
|
|
|
// PeerCertificates[0] is the leaf certificate the connection was verified against, so this
|
|
// is the client cert. Other certificates in the chain might be CAs or intermediates.
|
|
strings := tlsInfo.State.PeerCertificates[0].Subject.Organization
|
|
|
|
// TODO validate cert.KeyUsage, cert.ExtKeyUsage, cert.Issuer.Organization, other fields there?
|
|
|
|
roles, unknownRoles := role.Parse(strings)
|
|
i.logf("parsed peer's certificate orgs %v as %v (unknownRoles = %v)", strings, roles.Strings(), unknownRoles)
|
|
|
|
// trust gRPC metadata from clients with impersonator role if present
|
|
// (including requests proxied from other apid instances)
|
|
if roles.Includes(role.Impersonator) {
|
|
metadataRoles, ok := getFromMetadata(ctx, i.logf)
|
|
if ok {
|
|
return metadataRoles
|
|
}
|
|
|
|
// that's a real user with impersonator role then
|
|
i.logf("no roles in metadadata, returning parsed roles")
|
|
}
|
|
|
|
return roles
|
|
}
|
|
|
|
panic("unreachable")
|
|
}
|
|
|
|
// UnaryInterceptor returns grpc UnaryServerInterceptor.
|
|
func (i *Injector) UnaryInterceptor() grpc.UnaryServerInterceptor {
|
|
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
|
ctx = ContextWithRoles(ctx, i.extractRoles(ctx))
|
|
|
|
return handler(ctx, req)
|
|
}
|
|
}
|
|
|
|
// StreamInterceptor returns grpc StreamServerInterceptor.
|
|
func (i *Injector) StreamInterceptor() grpc.StreamServerInterceptor {
|
|
return func(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
|
ctx := stream.Context()
|
|
ctx = ContextWithRoles(ctx, i.extractRoles(ctx))
|
|
|
|
wrapped := grpc_middleware.WrapServerStream(stream)
|
|
wrapped.WrappedContext = ctx
|
|
|
|
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
|
|
}
|