Skip to content

Access Control

Access Control in Reactor is meant to provide a basic means of user authentication and protection of Reactor and the entities it supervises.

Security is a chain that includes many, many links. The success of implementing security lies not in how seriously you approach one link, but rather how you approach all of them. Securing Reactor alone will not harden your system from attacks. For example, if you secure Reactor but fail to adequately secure the host system on which Reactor runs, all of Reactor's security and encryption keys could be compromised and make Reactor's security mechanisms worthless. Here is a short list of other things to consider (this list is for example and not by any means exhaustive):

  • Adequately protect the user accounts, resources, and other services running on the system on which you run Reactor. In particular, weak passwords on user accounts or poor filesystem permissions practices can expose the key files Reactor uses to encrypt and decrypt data, generate access tokens, etc. Protect the user account under which Reactor runs and make sure your file permissions (including umask for the running Reactor process) are set to deny access to others. Never run Reactor or any service as root unless it is in a restricted container (like docker).
  • Use a firewall or equivalent device to protect your network from attacks from the outside. Note that Network Address Translation (NAT) alone is not sufficient as a firewall.
  • Secure your WiFi network with a strong password and strong encryption. Monitor your WiFi network continuously for unauthorized devices.
  • Have a separate, secure WiFi network for guests. Deny access from that network to anything but the Internet (i.e. no access to anything in your house).
  • Ideally, have a separate network for all of your home automation hubs and devices, and restrict access from that network to your other networks.
  • Apply well-known best practices for establishing secure passwords and managing them, and change passwords periodically. Avoid using the same password for multiple applications/purposes.

There are two kinds of skiers...

It is said that there are two kinds of skiers: those who have fallen, and those who are about to. Falling when skiing is inevitable. Anything secured is no different; it's not a matter of if it can be cracked, it's just a matter of when. Interest, effort, and capability are major factors in cracking systems. Especially today, systems are so complex and inter-dependent that latent exploits lay everywhere, so practices like layered protection and strong encryption/hashing are required strategies. Still, the best anyone can do is slow down an attacker, hopefully to make it impractical and not worth the effort. Encryption and hashing algorithms that were once considered "secure" have been defeated by ever-increasing computing power, and every algorithm and approach we use today will someday fall to it as well. It's not "if" it happens, it's "when." Often we don't know a system can be cracked until it is, and then there's a scramble to patch or replace it. A big part of your job handling your own security is to make yourself not worth the effort, to take away the incentive. A typical burglar alarm on a home is easily defeated; what makes it work is that it makes a neighbor who doesn't have one a more attractive target — easier to enter, lower risk of attention and capture. You'll never stop the rare, dedicated burglar, but a majority aren't that dedicated, they're just opportunistic, and will pick an easy target over a hard one.

Enabling Access Control and User Authetication

By default, Reactor's UI (admin and dashboard) is open for use without authentication. User authentication is supported, however, with some simple setup.

  1. Copy the template users.yaml-dist from the dist-config distribution directory to your config directory.

  2. Restart Reactor.

  3. Hard-refresh the Reactor UI. You should see a login screen. You can log in using the default user admin with password admin.

The distribution's example users.yaml file contains four users: admin, guest, john, and jane. The password for each is the same as the username by default, so make your testing easy. You may remove any or all of these users (and probably should).

Configuring Reactor Users

The users section of users.yaml is used to define the known usernames and their passwords. It is a YAML object that contains key/value pairs, where the key is the username and the value is the user's password.

users:
  admin: admin
  guest: guest
  john: $5$4CxEup.TT8DAB2Ym$pnfxYn.O2RIckFy6/bNNfqrgd.5xDVBPiYBUUjcDFG8
  jane: $6$mYI/d5POjy3zm8Pw$ZF1IHp6n8ocZ9RdRpxfUKIC.IpLk9pBjNd1akU4Ov1OSXA0rdB95FP/j5B9iyIyn8g6Il40XoCBX5Wwgn0qJh1

