# Dynamic Redfish Authorization Author: Nan Zhou (nanzhoumails@gmail.com) Created: 08/08/2022 ## Problem Description 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. ## Background and References ### Redfish Authorization Model 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: 1. https://redfish.dmtf.org/schemas/DSP0266_1.15.1.html#privilege-model 2. https://redfish.dmtf.org/schemas/DSP0266_1.15.1.html#redfish-service-operation-to-privilege-mapping 3. https://redfish.dmtf.org/schemas/DSP0266_1.15.1.html#roles 4. https://redfish.dmtf.org/registries/v1/Redfish_1.3.0_PrivilegeRegistry.json ### Phosphor-user-manager 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: 1. https://github.com/openbmc/docs/blob/master/architecture/user-management.md 2. https://github.com/openbmc/phosphor-user-manager 3. https://github.com/openbmc/phosphor-dbus-interfaces/tree/master/yaml/xyz/openbmc_project/User 4. https://linux.die.net/man/8/nslcd 5. https://linux.die.net/man/8/nscd ### BMCWeb 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: 1. PAM based: use Linux-PAM to do username/password style of authentication 2. TLS based: use the Public Key infrastructure to verify signature of peer's certificates; then use identities (in X509 certificates, these are Common Name or Subject Alternative Name) as user names. 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: 1. https://github.com/openbmc/bmcweb/blob/d9f6c621036162e9071ce3c3a333b4544c6db870/include/authentication.hpp 2. https://github.com/openbmc/bmcweb/blob/d9f6c621036162e9071ce3c3a333b4544c6db870/http/http_connection.hpp 3. https://github.com/openbmc/bmcweb/blob/d9f6c621036162e9071ce3c3a333b4544c6db870/redfish-core/lib/roles.hpp ### Gaps As mentioned above, majority of the current Redfish authorization settings are configured at compile time including: 1. the set of Phosphor-user-manager privileges 2. the mapping from Phosphor-user-manager privileges to Redfish roles 3. the set of Redfish roles 4. the mapping from Redfish roles to Redfish Privileges 5. the operation-to-privilege mapping 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. ## Requirements BMC implements a dynamic Redfish authorization system: 1. Clients shall be able to add new OEM Redfish privileges without recompile 2. Clients shall be able to add new OEM Redfish roles and assign it with any existing Redfish privileges without recompile 3. Clients shall be able to modify existing operation-to-privilege mappings without recompile 4. Above changes on systems shall be atomic; that is, once changed, all new queries will use the latest configurations 5. BMC shall perform sanity check on above modification; that is - It rejects ill formatted modification - It rejects modification of non-OEM privileges - It rejects deletion of OEM Redfish roles if any user (either local or remote) maps such roles - It rejects deletion of OEM Redfish privileges if any OEM Redfish role is assigned such privileges 6. BMC shall persist all the above modifications and recover from crash 7. Existing systems with the static authorization shall work as if this feature is not introduced, including non-Redfish routes (e.g., KVM websocket) 8. Default OEM roles and Redfish privileges must be selectable on a per system basis at compile time; default Redfish PrivilegeRegistry must be settable on a per system basis at compile time 9. The total storage used by this feature shall be limited; this is - The total rwfs disk usage increase is less than 100 KB on systems with the dynamic authorization feature enabled - The runtime memory usage increase is less than 1 MB on systems with the dynamic authorization feature enabled - The binary size increase of modified daemons is less than 100 KB on all systems 10. BMC implements a complete privilege registry; that is - It shall implement all overrides in the Redfish base Privilege registries at compile time; it shall support configuring overrides at runtime but implementation may begin with static configuring and reject runtime modification - BMC exposes PrivilegeRegistry which represents the current configuration and reflects runtime changes - Changes to resource entities shall be propagated to the current privilege registries automatically 11. New Redfish resource can be implemented without modifying custom PrivilegeRegistry 12. All the above shall be testable; CI must be capable of finding issues ## Proposed Design ### Mapping: Users, Redfish Roles, and Redfish Privileges 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: 1. It returns AllPrivileges dynamically by looking up the current groups 2. Phosphor-user-manager provides DBus APIs to query privileges of a given user 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`. ### Creation/Deletion: Users, Redfish Roles, and Redfish Privileges Base privileges and roles won't be allowed to change at runtime. #### OEM Redfish Privileges 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: 1. Phosphor-user-manager provides DBus APIs to create/delete OEM Redfish privileges; under the hood, it stores privileges as Linux groups, as today how base Redfish roles are stored 2. Phosphor-user-manager keeps a maximum number of Redfish privileges; we propose 32 as the first iteration considering fast bit manipulation 3. Phosphor-user-manager performs validation: - Names of OEM Redfish privileges are unique and valid; e.g., start with "openbmc-orfp-" - Reject deletion of a privilege that's currently in use (assigned to any Redfish roles that have a user associated with) Systems can also optionally add OEM Privileges at compile time via Yocto's GROUPADD_PARAM in the useradd class. #### OEM Redfish Roles 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: 1. Phosphor-user-manager provides DBus APIs to create Redfish role 2. Phosphor-user-manager keeps a maximum number of Redfish roles; we propose 32 as the first iteration considering fast bit manipulation 3. Phosphor-user-manager performs validation: - Names of OEM Redfish roles are unique and valid; e.g., start with "openbmc-orfr-" - Reject deletion of a RedfishRole that's currently in use (associated with users) #### Users 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. #### Typical Workflow 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 ``` ### Non-Redfish Routes or OEM Resources We still keep the current `privileges` C++ API to add explicit Redfish privileges for non-redfish routes via `BMCWEB_ROUTE`. ### Redfish Routes 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: 1. `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 entity 2. `subordinateTo`: 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. ### Operation-to-Privilege Mapping Data Structure in Memory 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. ### Generate Operation-to-Privilege Mapping Data Structure at Compile Time 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. ### Operation-to-Privilege Mapping Overrides 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. ### PrivilegeRegistry Resource 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. ### PATCH on PrivilegeRegistry 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. ### Persistent Data 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. ## Alternatives Considered ### Infer Entity and SubordinateTo from URL 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. ## Impacts 1. New DBus interfaces on phosphor-user-manager 2. New persistent data managed by BMCWeb will be added on BMCs 3. Binary size will increase on BMCWeb 4. Existing systems with the static authorization shall work as if this feature is not introduced ## Organizational No new repository is required. Phosphor-user-manager and BMCWeb will be modified to implement the design. ## Testing 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: 1. verify base Redfish roles, privileges, and base operation-to-privilege respect the Redfish spec when no runtime modification is made 2. verify Redfish OemPrivilege can be added via PATCH to PrivilegeRegistry and reflected in the PrivilegeRegistry resource 3. verify Redfish OemRole can be added via POST to ManagerAccountCollection with assigned OemPrivilege and reflected in the ManagerAccountCollection 4. verify operation-to-privilege can be modified via PATCH on PrivilegeRegistry; mapping of an action on a resource can be added with the above OemPrivilege, and finally the user of that OemRole now can access the resource 5. verify the 3 dynamic overriding is working as expected; e.g., a new override can be added to PrivilegeRegistry and should be reflected in new requests