Role-Based Access Control
Overview
VTAdmin provides an (optional) role-based access control (RBAC) system for deployments that need to, or would like to, restrict access to specific resources to specific users.
In VTAdmin, RBAC is governed by two distinct layers:
- Authentication: Given a request, determine who is attempting to take an action on a resource
- Authorization: Given an actor (obtained from the authentication layer), determine if that actor is allowed to take a certain action on a certain resource.
Let’s discuss each in turn.
Authentication
VTAdmin uses a plugin-based architecture for deployments to provide their own authentication implementation specific to their environment and needs.
The authentication plugin is installed as both an HTTP middleware and gRPC interceptor, and must implement the following interface:
type Authenticator interface {
// Authenticate returns an Actor given a context. This method is called
// from the stream and unary grpc server interceptors, and are passed the
// stream and request contexts, respectively.
//
// Returning an error from the authenticator will fail the request. To
// denote an authenticated request, return (nil, nil) instead.
Authenticate(ctx context.Context) (*Actor, error)
// AuthenticateHTTP returns an actor given an http.Request.
//
// Returning an error from the authenticator will fail the request. To
// denote an authenticated request, return (nil, nil) instead.
AuthenticateHTTP(r *http.Request) (*Actor, error)
}
If running with an authentication plugin installed, VTAdmin will invoke its Authenticate
method on all incoming gRPC requests, and its AuthenticateHTTP
method on all incoming HTTP requests.
Returning an error from either of these methods will fail the request with an UNAUTHENTICATED
code for gRPC requests and an UNAUTHORIZED
response for HTTP requests. In order to indicate “no authenticated actor” to the authorization layer, the methods must return (nil, nil)
instead.
Available Plugins
VTAdmin currently provides no authentication plugins out of the box, though this may change in future releases.
However, users are free to define their own implementations suited to the needs of their specific deployment and environment. As an example, here is an authentication plugin that extracts a “user” key from an HTTP cookie or gRPC incoming metadata.
Installing Plugins
VTAdmin supports two ways of installing an authentication plugin.
For universal support, users may recompile vtadmin-api
after adding their authentication plugin file within go/vt/vtadmin/rbac/plugin_<your_authn_name>.go
. If following this process, you must ensure to call RegisterAuthenticator("your_authn_name", yourAuthnConstructor())
in an init
function. This is the pattern followed by other components of Vitess; tracing plugins are one of many you can refer to.
If you plan to run vtadmin-api
on Linux, FreeBSD, or macOS, you can also install your authentication plugin using the Go plugin API. If following this process, your Authenticator must be built with go build -buildmode=plugin
, and its main
package must expose a function of the following name and type:
package main
import "vitess.io/vitess/go/vt/vtadmin/rbac"
func NewAuthenticator() rbac.Authenticator { return ... /* your implementation here */ }
Configuration
Finally, to instruct VTAdmin to use your Authenticator, specify its name in the "authenticator"
key in your rbac.yaml
:
authenticator: "./path/to/your_authn_name.so" # or just "your_authn_name" (see below)
If the name ends in .so
, VTAdmin will assume it is a Go plugin (the second option described in the previous section). VTAdmin will attempt to open the plugin and find a function named NewAuthenticator
that returns an rbac.Authenticator
implementation. If any of this fails, VTAdmin will refuse to start; attempting to use this option on platforms not supported by the Go plugin API will result in undefined behavior.
Otherwise, VTAdmin will assume it was (re-)compiled with a plugin_<your_authn_name>.go
file that invoked RegisterAuthenticator
with that name. If there is no plugin registered with that name, VTAdmin will refuse to start.
Authorization
Unlike authentication, which occurs at the incoming request boundary (both HTTP and gRPC), authorization happens within the vtadmin.API
layer itself.
In each method, the API extracts any Actor
from the authentication layer, and performs one or more checks to see if that actor is allowed to perform the actions necessary to fulfill the request. We’ll go over how this works in more detail, but as an example, here’s a snippet of the GetClusters
handler:
func (api *API) GetClusters(ctx context.Context, req *vtadminpb.GetClustersRequest) (*vtadminpb.GetClustersResponse, error) {
clusters, _ := api.getClustersForRequest(nil) // `nil` implies "all clusters"
resp := &vtadminpb.GetClustersResponse{
Clusters: make([]*vtadminpb.Cluster, 0, len(clusters)),
}
for _, c := range clusters {
if !api.authz.IsAuthorized(ctx, c.ID, rbac.ClusterResource, rbac.GetAction) {
continue
}
resp.Clusters = append(resp.Clusters, &vtadminpb.Cluster{
Id: c.ID,
Name: c.Name,
})
}
return resp, nil
}
First, it’s necessary to note that there’s a shim layer in the HTTP/gRPC middlewares that puts any Actor
from an authentication plugin into the ctx
that gets passed to the method you see here. The details of how this works are not particularly relevant to this documentation, but you can refer to these files if you would like to learn more.
Second, it is possible to run VTAdmin with authorization but without an authentication plugin installed. If you do this, all requests will implicitly be made by the “unauthenticated” actor, and therefore may only access resources that permit the wildcard Subject
(more on RBAC configs in a bit!).
Third, and most important: note that being unauthorized to access to a (cluster, resource, action)
does not fail the overall request. If a request involves multiple clusters, and the actor is permitted to access a subset of them, the request will proceed for those clusters. If a user tries to access a cluster they are not permitted to, including a cluster that does not exist, they will be unable to tell if (1) there is simply no data; (2) they do not have access to the cluster; or (3) the cluster exists at all. This is by design, to prevent a malicious actor from being able to enumerate resources by brute force and interpreting the authorization failure responses.
Configuration
Authorization rules are specified as a list under the rules
key of your rbac.yaml
configuration file. Each rule is a 4-key map, corresponding to the 4-tuple of (resource, cluster, subject, action)
.
In order to allow more consisely-expressed configurations, each “rule” element actually takes a list of clusters
, subjects
, and actions
(but only a singular resource
!), as well as a wildcard (*
) to stand in for “any {resource|cluster|subject|action}”. At startup, vtadmin-api
will expand these rulesets and wildcards into the individual 4-tuples discussed previously.
Example
For example, take the following configuration:
rules:
- resource: "*"
actions:
- "get"
- "ping"
subjects: ["*"]
clusters: ["*"]
- resource: "*"
actions:
- "create"
- "delete"
- "put"
subjects:
- "user:andrew"
- "role:admin"
clusters: ["*"]
- resource: "Shard"
actions:
- "emergency_failover_shard"
- "planned_failover_shard"
subjects:
- "role:admin"
clusters:
- "local"
This permits the following:
- Any subject can
get
orping
any resource in any cluster. - Any user with the name “andrew” or role of “admin” can
create
,delete
, orput
any resource in any cluster. - Any user with the role of “admin” can perform both emergency and planned failover operations on a
Shard
in only the cluster with the id of “local”.
Clusters and Subjects
cluster
and subject
values depend entirely on the details of your particular vtadmin deployment. Possible values for cluster
, aside from the wildcard, are the id
of any cluster you inform vtadmin-api
of (either via flags at start time or dynamically).
subject
values should be prefixed with either user:
or role:
. In the case of user:
, vtadmin’s authorization check will verify the actor’s Name
value matches. In the case of role:
, it will verify that one of the actor’s Roles
values matches. In code:
func (r *Rule) Allows(clusterID string, action Action, actor *Actor) bool {
if r.clusters.HasAny("*", clusterID) {
if r.actions.HasAny("*", string(action)) {
if r.subjects.Has("*") {
return true
}
if actor == nil {
return false
}
if r.subjects.Has(fmt.Sprintf("user:%s", actor.Name)) {
return true
}
for _, role := range actor.Roles {
if r.subjects.Has(fmt.Sprintf("role:%s", role)) {
return true
}
}
}
}
return false
}
Note that if you are using just authorization without authentication, you must use the wildcard subject in your rules.
Resources and Actions
The following table lists all current resources vtadmin has, and the actions that can be performed on them. Note that it’s technically possible to specify a rule for an action that cannot actually be performed on a particular resource (e.g. an action of planned_failover_shard
on a resource of Schema
), but this has no effect on the rest of your rules.
API | Rule(s) Needed (<action>, <resource>) form |
---|---|
CreateKeyspace | (create, Keyspace) |
CreateShard | (create, Shard) |
DeleteKeyspace | (delete, Keyspace) |
DeleteShards | (delete, Shard) |
DeleteTablet | (delete, Tablet) |
EmergencyFailoverShard | (emergency_failover_shard, Shard) |
FindSchema | (get, Schema) |
GetBackups | (get, Backup) |
GetCellInfos | (get, CellInfo) |
GetCellsAliases | (get, CellsAlias) |
GetClusters | (get, Cluster) |
GetGates | (get, VTGate) |
GetKeyspace | (get, Keyspace) |
GetKeyspaces | (get, Keyspace) |
GetSchema | (get, Schema) |
GetSchemas | (get, Schema) |
GetShardReplicationPositions | (get, ShardReplicationPosition) |
GetSrvVSchema | (get, SrvVSchema) |
GetSrvVSchemas | (get, SrvVSchema) |
GetTablet | (get, Tablet) |
GetTablets | (get, Tablet) |
GetVSchema | (get, VSchema) |
GetVSchemas | (get, VSchema) |
GetVtctlds | (get, Vtctld) |
GetWorkflow | (get, Workflow) |
GetWorkflows | (get, Workflow) |
PingTablet | (ping, Tablet) |
PlannedFailoverShard | (planned_failover_shard, Shard) |
RefreshState | (put, Tablet) |
RefreshTabletReplicationSource | (refresh_tablet_replication_source, Tablet) |
ReloadSchemas | (reload, Schema) |
RunHealthCheck | (get, Tablet) |
SetReadOnly | (manage_tablet_writability, Tablet) |
SetReadWrite | (manage_tablet_writability, Tablet) |
StartReplication | (manage_tablet_replication, Tablet) |
StopReplication | (manage_tablet_replication, Tablet) |
TabletExternallyPromoted | (tablet_externally_promoted, Shard) |
VTExplain | (get, VTExplain) |
ValidateKeyspace | (put, Keyspace) |
ValidateSchemaKeyspace | (put, Keyspace) |
ValidateVersionKeyspace | (put, Keyspace) |