The password values for users john and jane shown here are not literal passwords, they are hashed passwords. Hashing passwords makes it more difficult to ascertain the cleartext password if the users.yaml file falls into malicious hands. Hashing is one-way, so even if the hash string is known, it is improbable (but never impossible) that the password could be derived from it. There are only two supported hashes currently, SHA-256 and SHA-512, using the Ulrich Drepper algorithm also used by OpenSSL. User john is shown with a SHA-256 hash, and user jane with a SHA-512 hash. There are two ways to hash your password so you can store them in this way:

  1. Using OpenSSL, if you have it installed and available. Run the following command from your system's shell (you don't need to be root):

    openssl passwd [-5|-6] -stdin     # choose either -5 or -6 for SHA-256 or -512
    

    OpenSSL will wait for you to enter the password. It will then hash it and display the hash. Be aware that if you enter the password again, it will produce a different hash, because the salt is created randomly for each entry, so you can't compare your two hashes to determine if you typed the password correctly. Copy the entire hash string to the user's configuration line under users.

  2. If you don't have OpenSSL, you can use the tools/gen_pass_hash.js tool. Run this tool from your Reactor install directory (not from within the tools subdirectory):

    node tools/gen_pass_hash.js [-6]    # -6 is optional, use for SHA-512
    

    Follow the prompts from the tool. Copy the result hash to the user's configuration line under users.

Hashing is Highly Recommended

It is recommended that you store all passwords as hashes, but you do you. Just understand that if you store cleartext passwords in users.yaml, there's risk someone (or something) on your network could poach them. If you must use cleartext passwords, definitely do not use any password that you use for anything else anywhere.

Enable HTTPS!

If you are going to use Access Control in Reactor, best practices demand that you also enable SSL/TLS (i.e. HTTPS). See How To: HTTPS for information on enabling this feature.

User Groups and Group-level Permissions

The groups section defines what user groups are available, and what applications that group can access. A group can have any number of users.

groups:
  admin:             # define the admin group
    users:           # this is the list (array) of users in the group
      - admin
    applications: true  # special form allows access to ALL applications

  house:
    users:
      - patrick
      - erin
    applications:
      - dashboard

  guests:
    users:
      - guest
    applications:
      - dashboard

In the above group configuration, three groups are defined: admin, house, and guests. The admin group has access to all applications. The applications configuration key normally takes an array of applications names, but setting it to boolean true means all applications are permitted; setting it to false would deny all applications. Application names currently defined are reactor and dashboard, which are the Reactor Admin UI (where rules, reactions, and expressions are defined) and the Reactor Dashboard.

Normally, the value of users is an array of usernames. One exception is permitted: users: "*". Using this form, all users, including anonymous (not logged in), are defined as group members. If you want to make a group of all authenticated users (i.e. excluding anonymous), you need to name every user using array form.

Application Session Life

By default, a Reactor login session is valid for two hours. Thereafter, a new login is needed to re-authorize access. In addition, ongoing use of the UI or Dashboard does not restart the two-hour clock; it expires two hours from login by default.

You can change the session life by adding a session section to users.yaml and defining timeout to be the number of seconds of session life you want.

You can change the expiration policy to a rolling refresh, where activity on the UI pushes out the session life. You will remain logged in until there has been no activity for the session timeout period. To do that, set the rolling configuration key to the value true.

Here's an example session section with timeout and rolling values:

session:
  timeout: 3600  # 3600 seconds is 1 hour
  rolling: true
  clear_on_restart: false

The clear_on_restart (boolean) value determines if all sessions are erased when Reactor is restarted. By default, authorizations will persist across restarts. If this value is true, then authorizations will be cleared and all clients will need to log in again after a Reactor restart.

Access Control Lists for the API

To control access to the Reactor API, Access Control Lists (ACLs) can be defined. In fact, even if you don't define any, there is a default list to provide basic protection to the API. When user authentication/access control is enabled (i.e. config/users.yaml exists), these default rules will interfere with your unfettered access to the API, so odds are you will need to define at least a couple of your own ACLs.

The ACLs live in a top-level array called api_acls in users.yaml. Every element of api_acls is an ACL structured as follows:

api_acls:
  - url: "/api/path/being/allowed/or/protected"
    method: "GET|POST|PUT"
    user: username
    group: groupname
    params:
      name1: value1
      name2: value2
      # ..etc..
    headers:
      name1: value1
      name2: value2
      # ..etc..
    authorized: true|false
    type: basic|auth|llat|none
    token: token-string
    source_ip: ip-or-cidr
    allow: true|false   # default is false

All of the above configuration keys are optional when creating an ACL. ACLs are processed in the order in which they are defined. The first matching ACL determines whether the resource access is allowed or denied (as determined by the value of its allow). Matching is simple: for every ACL, all of the criteria must match the request for the ACL to take effect. If any criterion does not match, the ACL is not considered and the system moves on to the next ACL.

