A | B |
---|---|
Project | LXD |
Status | Rejected |
Author(s) | @markylaing |
Approver(s) | @tomp |
Release | |
Internal ID | LX055 |
This specification was implemented and subsequently reverted in favour of the Identity and Access Management Specification.
Abstract
Add a fine-grained authorization driver to LXD which uses OpenFGA (Open Fine-Grained Authorization) Relationship-Based Access Control (ReBAC).
Rationale
When using OIDC in LXD, there is currently no way to restrict access to particular projects and/or resources. The current RBAC authorization driver is tied to Candid authentication which will one-day be sunset in favour of OIDC. Additionally, authorization for RBAC and TLS can restrict users to particular resource types within a project, but cannot restrict users to a single resource (e.g. a single instance). Finally, the RBAC roles are not configurable by an end user. With the OpenFGA driver it will be possible to restrict users to a configurable set of entitlements on individual API resources.
Specification
Configuring the OpenFGA driver
To enable the OpenFGA authorization driver, the user must have a running OpenFGA server that is configured to use pre-shared key authentication. Additionally, the user must have created an OpenFGA store for LXD to use (this can be performed using the OpenFGA CLI). The user must then set the following configuration keys in LXD:
openfga.api.url
: The HTTP API URL of the OpenFGA server.openfga.api.token
: The pre-shared authentication key configured on the OpenFGA server.openfga.store.id
: The ID of the pre-created OpenFGA store.
The LXD cluster member that receives the updated configuration will check that all of the above configuration keys are present and if so, write the Authorization Model (see below) returning an authorization model ID. The initial member will write this value to another configuration key in the LXD cluster database: openfga.store.model_id
. Once this key is set, the initial cluster member will notify remaining members of the configuration change. This ensures that all members use the same authorization model ID as recommended by OpenFGA.
Each LXD cluster member will then synchronize API resources with OpenFGA by writing Relationship Tuples to the store. The format of these tuples is defined by the Authorization model. For example, to encode into OpenFGA that there exists a project named “project01” we write the following tuple:
user: "server:lxd"
relation: "server"
object: "project:project01"
All resources that exist in LXD but do not exist in the store will be added to the store. All resources that exist in the store but do not exist in LXD will be deleted. This ensures there are no dangling tuples in the OpenFGA store, as this could lead to permission leakage. The OpenFGA authorization driver is ready when all resources have been synchronized.
After the initial synchronization is performed. LXD must continue to keep all resources in sync. This will be performed via a call to the OpenFGA driver whenever a salient resource is created, renamed, or deleted. This will be performed on a best-effort basis as failures can be fixed by an administrator.
Daemon startup
On start up, the daemon will detect if the OpenFGA driver has been configured by checking for the configuration keys above. If the three configurable keys have been set, but openfga.store.model_id
is unset, this indicates that LXD previously failed to configure OpenFGA and the default TLS driver will be loaded. If the openfga.store.model_id
is non-empty, the driver will ensure that the loaded model is identical to the hard coded model defined below.
Authorization Model
The initial OpenFGA authorization model for LXD will be:
model
schema 1.1
type user
type group
relations
define member: [user]
type server
relations
define admin: [user, group#member]
define operator: [user, group#member] or admin
define viewer: [user, group#member] or operator
define user: [user:*]
define can_edit: admin
define can_view: user
define can_create_storage_pools: [user, group#member] or admin
define can_create_projects: [user, group#member] or operator
define can_view_resources: [user, group#member] or viewer
define can_create_certificates: [user, group#member] or admin
define can_view_metrics: [user, group#member] or viewer
define can_override_cluster_target_restriction: [user, group#member] or admin
define can_view_privileged_events: [user, group#member] or admin
type certificate
relations
define server: [server]
define can_edit: [user, group#member] or admin from server
define can_view: user from server
type storage_pool
relations
define server: [server]
define can_edit: [user, group#member] or admin from server
define can_view: user from server
type project
relations
define server: [server]
define manager: [user, group#member] or operator from server
define operator: [user, group#member] or manager or operator from server
define viewer: [user, group#member] or operator
define can_edit: manager
define can_view: viewer
define can_create_images: [user, group#member] or operator or operator from server
define can_create_image_aliases: [user, group#member] or operator or operator from server
define can_create_instances: [user, group#member] or operator or operator from server
define can_create_networks: [user, group#member] or operator or operator from server
define can_create_network_acls: [user, group#member] or operator or operator from server
define can_create_network_zones: [user, group#member] or operator or operator from server
define can_create_profiles: [user, group#member] or operator or operator from server
define can_create_storage_volumes: [user, group#member] or operator or operator from server
define can_create_storage_buckets: [user, group#member] or operator or operator from server
define can_view_operations: [user, group#member] or viewer
define can_view_events: [user, group#member] or viewer
type image
relations
define project: [project]
define can_edit: [user, group#member] or operator from project
define can_view: [user, group#member] or can_edit or viewer from project
type image_alias
relations
define project: [project]
define can_edit: [user, group#member] or operator from project
define can_view: [user, group#member] or can_edit or viewer from project
type instance
relations
define project: [project]
define manager: [user, group#member]
define operator: [user, group#member] or manager
define user: [user, group#member] or operator
define viewer: [user, group#member] or operator
define can_edit: manager or operator from project
define can_view: user or viewer or viewer from project
define can_update_state: [user, group#member] or operator or operator from project
define can_manage_snapshots: [user, group#member] or operator or operator from project
define can_manage_backups: [user, group#member] or operator or operator from project
define can_connect_sftp: [user, group#member] or user or operator from project
define can_access_files: [user, group#member] or user or operator from project
define can_access_console: [user, group#member] or user or operator from project
define can_exec: [user, group#member] or user or operator from project
type network
relations
define project: [project]
define can_edit: [user, group#member] or operator from project
define can_view: [user, group#member] or can_edit or viewer from project
type network_acl
relations
define project: [project]
define can_edit: [user, group#member] or operator from project
define can_view: [user, group#member] or can_edit or viewer from project
type network_zone
relations
define project: [project]
define can_edit: [user, group#member] or operator from project
define can_view: [user, group#member] or can_edit or viewer from project
type profile
relations
define project: [project]
define can_edit: [user, group#member] or operator from project
define can_view: [user, group#member] or can_edit or viewer from project
type storage_volume
relations
define project: [project]
define can_edit: [user, group#member] or operator from project
define can_view: [user, group#member] or can_edit or viewer from project
define can_manage_snapshots: [user, group#member] or can_edit
define can_manage_backups: [user, group#member] or can_edit
type storage_bucket
relations
define project: [project]
define can_edit: [user, group#member] or operator from project
define can_view: [user, group#member] or can_edit or viewer from project
This model will be hard-coded into LXD and cannot be configured by a user. This is because LXD relies on the correctness of the naming of types and relations in the model. It will be possible to migrate this model to a new version for future versions of LXD (see Migrations under Further Information).
Some key features of this model are as follows:
- The
user
type will be used to grant permissions to actual users of LXD. - A
user
can have relationmember
to agroup
. This allows an administrator to create custom sets of permissions and assign them to groups of useres. - The
server
type represents the top level resource for LXD. All subsequent types are children ofserver
(whether directly or viaproject
). There will only ever be a singleserver
object in the OpenFGA store. It will be namedserver:lxd
. - Some relations in the model are associated with particular actions that can be performed on that resource. For example, the relation
can_create_storage_pools
defined on the typeserver
. These relations are the most fine-grained and always take the formcan_perform_action
. - Some relations in the model are referenced by the more fine-grained actions. These relations allow us to build into the model some common use-cases. These are outlined below:
- The
admin
relation defined on typeserver
would grant a user (or group) full access to the LXD server. - The
operator
relation defined on typeserver
grants a user (or group) permission to view server level resources, but not edit them. An operator can create and manage projects and all project level resources. - The
viewer
relation defined onserver
allows a user to view all server level resources but not edit them. A server viewer cannot view projects or their contents. - The
user
relation defined onserver
is a type-bound public access. This is specifically to allow for any authenticated user to query the/1.0
endpoint (lxc info
) and is required for thelxc
CLI to function for users without any server level permissions. Additionally, theuser
relation is referenced in thecan_view
relation defined on thestorage_pool
type. This is to allow e.g. a project operator to see which storage volumes are available when they interact with storage volumes or buckets. - The
manager
relation defined on typeproject
grants full access to a single project, including access to edit it’s configuration. - The
operator
relation defined on typeproject
grants a user or group permission to view, create, edit, and delete all resources within a project, but not edit the project configuration. - The
viewer
relation defined on typeproject
grants a user or group permission to view project resources but not edit them. - The
manager
relation defined on typeinstance
grants a user or group permission full access to a single instance, including editing configuration and deletion. - The
operator
relation defined on typeinstance
grants a user or group permission to update the state of an instance, manage snapshots and backups, and interact with instance files or exec into it, but not the ability to edit configuration or delete it. - The
user
relation defined on typeinstance
grants a user access to exec into the instance, access it’s files and so on, but not the ability to start/stop or manage backups. Note that a user may still callshutdown
from within the instance unless further protections are in place.
- The
Entity names
API resources can have the same name if they are contained within different projects. Therefore all objects stored in the OpenFGA store (as part of a tuple) will have the following format: {OpenFGA type}:{projectName}/{entityName}
. If the entity name requires multiple fields (for example, to identity a unique storage volume we require the project name, storage pool name, and volume type) the fields of the entity name will be delimited by a forward slash. Therefore a custom storage volume vol1
in the default pool and default project will be represented in the store with the following tuples:
# Server to project
user: "server:lxd"
relation: "server"
object: "project:default"
# Storage volume to project
user: "project:default"
relation: "project"
object: "storage_pool_volume:default/default/custom/vol1"
All elements of the entity name will be path escaped so that we are able to construct and deconstruct them reliably.
Authorization flow
Once a user is authenticated, either via OIDC or TLS, their username is embedded in the request context. For OIDC authentication the username is their email address, for TLS authentication the username is their certificate fingerprint.
When an API route relates to a single resource (e.g. GET /1.0/instances/{instanceName}
) the OpenFGA driver will check that the user has the required relation against that particular resource. If the user does not have the required relation, the request is aborted with HTTP status code 403 Forbidden. Otherwise the request is allowed to continue.
When an API route relates to multiple resources (e.g. GET /1.0/instances
), we will list all resources for which the user has the required relation and filter the result accordingly. This has side-effect that changes API behaviour slightly compared with RBAC or TLS drivers. With RBAC, if a user does not have view
permission for project project01
, then GET /1.0/instances?project=project01
will return a 403 Forbidden. However, the OpenFGA driver will return a 200 OK but the list will be empty.
API Changes
None.
CLI Changes
None.
Database changes
None.
Further information
Migrations
OpenFGA model migrations are not covered in this initial specification however, future work could consider the approach detailed here: OpenFGA authorization driver by markylaing · Pull Request #12252 · canonical/lxd · GitHub which is similar to how we currently handle schema migrations.
User relations to deleted resources
If an instance “c1” is created with the default project, a tuple will be created for that instance:
user: project:default
relation: project
object: instance:default/c1
If a user “john.doe@example.com” is granted a permission directly against this instance, say can_exec
, an administrator will need to add the following tuple to the FGA store manually:
user: user:john.doe@example.com
relation: can_exec
object: instance:default/c1
If the instance is then deleted, neither LXD nor OpenFGA will not automatically delete the can_exec
relationship between the instance and the user. This means that if a new instance is created with the same name then the user will still have the can_exec
relation to the new instance.