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

Setting `timeout` to 0 disables time-based session expiration. Sessions will remain valid until the user explicitly logs out. This weakens the security of the system and is not recommended.

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.

The values for `user`, `group`, `source_ip`, and `method` may be prefixed with `!` (exclamation mark) to negate the sense of the test. That is, `user: "!james"` would match any user other than `james` (i.e. `!` means not, so not `james`).

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:
      api_allowed:
        users:
          - john
          - jane

    api_acls:
      - url: "/api"
        group: 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-Sep-12