Only two of the ACL definition keys have defaults: url and allow. If url is not given, the path / is assumed. If allow is not given, the default is false (deny access).

When processing ACLs, Reactor takes the full pathname of the request and attempts to match it to the ACL url. If no ACLs match, the last path component is removed and the ACLs checked again. That process repeats until a matching ACL is found. If no match is found, the access is denied. To illustrate this path handling, consider a request for /api/v1/variable/test/set. Reactor will first attempt to find an ACL matching that full path. If no match is found, Reactor will try to match /api/v1/variable/test, then /api/v1/variable, then /api/v1, and so on.

Here are a few details about some of the keys:

  • params defines an object containing key/value pairs, where the keys are query parameter names on the request. The values can be literal strings or regular expressions. To use a regular expression, use the /regexp/flags form, or the Perl-like m!regexp!flags form. In the Perl form, the ! can be any punctuation character; this form allows you to more easily write regular expressions to match strings containing /. All paramters defined in params must be present on the request and match in value for the ACL to be selected. This criterion applies only to requests made with the GET method with query parameters on the URL itself (e.g. /some/path?value=123 has a query parameter named value), and POST with parameters in a application/x-www-form-urlencoded request body (it does not work for JSON or other body types).
  • headers defines an object containing key/value pairs that are matched to the request's (HTTP) headers. Semantics are the same as for params described above.
  • authorized can be used to determine if a request has been made with a valid authorization or not. A valid authorization can be made by supplying auth_user and auth_pass on the request (and those matching a known user/password), using HTTP Basic authentication (e.g. using curl --basic -u user -p pass) for the request, or passing a Long-Lived Access Token (see below) in the Authorization header of the request.
  • type allows you drill down on specific types of authorization. This can allow, for example, an ACL that permits all requests to the API made with Long-Lived Access Tokens (type: llat), or requests from known users that have authenticated with HTTP basic authorization or auth_user/auth_pass query parameters (type: basic). If a request has no authentication, the type will be none; checking type: none is equivalent to authorized: false (and therefore it would be redundant to include both in an ACL).
  • token allows you to match a specific token. This is really only valid for matching Long-Lived Access Tokens. It would permit, for example, the creation of an LLAT to access one specific API resource while not opening up that resource to all LLATs or any request not using an LLAT at all.
  • source_ip matches a specific IP address, or an address in a CIDR range. CIDR ranges are specified as A.B.C.D/masklen. For example, 192.168.0.0/24 matches the common network 192.168.0.0 with netmask 255.255.255.0, or all addresses from 192.168.0.0 to 192.168.0.255. Because the mask length is 24 bits, only the first three elements of the CIDR address determine the network ID (each element is 8 bits), so you could also write the shorthand 192.168.0/24. Another common CIDR network is 127/8, which matches all addresses with the 127 in the first element; this is the loopback network in which all addresses are assumed to be on the local machine (localhost at 127.0.0.1 is the best known of these). A mask length of 32 matches all 32 bits of the address and so a specific host, and is the default mask length if none is given (that is, specifing A.B.C.D is the same as A.B.C.D/32).
  • user: there is a special user anonymous that matches a user for which the username is not known. This does not mean the request is not authorized (i.e. it is not equivalent to authorization: false): Long-Lived Access Tokens are not bound to a user, so requsts made with LLATs will match user: anonymous even though they have been authorized by the use of the token. For this reason, using user: anonymous as ACL criterion is discouraged; use authorized and/or type instead.

Best Practice: Strong IP Filtering

One easy link you can add to your security chain is IP filtering. Reactor has long had the allowed_ips configuration array in reactor.yaml to specify addresses or ranges that can access any part of Reactor, including the dashboard and API. Those restrictions are processed first. API ACLs are processed after. It's highly recommended that you use at least allowed_ips to allow only your home network to access Reactor. Further restricting API access by using ACLs with source_ip is then recommmended in addition to prevent cracked, misconfigured, or faulty devices inside your network from accessing things they should not.

Example #1: we want to allow access to the entire API for a special subset of users. First, create a group for those users, then create an ACL for them:

groups:
  all_api_allowed:
    users:
      - john
      - jane

api_acls:
  - url: "/api"
    group: all_api_allowed
    allow: true

Example #2: we want to allow all API accesses that are authenticated using any Long-Lived Access Token:

