Nabto Edge IAM - Decentralized Access Control

The Nabto Edge platform at the lowest level provides the authentication and authorization primitives to build a robust and secure solution - as described in the earlier sections. For instance, the embedded device application can access the public key associated with the current connection to validate it is in a list of allowed public keys.

In addition to the low level primitives, a higher level module “Nabto Edge IAM” is provided that builds on these core primitives. IAM means “Identity and Access Management”. With this module, you can implement decentralized access control solutions, ie involving just a client application and a device.

The Nabto Edge IAM simplifies the task of creating production quality applications by implementing:

  • pairing between clients and embedded devices using different strategies
  • mapping clients (users) into roles
  • assigning permissions to roles through policy definitions
  • an API for checking permissions from the embedded application
  • a service layer to manage the IAM configuration from clients

The IAM module is implemented as a set of CoAP services that can be invoked from the client (locally or remotely) to initiate pairing and manage the IAM configuration. The module provides an authorization API to be used by the device application to evaluate if a given operation can proceed.

Useful default policies and rule implementations are provided with all examples and standard applications, meaning that Nabto Edge IAM can be used as is without drafting a new complex configuration. This Nabto provided configuration can then be extended and modified as necessary.

The IAM configuration can be described as a JSON document or built using the IAM configuration API. The IAM configuration is defined prior to starting the device and cannot be modified after the device is started.

In addition to the static IAM configuration, the IAM module has parameters configurable at runtime, these are considered part of the IAM state.

Configuration

Roles

A role definition consists of an identifier (a name) and a reference to a set of policies. The standard applications and examples come with the following roles defined:

  • Unpaired: Attached policies allow only pairing operations
  • Guest: Attached policies allow limited interaction (if any)
  • Standard: Attached policies allow all regular use (ie primary IoT device use cases, such as control heat or unlock door)
  • Admin: Attached policies allow all regular use, configuration changes and user management

The corresponding JSON IAM configuration for some of these look like:

{
   "Roles" : [
      {
         "Id" : "Unpaired",
         "Policies" : [
            "Pairing"
         ]
      },
      {
         "Id" : "Standard",
         "Policies" : [
            "Tunnelling",
            "Pairing",
            "ManageOwnUser"
         ]
      },
}

If using the IAM API directly, roles are created using nm_iam_configuration_role_new() and related functions.

The IAM framework maps the current user to a given role based on the user’s public key associated with the current connection. The embedded device application queries the IAM framework if the current user is in a role with a policy attached that allows the given operation.

A user is mapped to a single role. This means that for instance, in addition to admin specific policies, the Admin role must have all the Standard role’s policies to allow both administration and regular use.

Policies

A policy consists of an identifier (name) and a list of statements. Each statement consists of an effect, some actions and optionally some conditions.

Actions are attributes that can be referenced by the embedded application. For instance, in the following example from the TCP Tunnel application, the TcpTunnel:Connect is referenced from the embedded application’s tunnel implementation: Is the current user in a role that allows opening a tunnel connection?

   "Policies" : [
      {
         "Id" : "Tunnelling",
         "Statements" : [
            {
               "Actions" : [
                  "TcpTunnel:GetService",
                  "TcpTunnel:Connect",
                  "TcpTunnel:ListServices"
               ],
               "Effect" : "Allow"
            }
         ]
      },

The optional conditions can be used to limit the scope of a policy. For instance, the following policy limits TCP tunnels to the ssh type only:

{
   "Policies" : [
      {
        "Id": "TunnelSsh",
        "Statements": [
          {
            "Actions": [
               "TcpTunnel:Connect"
            ],
            "Conditions": [
              { "StringEquals": {"TcpTunnel:ServiceType": [ "ssh" ] } }
            ]
            "Effect": "Allow",
          }
        ]
     },

The possible condition operators are described in the API documentation for the nm_iam_condition_operator enum.

The IAM configuration can be constructed using the [Configuration Builder API] - and serialized/deserialized to/from JSON using the Serializer API. The configuration must defined prior to starting the device.

State

Users

The IAM module maintains a list of users who have access to the device, manipulated through the pairing functions in the CoAP API or directly from the device application.

A user has the following properties:

  • a username:
  • a public key fingerprint used for authentication
  • a role used for authorization
  • an optional display name
  • an optional pairing password: set if using password invite pairing - for password open pairing, a system-wide password is used instead of a per-user password
  • an optional Server Connect Token - to allow remote pairing if the solution is configured to use this mechanism for centralized, coarse-grained access control

Other state

Additionally, different pairing specific parameters can be set at runtime: Pairing mode and details for the individual modes, such as the system wide open pairing password.

State life cycle

The IAM state can be constructed using the State Builder API - and serialized/deserialized to/from JSON using the Serializer API. Note that only a limited, thread-safe set of state accessor and mutator functions are allowed to be used from the application after the device is started.

The application is notified about changes in the IAM state and is responsible for persisting the state.

Developer Tasks

When using Nabto Edge IAM, the developer is responsible for the following 3 tasks. Note that if using standard applications (e.g., the Nabto Edge TCP tunnel applications), no or very little actual IAM integration and configuration is necessary.

1. Define static IAM config and prepare initial state

Define the required roles on the system and their permissions - either by referencing existing policies or by drafting new.

Decide the pairing mode(s) to use.

If the embedded application’s resources allow it, the configuration can be supplied as a JSON document using nm_iam_serializer_state_load_json() as seen in the tunnel application. Or the configuration can be built programmatically.

2. Invoke IAM services from client

Some IAM functionality is invoked implicitly from the client; for instance, when opening a connection towards a target device you specify a keypair to use. The public key is passed on to the target device which in turn uses the IAM functionality to lookup roles, policies and allowed operations - but nothing IAM module specific interaction is taking place in the client.

IAM functionality is explicitly invoked when the IAM runtime configuration is changed on the target device, e.g. to perform pairing. Similarly, explicit user management functions can be invoked, e.g. CoAP DELETE /iam/users/:username.

These explicit IAM operations are invoked by the client using the CoAP interface just as if invoking a regular Nabto Edge CoAP service as part of the embedded application.

All IAM services exposed are described in the IAM services reference.

3. Invoke IAM authorization checks on embedded device

If using standard applications, there is nothing further to do here. For instance, the TCP tunnel implementation already validates that the necessary tunnel actions are allowed for the current user.

If implementing your own embedded device application, you can query the IAM module at runtime using nm_iam_check_access(). For instance, a CoAP application can validate that a given operation is allowed:

if (!nm_iam_check_access(iam_, nabto_device_coap_request_get_connection_ref(request),
       "HeatPump:Set", NULL)) {
    nabto_device_coap_error_response(request, 403, "Unauthorized");
    nabto_device_coap_request_free(request);
}

In this example, the application checks the HeatPump:Set action. The IAM module looks up a policy with this action (and an “Allow” effect) and verifies that the current user is in a role associated with this policy.

Full Example Configuration

IAM configuration

The below configuration must be passed to nm_iam_serializer_state_load_json() to read the static IAM configuration on the device.

{
   "Config" : {
      "UnpairedRole" : "Unpaired"
   },
   "Policies" : [
      {
         "Id" : "Pairing",
         "Statements" : [
            {
               "Actions" : [
                  "Pairing:Get",
                  "Pairing:Password",
                  "Pairing:Local"
               ],
               "Effect" : "Allow"
            }
         ]
      },
      {
         "Id" : "Tunnelling",
         "Statements" : [
            {
               "Actions" : [
                  "TcpTunnel:GetService",
                  "TcpTunnel:Connect",
                  "TcpTunnel:ListServices"
               ],
               "Effect" : "Allow"
            }
         ]
      },
      {
         "Id" : "ManageUsers",
         "Statements" : [
            {
               "Actions" : [
                  "IAM:ListUsers",
                  "IAM:GetUser",
                  "IAM:DeleteUser",
                  "IAM:AddRoleToUser",
                  "IAM:RemoveRoleFromUser",
                  "IAM:ListRoles"
               ],
               "Effect" : "Allow"
            }
         ]
      },
      {
         "Id" : "ManageOwnUser",
         "Statements" : [
            {
               "Actions" : [
                  "IAM:GetUser",
                  "IAM:DeleteUser"
               ],
               "Conditions" : [
                  {
                     "StringEquals" : {
                        "IAM:UserId" : [
                           "${Connection:UserId}"
                        ]
                     }
                  }
               ],
               "Effect" : "Allow"
            }
         ]
      }
   ],
   "Roles" : [
      {
         "Id" : "Unpaired",
         "Policies" : [
            "Pairing"
         ]
      },
      {
         "Id" : "Admin",
         "Policies" : [
            "ManageUsers"
         ]
      },
      {
         "Id" : "Guest",
         "Policies" : [
            "Pairing",
            "ManageOwnUser"
         ]
      },
      {
         "Id" : "Standard",
         "Policies" : [
            "Tunnelling",
            "Pairing",
            "ManageOwnUser"
         ]
      }
   ],
   "Version" : 1
}

IAM state preparation

The following is an example of setting up an initial IAM state:

void create_default_iam_state(NabtoDevice* device, const char* filename, struct nn_log* logger)
{
    struct nm_iam_state* state = nm_iam_state_new();
    struct nm_iam_user* user = nm_iam_state_user_new("admin");
    char* sct = NULL;
    nabto_device_create_server_connect_token(device, &sct);
    nm_iam_state_user_set_sct(user, sct);
    nabto_device_string_free(sct);
    nm_iam_state_user_set_role(user, "Administrator");
    nm_iam_state_add_user(state, user);
    nm_iam_state_set_initial_pairing_username(state, "admin");
    nm_iam_state_set_local_initial_pairing(state, true);
    nm_iam_state_set_local_open_pairing(state, true);
    nm_iam_state_set_open_pairing_role(state, "Guest");
    save_iam_state(filename, state, logger);
    nm_iam_state_free(state);
}