OpenFGA Authorization Driver

Project LXD
Status Drafting
Author(s) @markylaing
Approver(s) @tomp
Release
Internal ID LX055

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 relation member to a group. 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 of server (whether directly or via project). There will only ever be a single server object in the OpenFGA store. It will be named server: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 type server. These relations are the most fine-grained and always take the form can_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 type server would grant a user (or group) full access to the LXD server.
    • The operator relation defined on type server 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 on server 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 on server 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 the lxc CLI to function for users without any server level permissions. Additionally, the user relation is referenced in the can_view relation defined on the storage_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 type project grants full access to a single project, including access to edit it’s configuration.
    • The operator relation defined on type project 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 type project grants a user or group permission to view project resources but not edit them.
    • The manager relation defined on type instance grants a user or group permission full access to a single instance, including editing configuration and deletion.
    • The operator relation defined on type instance 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 type instance 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 call shutdown from within the instance unless further protections are in place.

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: https://github.com/canonical/lxd/pull/12252#discussion_r1328632869 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.

2 Likes