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.
-
Copy the template
users.yaml-dist
from thedist-config
distribution directory to yourconfig
directory. -
Restart Reactor.
-
Hard-refresh the Reactor UI. You should see a login screen. You can log in using the default user
admin
with passwordadmin
.
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:
-
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
. -
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 thetools
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:
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-likem!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 inparams
must be present on the request and match in value for the ACL to be selected. This criterion applies only to requests made with theGET
method with query parameters on the URL itself (e.g./some/path?value=123
has a query parameter named value), andPOST
with parameters in aapplication/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 forparams
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 supplyingauth_user
andauth_pass
on the request (and those matching a known user/password), using HTTP Basic authentication (e.g. usingcurl --basic -u user -p pass
) for the request, or passing a Long-Lived Access Token (see below) in theAuthorization
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 orauth_user/auth_pass
query parameters (type: basic
). If a request has no authentication, the type will benone
; checkingtype: none
is equivalent toauthorized: 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 useranonymous
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 toauthorization: false
): Long-Lived Access Tokens are not bound to a user, so requsts made with LLATs will matchuser: anonymous
even though they have been authorized by the use of the token. For this reason, usinguser: anonymous
as ACL criterion is discouraged; useauthorized
and/ortype
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:
Example #2: we want to allow all API accesses that are authenticated using any Long-Lived Access Token:
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:
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
— whenlog_acls
istrue
, 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
— whendebug_acls
istrue
, 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:
-
Request Parameters — this is the easiest, but perhaps least desirable, mechanism for authentication. It involves simply adding
auth_user
andauth_pass
query parameters to your request. An example usingcurl
: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.
-
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. -
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