Author: Nan Zhou (nanzhoumails@gmail.com)
Created: 08/08/2022
The Redfish authorization subsystem controls which authenticated users have access to resources and the type of access that users have.
DMTF has already defined the authorization model and schemas. This design is to enhance the current implementation in BMCWeb Redfish interface so that OpenBMC systems exposes interfaces to change authorization behavior at runtime without Redfish service restart.
The Redfish authorization model consists of the privilege model and the operation-to-privilege mapping.
In the privilege model, there are fixed set of standard Redfish roles and each of them is assigned a fixed array of standard privileges (e.g., Login
, ConfigureManager
, etc). A service may define custom OEM roles (read-only). A service may even allow custom client-defined roles to be created, modified, and deleted via REST operations on RoleCollection
.
The operation-to-privilege mapping is defined for every resource type and applies to every resource the service implements for the applicable resource type. It is used to determine whether the identity privileges of an authenticated Redfish role are sufficient to complete the operation in the request. The Redfish Forum provides a Privilege Registry definition in its official registry collection as a base operation-to-privilege mapping. It also allows mapping overrides (Property override, Subordinate override, and Resource URI override) to customize mapping.
For example, a peer who is an Operator
that has both Login
, ConfigureComponents
, and ConfigureSelf
privileges, is authorized to send a GET
request to the ChassisCollection
, since the GET
operation on this resource only requires the Login
privilege. On the other hand, the same peer gets denied access if they send a POST request to CertificateService
, as the POST operation on certificates requires ConfigureManager
privilege that the peer is missing.
Note, in the Redfish spec, OEM roles can be added via POST to the RoleCollection
in the AccountService
; PrivilegesUsed
, OEMPrivilegesUsed
, and properties of Mappings
are all read-only.
References:
Phosphor-user-manager is an OpenBMC daemon that does user management.
It exposes DBus APIs to dynamically add users, manage users' attributes (e.g., group, privileges, status, and account policies). It has a hardcoded list of user groups (SSH, IPMI, Redfish, Web) and a hardcoded list of privileges ("priv-admin", "priv-operator", "priv-user", "priv-noaccess"). These privileges are implemented as Linux secondary groups.
It also integrates LDAP (supports either ActiveDirectory or OpenLDAP) for remote user management, where BMC acts as a LDAP client and uses nslcd to automatically convert Linux system calls to LDAP queries.
References:
BMCWeb is an OpenBMC Daemon which implements the Redfish service (it implements other management interfaces as well besides Redfish).
BMCWeb supports various "authentication" options, but under the hood, to verify the user is who they claim they are, there are two main authentication methods:
After getting the peer's user name, BMCWeb sends DBus queries to phosphor-user-manager to query the user's privileges and uses a hardcoded map to convert the privileges to Redfish roles. The hardcoded map is listed below:
Phosphor-user-manager privileges (implemented as groups) | BMCWeb Redfish Roles |
---|---|
priv-admin | Administrator |
priv-operator | Operator |
priv-user | ReadOnly |
priv-noaccess | NoAccess |
To map Redfish role to their assigned Redfish privileges, BMCWeb implements the following hardcoded map:
BMCWeb Redfish Roles | Assigned Redfish Privileges |
---|---|
Administrator | "Login", "ConfigureManager", "ConfigureUsers", "ConfigureSelf", "ConfigureComponents"} |
Operator | "Login", "ConfigureSelf", "ConfigureComponents" |
ReadOnly | "Login", "ConfigureSelf" |
NoAccess | NA |
At compile time, BMCWeb assigns each operation of each entity a set of required Redfish Privileges. An authorization action is performed when a BMCWeb route callback is performed: check if the assigned Redfish Privileges is a superset of the required Redfish Privileges.
In the following section, we refer a BMCWeb route as the handler of an operation of an given resource URI, which is what the BMCWEB_ROUTE macro has defined.
References:
As mentioned above, majority of the current Redfish authorization settings are configured at compile time including:
However, modern systems have use cases where Redfish roles, Redfish privileges, and operation-to-privilege mapping need to change when the system keeps running. E.g., a new micro-service is introduced and needs to talk to existing BMCs in the fleet, we need to propagate a configuration so that this new peer gets a proper Redfish role and is authorized to access certain resources without rolling out a new BMC firmware.
Another gap is that current Redfish roles and operation-to-privilege mapping lead to a very coarse-grained access control, and doesn't implement the principle of least privilege. Though these configurations are defined by DMTF, it is explicitly called out in the standard that implementation implement their own OEM roles and privileges if "the standard privilege is overly broad".
For systems which have requirement where a given user only has access to the resources it needs. For example, a micro-service responsible for remote power control can only send GET to Chassis and ComputerSystems, and POST to corresponding ResetActions. With the existing implementation, such micro-service has at least ConfigureComponents Redfish privilege, which leads to being able to patch an EthernetInterface resource.
BMC implements a dynamic Redfish authorization system:
As mentioned in the background section, existing Redfish roles are stored as Linux secondary groups with prefix "priv" (includes "priv-admin", "priv-operator", and "priv-user"). And a Linux user is in one of these groups representing that a user is a specific Redfish role. BMCWeb then uses a hardcoded table to map Redfish role to Redfish privileges.
However, modeling roles only as static Linux secondary groups doesn't worked well with OEM Redfish Roles where a Redfish role maps to a dynamic set of OEM privileges: secondary group can't represent associations.
To solve this, we propose the following solution:
Store Redfish Roles As Linux Users and Secondary Groups We propose to store Redfish Roles as both Linux users and secondary groups. Storing as secondary groups is to associate users with Redfish roles. On the other hand, storing as users is to associate Redfish roles with Redfish privileges. See below section for these mappings.
Users for Redfish roles won't be any predefined groups (web, redfish, ipmi). We can add extra attributes to distinguish them with real local and LDAP users. Users for Redfish roles won't have SSH permission as well.
Redfish roles will have a fixed prefix "openbmc-rfr-". "rfr" refers to Redfish role. OEM redfish roles will get prefix "openbmc-orfr-". "orfr" refers to OEM Redfish role. For example, the base Redfish role "Administrator" will result in a Linux secondary group "openbmc-rfr-administrator" and a local user "openbmc-rfr-administrator". We can also make the vendor name ("openbmc") configurable at compile time. Using acronym is to save characters since Linux username has 31 characters limit.
Store Redfish Privileges as Secondary Groups Redfish privileges will be stored as Linux secondary groups with a fixed prefix "openbmc-rfp". rfr" refers to Redfish privilege. OEM privileges will have fixed prefix "openbmc-orfp". "orfr" refers to OEM Redfish privilege.
Username to Redfish Role Mapping Mapping a username to Redfish role becomes searching the group starting with "openbmc-rfr" that the user is in.
Redfish Role to Redfish Privileges Mapping Mapping a Redfish Role to Redfish privileges becomes searching all the groups starting with "openbmc-rfp" or "openbmc-orfp" of the user.
A user maps be in multiple linux secondary groups meaning they are assigned certain privileges; for example, user "PowerService" is in "openbmc-orfr-power" group meaning its Redfish role is "openbmc-orfr-power"; local user "openbmc-orfr-power" is in secondary groups "openbmc-rfp-configureself" and "openbmc-orfp-power" groups meaning "openbmc-orfr-power" is assigned to Redfish privileges "ConfigureSelf" and "OemPrivPower".
The following diagram shows how assigned privileges of a power control service with identity (username in PAM, or CN/SAN in TLS) "power-service" is resolved.
+-----------------+ | power service | | | +--------|--------+ | | +-----------------+ | authentication | | (PAM or TLS) | | BMCWeb | +--------|--------+ | |username:power-service | +-----------------+ +-----------------+ +----------------------------+ | BMCWeb |DBus: privileges | phosphor- | | Linux | | ------------------------>| user-manager | |user: power-service | | |<-----------------------| | |group: | | | privileges: | <----------->| openbmc-orfr-power | | | Login, ConfigureSelf | | | | | | OemPrivPower | | |user: openbmc-orfr-power | +-----------------+ +-----------------+ |group: | | openbmc-rfp-configureself | | openbmc-orfp-power | | openbmc-rfp-login | +----------------------------+
The above diagram works for LDAP users as well. A remote role or group can map to base Redfish roles or OEM Redfish roles via RemoteRoleMapping: an LDAP group maps to a single Redfish role stored as local users.
We propose to extend the existing phosphor-user-manager:
The legacy groups (e.g., priv-user
) still works as before. For example, a user in both priv-user
and openbmc-orfp-power
will have the following Redfish privileges: Login
, ConfigureSelf
, OemPrivPower
.
Base privileges and roles won't be allowed to change at runtime.
PATCH OEMPrivilegesUsed in PrivilegeRegistry creating/deleting OEM privileges to create or delete OEM Privileges at runtime.
We propose to extend the existing phosphor-user-manager:
Systems can also optionally add OEM Privileges at compile time via Yocto's GROUPADD_PARAM in the useradd class.
POST on the RoleCollection in the AccountService to create an OEM role, with assigned Privileges in the AssignedPrivileges and OemPrivileges properties in the Role resource.
DELETE on the specific Role in the RoleCollection to delete an OEM role. Predefined roles are not allowed to be deleted.
We propose to extend the existing phosphor-user-manager:
POST on the ManagerAccountCollection to create a user, with a RoleId to the assigned Redfish role.
DELETE on the specific user in the ManagerAccountCollection to delete a user.
Note: modification on ManagerAccountCollection need authorization as well. For example, a user with only "ConfigureSelf" permission is not allowed to delete other users.
In summary, a typical workflow to create a new local user with an new OEM Redfish role which is assigned a new set of OEM Redfish Privileges is mapped out below.
Root User BMCWeb Phosphor-User-Manager Linux | PATCH PrivilegeRegistry | | | |-------------------------->| DBus: createGroup | | | Add OemAddPowerService |----------------------------->| groupadd | Create | | |----------------------->| OemPrivilege| | Okay |<-----------------------| | Okay |<-----------------------------| Okay | |<--------------------------| | | | | | | | | | | | POST RoleCollection | | | |-------------------------->| DBus: createUser | | | |----------------------------->| useradd + groupadd | Create | | |----------------------->| OemRole | | Okay |<-----------------------| | Okay |<-----------------------------| Okay | |<--------------------------| | | | | | | | | | | |POST | | | | ManagerAccountCollection | | | |-------------------------->| DBus: createUser | | | |----------------------------->| useradd | Create | | |----------------------->| User | | |<-----------------------| | |<-----------------------------| Okay | |<--------------------------| Okay | | | Okay | | | Time | | | | v v v v
We still keep the current privileges
C++ API to add explicit Redfish privileges for non-redfish routes via BMCWEB_ROUTE
.
We propose to create a new macro REDFISH_ROUTE
so existing REDFISH_ROUTE
stay unchanged for non-redfish routes.
For each REDFISH_ROUTE
, instead of taking a privileges array (or reference to a privileges array), this design proposes to create the following extra C++ APIs:
entity
: it takes a string representing the Resource name (the same definition as it in the PrivilegeRegistry); for example, "/redfish/v1/Managers/${ManagerId}/EthernetInterfaces/" will provide a string "EthernetInterfaceCollection" as the entitysubordinateTo
: it takes an array of string representing what resource this router is subordinate to (the same definition as it in the PrivilegeRegistry); for example, a route with URL "/redfish/v1/Managers/${ManagerId}/EthernetInterfaces/" will provide an array of {"Manager", "EthernetInterfaceCollection"} as the value of subordinateTo
.Any Redfish route must provide these attributes. Non Redfish route shall not provide them, instead, they specify privileges
directly. The values of these attributes can be generated from the schema at compile time. To guarantee this in C++ code, we can make them required parameters in constructors.
See below for how we propose to get required Redfish privileges for a given method on a given resource by using the proposed entity
& subordinateTo
, the existing methods
, and the URL from the request.
See the alternatives section for how we can get rid of setting these attributes at manually.
BMCWeb keeps a JSON like data structure in memory to represent the whole Operation-to-Privilege Mapping. For a given route with known entity name, HTTP method, and resource URL, the required set of privileges can be retrieved efficiently.
The operation to check if a user is authorized to perform a Redfish operation is still the existing bit-wise isSupersetOf
between the required privileges of a given operation on a given resource and the assigned privileges of a role.
BMCWeb has requirements that it doesn't prefer to load a large file at service start time since it slows down the service, and whatever services rely on it.
Thus, we propose to generate the data structure at compile time, it takes a PrivilegeRegistry JSON, and generate necessary headers and sources files to hold a variable that represent the whole Operation-to-Privilege Mapping. The input JSON can change across systems.
This can be implemented as a customized Meson generator.
We will delete the current static privileges header, but the generated header will increase the binary size. We shall limit the increase to <= 100KB. The size of Redfish_1.3.0_PrivilegeRegistry.json
is about 275 KB; the minified version of it (no whitespace) is about 62 KB. When parsing this JSON and converting it to C++ codes, we shall not increase the binary size a lot otherwise we can just store the whole registry as a Nlohmann JSON object.
In routing codes, we can parse the Operation-to-Privilege Mapping Data Structure and look for SubordinateOverrides and ResourceURIOverrides, combine them with the attributes from route and request, and perform authorization.
Most part of the Authorization codes run before resource handlers. However, PropertyOverrides for read operation can only be executed when response is ready since we need to inspect the response attributes. PropertyOverrides for write operator shall still run before the handler codes: the authorization code inspect the request payload and corresponding properties, and look them up in the Operation-to-Privilege Mapping in-memory Data Structure.
A example execution flow for a read operation is mapped out below.
+---------------+ | BMCWeb | Get | routing | /redfish/v1/Managers/${ManagerId}/EthernetInterfaces/ +-------|-------+ | Known information: | Request.URL | Request.method | Route.entity | Route.subordinateTo | +-----------------------+ +--------------------------------------------------------------+ | Any | |Operation-to-Privilege Mapping in-memory Data Structure | | ResourceURIOverrides? <------>| | | | |{ | +-----------|-----------+ | "Entity": "EthernetInterface", | |Updated | "OperationMap": { | |RequiredPrivileges | "Get": [{ | +-----------------------+ | "Privilege": ["ConfigureComponents"] | | Any | | }] | | SubordinateOverrides? |<----->| }, | | | | "SubordinateOverrides": [{ | +-----------------------+ | "Targets": ["Manager", "EthernetInterfaceCollection"], | |Updated | "OperationMap": { | |RequiredPrivileges | "Get": [{ | v | "Privilege": ["ConfigureManager"] | +-----------------------+ | }] | | Authorization | | } | +-----------|-----------+ | }] | |Okay |} | |Got Response | | | +--------------------------------------------------------------+ +-----------------------+ ^ | Any | | | PropertyOverrides? |<----------------------+ +-----------|-----------+ | | v Final Response
The implementation may start with implementing just the overrides specified in DMTF's base PrivilegeRegistry at compile time and reject any PATCH on overrides properties.
BMCWeb will implement PrivilegeRegistry in a new route. The route will reflect the current PrivilegeRegistry in used including dynamic changes.
With the proposed Dynamic Operation-to-Privilege Mapping Data Structure, and DBus APIs that phosphor-user-manager provides, the implementation is trivial: convert the JSON data structure into a JSON response.
Though the Redfish spec declares PrivilegeRegistry to be read-only, this design still proposes implementing PATCH on PrivilegeRegistry: users can PATCH any attributes directly, e.g., patch the POST privilege array of OperationMap of Entity EthernetInterface so that users with "OemEthernetManager" can also send GET to EthernetInterface.
{ Entity": "EthernetInterface", "OperationMap": { "GET": [ { "Privilege": [ "Login" ] }, { "Privilege": [ "OemEthernetManager" ] } ] } }
The implementation might reject modification on certain attributes when corresponding implementation is not ready. E.g., it rejects modifying SubordinateOverrides when the service doesn't have SubordinateOverrides implemented.
Changes against the base PrivilegeRegistry will be rejected, e.g., deleting ConfigureSelf from a OperationMap. This design is for OEMPrivileges and OEMRoles.
OEM Redfish roles, Redfish privileges, and users are persisted by Linux. With a maximum number of roles and privileges being set, the total persistent data will be very small (around several KB).
To minimize size of persistent data, we propose that BMCWeb only stores the serial of PATCH requests on the PrivilegeRegistry. This data can be stored in the existing bmcweb_persistent_data.json
. When restart from crash or reset, BMCWeb will be able to execute the serial of PATCH requests to recover the PrivilegeRegistry. A change on existing bmcweb_persistent_data.json
is that now BMCWeb will write changes to disk (commit) before it returns okay to clients' PATCH on PrivilegeRegistry. Given that operations on PrivilegeRegistry is much less often than other management and monitoring resources, writing a small piece of data to disk is acceptable.
We can set a maximum number of PATCH requests that BMCWeb allows to limit the disk usage. Most upstream systems also have several MB of read-write partition configured. For example, romulus
as of the design was written, 4194304 bytes for rwfs. We propose to start with allowing 1000 requests.
Systems without enabling dynamic authorization feature won't have any new persistent data added.
We can infer the entity from the URL by building a Trie like data structure. However, it's not a big deal to hardcode an entity for a route, since entity and SubordinateTo never change at runtime.
No new repository is required. Phosphor-user-manager and BMCWeb will be modified to implement the design.
Existing tests shall still pass as if this feature is not introduced. New Robot Framework test can be added to test runtime modification of PrivilegeRegistry.
Test cases should include: