From eaa71e029d514bf05427ff9a480b0ded163f0945 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Wed, 2 Jun 2021 18:36:48 +0000 Subject: [PATCH] feat: implement basic RBAC interceptors It is not enforced yet. Refs #3421. Signed-off-by: Alexey Palazhchenko --- cmd/talosctl/cmd/mgmt/cluster/create.go | 2 +- cmd/talosctl/cmd/mgmt/config.go | 6 +- cmd/talosctl/cmd/mgmt/gen/csr.go | 15 ++- go.mod | 2 +- go.sum | 4 +- internal/app/apid/main.go | 15 +++ internal/app/apid/pkg/backend/apid.go | 3 + internal/app/apid/pkg/backend/apid_test.go | 58 ++++++--- internal/app/apid/pkg/provider/tls.go | 8 +- .../machined/pkg/controllers/k8s/templates.go | 2 +- .../machined/pkg/system/services/machined.go | 27 ++++ internal/app/maintenance/main.go | 2 +- internal/app/trustd/main.go | 3 +- pkg/grpc/factory/factory.go | 40 +++--- pkg/grpc/middleware/auth/basic/token.go | 4 +- pkg/grpc/middleware/authz/authorizer.go | 120 ++++++++++++++++++ pkg/grpc/middleware/authz/authorizer_test.go | 35 +++++ pkg/grpc/middleware/authz/context.go | 44 +++++++ pkg/grpc/middleware/authz/injector.go | 95 ++++++++++++++ pkg/grpc/middleware/authz/metadata.go | 37 ++++++ pkg/grpc/proxy/backend/local.go | 15 ++- pkg/grpc/proxy/backend/local_test.go | 51 ++++++-- .../types/v1alpha1/generate/generate.go | 6 +- .../types/v1alpha1/generate/generate_test.go | 3 +- pkg/machinery/constants/constants.go | 4 - pkg/machinery/go.mod | 2 +- pkg/machinery/go.sum | 4 +- pkg/machinery/role/role.go | 90 +++++++++++++ pkg/machinery/role/role_test.go | 29 +++++ website/content/docs/v0.11/Reference/cli.md | 15 ++- 30 files changed, 656 insertions(+), 85 deletions(-) create mode 100644 pkg/grpc/middleware/authz/authorizer.go create mode 100644 pkg/grpc/middleware/authz/authorizer_test.go create mode 100644 pkg/grpc/middleware/authz/context.go create mode 100644 pkg/grpc/middleware/authz/injector.go create mode 100644 pkg/grpc/middleware/authz/metadata.go create mode 100644 pkg/machinery/role/role.go create mode 100644 pkg/machinery/role/role_test.go diff --git a/cmd/talosctl/cmd/mgmt/cluster/create.go b/cmd/talosctl/cmd/mgmt/cluster/create.go index 81f453a7e45..6c910d006de 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create.go @@ -757,7 +757,7 @@ func init() { createCmd.Flags().StringVar(&nodeInstallImage, "install-image", helpers.DefaultImage(images.DefaultInstallerImageRepository), "the installer image to use") createCmd.Flags().StringVar(&nodeVmlinuzPath, "vmlinuz-path", helpers.ArtifactPath(constants.KernelAssetWithArch), "the compressed kernel image to use") createCmd.Flags().StringVar(&nodeISOPath, "iso-path", "", "the ISO path to use for the initial boot (VM only)") - createCmd.Flags().StringVar(&nodeInitramfsPath, "initrd-path", helpers.ArtifactPath(constants.InitramfsAssetWithArch), "the uncompressed kernel image to use") + createCmd.Flags().StringVar(&nodeInitramfsPath, "initrd-path", helpers.ArtifactPath(constants.InitramfsAssetWithArch), "initramfs image to use") createCmd.Flags().StringVar(&nodeDiskImagePath, "disk-image-path", "", "disk image to use") createCmd.Flags().BoolVar(&applyConfigEnabled, "with-apply-config", false, "enable apply config when the VM is starting in maintenance mode") createCmd.Flags().BoolVar(&bootloaderEnabled, "with-bootloader", true, "enable bootloader to load kernel and initramfs from disk image after install") diff --git a/cmd/talosctl/cmd/mgmt/config.go b/cmd/talosctl/cmd/mgmt/config.go index a71d0f05507..73816cdb41f 100644 --- a/cmd/talosctl/cmd/mgmt/config.go +++ b/cmd/talosctl/cmd/mgmt/config.go @@ -52,9 +52,9 @@ var genConfigCmd = &cobra.Command{ Use: "config ", Short: "Generates a set of configuration files for Talos cluster", Long: `The cluster endpoint is the URL for the Kubernetes API. If you decide to use - a control plane node, common in a single node control plane setup, use port 6443 as - this is the port that the API server binds to on every control plane node. For an HA - setup, usually involving a load balancer, use the IP and port of the load balancer.`, +a control plane node, common in a single node control plane setup, use port 6443 as +this is the port that the API server binds to on every control plane node. For an HA +setup, usually involving a load balancer, use the IP and port of the load balancer.`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { // Validate url input to ensure it has https:// scheme before we attempt to gen diff --git a/cmd/talosctl/cmd/mgmt/gen/csr.go b/cmd/talosctl/cmd/mgmt/gen/csr.go index 4bfb11f9ad2..eeb55c44441 100644 --- a/cmd/talosctl/cmd/mgmt/gen/csr.go +++ b/cmd/talosctl/cmd/mgmt/gen/csr.go @@ -17,12 +17,13 @@ import ( "github.com/talos-systems/crypto/x509" "github.com/talos-systems/talos/pkg/cli" - "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/machinery/role" ) var genCSRCmdFlags struct { - key string - ip string + key string + ip string + roles []string } // genCSRCmd represents the `gen csr` command. @@ -54,8 +55,13 @@ var genCSRCmd = &cobra.Command{ return fmt.Errorf("invalid IP: %s", genCSRCmdFlags.ip) } + roles, err := role.Parse(genCSRCmdFlags.roles) + if err != nil { + return err + } + ips := []net.IP{parsed} - opts = append(opts, x509.Organization(constants.RoleAdmin)) + opts = append(opts, x509.Organization(roles.Strings()...)) opts = append(opts, x509.IPAddresses(ips)) csr, err := x509.NewCertificateSigningRequest(keyEC, opts...) @@ -76,6 +82,7 @@ func init() { cli.Should(cobra.MarkFlagRequired(genCSRCmd.Flags(), "key")) genCSRCmd.Flags().StringVar(&genCSRCmdFlags.ip, "ip", "", "generate the certificate for this IP address") cli.Should(cobra.MarkFlagRequired(genCSRCmd.Flags(), "ip")) + genCSRCmd.Flags().StringSliceVar(&genCSRCmdFlags.roles, "roles", role.MakeSet(role.Admin).Strings(), "roles") Cmd.AddCommand(genCSRCmd) } diff --git a/go.mod b/go.mod index c4a58cac908..7f0b46003c2 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/smira/go-xz v0.0.0-20201019130106-9921ed7a9935 github.com/spf13/cobra v1.1.3 github.com/stretchr/testify v1.7.0 - github.com/talos-systems/crypto v0.2.1-0.20210526123943-7776057f5086 + github.com/talos-systems/crypto v0.2.1-0.20210601174604-cd18ef62eb9f github.com/talos-systems/go-blockdevice v0.2.1-0.20210526155905-30c2bc3cb62a github.com/talos-systems/go-cmd v0.0.0-20210216164758-68eb0067e0f0 github.com/talos-systems/go-debug v0.2.1-0.20210525175311-3d0a6e1bf5e3 diff --git a/go.sum b/go.sum index 83a0c6a8a15..f56fd70b67b 100644 --- a/go.sum +++ b/go.sum @@ -1220,8 +1220,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/talos-systems/crypto v0.2.1-0.20210526123943-7776057f5086 h1:SAyrAftTtxzEUqr9alFt1iezS5vuwCm7/yE8ydR0h+A= -github.com/talos-systems/crypto v0.2.1-0.20210526123943-7776057f5086/go.mod h1:xaNCB2/Bxaj+qrkdeodhRv5eKQVvKOGBBMj58MrIPY8= +github.com/talos-systems/crypto v0.2.1-0.20210601174604-cd18ef62eb9f h1:Xk3zeUZPhvEl9Vs4PlYBohin3QZmizA/YR4URKEyULY= +github.com/talos-systems/crypto v0.2.1-0.20210601174604-cd18ef62eb9f/go.mod h1:xaNCB2/Bxaj+qrkdeodhRv5eKQVvKOGBBMj58MrIPY8= github.com/talos-systems/go-blockdevice v0.2.1-0.20210526155905-30c2bc3cb62a h1:NLuIVKi5tBnRMgxk185AVGmMUzlRcggb2Abrw9uUq3E= github.com/talos-systems/go-blockdevice v0.2.1-0.20210526155905-30c2bc3cb62a/go.mod h1:qnn/zDc09I1DA2BUDDCOSA2D0P8pIDjN8pGiRoRaQig= github.com/talos-systems/go-cmd v0.0.0-20210216164758-68eb0067e0f0 h1:DI+BjK+fcrLBc70Fi50dZocQcaHosqsuWHrGHKp2NzE= diff --git a/internal/app/apid/main.go b/internal/app/apid/main.go index 52d4302e0d5..40802bd1a61 100644 --- a/internal/app/apid/main.go +++ b/internal/app/apid/main.go @@ -21,6 +21,7 @@ import ( "github.com/talos-systems/talos/internal/app/apid/pkg/director" "github.com/talos-systems/talos/internal/app/apid/pkg/provider" "github.com/talos-systems/talos/pkg/grpc/factory" + "github.com/talos-systems/talos/pkg/grpc/middleware/authz" "github.com/talos-systems/talos/pkg/grpc/proxy/backend" "github.com/talos-systems/talos/pkg/machinery/config/configloader" "github.com/talos-systems/talos/pkg/machinery/constants" @@ -118,6 +119,11 @@ func Main() { var errGroup errgroup.Group errGroup.Go(func() error { + injector := &authz.Injector{ + TrustMetadata: false, + Logger: log.New(log.Writer(), "apid/authz/injector/http ", log.Flags()).Printf, + } + return factory.ListenAndServe( router, factory.Port(constants.ApidPort), @@ -133,10 +139,17 @@ func Main() { proxy.WithStreamedDetector(router.StreamedDetector), )), ), + factory.WithUnaryInterceptor(injector.UnaryInterceptor()), + factory.WithStreamInterceptor(injector.StreamInterceptor()), ) }) errGroup.Go(func() error { + injector := &authz.Injector{ + TrustMetadata: true, + Logger: log.New(log.Writer(), "apid/authz/injector/unix ", log.Flags()).Printf, + } + return factory.ListenAndServe( router, factory.Network("unix"), @@ -150,6 +163,8 @@ func Main() { proxy.WithStreamedDetector(router.StreamedDetector), )), ), + factory.WithUnaryInterceptor(injector.UnaryInterceptor()), + factory.WithStreamInterceptor(injector.StreamInterceptor()), ) }) diff --git a/internal/app/apid/pkg/backend/apid.go b/internal/app/apid/pkg/backend/apid.go index d8448492e99..01f2d285d37 100644 --- a/internal/app/apid/pkg/backend/apid.go +++ b/internal/app/apid/pkg/backend/apid.go @@ -18,6 +18,7 @@ import ( "google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/proto" + "github.com/talos-systems/talos/pkg/grpc/middleware/authz" "github.com/talos-systems/talos/pkg/machinery/api/common" "github.com/talos-systems/talos/pkg/machinery/constants" ) @@ -64,6 +65,8 @@ func (a *APID) GetConnection(ctx context.Context) (context.Context, *grpc.Client md.Set("proxyfrom", "unknown") } + authz.SetRolesToMetadata(ctx, md) + outCtx := metadata.NewOutgoingContext(ctx, md) a.mu.Lock() diff --git a/internal/app/apid/pkg/backend/apid_test.go b/internal/app/apid/pkg/backend/apid_test.go index 7112448d11f..381adadfcbb 100644 --- a/internal/app/apid/pkg/backend/apid_test.go +++ b/internal/app/apid/pkg/backend/apid_test.go @@ -18,7 +18,9 @@ import ( "google.golang.org/protobuf/proto" "github.com/talos-systems/talos/internal/app/apid/pkg/backend" + "github.com/talos-systems/talos/pkg/grpc/middleware/authz" "github.com/talos-systems/talos/pkg/machinery/api/common" + "github.com/talos-systems/talos/pkg/machinery/role" ) func TestAPIDInterfaces(t *testing.T) { @@ -38,29 +40,55 @@ func (suite *APIDSuite) SetupSuite() { } func (suite *APIDSuite) TestGetConnection() { - md := metadata.New(nil) - md.Set(":authority", "127.0.0.2") - md.Set("nodes", "127.0.0.1") - md.Set("key", "value1", "value2") - ctx := metadata.NewIncomingContext(context.Background(), md) + md1 := metadata.New(nil) + md1.Set(":authority", "127.0.0.2") + md1.Set("nodes", "127.0.0.1") + md1.Set("key", "value1", "value2") + ctx1 := metadata.NewIncomingContext(authz.ContextWithRoles(context.Background(), role.MakeSet(role.Admin)), md1) - outCtx1, conn1, err1 := suite.b.GetConnection(ctx) + outCtx1, conn1, err1 := suite.b.GetConnection(ctx1) suite.Require().NoError(err1) suite.Assert().NotNil(conn1) + suite.Assert().Equal(role.MakeSet(role.Admin), authz.GetRoles(outCtx1)) mdOut1, ok1 := metadata.FromOutgoingContext(outCtx1) suite.Require().True(ok1) suite.Assert().Equal([]string{"value1", "value2"}, mdOut1.Get("key")) suite.Assert().Equal([]string{"127.0.0.2"}, mdOut1.Get("proxyfrom")) - - outCtx2, conn2, err2 := suite.b.GetConnection(ctx) - suite.Require().NoError(err2) - suite.Assert().Equal(conn1, conn2) // connection is cached - - mdOut2, ok2 := metadata.FromOutgoingContext(outCtx2) - suite.Require().True(ok2) - suite.Assert().Equal([]string{"value1", "value2"}, mdOut2.Get("key")) - suite.Assert().Equal([]string{"127.0.0.2"}, mdOut2.Get("proxyfrom")) + suite.Assert().Equal([]string{"os:admin"}, mdOut1.Get("talos-role")) + + suite.Run("Same context", func() { + ctx2 := ctx1 + outCtx2, conn2, err2 := suite.b.GetConnection(ctx2) + suite.Require().NoError(err2) + suite.Assert().Equal(conn1, conn2) // connection is cached + suite.Assert().Equal(role.MakeSet(role.Admin), authz.GetRoles(outCtx2)) + + mdOut2, ok2 := metadata.FromOutgoingContext(outCtx2) + suite.Require().True(ok2) + suite.Assert().Equal([]string{"value1", "value2"}, mdOut2.Get("key")) + suite.Assert().Equal([]string{"127.0.0.2"}, mdOut2.Get("proxyfrom")) + suite.Assert().Equal([]string{"os:admin"}, mdOut2.Get("talos-role")) + }) + + suite.Run("Other context", func() { + md3 := metadata.New(nil) + md3.Set(":authority", "127.0.0.2") + md3.Set("nodes", "127.0.0.1") + md3.Set("key", "value3", "value4") + ctx3 := metadata.NewIncomingContext(authz.ContextWithRoles(context.Background(), role.MakeSet(role.Reader)), md3) + + outCtx3, conn3, err3 := suite.b.GetConnection(ctx3) + suite.Require().NoError(err3) + suite.Assert().Equal(conn1, conn3) // connection is cached + suite.Assert().Equal(role.MakeSet(role.Reader), authz.GetRoles(outCtx3)) + + mdOut3, ok3 := metadata.FromOutgoingContext(outCtx3) + suite.Require().True(ok3) + suite.Assert().Equal([]string{"value3", "value4"}, mdOut3.Get("key")) + suite.Assert().Equal([]string{"127.0.0.2"}, mdOut3.Get("proxyfrom")) + suite.Assert().Equal([]string{"os:reader"}, mdOut3.Get("talos-role")) + }) } func (suite *APIDSuite) TestAppendInfoUnary() { diff --git a/internal/app/apid/pkg/provider/tls.go b/internal/app/apid/pkg/provider/tls.go index 4cdf86db5f0..037717b904c 100644 --- a/internal/app/apid/pkg/provider/tls.go +++ b/internal/app/apid/pkg/provider/tls.go @@ -20,6 +20,7 @@ import ( "github.com/talos-systems/talos/pkg/grpc/gen" "github.com/talos-systems/talos/pkg/machinery/config" + "github.com/talos-systems/talos/pkg/machinery/role" ) // TLSConfig provides client & server TLS configs for apid. @@ -67,13 +68,14 @@ func NewTLSConfig(config config.Provider, endpoints Endpoints) (*TLSConfig, erro endpointList, ) if err != nil { - return nil, fmt.Errorf("failed to create remote certificate genertor: %w", err) + return nil, fmt.Errorf("failed to create remote certificate generator: %w", err) } tlsConfig.certificateProvider, err = tls.NewRenewingCertificateProvider( tlsConfig.generator, - dnsNames, - ips, + x509.DNSNames(dnsNames), + x509.IPAddresses(ips), + x509.Organization(string(role.Impersonator)), ) if err != nil { return nil, err diff --git a/internal/app/machined/pkg/controllers/k8s/templates.go b/internal/app/machined/pkg/controllers/k8s/templates.go index 94ee410bbf4..aece6c57f58 100644 --- a/internal/app/machined/pkg/controllers/k8s/templates.go +++ b/internal/app/machined/pkg/controllers/k8s/templates.go @@ -81,7 +81,7 @@ roleRef: // certificates. // // This binding should be altered in the future to hold a list of node -// names instead of targeting `system:nodes` so we can revoke invidivual +// names instead of targeting `system:nodes` so we can revoke individual // node's ability to renew its certs. var csrRenewalRoleBindingTemplate = []byte(`kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/internal/app/machined/pkg/system/services/machined.go b/internal/app/machined/pkg/system/services/machined.go index 26748064def..208614b5f34 100644 --- a/internal/app/machined/pkg/system/services/machined.go +++ b/internal/app/machined/pkg/system/services/machined.go @@ -8,6 +8,7 @@ package services import ( "context" "io" + "log" v1alpha1server "github.com/talos-systems/talos/internal/app/machined/internal/server/v1alpha1" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" @@ -16,7 +17,9 @@ import ( "github.com/talos-systems/talos/internal/app/machined/pkg/system/runner/goroutine" "github.com/talos-systems/talos/pkg/conditions" "github.com/talos-systems/talos/pkg/grpc/factory" + "github.com/talos-systems/talos/pkg/grpc/middleware/authz" "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/machinery/role" ) type machinedService struct { @@ -25,12 +28,36 @@ type machinedService struct { // Main is an entrypoint the the API service. func (s *machinedService) Main(ctx context.Context, r runtime.Runtime, logWriter io.Writer) error { + injector := &authz.Injector{ + TrustMetadata: true, + Logger: log.New(logWriter, "machined/authz/injector ", log.Flags()).Printf, + } + + authorizer := &authz.Authorizer{ + Rules: map[string]role.Set{ + "/cluster.ClusterService/HealthCheck": role.MakeSet(role.Admin, role.Reader), + "/machine.MachineService/List": role.MakeSet(role.Admin, role.Reader), + "/machine.MachineService/Version": role.MakeSet(role.Admin, role.Reader), + + // TODO(rbac): More rules + }, + FallbackRoles: role.MakeSet(role.Admin), + DontEnforce: true, // TODO(rbac): Should be configurable with a feature gate + Logger: log.New(logWriter, "machined/authz/authorizer ", log.Flags()).Printf, + } + // Start the API server. server := factory.NewServer( &v1alpha1server.Server{ Controller: s.c, }, factory.WithLog("machined ", logWriter), + + factory.WithUnaryInterceptor(injector.UnaryInterceptor()), + factory.WithStreamInterceptor(injector.StreamInterceptor()), + + factory.WithUnaryInterceptor(authorizer.UnaryInterceptor()), + factory.WithStreamInterceptor(authorizer.StreamInterceptor()), ) listener, err := factory.NewListener(factory.Network("unix"), factory.SocketPath(constants.MachineSocketPath)) diff --git a/internal/app/maintenance/main.go b/internal/app/maintenance/main.go index c824056fdcf..57fdd0691c9 100644 --- a/internal/app/maintenance/main.go +++ b/internal/app/maintenance/main.go @@ -126,7 +126,7 @@ func genTLSConfig(ips []net.IP) (tlsConfig *tls.Config, provider ttls.Certificat return nil, nil, fmt.Errorf("failed to create local generator provider: %w", err) } - provider, err = ttls.NewRenewingCertificateProvider(generator, dnsNames, ips) + provider, err = ttls.NewRenewingCertificateProvider(generator, x509.DNSNames(dnsNames), x509.IPAddresses(ips)) if err != nil { return nil, nil, fmt.Errorf("failed to create local certificate provider: %w", err) } diff --git a/internal/app/trustd/main.go b/internal/app/trustd/main.go index 083aef5b375..bf3a1ab06eb 100644 --- a/internal/app/trustd/main.go +++ b/internal/app/trustd/main.go @@ -11,6 +11,7 @@ import ( stdlibnet "net" "github.com/talos-systems/crypto/tls" + "github.com/talos-systems/crypto/x509" debug "github.com/talos-systems/go-debug" "github.com/talos-systems/net" "google.golang.org/grpc" @@ -84,7 +85,7 @@ func Main() { var provider tls.CertificateProvider - provider, err = tls.NewRenewingCertificateProvider(generator, dnsNames, ips) + provider, err = tls.NewRenewingCertificateProvider(generator, x509.DNSNames(dnsNames), x509.IPAddresses(ips)) if err != nil { log.Fatalln("failed to create local certificate provider:", err) } diff --git a/pkg/grpc/factory/factory.go b/pkg/grpc/factory/factory.go index 02151671544..994f05d4dcc 100644 --- a/pkg/grpc/factory/factory.go +++ b/pkg/grpc/factory/factory.go @@ -40,8 +40,8 @@ type Options struct { LogPrefix string LogDestination io.Writer ServerOptions []grpc.ServerOption - StreamInterceptors []grpc.StreamServerInterceptor UnaryInterceptors []grpc.UnaryServerInterceptor + StreamInterceptors []grpc.StreamServerInterceptor } // Option is the functional option func. @@ -82,17 +82,17 @@ func ServerOptions(o ...grpc.ServerOption) Option { } } -// WithStreamInterceptor appends to the list of gRPC server stream interceptors. -func WithStreamInterceptor(i grpc.StreamServerInterceptor) Option { +// WithUnaryInterceptor appends to the list of gRPC server unary interceptors. +func WithUnaryInterceptor(i grpc.UnaryServerInterceptor) Option { return func(args *Options) { - args.StreamInterceptors = append(args.StreamInterceptors, i) + args.UnaryInterceptors = append(args.UnaryInterceptors, i) } } -// WithUnaryInterceptor appends to the list of gRPC server unary interceptors. -func WithUnaryInterceptor(i grpc.UnaryServerInterceptor) Option { +// WithStreamInterceptor appends to the list of gRPC server stream interceptors. +func WithStreamInterceptor(i grpc.StreamServerInterceptor) Option { return func(args *Options) { - args.UnaryInterceptors = append(args.UnaryInterceptors, i) + args.StreamInterceptors = append(args.StreamInterceptors, i) } } @@ -114,7 +114,7 @@ func WithDefaultLog() Option { func recoveryHandler(logger *log.Logger) grpc_recovery.RecoveryHandlerFunc { return func(p interface{}) error { if logger != nil { - logger.Printf("panic:\n%s", string(debug.Stack())) + logger.Printf("panic: %v\n%s", p, string(debug.Stack())) } return status.Errorf(codes.Internal, "%v", p) @@ -136,25 +136,25 @@ func NewDefaultOptions(setters ...Option) *Options { if opts.LogDestination != nil { logger = log.New(opts.LogDestination, opts.LogPrefix, log.Flags()) + } - logMiddleware := grpclog.NewMiddleware(logger) + // Recovery is installed as the the first middleware in the chain to handle panics (via defer and recover()) in all subsequent middlewares. + recoveryOpt := grpc_recovery.WithRecoveryHandler(recoveryHandler(logger)) + opts.UnaryInterceptors = append([]grpc.UnaryServerInterceptor{grpc_recovery.UnaryServerInterceptor(recoveryOpt)}, opts.UnaryInterceptors...) + opts.StreamInterceptors = append([]grpc.StreamServerInterceptor{grpc_recovery.StreamServerInterceptor(recoveryOpt)}, opts.StreamInterceptors...) - // Logging is installed as the first middleware so that request in the form it was received, - // and status sent on the wire is logged (error/success). It also tracks whole duration of the - // request, including other middleware overhead. + if logger != nil { + // Logging is installed as the first middleware (even before recovery middleware) in the chain + // so that request in the form it was received and status sent on the wire is logged (error/success). + // It also tracks the whole duration of the request, including other middleware overhead. + logMiddleware := grpclog.NewMiddleware(logger) opts.UnaryInterceptors = append([]grpc.UnaryServerInterceptor{logMiddleware.UnaryInterceptor()}, opts.UnaryInterceptors...) opts.StreamInterceptors = append([]grpc.StreamServerInterceptor{logMiddleware.StreamInterceptor()}, opts.StreamInterceptors...) } - // Install default recovery interceptors. - // Recovery is installed as the last middleware in the chain so that earlier middlewares in the chain - // have a chance to process the error (e.g. logging middleware). - opts.StreamInterceptors = append(opts.StreamInterceptors, grpc_recovery.StreamServerInterceptor(grpc_recovery.WithRecoveryHandler(recoveryHandler(logger)))) - opts.UnaryInterceptors = append(opts.UnaryInterceptors, grpc_recovery.UnaryServerInterceptor(grpc_recovery.WithRecoveryHandler(recoveryHandler(logger)))) - opts.ServerOptions = append(opts.ServerOptions, - grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(opts.StreamInterceptors...)), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(opts.UnaryInterceptors...)), + grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(opts.StreamInterceptors...)), ) return opts @@ -205,7 +205,7 @@ func NewListener(setters ...Option) (net.Listener, error) { return net.Listen(opts.Network, address) } -// ListenAndServe configures TLS for mutual authtentication by loading the CA into a +// ListenAndServe configures TLS for mutual authentication by loading the CA into a // CertPool and configuring the server's policy for TLS Client Authentication. // Once TLS is configured, the gRPC options are built to make use of the TLS // configuration and the receiver (Server) is registered to the gRPC server. diff --git a/pkg/grpc/middleware/auth/basic/token.go b/pkg/grpc/middleware/auth/basic/token.go index c5887ba0d10..5e4fc0dd7bb 100644 --- a/pkg/grpc/middleware/auth/basic/token.go +++ b/pkg/grpc/middleware/auth/basic/token.go @@ -43,7 +43,7 @@ func (b *TokenCredentials) RequireTransportSecurity() bool { return true } -func (b *TokenCredentials) authorize(ctx context.Context) error { +func (b *TokenCredentials) authenticate(ctx context.Context) error { if md, ok := metadata.FromIncomingContext(ctx); ok { if len(md["token"]) > 0 && md["token"][0] == b.Token { return nil @@ -59,7 +59,7 @@ func (b *TokenCredentials) UnaryInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { start := time.Now() - if err := b.authorize(ctx); err != nil { + if err := b.authenticate(ctx); err != nil { return nil, err } diff --git a/pkg/grpc/middleware/authz/authorizer.go b/pkg/grpc/middleware/authz/authorizer.go new file mode 100644 index 00000000000..9b9041e2e1a --- /dev/null +++ b/pkg/grpc/middleware/authz/authorizer.go @@ -0,0 +1,120 @@ +// 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" + "strings" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/talos-systems/talos/pkg/machinery/role" +) + +// Authorizer checks that the user is authorized (has a valid role) to call intercepted gRPC method. +// User roles should be set the Injector interceptor. +type Authorizer struct { + // Maps full gRPC method names to roles. The user should have at least one of them. + Rules map[string]role.Set + + // Defines roles for gRPC methods not present in Rules. + FallbackRoles role.Set + + // If true, makes the authorizer never return authorization error. + DontEnforce bool + + // Logger. + Logger func(format string, v ...interface{}) +} + +// nextPrefix returns path's prefix, stopping on slashes and dots: +// /machine.MachineService/List -> /machine.MachineService -> /machine -> / -> / -> ... +// The chain ends with "/" no matter what. +func nextPrefix(path string) string { + if path == "" || path[0] != '/' { + return "/" + } + + i := strings.LastIndexAny(path, "/.") + if i <= 0 { + return "/" + } + + return path[:i] +} + +func (a *Authorizer) logf(format string, v ...interface{}) { + if a.Logger != nil { + a.Logger(format, v...) + } +} + +// authorize returns error if the user is not authorized (doesn't have a valid role) to call the given gRPC method. +// User roles should be previously set the Injector interceptor. +func (a *Authorizer) authorize(ctx context.Context, method string) (context.Context, error) { + clientRoles := GetRoles(ctx) + + var allowedRoles role.Set + + prefix := method + for prefix != "/" { + if allowedRoles = a.Rules[prefix]; allowedRoles != nil { + break + } + + prefix = nextPrefix(prefix) + } + + if allowedRoles == nil { + a.logf("no explicit rule found for %q, falling back to %v", method, a.FallbackRoles.Strings()) + allowedRoles = a.FallbackRoles + } + + if allowedRoles.IncludesAny(clientRoles) { + a.logf("authorized (%v includes %v)", allowedRoles.Strings(), clientRoles.Strings()) + + return ctx, nil + } + + if a.DontEnforce { + a.logf("not authorized (%v doesn't include %v), but authorization wasn't enforced", allowedRoles.Strings(), clientRoles.Strings()) + + return ctx, nil + } + + a.logf("not authorized (%v doesn't include %v)", allowedRoles.Strings(), clientRoles.Strings()) + + return nil, status.Error(codes.PermissionDenied, "not authorized") +} + +// UnaryInterceptor returns grpc UnaryServerInterceptor. +func (a *Authorizer) UnaryInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + ctx, err := a.authorize(ctx, info.FullMethod) + if err != nil { + return nil, err + } + + return handler(ctx, req) + } +} + +// StreamInterceptor returns grpc StreamServerInterceptor. +func (a *Authorizer) StreamInterceptor() grpc.StreamServerInterceptor { + return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + ctx, err := a.authorize(stream.Context(), info.FullMethod) + if err != nil { + return err + } + + wrapped := grpc_middleware.WrapServerStream(stream) + wrapped.WrappedContext = ctx + + return handler(srv, wrapped) + } +} diff --git a/pkg/grpc/middleware/authz/authorizer_test.go b/pkg/grpc/middleware/authz/authorizer_test.go new file mode 100644 index 00000000000..7a62e188ca5 --- /dev/null +++ b/pkg/grpc/middleware/authz/authorizer_test.go @@ -0,0 +1,35 @@ +// 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 //nolint:testpackage // to test unexported method + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNextPrefix(t *testing.T) { + t.Parallel() + + for _, paths := range [][]string{ + {"/machine.MachineService/List", "/machine.MachineService", "/machine", "/", "/"}, + {"/.x", "/", "/"}, + {".", "/", "/"}, + {"./", "/", "/"}, + {"foo", "/", "/"}, + {"", "/", "/"}, + } { + paths := paths + t.Run(paths[0], func(t *testing.T) { + t.Parallel() + + for i, path := range paths[:len(paths)-1] { + expected := paths[i+1] + actual := nextPrefix(path) + assert.Equal(t, expected, actual, "path = %q", path) + } + }) + } +} diff --git a/pkg/grpc/middleware/authz/context.go b/pkg/grpc/middleware/authz/context.go new file mode 100644 index 00000000000..32361f7278c --- /dev/null +++ b/pkg/grpc/middleware/authz/context.go @@ -0,0 +1,44 @@ +// 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" + + "github.com/talos-systems/talos/pkg/machinery/role" +) + +// ctxKey is used to store parsed roles in the context. +// Should be used only in this file. +type ctxKey struct{} + +// GetRoles returns roles stored in the context by the Injector interceptor. +// May be used for additional checks in the API method handler. +func GetRoles(ctx context.Context) role.Set { + roles := rolesFromContext(ctx) + + if roles == nil { + panic("no roles in the context") + } + + return roles +} + +// rolesFromContext returns roles stored in the context, or nil. +func rolesFromContext(ctx context.Context) role.Set { + roles, _ := ctx.Value(ctxKey{}).(role.Set) //nolint:errcheck + + return roles +} + +// ContextWithRoles returns derived context with roles. +func ContextWithRoles(ctx context.Context, roles role.Set) context.Context { + // sanity check + if ctx.Value(ctxKey{}) != nil { + panic("roles already stored in the context") + } + + return context.WithValue(ctx, ctxKey{}, roles) +} diff --git a/pkg/grpc/middleware/authz/injector.go b/pkg/grpc/middleware/authz/injector.go new file mode 100644 index 00000000000..d027806999f --- /dev/null +++ b/pkg/grpc/middleware/authz/injector.go @@ -0,0 +1,95 @@ +// 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" + + 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/talos-systems/talos/pkg/machinery/role" +) + +// Injector sets roles to the context. +type Injector struct { + // If true, trust roles in gRPC metadata, do not check certificate. + TrustMetadata bool + + // Logger. + Logger func(format string, v ...interface{}) +} + +func (i *Injector) logf(format string, v ...interface{}) { + 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 or machined). +func (i *Injector) extractRoles(ctx context.Context) role.Set { + // sanity check + if rolesFromContext(ctx) != nil { + panic("roles should not be present in the context at this point") + } + + // check certificate first, if needed + if !i.TrustMetadata { + 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) != 1 { + panic(fmt.Sprintf("expected one certificate, got %d", len(tlsInfo.State.PeerCertificates))) + } + + strings := tlsInfo.State.PeerCertificates[0].Subject.Organization + + // TODO validate cert.KeyUsage, cert.ExtKeyUsage, cert.Issuer.Organization, other fields there? + + roles, err := role.Parse(strings) + i.logf("parsed peer's orgs %v as %v (err = %v)", strings, roles.Strings(), err) + + // not impersonator (not proxied request), return extracted roles + if _, ok := roles[role.Impersonator]; !ok { + return roles + } + } + + // trust gRPC metadata from clients with impersonator role (that's proxied request), or if configured + return getFromMetadata(ctx, i.logf) +} + +// UnaryInterceptor returns grpc UnaryServerInterceptor. +func (i *Injector) UnaryInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + ctx = ContextWithRoles(ctx, i.extractRoles(ctx)) + + return handler(ctx, req) + } +} + +// StreamInterceptor returns grpc StreamServerInterceptor. +func (i *Injector) StreamInterceptor() grpc.StreamServerInterceptor { + return func(srv interface{}, 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) + } +} diff --git a/pkg/grpc/middleware/authz/metadata.go b/pkg/grpc/middleware/authz/metadata.go new file mode 100644 index 00000000000..07df1bcd19c --- /dev/null +++ b/pkg/grpc/middleware/authz/metadata.go @@ -0,0 +1,37 @@ +// 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" + + "google.golang.org/grpc/metadata" + + "github.com/talos-systems/talos/pkg/machinery/role" +) + +// mdKey is used to store roles in gRPC metadata. +// Should be used only in this file. +const mdKey = "talos-role" + +// SetRolesToMetadata gets roles from the context (where they were previously set by the Injector interceptor) +// and sets them the metadata. +func SetRolesToMetadata(ctx context.Context, md metadata.MD) { + md.Set(mdKey, GetRoles(ctx).Strings()...) +} + +// getFromMetadata returns roles extracted from from gRPC metadata. +func getFromMetadata(ctx context.Context, logf func(format string, v ...interface{})) role.Set { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + panic("no request metadata") + } + + strings := md.Get(mdKey) + roles, err := role.Parse(strings) + logf("parsed metadata %v as %v (err = %v)", strings, roles.Strings(), err) + + return roles +} diff --git a/pkg/grpc/proxy/backend/local.go b/pkg/grpc/proxy/backend/local.go index 852f9867b26..7f340b8ebc9 100644 --- a/pkg/grpc/proxy/backend/local.go +++ b/pkg/grpc/proxy/backend/local.go @@ -11,6 +11,8 @@ import ( "github.com/talos-systems/grpc-proxy/proxy" "google.golang.org/grpc" "google.golang.org/grpc/metadata" + + "github.com/talos-systems/talos/pkg/grpc/middleware/authz" ) // Local implements local backend (proxying one2one to local service). @@ -36,15 +38,20 @@ func (l *Local) String() string { // GetConnection returns a grpc connection to the backend. func (l *Local) GetConnection(ctx context.Context) (context.Context, *grpc.ClientConn, error) { - l.mu.Lock() - defer l.mu.Unlock() + origMd, ok := metadata.FromIncomingContext(ctx) + + md := origMd.Copy() + + authz.SetRolesToMetadata(ctx, md) - // copy metadata outCtx := ctx - if md, ok := metadata.FromIncomingContext(ctx); ok { + if ok { outCtx = metadata.NewOutgoingContext(ctx, md) } + l.mu.Lock() + defer l.mu.Unlock() + if l.conn != nil { return outCtx, l.conn, nil } diff --git a/pkg/grpc/proxy/backend/local_test.go b/pkg/grpc/proxy/backend/local_test.go index 87c2fa6d5ff..6f4ce621023 100644 --- a/pkg/grpc/proxy/backend/local_test.go +++ b/pkg/grpc/proxy/backend/local_test.go @@ -12,7 +12,9 @@ import ( "github.com/talos-systems/grpc-proxy/proxy" "google.golang.org/grpc/metadata" + "github.com/talos-systems/talos/pkg/grpc/middleware/authz" "github.com/talos-systems/talos/pkg/grpc/proxy/backend" + "github.com/talos-systems/talos/pkg/machinery/role" ) func TestLocalInterfaces(t *testing.T) { @@ -20,25 +22,54 @@ func TestLocalInterfaces(t *testing.T) { } func TestLocalGetConnection(t *testing.T) { + t.Parallel() + l := backend.NewLocal("test", "/tmp/test.sock") - md := metadata.New(nil) - md.Set("key", "value1", "value2") - ctx := metadata.NewIncomingContext(context.Background(), md) + md1 := metadata.New(nil) + md1.Set("key", "value1", "value2") + ctx1 := metadata.NewIncomingContext(authz.ContextWithRoles(context.Background(), role.MakeSet(role.Admin)), md1) - outCtx1, conn1, err1 := l.GetConnection(ctx) + outCtx1, conn1, err1 := l.GetConnection(ctx1) assert.NoError(t, err1) assert.NotNil(t, conn1) + assert.Equal(t, role.MakeSet(role.Admin), authz.GetRoles(outCtx1)) mdOut1, ok1 := metadata.FromOutgoingContext(outCtx1) assert.True(t, ok1) assert.Equal(t, []string{"value1", "value2"}, mdOut1.Get("key")) + assert.Equal(t, []string{"os:admin"}, mdOut1.Get("talos-role")) + + t.Run("Same context", func(t *testing.T) { + t.Parallel() + + ctx2 := ctx1 + outCtx2, conn2, err2 := l.GetConnection(ctx2) + assert.NoError(t, err2) + assert.Equal(t, conn1, conn2) // connection is cached + assert.Equal(t, role.MakeSet(role.Admin), authz.GetRoles(outCtx2)) + + mdOut2, ok2 := metadata.FromOutgoingContext(outCtx2) + assert.True(t, ok2) + assert.Equal(t, []string{"value1", "value2"}, mdOut2.Get("key")) + assert.Equal(t, []string{"os:admin"}, mdOut2.Get("talos-role")) + }) + + t.Run("Other context", func(t *testing.T) { + t.Parallel() + + md3 := metadata.New(nil) + md3.Set("key", "value3", "value4") + ctx3 := metadata.NewIncomingContext(authz.ContextWithRoles(context.Background(), role.MakeSet(role.Reader)), md3) - outCtx2, conn2, err2 := l.GetConnection(ctx) - assert.NoError(t, err2) - assert.Equal(t, conn1, conn2) // connection is cached + outCtx3, conn3, err3 := l.GetConnection(ctx3) + assert.NoError(t, err3) + assert.Equal(t, conn1, conn3) // connection is cached + assert.Equal(t, role.MakeSet(role.Reader), authz.GetRoles(outCtx3)) - mdOut2, ok2 := metadata.FromOutgoingContext(outCtx2) - assert.True(t, ok2) - assert.Equal(t, []string{"value1", "value2"}, mdOut2.Get("key")) + mdOut3, ok3 := metadata.FromOutgoingContext(outCtx3) + assert.True(t, ok3) + assert.Equal(t, []string{"value3", "value4"}, mdOut3.Get("key")) + assert.Equal(t, []string{"os:reader"}, mdOut3.Get("talos-role")) + }) } diff --git a/pkg/machinery/config/types/v1alpha1/generate/generate.go b/pkg/machinery/config/types/v1alpha1/generate/generate.go index 767d0084b4c..0c5cee1d564 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/generate.go +++ b/pkg/machinery/config/types/v1alpha1/generate/generate.go @@ -21,6 +21,7 @@ import ( v1alpha1 "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine" "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/machinery/role" ) // Config returns the talos config for a given node type. @@ -382,11 +383,11 @@ func NewTalosCA(currentTime time.Time) (ca *x509.CertificateAuthority, err error } // NewAdminCertificateAndKey generates the admin Talos certificate and key. -func NewAdminCertificateAndKey(currentTime time.Time, ca *x509.PEMEncodedCertificateAndKey, loopback string) (p *x509.PEMEncodedCertificateAndKey, err error) { +func NewAdminCertificateAndKey(currentTime time.Time, ca *x509.PEMEncodedCertificateAndKey, loopback string, role role.Role) (p *x509.PEMEncodedCertificateAndKey, err error) { ips := []net.IP{net.ParseIP(loopback)} opts := []x509.Option{ - x509.Organization(constants.RoleAdmin), + x509.Organization(string(role)), x509.IPAddresses(ips), x509.NotAfter(currentTime.Add(87600 * time.Hour)), x509.NotBefore(currentTime), @@ -432,6 +433,7 @@ func NewInput(clustername, endpoint, kubernetesVersion string, secrets *SecretsB secrets.Clock.Now(), secrets.Certs.OS, loopback, + role.Admin, ) if err != nil { diff --git a/pkg/machinery/config/types/v1alpha1/generate/generate_test.go b/pkg/machinery/config/types/v1alpha1/generate/generate_test.go index c7c1cd74256..df8857958c1 100644 --- a/pkg/machinery/config/types/v1alpha1/generate/generate_test.go +++ b/pkg/machinery/config/types/v1alpha1/generate/generate_test.go @@ -15,6 +15,7 @@ import ( genv1alpha1 "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/generate" "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine" "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/machinery/role" ) type GenerateSuite struct { @@ -86,5 +87,5 @@ func (suite *GenerateSuite) TestGenerateTalosconfigSuccess() { cert, err := x509.ParseCertificate(creds.Crt.Certificate[0]) suite.Require().NoError(err) - suite.Equal([]string{constants.RoleAdmin}, cert.Subject.Organization) + suite.Equal([]string{string(role.Admin)}, cert.Subject.Organization) } diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index 1c44d05ab7e..ab85b7ff2f5 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -414,10 +414,6 @@ const ( // TODO: Once we get naming sorted we need to apply for a project specific address // https://manage.ntppool.org/manage/vendor DefaultNTPServer = "pool.ntp.org" - - // RoleAdmin defines Talos role for admins. - // It matches Organization value of Talos client certificate. - RoleAdmin = "os:admin" ) // See https://linux.die.net/man/3/klogctl diff --git a/pkg/machinery/go.mod b/pkg/machinery/go.mod index efac8b9ddb2..912f3d09adc 100644 --- a/pkg/machinery/go.mod +++ b/pkg/machinery/go.mod @@ -24,7 +24,7 @@ require ( github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/testify v1.7.0 - github.com/talos-systems/crypto v0.2.1-0.20210526123943-7776057f5086 + github.com/talos-systems/crypto v0.2.1-0.20210601174604-cd18ef62eb9f github.com/talos-systems/go-blockdevice v0.2.1-0.20210526155905-30c2bc3cb62a github.com/talos-systems/net v0.2.1-0.20210212213224-05190541b0fa golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea diff --git a/pkg/machinery/go.sum b/pkg/machinery/go.sum index 36d6d80f439..4d9ce32e65f 100644 --- a/pkg/machinery/go.sum +++ b/pkg/machinery/go.sum @@ -144,8 +144,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/talos-systems/crypto v0.2.1-0.20210526123943-7776057f5086 h1:SAyrAftTtxzEUqr9alFt1iezS5vuwCm7/yE8ydR0h+A= -github.com/talos-systems/crypto v0.2.1-0.20210526123943-7776057f5086/go.mod h1:xaNCB2/Bxaj+qrkdeodhRv5eKQVvKOGBBMj58MrIPY8= +github.com/talos-systems/crypto v0.2.1-0.20210601174604-cd18ef62eb9f h1:Xk3zeUZPhvEl9Vs4PlYBohin3QZmizA/YR4URKEyULY= +github.com/talos-systems/crypto v0.2.1-0.20210601174604-cd18ef62eb9f/go.mod h1:xaNCB2/Bxaj+qrkdeodhRv5eKQVvKOGBBMj58MrIPY8= github.com/talos-systems/go-blockdevice v0.2.1-0.20210526155905-30c2bc3cb62a h1:NLuIVKi5tBnRMgxk185AVGmMUzlRcggb2Abrw9uUq3E= github.com/talos-systems/go-blockdevice v0.2.1-0.20210526155905-30c2bc3cb62a/go.mod h1:qnn/zDc09I1DA2BUDDCOSA2D0P8pIDjN8pGiRoRaQig= github.com/talos-systems/go-cmd v0.0.0-20210216164758-68eb0067e0f0/go.mod h1:kf+rZzTEmlDiYQ6ulslvRONnKLQH8x83TowltGMhO+k= diff --git a/pkg/machinery/role/role.go b/pkg/machinery/role/role.go new file mode 100644 index 00000000000..132990255b2 --- /dev/null +++ b/pkg/machinery/role/role.go @@ -0,0 +1,90 @@ +// 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 role + +import ( + "fmt" + "sort" + + "github.com/hashicorp/go-multierror" +) + +// Role represents Talos user role. +// Its string value is used everywhere: as the the Organization value of Talos client certificate, +// as the value of talosctl flag, etc. +type Role string + +const ( + // Admin defines Talos role for admins. + Admin = Role("os:admin") + + // Reader defines Talos role for readers who can access read-only APIs that do not expose secrets. + Reader = Role("os:reader") + + // Impersonator defines internal Talos role for impersonating another user (and their role). + Impersonator = Role("os:impersonator") +) + +// Set represents a set of roles. +type Set map[Role]struct{} + +// all roles, including internal ones. +var all = MakeSet(Admin, Reader, Impersonator) + +// MakeSet makes a set of roles from constants. +// Use Parse in other cases. +func MakeSet(roles ...Role) Set { + res := make(Set, len(roles)) + for _, r := range roles { + res[r] = struct{}{} + } + + return res +} + +// Parse parses a set of roles. +// Returned set is always non-nil and contains all parsed roles, even if some erroneous roles were present. +func Parse(strings []string) (Set, error) { + res := make(Set) + + var err *multierror.Error + + for _, r := range strings { + role := Role(r) + if _, ok := all[role]; !ok { + err = multierror.Append(err, fmt.Errorf("unexpected role %q", r)) + + continue + } + + res[role] = struct{}{} + } + + return res, err.ErrorOrNil() +} + +// Strings returns a set as a slice of strings. +func (s Set) Strings() []string { + res := make([]string, 0, len(s)) + + for r := range s { + res = append(res, string(r)) + } + + sort.Strings(res) + + return res +} + +// IncludesAny returns true if there is a non-empty intersection between sets. +func (s Set) IncludesAny(other Set) bool { + for r := range other { + if _, ok := s[r]; ok { + return true + } + } + + return false +} diff --git a/pkg/machinery/role/role_test.go b/pkg/machinery/role/role_test.go new file mode 100644 index 00000000000..2d35bf2cf3e --- /dev/null +++ b/pkg/machinery/role/role_test.go @@ -0,0 +1,29 @@ +// 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 role_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/talos-systems/talos/pkg/machinery/role" +) + +func TestRole(t *testing.T) { + t.Parallel() + + set, err := role.Parse([]string{"os:admin", "os:reader", "os:wrong", "os:impersonator"}) + assert.EqualError(t, err, "1 error occurred:\n\t* unexpected role \"os:wrong\"\n\n") + assert.Equal(t, role.MakeSet(role.Admin, role.Reader, role.Impersonator), set) + + assert.Equal(t, []string{"os:admin", "os:impersonator", "os:reader"}, set.Strings()) + assert.Equal(t, []string{}, role.Set.Strings(nil)) + + _, ok := set[role.Admin] + assert.True(t, ok) + assert.True(t, set.IncludesAny(role.MakeSet(role.Admin))) + assert.False(t, set.IncludesAny(nil)) +} diff --git a/website/content/docs/v0.11/Reference/cli.md b/website/content/docs/v0.11/Reference/cli.md index 1727df60eee..94312a6a062 100644 --- a/website/content/docs/v0.11/Reference/cli.md +++ b/website/content/docs/v0.11/Reference/cli.md @@ -112,7 +112,7 @@ talosctl cluster create [flags] -h, --help help for create --image string the image to use (default "ghcr.io/talos-systems/talos:latest") --init-node-as-endpoint use init node as endpoint instead of any load balancer endpoint - --initrd-path string the uncompressed kernel image to use (default "_out/initramfs-${ARCH}.xz") + --initrd-path string initramfs image to use (default "_out/initramfs-${ARCH}.xz") -i, --input-dir string location of pre-generated config files --install-image string the installer image to use (default "ghcr.io/talos-systems/installer:latest") --ipv4 enable IPv4 network in the cluster (default true) @@ -1071,9 +1071,9 @@ Generates a set of configuration files for Talos cluster ### Synopsis The cluster endpoint is the URL for the Kubernetes API. If you decide to use - a control plane node, common in a single node control plane setup, use port 6443 as - this is the port that the API server binds to on every control plane node. For an HA - setup, usually involving a load balancer, use the IP and port of the load balancer. +a control plane node, common in a single node control plane setup, use port 6443 as +this is the port that the API server binds to on every control plane node. For an HA +setup, usually involving a load balancer, use the IP and port of the load balancer. ``` talosctl gen config [flags] @@ -1155,9 +1155,10 @@ talosctl gen csr [flags] ### Options ``` - -h, --help help for csr - --ip string generate the certificate for this IP address - --key string path to the PEM encoded EC or RSA PRIVATE KEY + -h, --help help for csr + --ip string generate the certificate for this IP address + --key string path to the PEM encoded EC or RSA PRIVATE KEY + --roles strings roles (default [os:admin]) ``` ### Options inherited from parent commands