Project | LXD |
Status | Completed |
Author(s) | @markylaing |
Approver(s) | @tomp |
Release | 5.21.0 |
Internal ID | LX061 |
Abstract
Identity and Access Management (IAM) is critical to infrastructure services. This is especially pertinent for LXD/microcloud because privileged containers allow root access to the host machine. This specification outlines the current shortfalls of IAM in LXD and discusses the constraints under which LXD may be running that impact an IAM solution. Subsequently, a detailed plan for IAM in LXD is given. The full specification includes an OpenFGA driver built directly into LXD, new API routes for identity, group, and permission management, extraction of group membership from OIDC identity tokens and more. Lastly, some potential issues with the approach are identified and discussed.
Rationale
It is currently possible to authenticate with the LXD HTTPS API via TLS, Candid, or OpenID Connect (OIDC). It is possible to limit permissions when a user authenticates will TLS by restricting their certificate to a set of projects. Additionally, limits can be set on these projects to e.g. disallow creation of privileged containers. Permissions can also be restricted when users authenticate with Candid (if using Canonical role-based access control (RBAC)). However, it is not currently possible to manage user permissions if a user authenticates with OIDC. This was addressed in the initial OpenFGA specification, however these changes were reverted for the following reasons:
- It was not possible to grant user permissions via the LXD API. Instead, we required the user to interact with the OpenFGA server directly (e.g. via the OpenFGA CLI). This was a bad user experience.
- The initial design was not compatible with TLS authentication because the existing permissions of the certificate were not considered.
- It required set up and maintenance of the external OpenFGA server. Some LXD or microcloud configurations may be air-gapped from external services, or may be deployed in an environment with limited resources. An external OpenFGA server that is highly available requires at minimum a highly available Postgres or MySQL cluster.
- For OIDC authentication, it was not possible to make use of groups defined by the Identity Provider (IdP) for access control decisions.
With this hindsight, the following constraints on an IAM solution for LXD have been determined:
- Fine-grained authorization should work out-of-the-box with TLS and OIDC authentication, with existing permissions defined on TLS certificates being respected.
- An external OpenFGA server should still be supported.
- It should be possible to allow usage of groups set by an external IdP.
- Management of user and group permissions should be possible via the LXD API. These APIs should be consistent for both built-in fine-grained permissions and with the external OpenFGA server.
Specification
Definitions
- Identity. An identity is any authenticated party making requests to the LXD HTTPS API.
- Group. A group is a collection of identities. Identities may belong to multiple groups.
- LXD Entity. A LXD entity is a uniquely accessible LXD API resource with it’s own URL.
- Entity Type. An entity type is a type of LXD entity. Entity types are associated with LXD API groups. For example, the
/1.0/instances
API is for management ofinstance
entity types. Entity types are already in use in LXD under the/1.0/warnings
API. - Entity Reference. A URL to a LXD entity.
- Entity ID. The ID of the LXD entity in the LXD database.
- Object. A concatenation of entity type and entity reference delimited by a colon (this will only be used for OpenFGA representations where necessary). For example, an OpenFGA object representation of an instance with name
my-instance
in projectmy-project
would beinstance:/1.0/instances/my-instance?project=my-project
. - Relation. An OpenFGA relation in our OpenFGA model as defined by OpenFGA.
- Entitlement. The subset of relations in our OpenFGA model that directly relate identities, service accounts, or group members to object types. For example,
can_edit
is a relation defined against theserver
object type and has[identity, service_account, group#member]
as directly related user types, socan_edit
is an entitlement on theserver
object type. A counterexample is theserver
relation defined againstproject
, these relations are added to allow computed relations for built in roles (see the OpenFGA modelling guide for more information). - Permission. The relationship tuple which relates an identity or group to an object via an entitlement.
- Role. A role is a collection of permissions. The functionality provided by roles is catered for by entitlements that are baked into the LXD OpenFGA model. For example, the
admin
entitlement defined on object typeserver
, which grants full access to LXD via implied relationships. Roles are therefore considered to be an unnecessary indirection for LXD and will not be implemented in this specification. - Custom Claim. When performing an OIDC flow, scopes are requested by the application to gain more information about the identity that is being authenticated. Depending on the requested scopes, the IdP will add additional fields to access or identity tokens. These additional fields are called “claims”. A custom claim is a claim that is added to a token that is not part of the OAuth2.0 or OIDC standard. IdPs may be configured to add custom claims during login flow.
Overview
- For built-in fine-grained authorization an OpenFGA server will be embedded into LXD. The backing data store will read directly from the LXD cluster database.
- Authorization will be managed via new APIs for groups, identities, and permissions.
- All permissions will be granted to identities via their group membership. It will not be possible to grant permissions directly to identities unless a Centralised IAM solution is used.
- A new setting will configure a custom claim to be requested from the IdP. The contents of the claim will define group membership at IdP level. A mapping API will define how IdP groups are translated into LXD groups.
Embedded OpenFGA authorization driver
A key constraint on the IAM solution for LXD is that it should work without any external set up required. It is also required that permission management APIs function identically when using built-in permission management, or when using an external OpenFGA server. Since OpenFGA already provides a concise language and ecosystem for authorization, it makes sense to use OpenFGA as our built-in solution as well.
The OpenFGA server code base is Apache-2.0 licensed and is well structured. A github.com/openfga/openfga/pkg/server.Server
can be instantiated directly within our code base. Methods can then be invoked directly, rather than via gRPC. To do this, it will be necessary to implement the github.com/openfga/openfga/pkg/storage.OpenFGADatastore
interface. The LXD implementation of OpenFGADatastore
will be function by querying the LXD database directly.
To implement the OpenFGADatastore
, some new tables, views, and triggers will be added to the database schema (see OpenFGADatastore
schema):
- The
groups
table contains minimal information about a group. A group is uniquely identified by its name. Group properties, e.g. identities and permissions will be defined by associative tables. - The
permissions
table contains an entitlement, the entity type that the entitlement is defined against, and the entity ID that the permission applies to. Care must be taken to ensure that theentitlement
field correctly applies to theentity_type
, this must be validated against the builtin OpenFGA model before writing to this table. The top levelserver
type in the OpenFGA model has no database analogue, so theentity_id
will be set to 0. - The
groups_permissions
table is an associative table for groups and permissions. The same permission may belong to many groups, and groups may have multiple permissions. This association between groups and permissions provides the mechanism by which OpenFGA relationship tuples can be generated to allow group members access to LXD entities. - The
openfga_entity_ref
view aggregates all LXD entities into a structured format from which OpenFGA tuples can be derived. This is necessary because there is no reliable delimiter that can be used to reliably generate entity references (URLs) directly in the view. Instead, theOpenFGADatastore
implementation will convert each row into an OpenFGA tuple as it is read from the database. This view is quite large and may be a performance concern, see embedded OpenFGA driver performance. - The
openfga_tuple_ref
view is what will be queried by theOpenFGADatastore
implementation. This view combinesopenfga_entity_ref
with thegroups
,permissions
, andgroups_permissions
tables such that each group is granted the correct permissions.
The OpenFGADatastore
will be read only, because all tuples are created via other API calls. Additionally, note that permissions
are associated with groups
only. Permissions are not granted directly to identities.
Once the OpenFGADatastore
is implemented, a github.com/openfga/openfga/pkg/server.Server
can be instantiated as part of a new implementation of the github.com/canonical/lxd/lxd/auth.Authorizer
interface.
TLS authentication and authorization
With the current TLS authorization driver it is possible to restrict a certificate to a set of projects. Restricted certificates cannot edit project settings. Additionally, restricted certificates may not make use of the all-projects
query parameter for any resource.
The TLS authorization driver will be removed and replaced with the embedded OpenFGA driver. To maintain compatibility, the embedded and remote OpenFGA drivers will:
-
Check if the authentication method is TLS, and if so disallow the
all-projects
query parameter when the certificate is restricted. -
For unrestricted certificates, all authorization queries will be allowed.
-
For restricted certificates, all queries to the embedded
Server
object will include a contextual tuple for each allowed project:{ "user": "identity:<identity_entity_ref>", "relation": "operator", "object": "project:<project_entity_ref>" }
This is an equivalent permission from the LXD OpenFGA model. It allows the identity to manage resources within that project but does not allow the identity to edit the project settings.
The above contextual tuples are the only cases in which an identity will be granted a permission directly. In all other cases, permissions will be determined via group membership.
To manage fine-grained permissions for certificates a new groups
field will be added to the certificate API resource:
type Certificate struct {
/*
... Existing fields ...
*/
// List of groups the certificate belongs to
// Example: ["administrators"]
//
// API extension: permission_management
Groups []string `json:"groups" yaml:"groups"`
}
Group membership will be tracked via an associative table between groups and certificates:
CREATE TABLE certificates_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
certificate_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
FOREIGN KEY (certificate_id) REFERENCES certificates (id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
UNIQUE (group_id, certificate_id)
);
It will be possible to edit the groups that a certificate belongs to via the /1.0/certificates
API. Editing a certificate will require the can_edit
direct entitlement on the certificate, or can_edit_identities
on server
.
To factor group membership into Authorization decisions, group memberships will be included as contextual tuples when making requests to the embedded Server
object or to the remote OpenFGA server:
{
"user": "identity:<identity_entity_ref>",
"relation": "member",
"object": "group:<group_entity_ref>"
}
This works because all permissions associated with the group are baked into our embedded OpenFGADatastore
via the openfga_tuple_ref
view.
LXD currently has a cache of certificates that it uses for authentication and authorization. The cache includes the certificates and the projects they belong to. This cache will need to be extended to include group membership.
OIDC authentication and authorization
It is currently not possible to perform authorization decisions for identities that authenticate via OIDC; currently they are simply given full access to LXD. To enable fine-grained access control for OIDC identities, their details will be stored so that their group membership can be established. To do this, two tables will be added to the database:
CREATE TABLE identities (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
identifier TEXT NOT NULL,
name TEXT NOT NULL,
type INTEGER NOT NULL,
UNIQUE (identifier)
);
CREATE TABLE identities_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
identity_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
UNIQUE (group_id, identity_id)
);
For OIDC identities, the identifier
column of the identities
table will be populated with the sub
claim of the identity token (if using the user interface) or the sub
claim of the access token (if using the CLI). The subject
is a unique identifier for the identity from the IdP. When performing the OIDC flow, LXD (or the CLI) requests the email
claim but this may not necessarily be present; this will be stored in the name
column. The type
column of the identities
table will be an enumerated type that indicates the authentication method and the type of identity that was authenticated. The identities_groups
table is an associative table that links identities to groups.
OIDC identities will be added to the identities
table as soon as they authenticate with the LXD server. These identities will not be deleted (see Dangling identities).
To factor group membership into Authorization decisions, groups will be included as contextual tuples when making requests to the embedded Server
object or to the remote OpenFGA server:
{
"user": "identity:<identity_entity_ref>",
"relation": "member",
"object": "group:<group_entity_ref>"
}
A cache of OIDC identities and their groups will be implemented. This cache will be updated whenever an identity is added or removed from a group, and when a group is renamed or deleted.
IdP governed groups
It is common for organisations to manage identities and groups at the level of the identity provider e.g. organising groups by department and team membership. In order to make use of these groups, a oidc.groups_claim
configuration key will be added to the server settings. When this configuration key is set, the value will be used as an extra OIDC scope to be requested as part of the OIDC flow. For the device authorization grant, the server will indicate the claim to the client with an X-LXD-OIDC-groups-claim
header. Then, when verifying the access or identity token the contents of the claim will be extracted and set in the request context.
Question: How will we know what format to expect this as? Could be comma delimited, space delimited, JSON array?
IdP groups must be mapped to LXD groups. This ensures that permissions cannot be inadvertently escalated. The following tables will define the mapping of IdP groups to LXD groups (and vice versa):
CREATE TABLE identity_provider_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
UNIQUE (name)
);
CREATE TABLE groups_identity_provider_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
group_id INTEGER NOT NULL,
identity_provider_group_id INTEGER NOT NULL,
FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
FOREIGN KEY (identity_provider_group_id) REFERENCES identity_provider_groups (id) ON DELETE CASCADE,
UNIQUE (group_id, identity_provider_group_id)
);
To use the IdP groups in authorization queries, the following steps will be followed:
- Extract the IdP groups from the request context.
- For each IdP group, check if there is a mapping to a LXD group. If there is a mapping, collect all groups in the mapping and append them to a list of contextual tuples.
- If no IdP group mappings were found the identity is not authorized. In addition to sending the
403 Forbidden
, an error will be returned to indicate that this may be due to a configuration error and can be raised to an administrator.
IdP group mappings will be cached so that authorization queries are as fast as possible.
Note: If a cluster member must forward a request to another member, it is important that the IdP groups are forwarded along with other request details such as the username (IdP subject) and authentication method.
Authorization driver for a remote OpenFGA server
This section will discuss how the embedded OpenFGA driver will be leveraged to improve the implementation of the remote OpenFGA driver. Please read this section alongside the previous OpenFGA specification.
Initial configuration of the remote OpenFGA authorization driver will be identical to the previous specification. However, the initial synchronisation will consider the tuples read from the openfga_tuple_ref
as the source of truth. Additionally, only the database leader will perform the synchronisation. Furthermore, when a resource is created in LXD, say an instance, the authorization driver must write a tuple to relate this instance to its parent project. Previously, all object references were generated via Golang methods. Instead, the tuple that will be written to or deleted from the OpenFGA store will be selected directly from the openfga_tuple_ref
view by the object_type
and entity_id
columns.
After the embedded tuples have been synced with the remote authorization checks will function identically to the embedded driver, with the exception that we will use the OpenFGA client instead.
Management of groups, identities, and permissions
The concepts of groups, identities, and permissions have been discussed in the previous sections. To manage these entities, the LXD API will be extended to include some new endpoints. The new API routes will be grouped under /1.0/auth
. The client will be extended to include the new API routes, and new commands will be added to the CLI.
Identities API
The identities API will be used for viewing identities and managing their group membership.
Types
The Identity
API types will be:
// Identity is the type for an authenticated party that can make requests to the HTTPS API.
//
// swagger:model
type Identity struct {
// AuthenticationMethod is the authentication method that the identity
// authenticates to LXD with.
// Example: tls
AuthenticationMethod string `json:"authentication_method" yaml:"authentication_method"`
// Type is the type of identity.
// Example: oidc-service-account
Type string `json:"type" yaml:"type"`
// Identifier is a unique identifier for the identity (e.g. certificate fingerprint or OIDC subject).
// Example: auth0|4daf5e37ce230e455b64b65b
Identifier string `json:"id" yaml:"id"`
// Name is the email of the identity if authenticated via OIDC, or the name
// of the certificate if authenticated with TLS.
// Example: jane.doe@example.com
Name string `json:"name" yaml:"name"`
}
// IdentityInfo expands an Identity to include group membership.
//
// swagger:model
type IdentityInfo struct {
IdentityPut `yaml:",inline"`
Identity `yaml:",inline"`
}
// IdentityPut contains the editable fields of an IdentityFull.
//
// swagger:model
type IdentityPut struct {
// Groups is the list of groups for which the identity is a member.
// Example: ["foo", "bar"]
Groups []string `json:"groups" yaml:"groups"`
}
Routes
GET /1.0/auth/identities
. Returns a list of identity URLs. The results will be filtered by what the caller identity is authorized to view (e.g. all if related toserver
viacan_manage_permissions
, only self if not). Only the identifier of the identities will be given in the URLs. For example:
[
"/1.0/auth/identities/tls/e1e06266e36f67431c996d5678e66d732dfd12fe5073c161e62e6360619fc226",
"/1.0/auth/identities/oidc/auth0|4daf5e37ce230e455b64b65b",
]
GET /1.0/auth/identities?recursion=1
. Returns a list ofIdentity
objects. Results will be filtered by what the identity is authorized to view.GET /1.0/auth/identities?recursion=2
. Returns a list ofIdentityInfo
objects. Results will be filtered by what the identity is authorized to view.GET /1.0/auth/identities/{authenticationMethod}
. Returns a list of identity URLs (as inGET /1.0/auth/identities
) for only the specified authentication method.GET /1.0/auth/identities/{authenticationMethod}?recursion=1
. Is the same asGET /1.0/auth/identities?recursion=1
but filtered by the authentication method.GET /1.0/auth/identities/{authenticationMethod}?recursion=2
. Is the same asGET /1.0/auth/identities?recursion=2
but filtered by the authentication method.GET /1.0/auth/identities/{authenticationMethod}/{id|name}
. Returns a singleIdentityFull
. The shortname will be allowed as a path parameter but this must resolve to a unique identity.PUT /1.0/auth/identities/{authenticationMethod}/{id|name}
. SendsIdentityPut
in the request body and replaces all groups for which the identity is a member.PATCH /1.0/auth/identities/{authenticationMethod}/{id|name}
. SendsIdentityPut
in the request body and appends groups to the identity (e.g. adds the identity to the given groups).
Groups API
The groups API will be used for creating, deleting, renaming, and assigning permissions to groups.
Types
The Group
API types will be:
// Group is the type for a LXD Group.
//
// swagger:model
type Group struct {
GroupsPost `yaml:",inline"`
// Identities are the identities that are members of the group.
Identities []Identity `json:"identities" yaml:"identities"`
// IdentityProviderGroups are a list of groups from the IdP whose mapping
// includes this group.
// Example: ["sales", "operations"]
IdentityProviderGroups []string
}
// GroupsPost is used for creating a new group.
//
// swagger:model
type GroupsPost struct {
GroupPost `yaml:",inline"`
GroupPut `yaml:",inline"`
}
// GroupPost is used for renaming a group.
//
// swagger:model
type GroupPost struct {
// Name is the name of the group.
// Example: default-c1-viewers
Name string `json:"name" yaml:"name"`
}
// GroupPut contains the editable fields of a group.
//
// swagger:model
type GroupPut struct {
// Description is a short description of the group.
// Example: Viewers of instance c1 in the default project.
Description string `json:"description" yaml:"description"`
// Permissions are a list of permissions.
Permissions []Permission `json:"permissions" yaml:"permissions"`
}
// IdentityProviderGroup represents a mapping between LXD groups and groups defined by an identity provider.
//
// swagger:model
type IdentityProviderGroup struct {
IdentityProviderGroupPost `yaml:",inline"`
IdentityProviderGroupPut `yaml:",inline"`
}
// IdentityProviderGroupPost is used for renaming an IdentityProviderGroup.
//
// swagger:model
type IdentityProviderGroupPost struct {
// Name is the name of the IdP group.
Name string `json:"name" yaml:"name"`
}
// IdentityProviderGroupPut contains the editable fields of an IdentityProviderGroup.
//
// swagger:model
type IdentityProviderGroupPut struct {
// Groups are the groups the IdP group resolves to.
// Example: ["foo", "bar"]
Groups []string `json:"groups" yaml:"groups"`
}
Routes
GET /1.0/auth/groups
. Returns a list of group URLs filtered by those the caller is allowed to view. All identities will be able to view groups that they are a member of:
[
"/1.0/auth/groups/default-project-operators",
"/1.0/auth/groups/instance-c1-users",
]
GET /1.0/auth/groups?recursion=1
. Returns a list ofGroup
objects filtered by those the identity is allowed to view. All identities will be able to view groups that they are a member of. Theidentities
belonging to the group will be filtered by what the caller is allowed to view (unless they are related toserver
viacan_manage_permissions
this will display only the identity that made the request).GET /1.0/auth/groups/{groupName}
. Gets a singleGroup
object.POST /1.0/auth/groups
. Creates a new group.PUT /1.0/auth/groups/{groupName}
. Replaces the description and permissions of the group.POST /1.0/auth/groups/{groupName}
. Renames the group.PATCH /1.0/auth/groups/{groupName}
. Partially updates the description (replace if not empty) and permissions (append) of the group.DELETE /1.0/auth/groups/{groupName}
. Deletes the group.GET /1.0/auth/identity-provider-groups
. Returns a list of IdP group URLs:
[
"/1.0/auth/identity-provider-groups/sales",
"/1.0/auth/identity-provider-groups/operations"
]
GET /1.0/auth/identity-provider-groups?recursion=1
. Returns a list ofIdentityProviderGroup
objects.GET /1.0/auth/identity-provider-groups/{idpGroupName}
. Returns a singleIdentityProviderGroup
with the given name.POST /1.0/auth/identity-provider-groups
creates a new IdP group.POST /1.0/auth/identity-provider-groups/{idpGroupName}
renames an IdP group.PUT /1.0/auth/identity-provider-groups/{idpGroupName}
replaces the LXD groups that the IdP group resolves to.PATCH /1.0/auth/identity-provider-groups/{idpGroupName}
appends a LXD group to the list of groups that the IdP group resolves to.DELETE /1.0/auth/identity-provider-groups/{idpGroupName}
deletes the IdP group.
Permissions API
The permissions API will be read-only. The primary purpose of this API is to allow discovery of available permissions so that they can be granted to groups. These will only be viewable by an identity that is related to server
via can_view_permissions
.
Types
The Permission
API types will be:
// Permission represents a permission that may be granted to a group.
//
// swagger:model
type Permission struct {
// EntityType is the string representation of the entity type.
// Example: instance
EntityType string `json:"entity_type" yaml:"entity_type"`
// EntityReference is the URL of the entity that the permission applies to.
// Example: /1.0/instances/c1?project=default
EntityReference string `json:"url" yaml:"url"`
// Entitlement is the entitlement define for the entity type.
// Example: can_view
Entitlement string `json:"entitlement" yaml:"entitlement"`
}
// PermissionInfo expands a Permission to include any groups that may have the specified Permission.
//
// swagger:model
type PermissionInfo struct {
Permission `yaml:",inline"`
// Groups is a list of groups that have the Entitlement on the Entity.
// Example: ["foo", "bar"]
Groups []string `json:"groups" yaml:"groups"`
}
Routes
GET /1.0/auth/permissions
. Returns all available permissions. Does not populate groups.GET /1.0/auth/permissions?recursion=1
. Returns all available permissions. Populates groups.
The permissions API will be filterable on the following parameters:project={projectName}
: The project of the entity.entity_type={entityType}
: The entity type e.g.server
.
New CLI commands
New CLI commands will be required for managing authorization. All new commands will be grouped under an auth
sub-command.
Groups
lxc auth group create [<remote>:]<name>
. Create a new group.lxc auth group delete [<remote>:]<name>
. Delete the group.lxc auth group edit [<remote>:]<name>
. Edit the group as yaml.lxc auth group show [<remote>:]<name>
. Show group details.lxc auth group list [<remote>:]
. List groups.lxc auth group permission add <group_name> <object_type> [<object_name>] <entitlement> [<key>=<value>...]
. Grant a single permission to the group. Key-value pairs can be added as supplementary arguments to uniquely specify the entity for which the permission is being granted against. Examples:lxc auth group permission add foo project default operator
. Grant groupfoo
entitlementoperator
on projectdefault
.lxc auth group permission add bar server admin
. Grant groupbar
entitlementadmin
on the server.lxc auth group permission add baz storage_volume vol1 can_manage_backups project=default pool=default location=node01 type=custom
. Grant groupbaz
entitlementcan_manage_backups
on storage volumevol1
, which is of typecustom
, in projectdefault
, in storage pooldefault
, and on cluster membernode01
.
lxc auth group permission remove <group_name> <object_type> <object_name> <entitlement> [<key>=<value>...]
. Remove a single permission from the group.
Identities
lxc auth identity list [<remote>:]
. List identities.lxc auth identity edit [<remote>:]<authentication_method>/<identity_id|identity_name>
. Edit the identity as yaml.lxc auth identity show [<remote>:]<authentication_method>/<identity_id|identity_name>
. Show the identity.lxc auth identity group add [<remote>:]<authentication_method>/<identity_id|identity_name> <group>
. Add an identity to a group.lxc auth identity group remove [<remote>:]<authentication_method>/<identity_id|identity_name> <group>
. Remove an identity from a group.
Permissions
lxc auth permission list [<key>=<value>...]
. Lists permissions. Key-value pairs can be provided for filtering on project and object type.
OpenFGA model migrations
When using the embedded driver updating the OpenFGA model is very simple. A new model will be added, then any relevant changes to the openfga_tuple_ref
view can be made via the existing schema update mechanism.
To update the OpenFGA model when using the remote driver we will:
- Detect the version of the model that is currently running. Successive versions of the OpenFGA model will be stored in ascending order under
lxd/auth/openfga-models
. A script will convert the models into JSON and store them in a generated go map. When the authorization driver connects to the OpenFGA store, it will read the latest authorization model and compare it against the models defined in the map. - If the authorization model is not the latest, the driver will apply updates in the order that they are defined in the model map. Hooks will be defined in a corresponding map the will run before (
PreModelWrite
) and after the new model is written (PostModelWrite
). The hooks will be given access to the LXD database, the OpenFGA driver configuration, an OpenFGA client for the store, and the model IDs of the previous and latest models. - Once the latest authorization model has been written, all resources will be synchronised with the OpenFGA store and the authorization driver can be considered ready.
Discussion
Embedded OpenFGA Driver Performance
The openfga_tuple_ref
view contains 14 select statements, some of which are non-trivial. In SQLite, the results of a view are not cached, only the execution plan is cached. When using the embedded OpenFGA driver, this view will be queried at least once per API request, therefore it is very important that the driver is performant even for a large cluster.
Performance of the embedded driver must be tested on a large cluster with many LXD entities before this feature is released. If the performance impact is high, it may be necessary to implement a caching strategy for OpenFGA tuples. The github.com/openfga/openfga/pkg/storage/memory.MemoryBackend
could then be used. Unfortunately any caching strategy is likely to be quite complex because the cache will need to be refreshed on all nodes if any instance, storage pool, storage volume, network, etc. is created, deleted, or renamed.
Future Work
Dangling permissions
In the previous OpenFGA specification the removal of permissions that are no longer valid was left to the end user. When using the embedded OpenFGA driver invalid permissions will never be returned by the OpenFGADatastore
, but they will remain in the permissions table until manually deleted. A periodic background task can be configured to remove these.
Dangling Identities
If left unchecked, the identities
table (see OIDC authentication and authorization) could grow unnecessarily large. This is because all identities that log in via OIDC will be stored in this table. If an identity does not belong to any groups there is no reason to store their details. A periodic background task could remove these identities if necessary.
Centralised IAM solutions based on OIDC and OpenFGA
When using a centralised IAM solution, identities, groups, and permissions will be managed externally. It is expected that groups will be added to the OIDC identity token as a custom claim. It will be possible to configure LXD to extract this claim via the oidc.groups_claim
server configuration key (see IdP governed groups). However it will no longer be necessary to verify that the groups exist (or have an associated IdP group mapping). To force LXD to skip this verification step a new server configuration key will be added: idp.managed
(default false).
Another consequence of using a centralised IAM solution is that it will no longer be necessary to synchronise groups or permissions with the OpenFGA store. Otherwise, LXD will delete any groups created centrally and replace them with what is stored in the LXD database. The value of idp.managed
will be passed into the remote OpenFGA authorization driver so that this can be accounted for.
Lastly, a centralised IAM solution will affect OpenFGA model migrations. For example, if a permission is granted centrally that has been renamed in the new model, we will need to collate these permissions and reset them on the new model version. The PreModelWrite
and PostModelWrite
hooks may be used for this.
Service accounts
A service_account
type has been added to the new OpenFGA model. This is to facilitate the introduction of service accounts in future LXD releases but will not be used in the initial implementation of this specification. This is because it is not currently possible to distinguish between service accounts (who will use a Client Credentials Grant) and CLI users (who are using the Device Authorization Grant ) when performing OIDC authentication, as both clients will send an access token to LXD as a bearer token.
In future, we may introduce another custom claim configuration option so that these flows can be distinguished. Additionally, we may replace the metrics
type certificate with a service_account
type certificate.
Reference material
OpenFGA model
There are some changes from the model in the original specification:
- The
group
type has a parent relation toserver
and new entitlementcan_view
. Members of a group can view the group (and see themselves as a member, but they won’t be able to see other identities). - The
user
type has been renamedidentity
. - A new
service_account
type has been added, it is equivalent toidentity
but may be used to distinguish between human and robotic users in future IAM deisgn. - The
can_edit
entitlement for all entity types has been split intocan_edit
andcan_delete
. Where an entity type has a relation toserver
orproject
theserver
andproject
have a corresponding entitlement forcan_view
,can_edit
, andcan_delete
. E.g.project
hascan_view_instances
, andinstance
hascan_view
. - New entitlements for permission management have been added to
server
. These are used for editing groups, identities, and permissions. Additionally, they are used in thecan_edit
relation ofcertificate
, since editing a certificate to e.g. unrestrict it is the same as editing a permission.
model
schema 1.1
type identity
type service_account
type group
relations
define server: [server]
define member: [identity, service_account]
define can_view: member or can_view_groups from server
define can_edit: can_edit_groups from server
define can_delete: can_delete_groups from server
type server
relations
define admin: [identity, service_account, group#member]
define viewer: [identity, service_account, group#member]
define can_edit: [identity, service_account, group#member] or admin
define can_view: [identity:*, service_account:*]
define can_view_configuration: [identity, service_account, group#member] or can_edit or viewer
define permission_manager: [identity, service_account, group#member]
define can_create_identities: [identity, service_account, group#member] or permission_manager or admin
define can_view_identities: [identity, service_account, group#member] or permission_manager or admin or viewer
define can_edit_identities: [identity, service_account, group#member] or permission_manager or admin
define can_delete_identities: [identity, service_account, group#member] or permission_manager or admin
define can_create_groups: [identity, service_account, group#member] or permission_manager or admin
define can_view_groups: [identity, service_account, group#member] or permission_manager or admin or viewer
define can_edit_groups: [identity, service_account, group#member] or permission_manager or admin
define can_delete_groups: [identity, service_account, group#member] or permission_manager or admin
define storage_pool_manager: [identity, service_account, group#member]
define can_create_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin
define can_edit_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin
define can_delete_storage_pools: [identity, service_account, group#member] or storage_pool_manager or admin
define project_manager: [identity, service_account, group#member]
define can_create_projects: [identity, service_account, group#member] or project_manager or admin
define can_view_projects: [identity, service_account, group#member] or project_manager or viewer or admin
define can_edit_projects: [identity, service_account, group#member] or project_manager or admin
define can_delete_projects: [identity, service_account, group#member] or project_manager or admin
define can_override_cluster_target_restriction: [identity, service_account, group#member] or admin
define can_view_privileged_events: [identity, service_account, group#member] or admin or viewer
define can_view_resources: [identity, service_account, group#member] or admin or viewer
define can_view_metrics: [identity, service_account, group#member] or admin or viewer
define can_view_warnings: [identity, service_account, group#member] or admin or viewer
type certificate
relations
define server: [server]
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_identities from server
define can_edit: [identity, service_account, group#member] or can_edit_identities from server
define can_delete: [identity, service_account, group#member] or can_delete_identities from server
type storage_pool
relations
define server: [server]
define can_view: can_view from server
define can_edit: [identity, service_account, group#member] or can_edit_storage_pools from server
define can_delete: [identity, service_account, group#member] or can_delete_storage_pools from server
type project
relations
define server: [server]
define operator: [identity, service_account, group#member]
define viewer: [identity, service_account, group#member]
define can_view: [identity, service_account, group#member] or viewer or operator or can_view_projects from server
define can_edit: [identity, service_account, group#member] or can_edit_projects from server
define can_delete: [identity, service_account, group#member] or can_delete_projects from server
define image_manager: [identity, service_account, group#member]
define can_create_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server
define can_view_images: [identity, service_account, group#member] or operator or viewer or image_manager or can_view_projects from server
define can_edit_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server
define can_delete_images: [identity, service_account, group#member] or operator or image_manager or can_edit_projects from server
define image_alias_manager: [identity, service_account, group#member]
define can_create_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server
define can_view_image_aliases: [identity, service_account, group#member] or operator or viewer or image_alias_manager or can_view_projects from server
define can_edit_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server
define can_delete_image_aliases: [identity, service_account, group#member] or operator or image_alias_manager or can_edit_projects from server
define instance_manager: [identity, service_account, group#member]
define can_create_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server
define can_view_instances: [identity, service_account, group#member] or operator or viewer or instance_manager or can_view_projects from server
define can_edit_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server
define can_delete_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server
define can_operate_instances: [identity, service_account, group#member] or operator or instance_manager or can_edit_projects from server
define network_manager: [identity, service_account, group#member]
define can_create_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server
define can_view_networks: [identity, service_account, group#member] or operator or viewer or network_manager or can_view_projects from server
define can_edit_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server
define can_delete_networks: [identity, service_account, group#member] or operator or network_manager or can_edit_projects from server
define network_acl_manager: [identity, service_account, group#member]
define can_create_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server
define can_view_network_acls: [identity, service_account, group#member] or operator or viewer or network_acl_manager or can_view_projects from server
define can_edit_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server
define can_delete_network_acls: [identity, service_account, group#member] or operator or network_acl_manager or can_edit_projects from server
define network_zone_manager: [identity, service_account, group#member]
define can_create_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server
define can_view_network_zones: [identity, service_account, group#member] or operator or viewer or network_zone_manager or can_view_projects from server
define can_edit_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server
define can_delete_network_zones: [identity, service_account, group#member] or operator or network_zone_manager or can_edit_projects from server
define profile_manager: [identity, service_account, group#member]
define can_create_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server
define can_view_profiles: [identity, service_account, group#member] or operator or viewer or profile_manager or can_view_projects from server
define can_edit_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server
define can_delete_profiles: [identity, service_account, group#member] or operator or profile_manager or can_edit_projects from server
define storage_volume_manager: [identity, service_account, group#member]
define can_create_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server
define can_view_storage_volumes: [identity, service_account, group#member] or operator or viewer or storage_volume_manager or can_view_projects from server
define can_edit_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server
define can_delete_storage_volumes: [identity, service_account, group#member] or operator or storage_volume_manager or can_edit_projects from server
define storage_bucket_manager: [identity, service_account, group#member]
define can_create_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server
define can_view_storage_buckets: [identity, service_account, group#member] or operator or viewer or storage_bucket_manager or can_view_projects from server
define can_edit_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server
define can_delete_storage_buckets: [identity, service_account, group#member] or operator or storage_bucket_manager or can_edit_projects from server
define can_view_operations: [identity, service_account, group#member] or operator or viewer or can_view_projects from server
define can_view_events: [identity, service_account, group#member] or operator or viewer or can_view_projects from server
type image
relations
define project: [project]
define can_edit: [identity, service_account, group#member] or can_edit_images from project
define can_delete: [identity, service_account, group#member] or can_delete_images from project
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_images from project
type image_alias
relations
define project: [project]
define can_edit: [identity, service_account, group#member] or can_edit_image_aliases from project
define can_delete: [identity, service_account, group#member] or can_delete_image_aliases from project
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_image_aliases from project
type instance
relations
define project: [project]
define user: [identity, service_account, group#member]
define operator: [identity, service_account, group#member]
define can_edit: [identity, service_account, group#member] or can_edit_instances from project
define can_delete: [identity, service_account, group#member] or can_delete_instances from project
define can_view: [identity, service_account, group#member] or user or operator or can_edit or can_delete or can_view_instances from project
define can_update_state: [identity, service_account, group#member] or operator or can_operate_instances from project
define can_manage_snapshots: [identity, service_account, group#member] or operator or can_operate_instances from project
define can_manage_backups: [identity, service_account, group#member] or operator or can_operate_instances from project
define can_connect_sftp: [identity, service_account, group#member] or user or operator or can_operate_instances from project
define can_access_files: [identity, service_account, group#member] or user or operator or can_operate_instances from project
define can_access_console: [identity, service_account, group#member] or user or operator or can_operate_instances from project
define can_exec: [identity, service_account, group#member] or user or operator or can_operate_instances from project
type network
relations
define project: [project]
define can_edit: [identity, service_account, group#member] or can_edit_networks from project
define can_delete: [identity, service_account, group#member] or can_delete_networks from project
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_networks from project
type network_acl
relations
define project: [project]
define can_edit: [identity, service_account, group#member] or can_edit_network_acls from project
define can_delete: [identity, service_account, group#member] or can_delete_network_acls from project
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_network_acls from project
type network_zone
relations
define project: [project]
define can_edit: [identity, service_account, group#member] or can_edit_network_zones from project
define can_delete: [identity, service_account, group#member] or can_delete_network_zones from project
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_network_zones from project
type profile
relations
define project: [project]
define can_edit: [identity, service_account, group#member] or can_edit_profiles from project
define can_delete: [identity, service_account, group#member] or can_delete_profiles from project
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_profiles from project
type storage_volume
relations
define project: [project]
define can_edit: [identity, service_account, group#member] or can_edit_storage_volumes from project
define can_delete: [identity, service_account, group#member] or can_delete_storage_volumes from project
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_storage_volumes from project
define can_manage_snapshots: [identity, service_account, group#member] or can_edit_storage_volumes from project
define can_manage_backups: [identity, service_account, group#member] or can_edit_storage_volumes from project
type storage_bucket
relations
define project: [project]
define can_edit: [identity, service_account, group#member] or can_edit_storage_buckets from project
define can_delete: [identity, service_account, group#member] or can_delete_storage_buckets from project
define can_view: [identity, service_account, group#member] or can_edit or can_delete or can_view_storage_buckets from project
OpenFGADatastore
schema
CREATE TABLE groups (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
UNIQUE (name)
);
CREATE TABLE permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
entitlement TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id INTEGER NOT NULL,
UNIQUE (entitlement, entity_type, entity_id)
);
CREATE TABLE groups_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
group_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions (id) ON DELETE CASCADE,
UNIQUE (group_id, permission_id)
);
CREATE VIEW openfga_entity_ref (user_entity_type, user_entity_ref, user_relation, relation, entity_type, entity_ref, entity_id) AS
-- Type-bound public access for identities for "can_view" on "server".
SELECT 23, '*', '', 'can_view', 22, json_array(), 0
-- Type-bound public access for service accounts for "can_view" on "server".
UNION SELECT 25, '*', 'can_view', 'service_account', 22, json_array(), 0
-- Server to project relations.
UNION SELECT 22, json_array(), '', 'server', 3, json_array(projects.name), projects.id
FROM projects
-- Server to group relations.
UNION SELECT 22, json_array(), '', 'server', 24, json_array(groups.name), groups.id
FROM groups
-- Server to certificate relations.
UNION SELECT 22, json_array(), '', 'server', 4, json_array(certificates.fingerprint), certificates.id
FROM certificates
-- Server to storage pool relations.
UNION SELECT 22, json_array(), '', 'server', 12, json_array(storage_pools.name), storage_pools.id
FROM storage_pools
-- Project to image relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 1, json_array(projects.name, images.fingerprint), images.id
FROM images JOIN projects ON project_id=projects.id
-- Project to image alias relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 25, json_array(projects.name, images_aliases.name), images_aliases.id
FROM images_aliases JOIN projects ON project_id=projects.id
-- Project to instance relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 5, json_array(projects.name, instances.name), instances.id
FROM instances JOIN projects ON project_id=projects.id
-- Project to network relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 8, json_array(projects.name, networks.name), networks.id
FROM networks JOIN projects ON project_id=projects.id
-- Project to network ACL relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 9, json_array(projects.name, networks_acls.name), networks_acls.id
FROM networks_acls JOIN projects ON project_id=projects.id
-- Project to network zone relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 26, json_array(projects.name, networks_zones.name), networks_zones.id
FROM networks_zones JOIN projects ON project_id=projects.id
-- Project to profile relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 2, json_array(projects.name, profiles.name), profiles.id
FROM profiles JOIN projects ON project_id=projects.id
-- Project to storage volume relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 13,
json_array(projects.name, storage_pools.name,
-- Use case statement to resolve type to name.
CASE storage_volumes.type
WHEN 0 THEN 'container'
WHEN 1 THEN 'image'
WHEN 2 THEN 'custom'
WHEN 3 THEN 'virtual-machine'
END, storage_volumes.name, replace(coalesce(nodes.name, ''), 'none', '')),
storage_volumes.id
FROM storage_volumes
JOIN projects ON project_id=projects.id
JOIN storage_pools ON storage_pool_id=storage_pools.id
LEFT JOIN nodes ON node_id=nodes.id
-- Project to storage bucket relations.
UNION SELECT 3, json_array(projects.name), '', 'project', 18,
json_array(projects.name, storage_pools.name, storage_buckets.name, replace(coalesce(nodes.name, ''), 'none', '')),
storage_buckets.id
FROM storage_buckets
JOIN projects ON project_id=projects.id
JOIN storage_pools ON storage_pool_id=storage_pools.id
LEFT JOIN nodes ON node_id=nodes.id;
CREATE VIEW openfga_tuple_ref (user_entity_type, user_entity_ref, user_relation, relation, entity_type, entity_ref, entity_id) AS
-- Group permissions.
SELECT 24, json_array(groups.name), 'member',
permissions.entitlement,
openfga_entity_ref.entity_type,
openfga_entity_ref.entity_ref,
groups_permissions.id
FROM permissions
JOIN groups_permissions ON permissions.id = groups_permissions.permission_id
JOIN groups ON groups_permissions.group_id = groups.id
JOIN openfga_entity_ref ON permissions.entity_type = openfga_entity_ref.entity_type AND permissions.entity_id = openfga_entity_ref.entity_id
UNION SELECT user_entity_type, user_entity_ref, user_relation, relation, entity_type, entity_ref, entity_id FROM openfga_entity_ref;