api_acls:
  - url: "/api"
    type: llat
    allow: true

Example #3: we want any device in our device network 10.0.0.0 with netmask 255.0.0.0 to access any part of the API:

  - url: "/api"
    source_ip: 10/8  # or 10.0.0.0/8
    allow: true

K.I.S.S.

Keep your ACLs as simple and restrictive as your needs allow. It's better to grant specific permission to individual resources than it is to do a number of denies for things you don't want external access to followed by a more general allow. This is especially true because any additions to the API may then not be protected. Always use the longest url path possible, and always use source_ip to be specific about what source addresses are allowed access.

Processed In Order

Don't forget that ACLs are processed in the order in which you define them. The first matching ACL wins. You may have to put some thought into the interactions between ACLs, and I recommend testing using the various authentication methods available to make sure everything works as expected and you don't leave open holes.

Debugging ACLs

In order to help you troubleshoot ACLs, your first line of defense is always the logs. Always look in the logs when something unexpected is happening. This applies to ACLs and everything else in the Reactor world.

By default, any access that is denied by an ACL will be logged. If you need more than that, there are two configuration keys you can add to users.yaml at the top level:

  • log_acls: true — when log_acls is true, Reactor will log the ACL that it found to apply to the request. This should help you identify which of your ACLs is being selected, and from there, you may be able to reason out why.

  • debug_acls: true — when debug_acls is true, considerably more detail about the ACL search is logged. This will often help you determine why an ACL that you thought would apply ended up not being selected.

Making API Requests with Authorization

As stated above, when access control and user authentication are enabled (by the existence of config/users.yaml), access to the API is limited by default: only localhost and addresses in the loopback network are permitted to access the API. Once you have users, groups, and ACLs configured to allow authenticated access from other network addresses, the following mechanisms are available to you to authorize an API request:

  1. Request Parameters — this is the easiest, but perhaps least desirable, mechanism for authentication. It involves simply adding auth_user and auth_pass query parameters to your request. An example using curl:

    curl -k 'https://192.168.0.66:8111/api/v1/logging/httpapi?auth_user=jane&auth_pass=jane&other=...'
    

    As you can see, in this form, the username and password are passed on the URL in cleartext, which could expose them.

  2. HTTP Basic Authentication — this is a common standard.

    curl -k --basic -u jane:jane 'https://192.168.0.66:8111/api/v1/logging/httpapi'
    

    or

    curl -k -H 'Authorization: Basic amFuZTpqYW5l' 'https://192.168.0.66:8111/api/v1/logging/httpapi'
    

    The former command with --basic -u is just a shortcut for creating the latter. The odd string in the second example's authorization header is created by Base64-encoding the username and password separated by a colon. There are a number of online tools available for doing this. This provides at least a little obscurity to the username and password from the eyes of a casual observer. However, Base64 is not encryption, and is well-known and easily reversed, so anyone with the small of amount of know-how required will be able to derive the password easily.

  3. Long-Lived Access Token — this allows you to use a LLAT to access the resource. It's not much different from creating a special user account and password and using either of the above methods (e.g. a user account and an LLAT could be quickly revoked if discovered).

    curl -k --H 'Authorization: Bearer llat-token-string' 'https://192.168.0.66:8111/api/v1/logging/httpapi'
    

All of these examples use curl, but you can any mechanism capable of making an HTTP request that is available to you. See the documentation for those tools for information on adding the necessary request header for LLAT authentication or using HTTP Basic Authentication.

Generating Long-Lived Access Tokens (LLATs)

A Long-Lived Access Token (LLAT) is a bearer token that has no effective expiration. Rather than using user authentication, the LLAT is given in the Authorization header of any requests made to the API.

The easiest way to generate an LLAT is using curl or similar facility on your host. You can also use your browser is you must. You will fetch /api/v1/gen_llat from the Reactor host, and it will reply with the token text.

    curl -k -u user:pass 'https://192.168.0.66:8554/api/v1/gen_llat'

(Almost) Permanent Access

An LLAT is an unrestricted token to access the API. They should be used sparingly, and protected from disclosure where they are used. The only way to invalidate an LLAT is to remove the storage/access_token.hex file. Reactor will then generate a new one, and all previously-known LLATs will immediately become invalid (i.e. you are starting over). Removing this file only affects LLATs; it has no effect on other forms of access.

Updated: 2024-May-23