Skip to content

Commit

Permalink
feat: add 'os:operator' role
Browse files Browse the repository at this point in the history
This introduces a new role for Talos API which fills the gap between
`os:reader` and `os:admin` roles.

Fixes #6898

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
  • Loading branch information
smira committed Mar 1, 2023
1 parent 40e69af commit 337aaba
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 65 deletions.
3 changes: 2 additions & 1 deletion cmd/talosctl/cmd/mgmt/inject/serviceaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/siderolabs/talos/pkg/kubernetes/inject"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/role"
)

var serviceAccountCmdFlags struct {
Expand Down Expand Up @@ -62,7 +63,7 @@ cat deployment.yaml | talosctl inject %[1]s --roles="os:admin" -f - > deployment
func init() {
serviceAccountCmd.Flags().StringVarP(&serviceAccountCmdFlags.file, "file", "f", "",
fmt.Sprintf("file with Kubernetes manifests to be injected with %s", constants.ServiceAccountResourceKind))
serviceAccountCmd.Flags().StringSliceVarP(&serviceAccountCmdFlags.roles, "roles", "r", []string{"os:reader"},
serviceAccountCmd.Flags().StringSliceVarP(&serviceAccountCmdFlags.roles, "roles", "r", []string{string(role.Reader)},
fmt.Sprintf("roles to add to the generated %s manifests", constants.ServiceAccountResourceKind))
Cmd.AddCommand(serviceAccountCmd)
}
8 changes: 8 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ Talos now supports resetting user disks through the Reset API,
the list of disks to wipe is set using the `--user-disks-to-wipe` parameter in `talosctl`.
Additionally, the Reset API can now function in maintenance mode
and has the capability to wipe the node's system disk (partial wipe is not supported).
"""

[notes.roles]
title = "New Talos API os:operator role"
description="""\
Talos now supports a new `os:operator` role for the Talos API.
This role allows everything `os:reader` role allows plus access to maintenance APIs:
rebooting, shutting down a node, accessing packet capture, etcd alarm APIs, etcd backup, etc.
"""

[make_deps]
Expand Down
84 changes: 42 additions & 42 deletions internal/app/machined/pkg/system/services/machined.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,72 +30,72 @@ import (
const machinedServiceID = "machined"

var rules = map[string]role.Set{
"/cluster.ClusterService/HealthCheck": role.MakeSet(role.Admin, role.Reader),
"/cluster.ClusterService/HealthCheck": role.MakeSet(role.Admin, role.Operator, role.Reader),

"/inspect.InspectService/ControllerRuntimeDependencies": role.MakeSet(role.Admin, role.Reader),
"/inspect.InspectService/ControllerRuntimeDependencies": role.MakeSet(role.Admin, role.Operator, role.Reader),

"/machine.MachineService/ApplyConfiguration": role.MakeSet(role.Admin),
"/machine.MachineService/Bootstrap": role.MakeSet(role.Admin),
"/machine.MachineService/CPUInfo": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/Containers": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/CPUInfo": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Containers": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Copy": role.MakeSet(role.Admin),
"/machine.MachineService/DiskStats": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/DiskUsage": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/Dmesg": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/EtcdAlarmList": role.MakeSet(role.Admin),
"/machine.MachineService/EtcdAlarmDisarm": role.MakeSet(role.Admin),
"/machine.MachineService/EtcdDefragment": role.MakeSet(role.Admin),
"/machine.MachineService/DiskStats": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/DiskUsage": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Dmesg": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/EtcdAlarmList": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/EtcdAlarmDisarm": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/EtcdDefragment": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/EtcdForfeitLeadership": role.MakeSet(role.Admin),
"/machine.MachineService/EtcdLeaveCluster": role.MakeSet(role.Admin),
"/machine.MachineService/EtcdMemberList": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/EtcdMemberList": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/EtcdRecover": role.MakeSet(role.Admin),
"/machine.MachineService/EtcdRemoveMember": role.MakeSet(role.Admin),
"/machine.MachineService/EtcdRemoveMemberByID": role.MakeSet(role.Admin),
"/machine.MachineService/EtcdSnapshot": role.MakeSet(role.Admin, role.EtcdBackup),
"/machine.MachineService/EtcdStatus": role.MakeSet(role.Admin),
"/machine.MachineService/Events": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/EtcdSnapshot": role.MakeSet(role.Admin, role.Operator, role.EtcdBackup),
"/machine.MachineService/EtcdStatus": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/Events": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/GenerateClientConfiguration": role.MakeSet(role.Admin),
"/machine.MachineService/GenerateConfiguration": role.MakeSet(role.Admin),
"/machine.MachineService/Hostname": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/Hostname": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Kubeconfig": role.MakeSet(role.Admin),
"/machine.MachineService/List": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/LoadAvg": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/Logs": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/Memory": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/Mounts": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/NetworkDeviceStats": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/PacketCapture": role.MakeSet(role.Admin),
"/machine.MachineService/Processes": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/List": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/LoadAvg": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Logs": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Memory": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Mounts": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/NetworkDeviceStats": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/PacketCapture": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/Processes": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Read": role.MakeSet(role.Admin),
"/machine.MachineService/Reboot": role.MakeSet(role.Admin),
"/machine.MachineService/Reboot": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/Reset": role.MakeSet(role.Admin),
"/machine.MachineService/Restart": role.MakeSet(role.Admin),
"/machine.MachineService/Restart": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/Rollback": role.MakeSet(role.Admin),
"/machine.MachineService/ServiceList": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/ServiceRestart": role.MakeSet(role.Admin),
"/machine.MachineService/ServiceStart": role.MakeSet(role.Admin),
"/machine.MachineService/ServiceStop": role.MakeSet(role.Admin),
"/machine.MachineService/Shutdown": role.MakeSet(role.Admin),
"/machine.MachineService/Stats": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/SystemStat": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/ServiceList": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/ServiceRestart": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/ServiceStart": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/ServiceStop": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/Shutdown": role.MakeSet(role.Admin, role.Operator),
"/machine.MachineService/Stats": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/SystemStat": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/machine.MachineService/Upgrade": role.MakeSet(role.Admin),
"/machine.MachineService/Version": role.MakeSet(role.Admin, role.Reader),
"/machine.MachineService/Version": role.MakeSet(role.Admin, role.Operator, role.Reader),

// per-type authorization is handled by the service itself
"/resource.ResourceService/Get": role.MakeSet(role.Admin, role.Reader),
"/resource.ResourceService/List": role.MakeSet(role.Admin, role.Reader),
"/resource.ResourceService/Watch": role.MakeSet(role.Admin, role.Reader),
"/resource.ResourceService/Get": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/resource.ResourceService/List": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/resource.ResourceService/Watch": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/cosi.resource.State/Create": role.MakeSet(role.Admin),
"/cosi.resource.State/Destroy": role.MakeSet(role.Admin),
"/cosi.resource.State/Get": role.MakeSet(role.Admin, role.Reader),
"/cosi.resource.State/List": role.MakeSet(role.Admin, role.Reader),
"/cosi.resource.State/Get": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/cosi.resource.State/List": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/cosi.resource.State/Update": role.MakeSet(role.Admin),
"/cosi.resource.State/Watch": role.MakeSet(role.Admin, role.Reader),
"/cosi.resource.State/Watch": role.MakeSet(role.Admin, role.Operator, role.Reader),

"/storage.StorageService/Disks": role.MakeSet(role.Admin, role.Reader),
"/storage.StorageService/Disks": role.MakeSet(role.Admin, role.Operator, role.Reader),

"/time.TimeService/Time": role.MakeSet(role.Admin, role.Reader),
"/time.TimeService/TimeCheck": role.MakeSet(role.Admin, role.Reader),
"/time.TimeService/Time": role.MakeSet(role.Admin, role.Operator, role.Reader),
"/time.TimeService/TimeCheck": role.MakeSet(role.Admin, role.Operator, role.Reader),
}

type machinedService struct {
Expand Down
78 changes: 61 additions & 17 deletions internal/integration/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,15 @@ func (suite *TalosconfigSuite) TestNew() {

node := suite.RandomDiscoveredNodeInternalIP(machine.TypeControlPlane)

readerConfig := filepath.Join(tempDir, "talosconfig")
readerConfig := filepath.Join(tempDir, "readerconfig")
suite.RunCLI([]string{"--nodes", node, "config", "new", "--roles", "os:reader", readerConfig},
base.StdoutEmpty())

// commands that work for both admin and reader, with and without RBAC
operatorConfig := filepath.Join(tempDir, "operatorconfig")
suite.RunCLI([]string{"--nodes", node, "config", "new", "--roles", "os:operator", operatorConfig},
base.StdoutEmpty())

// commands that work for admin, operator and reader, with and without RBAC
for _, tt := range []struct {
args []string
opts []base.RunOption
Expand All @@ -105,24 +109,26 @@ func (suite *TalosconfigSuite) TestNew() {
suite.Run(name, func() {
suite.T().Parallel()

args := append([]string{"--nodes", node}, tt.args...)
suite.RunCLI(args, tt.opts...)
for _, config := range []string{readerConfig, operatorConfig} {
args := append([]string{"--nodes", node}, tt.args...)
suite.RunCLI(args, tt.opts...)

args = append([]string{"--talosconfig", readerConfig}, args...)
suite.RunCLI(args, tt.opts...)
args = append([]string{"--talosconfig", config}, args...)
suite.RunCLI(args, tt.opts...)
}
})
}

// commands that work for admin, but not for reader (when RBAC is enabled)
// commands that work for admin, but not for reader&operator (when RBAC is enabled)
for _, tt := range []struct {
args []string
adminOpts []base.RunOption
readerOpts []base.RunOption
args []string
adminOpts []base.RunOption
nonprivOpts []base.RunOption
}{
{
args: []string{"read", "/etc/hosts"},
adminOpts: []base.RunOption{base.StdoutShouldMatch(regexp.MustCompile(`localhost`))},
readerOpts: []base.RunOption{
nonprivOpts: []base.RunOption{
base.StdoutEmpty(),
base.StderrShouldMatch(regexp.MustCompile(`\Qrpc error: code = PermissionDenied desc = not authorized`)),
base.ShouldFail(),
Expand All @@ -131,7 +137,7 @@ func (suite *TalosconfigSuite) TestNew() {
{
args: []string{"get", "mc"},
adminOpts: []base.RunOption{base.StdoutShouldMatch(regexp.MustCompile(`MachineConfig`))},
readerOpts: []base.RunOption{
nonprivOpts: []base.RunOption{
base.ShouldFail(),
base.StdoutShouldMatch(regexp.MustCompile(`\QNODE NAMESPACE TYPE ID VERSION`)),
base.StderrShouldMatch(regexp.MustCompile(`\Qrpc error: code = PermissionDenied desc = not authorized`)),
Expand All @@ -140,7 +146,7 @@ func (suite *TalosconfigSuite) TestNew() {
{
args: []string{"get", "osrootsecret"},
adminOpts: []base.RunOption{base.StdoutShouldMatch(regexp.MustCompile(`OSRootSecret`))},
readerOpts: []base.RunOption{
nonprivOpts: []base.RunOption{
base.ShouldFail(),
base.StdoutShouldMatch(regexp.MustCompile(`\QNODE NAMESPACE TYPE ID VERSION`)),
base.StderrShouldMatch(regexp.MustCompile(`\Qrpc error: code = PermissionDenied desc = not authorized`)),
Expand All @@ -149,7 +155,7 @@ func (suite *TalosconfigSuite) TestNew() {
{
args: []string{"kubeconfig", "--force", tempDir},
adminOpts: []base.RunOption{base.StdoutEmpty()},
readerOpts: []base.RunOption{
nonprivOpts: []base.RunOption{
base.ShouldFail(),
base.StdoutEmpty(),
base.StderrShouldMatch(regexp.MustCompile(`\Qrpc error: code = PermissionDenied desc = not authorized`)),
Expand All @@ -164,12 +170,50 @@ func (suite *TalosconfigSuite) TestNew() {
args := append([]string{"--nodes", node}, tt.args...)
suite.RunCLI(args, tt.adminOpts...)

args = append([]string{"--talosconfig", readerConfig}, args...)
for _, config := range []string{readerConfig, operatorConfig} {
if rbacEnabled {
suite.RunCLI(append([]string{"--talosconfig", config}, args...), tt.nonprivOpts...)
} else {
// check that it works the same way as for admin with reader's config
suite.RunCLI(append([]string{"--talosconfig", config}, args...), tt.adminOpts...)
}
}
})
}

// commands which work for operator, but not reader (when RBAC is enabled)
for _, tt := range []struct {
args []string
privOpts []base.RunOption
nonprivOpts []base.RunOption
}{
{
args: []string{"etcd", "alarm", "list"},
privOpts: []base.RunOption{
base.StdoutEmpty(),
},
nonprivOpts: []base.RunOption{
base.StdoutEmpty(),
base.StderrShouldMatch(regexp.MustCompile(`\Qrpc error: code = PermissionDenied desc = not authorized`)),
base.ShouldFail(),
},
},
} {
tt := tt
name := strings.Join(tt.args, "_")
suite.Run(name, func() {
suite.T().Parallel()

args := append([]string{"--nodes", node}, tt.args...)
suite.RunCLI(args, tt.privOpts...)

suite.RunCLI(append([]string{"--talosconfig", operatorConfig}, args...), tt.privOpts...)

if rbacEnabled {
suite.RunCLI(args, tt.readerOpts...)
suite.RunCLI(append([]string{"--talosconfig", readerConfig}, args...), tt.nonprivOpts...)
} else {
// check that it works the same way as for admin with reader's config
suite.RunCLI(args, tt.adminOpts...)
suite.RunCLI(append([]string{"--talosconfig", readerConfig}, args...), tt.privOpts...)
}
})
}
Expand Down
7 changes: 5 additions & 2 deletions pkg/machinery/role/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ const (
// Prefix for all built-in roles.
Prefix = string("os:")

// Admin defines Talos role for admins.
// Admin defines Talos role for admins (every API is available).
Admin = Role(Prefix + "admin")

// Operator defines Talos role for operators (Reader + management APIs which do not allow secret access, e.g. rebooting a node).
Operator = Role(Prefix + "operator")

// Reader defines Talos role for readers who can access read-only APIs that do not expose secrets.
Reader = Role(Prefix + "reader")

Expand All @@ -41,7 +44,7 @@ type Set struct {

var (
// All roles that can be granted to users.
All = MakeSet(Admin, Reader, EtcdBackup, Impersonator)
All = MakeSet(Admin, Operator, Reader, EtcdBackup, Impersonator)

// Zero is an empty set of roles.
Zero = MakeSet()
Expand Down
6 changes: 3 additions & 3 deletions pkg/machinery/role/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import (
func TestSet(t *testing.T) {
t.Parallel()

roles, unknownRoles := role.Parse([]string{"os:admin", "os:reader", "os:future", "os:impersonator", "", " "})
roles, unknownRoles := role.Parse([]string{"os:admin", "os:operator", "os:reader", "os:future", "os:impersonator", "", " "})
assert.Equal(t, []string{"os:future"}, unknownRoles)
assert.Equal(t, role.MakeSet(role.Admin, role.Reader, role.Role("os:future"), role.Impersonator), roles)
assert.Equal(t, role.MakeSet(role.Admin, role.Operator, role.Reader, role.Role("os:future"), role.Impersonator), roles)

assert.Equal(t, []string{"os:admin", "os:future", "os:impersonator", "os:reader"}, roles.Strings())
assert.Equal(t, []string{"os:admin", "os:future", "os:impersonator", "os:operator", "os:reader"}, roles.Strings())
assert.Equal(t, []string(nil), role.MakeSet().Strings())

assert.True(t, roles.Includes(role.Admin))
Expand Down
1 change: 1 addition & 0 deletions website/content/v1.4/talos-guides/configuration/rbac.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The certificate subject's organization field is used to encode user roles.
There is a set of predefined roles that allow access to different [API methods]({{< relref "../../reference/api" >}}):

* `os:admin` grants access to all methods;
* `os:operator` grants everything `os:reader` role does, plus additional methods: rebooting, shutting down, etcd backup, etcd alarm management, and so on;
* `os:reader` grants access to "safe" methods (for example, that includes the ability to list files, but does not include the ability to read files content);
* `os:etcd:backup` grants access to [`/machine.MachineService/EtcdSnapshot`]({{< relref "../../reference/api#machine.EtcdSnapshotRequest" >}}) method.

Expand Down

0 comments on commit 337aaba

Please sign in to comment.