Sign inSign up

iegomez/mosquitto-go-auth

By iegomez

Updated over 1 year ago

Mosquitto with mosquitto-go-auth plugin image.

Image
9

500K+

iegomez/mosquitto-go-auth repository overview

Mosquitto Go Auth

Mosquitto Go Auth is an authentication and authorization plugin for the Mosquitto MQTT broker. The name is terrible, I know, but it's too late to change it. And, you know: naming, cache invalidation, off-by-one errors and whatnot.

Current state

  • The plugin is up to date and is compatible with the recent 2.0 Mosquitto version.
  • Bug reports will be attended as they appear and will take priority over any work in progress.
  • Reviewing ongoing PRs is my next priority.
  • Feature requests are the lowest priority. Unless they are a super easy win in importance and implementation effort, I'll accept contributions and review PRs before considering implementing them myself.
Intro

This is an authentication and authorization plugin for mosquitto, a well known open source MQTT broker.
It's written (almost) entirely in Go: it uses cgo to expose mosquitto's auth plugin needed functions, but internally just calls Go to get everything done.

It is greatly inspired in jpmens' mosquitto-auth-plug.

These are the backends that this plugin implements right now:

  • Files
  • PostgreSQL
  • JWT (with local DB or remote API)
  • HTTP
  • Redis
  • Mysql
  • SQLite3
  • MongoDB
  • Custom (experimental)
  • gRPC
  • Javascript interpreter

Every backend offers user, superuser and acl checks, and include proper tests.

Please open an issue with the feature or enhancement tag to request new backends or additions to existing ones.

Table of contents
Requirements

This package uses Go modules to manage dependencies. As it interacts with mosquitto, it makes use of cgo. Also, it (optionally) uses Redis for cache purposes.

Build

Before building, you need to build mosquitto. For completeness, we'll build it with websockets, tls and srv support.

First, install dependencies (tested on Debian 9 and later, Linux Mint 18, 19 and 20):

sudo apt-get install libwebsockets8 libwebsockets-dev libc-ares2 libc-ares-dev openssl uuid uuid-dev

Download mosquitto and extract it (change versions accordingly):

wget http://mosquitto.org/files/source/mosquitto-1.6.10.tar.gz
tar xzvf mosquitto-1.6.10.tar.gz
cd mosquitto-1.6.10

Modify config.mk, setting websockets support. Then build mosquitto, add a mosquitto user and set ownership for /var/log/mosquitto and /var/lib/mosquitto/ (default log and persistence locations).

make
sudo make install
sudo groupadd mosquitto
sudo useradd -s /sbin/nologin mosquitto -g mosquitto -d /var/lib/mosquitto
sudo mkdir -p /var/log/mosquitto/ /var/lib/mosquitto/
sudo chown -R mosquitto:mosquitto /var/log/mosquitto/
sudo chown -R mosquitto:mosquitto /var/lib/mosquitto/

Finally, you may create a service for mosquitto. Create the file /etc/systemd/system/mosquitto.service with these annotations:

[Unit]
Description=Mosquitto MQTT v3.1/v3.1.1 server
Wants=network.target
Documentation=http://mosquitto.org/documentation/

[Service]
Type=simple
User=mosquitto
Group=mosquitto
ExecStart=/usr/local/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf
Restart=on-failure
SyslogIdentifier=Mosquitto

[Install]
WantedBy=multi-user.target

If you are running another distro or need more details on building mosquitto, please check the offical mosquitto docs.

Building the plugin

Only Linux (tested in Debian, Ubuntu and Mint ùs) and MacOS are supported.

Before attempting to build the plugin, make sure you have go installed on the system. The minimum required GO version for the current release is 1.13.8. To check which version (if any) of Go is installed on the system, simply run the following:

go version

If Go is not installed or the installed version is older than 1.13.8, please update it. You can retrieve and install the latest version of Go from the official Go download website:

# Update the following as per your system configuration
export GO_VERSION=1.16.4
export GO_OS=linux
export GO_ARCH=amd64

wget https://dl.google.com/go/go${GO_VERSION}.${GO_OS}-${GO_ARCH}.tar.gz -O golang.tar.gz
sudo tar -C /usr/local -xzf golang.tar.gz
export PATH=$PATH:/usr/local/go/bin
rm golang.tar.gz

# Prints the Go version
go version

This will fetch all the go dependecies and then build go-auth.so shared object:

make

This assumes that mosquitto.h, mosquitto_plugin.h and mosquitto_broker.h are located at /usr/local/include, which is true for a manually built mosquitto version in debian based systems (and probably others too), or manually built or installed through brew (brew install mosquitto) mosquitto version in MacOS.

If this doesn't work for your distribution or OS version, please check Makefile CFLAGS and LDFLAGS and adjust accordingly. File an issue or open a PR if you wish to contribute correct flags for your system.

Raspberry Pi

Important notice: RPi support has been tested only until versions 1.4.x. The introduction of new plugin functions in Mosquitto may result in some issue compiling versions 1.5.x and later. Please reach me with any solutions you may find when resolving said issues.

To build on a Raspberry Pi (tested with Pi 3 B), you'll need to have Go installed first. You can install latest version (last tested was 1.10.1, change it to suit your needs) with something like this:

wget https://storage.googleapis.com/golang/go1.10.1.linux-armv6l.tar.gz
sudo tar -C /usr/local -xzf go1.10.1.linux-armv6l.tar.gz

Add Go to your path at .profile:

export PATH=$PATH:/usr/local/go/bin:~/go/bin

Source the file (source ~/.profile) and check Go was correctly installed (go version).

Now get requirements and build as usual (just have some more patience).

Openssl and websockets notes

There seems to be missing packages in some Raspbian versions, so you should try to apt update before installing dependencies. Alternatively, you cand build openssl like this:

git clone git://git.openssl.org/openssl.git
cd openssl
./config
make
make test
sudo make install

For websockets support, you'll have to build libwebsockets, which needs cmake. So something like this should do the trick:

sudo apt-get install cmake
git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets
mkdir build
cd build
cmake ..
make
make install
Configuration

The plugin is configured in Mosquitto's configuration file (typically mosquitto.conf). You may define all options there, or include e.g. a conf.d dir for plugin configuration:

include_dir /etc/mosquitto/conf.d

Create some conf file (e.g., go-auth.conf) at your preferred location, e.g. /etc/mosquitto/conf.d/, and register the plugin's shared object path and desired backends with:

auth_plugin /etc/mosquitto/conf.d/go-auth.so

auth_opt_backends files, postgres, jwt

Set all other plugin options below in the same file.

Cache

There are 2 types of caches supported: an in memory one using go-cache, or a Redis backed one.

Set cache option to true to use a cache (defaults to false when missing) and cache_type to set the type of the cache. By default the plugin will use go-cache unless explicitly told to use Redis. Set cache_reset to flush the cache on mosquitto startup (hydrating go-cache on startup is not yet supported).

Update v1.2: Set cache_refresh to refresh expiration each time a record is found in the cache (defaults to false). Before v1.2 cache was always refreshed upon check. In order to prevent security issues, where an attacker would frequently check on a topic to keep their granted status, even when revoked in the underlying backend, this has been turned into an option that defaults to no refreshing.

Finally, set expiration times in seconds for authentication (auth) and authorization (acl) caches:

auth_opt_cache true
auth_opt_cache_type redis
auth_opt_cache_reset true
auth_opt_cache_refresh true

auth_opt_auth_cache_seconds 30
auth_opt_acl_cache_seconds 30
auth_opt_auth_jitter_seconds 3
auth_opt_acl_jitter_seconds 3

auth_jitter_seconds and acl_jitter_seconds options allow to randomize cache expiration time by a given offset The value used for expiring a cache record would then be cache_seconds +/- jitter_seconds. With above values (30 seconds for cache and 3 seconds for jitter), effective expiration would yield any value between 27 and 33 seconds. Setting a jitter value is useful to reduce lookups storms that could occur every auth/acl_cache_seconds if lots of clients connected at the same time, e.g. after a server restart when all clients may reconnect immediately creating lots of entries expiring at the same time. You may omit or set jitter options to 0 to disable this feature.

If cache_reset is set to false or omitted, cache won't be flushed upon service start.

When using Redis, the following defaults will be used if no values are given. Also, these are the available options for cache:

auth_opt_cache_host localhost
auth_opt_cache_port 6379
auth_opt_cache_password pwd
auth_opt_cache_db 3

If you want to use a Redis cluster as your cache, you may omit previous Redis options and instead need to set auth_opt_cache_mode to cluster and provide the different addresses as a list of comma separated host:port strings with the auth_opt_cache_addresses options:

auth_opt_cache_mode cluster
auth_opt_cache_addresses host1:port1,host2:port2,host3:port3

Notice that if cache_mode is not provided or isn't equal to cluster, cache will default to use a single instance with the common options. If instead the mode is set to cluster but no addresses are given, the plugin will default to not use a cache.

Hashing

There are 3 options for password hashing available: PBKDF2 (default), Bcrypt and Argon2ID. Every backend that needs one -that's all but grpc, http and custom- gets a hasher and whether it uses specific options or general ones depends on the auth opts passed.

Provided options define what hasher each backend will use:

  • If there are general hashing options available but no backend ones, then every backend will use those general ones for its hasher.
  • If there are no options available in general and none for a given backend either, that backend will use defaults (see hashing/hashing.go for default values).
  • If there are options for a given backend but no general ones, the backend will use its own hasher and any backend that doesn't register a hasher will use defaults.

You may set the desired general hasher with this option, passing either pbkdf2, bcrypt or argon2id values. When not set, the option will default to pbkdf2.

auth_opt_hasher pbkdf2

Each hasher has specific options. Notice that when using the pw utility, these values must match those used to generate the password.

PBKDF2
auth_opt_hasher_salt_size 16           # salt bytes length
auth_opt_hasher_iterations 100000      # number of iterations
auth_opt_hasher_keylen 64              # key length
auth_opt_hasher_algorithm sha512       # hashing algorithm, either sha512 (default) or sha256
auth_opt_hasher_salt_encoding          # salt encoding, either base64 (default) or utf-8
Bcrypt
auth_opt_hasher_cost 10                # key expansion iteration count
Argon2ID
auth_opt_hasher_salt_size 16           # salt bytes length
auth_opt_hasher_iterations 3           # number of iterations
auth_opt_hasher_keylen 64              # key length
auth_opt_hasher_memory 4096            # amount of memory (in kibibytes) to use
auth_opt_hasher_parallelism 2          # degree of parallelism (i.e. number of threads)

These options may be defined for each backend that needs a hasher by prepending the backend's name to the option, e.g. for setting argon2id as Postgres' hasher:

auth_opt_pg_hasher argon2id
auth_opt_pg_hasher_salt_size 16           # salt bytes length
auth_opt_pg_hasher_iterations 3           # number of iterations
auth_opt_pg_hasher_keylen 64              # key length
auth_opt_pg_hasher_memory 4096            # amount of memory (in kibibytes) to use
auth_opt_pg_hasher_parallelism            # degree of parallelism (i.e. number of threads)
Logging

You can set the log level with the log_level option. Valid values are: debug, info, warn, error, fatal and panic. If not set, default value is info.

auth_opt_log_level debug

Log destination may be set with log_dest option. Valid values are stderr (default), stdout and file. In the latter case the log_file option needs to be set, e.g.:

auth_opt_log_dest file
auth_opt_log_file /var/log/mosquitto/mosquitto.log

If log_dest or log_file are invalid, or if there's an error opening the file (e.g. no permissions), logging will default to stderr.

Do not, I repeat, do not set log_level to debug in production, it may leak sensitive information. Reason? When debugging it's quite useful to log actual passwords, hashes, etc. to check which backend or hasher is failing to do its job. This should be used only when debugging locally, I can't stress enough how log level should never, ever be set to debug in production.

You've been warned.

Retry

By default, if backend had an error (and no other backend granted access), an error is returned to Mosquitto.

It's possible to enable retry, which will immediately retry all configured backends. This could be useful if the backend may be behind a load-balancer (like HTTP backend) and one instance may fail:

auth_opt_retry_count 2

The above example will do up to 2 retries (3 calls in total considering the original one) if the responsible backend had an error or was down while performing the check.

Prefixes

Though the plugin may have multiple backends enabled, there's a way to specify which backend must be used for a given user: prefixes. When enabled, prefixes allow to check if the username contains a predefined prefix in the form prefix_username and use the configured backend for that prefix. Options to enable and set prefixes are the following:

auth_opt_check_prefix true
auth_opt_prefixes filesprefix, pgprefix, jwtprefix

Prefixes must meet the declared backends order and number. If amounts don't match, the plugin will default to prefixes disabled.

Underscores (_) are not allowed in the prefixes, as a username's prefix will be checked against the first underscore's index. Of course, if a username has no underscore or valid prefix, it'll be checked against all backends.

Superuser checks

By default superuser checks are supported and enabled in all backends but Files (see details below). They may be turned off per backend by either setting individual disable options or not providing necessary options such as queries for DB backends, or for all of them by setting this global option to true:

auth_opt_disable_superuser true

Any other value or missing option will have superuser enabled.

ACL access values

Mosquitto 1.5 introduced a new ACL access value, MOSQ_ACL_SUBSCRIBE, which is similar to the classic MOSQ_ACL_READ value but not quite the same:

 *  MOSQ_ACL_SUBSCRIBE when a client is asking to subscribe to a topic string.
 *                     This differs from MOSQ_ACL_READ in that it allows you to
 *                     deny access to topic strings rather than by pattern. For
 *                     example, you may use MOSQ_ACL_SUBSCRIBE to deny
 *                     subscriptions to '#', but allow all topics in
 *                     MOSQ_ACL_READ. This allows clients to subscribe to any
 *                     topic they want, but not discover what topics are in use
 *                     on the server.
 *  MOSQ_ACL_READ      when a message is about to be sent to a client (i.e. whether
 *                     it can read that topic or not).

The main difference is that subscribe is checked at first, when a client connects and tells the broker it wants to subscribe to some topic, while read is checked when an actual message is being published to that topic, which makes it particular. So in practice you could deny general subscriptions such as # by returning false from the acl check when you receive MOSQ_ACL_SUBSCRIBE, but allow any particular one by returning true on MOSQ_ACL_READ. Please take this into consideration when designing your ACL records on every backend.

Also, these are the current available values from mosquitto:

#define MOSQ_ACL_NONE 0x00
#define MOSQ_ACL_READ 0x01
#define MOSQ_ACL_WRITE 0x02
#define MOSQ_ACL_SUBSCRIBE 0x04

If you're using prior versions then MOSQ_ACL_SUBSCRIBE is not available and you don't need to worry about it.

Backend options

Any other options with a leading auth_opt_ are handed to the plugin and used by the backends. Individual backends have their options described in the sections below.

Testing

As of now every backend has proper but really ugly tests in place: they expect services running for each backend, and are also pretty outdated and cumbersome to work with in general. This issue captures these concerns and a basic plan to refactor tests: https://github.com/iegomez/mosquitto-go-auth/issues/67.

You may run all tests (see Testing X for each backend's testing requirements) like this:

make test
Registering checks

Backends may register which checks they'll run, enabling the option to only check user auth through some backends, for example an HTTP one, while delegating ACL checks to another backend, e.g. Files. By default, when the option is not present, all checks for that backend will be enabled (unless superuser is globally disabled in the case of superuser checks). For user and acl checks, at least one backend needs to be registered, either explicitly or by default.

You may register which checks a backend will perform with the option auth_opt_backend_register followed by comma separated values of the registered checks, e.g.:

auth_opt_http_register user
auth_opt_files_register user, acl
auth_opt_redis_register superuser

Possible values for checks are user, superuser and acl. Any other value will result in an error on plugin initialization.

Files

The files backend implements the regular password and acl checks as described in mosquitto. Passwords should be in PBKDF2, Bcrypt or Argon2ID format (for other backends too), see Hashing for more details about different hashing strategies. Hashes may be generated using the pw utility (built by default when running make) included in the plugin (or one of your own). Passwords may also be tested using the pw-test package.

Usage of pw:

Usage of ./pw:
  -a string
    	algorithm: sha256 or sha512 (default "sha512")
  -c int
    	bcrypt ost param (default 10)
  -e string
    	salt encoding (default "base64")
  -h string
    	hasher: pbkdf2, argon2 or bcrypt (default "pbkdf2")
  -i int
    	hash iterations: defaults to 100000 for pbkdf2, please set to a reasonable value for argon2 (default 100000)
  -l int
    	key length, recommended values are 32 for sha256 and 64 for sha512
  -m int
    	memory for argon2 hash (default 4096)
  -p string
    	password
  -pl int
    	parallelism for argon2 (default 2)
  -s int
    	salt size (default 16)

For this backend passwords and acls file paths must be given:

auth_opt_files_password_path /path/to/password_file
auth_opt_files_acl_path /path/to/acl_file

The following are correctly formatted examples of password and acl files:

Passwords file
test1:PBKDF2$sha512$100000$2WQHK5rjNN+oOT+TZAsWAw==$TDf4Y6J+9BdnjucFQ0ZUWlTwzncTjOOeE00W4Qm8lfPQyPCZACCjgfdK353jdGFwJjAf6vPAYaba9+z4GWK7Gg==
test2:PBKDF2$sha512$100000$o513B9FfaKTL6xalU+UUwA==$mAUtjVg1aHkDpudOnLKUQs8ddGtKKyu+xi07tftd5umPKQKnJeXf1X7RpoL/Gj/ZRdpuBu5GWZ+NZ2rYyAsi1g==
ACL file
user test1
topic write test/topic/1
topic read test/topic/2

user test2
topic read test/topic/+

user test3
topic read test/#

pattern read test/%u
pattern read test/%c

The ACLs file follows mosquitto's regular syntax: mosquitto(5).

There's no special superuser check for this backend since granting a user all permissions on # works in the same way. Furthermore, if this is the only backend registered, then providing no ACLs file path will default to grant all permissions for authenticated users when doing ACL checks (but then, why use a plugin if you can just use Mosquitto's static file checks, right?): if, instead, no ACLs file path is provided but there are more backends registered, this backend will default to deny any permissions for any user (again, back to basics).

Testing Files

Proper test files are provided in the repo (see test-files dir) and are needed in order to test this backend.

PostgreSQL

The postgres backend allows to specify queries for user, superuser and acl checks to be tested against your schema.

The following auth_opt_ options are supported:

OptiondefaultMandatoryMeaning
pg_hostlocalhosthostname/address
pg_port5432TCP port
pg_userYusername
pg_passwordYpassword
pg_dbnameYdatabase name
pg_userqueryYSQL for users
pg_superqueryNSQL for superusers
pg_aclqueryNSQL for ACLs
pg_sslmodedisableNSSL/TLS mode.
pg_sslcertNSSL/TLS Client Cert.
pg_sslkeyNSSL/TLS Client Cert. Key
pg_sslrootcertNSSL/TLS Root Cert
pg_connect_tries-1Nx < 0: try forever, x > 0: try x times

Depending on the sslmode given, sslcert, sslkey and sslrootcert will be used. Options for sslmode are:

disable - No SSL
require - Always SSL (skip verification)
verify-ca - Always SSL (verify that the certificate presented by the server was signed by a trusted CA)
verify-full - Always SSL (verify that the certification presented by the server was signed by a trusted CA and the server host name matches the one in the certificate)

Queries work pretty much the same as in jpmen's plugin, so here's his discription (with some little changes) about them:

The SQL query for looking up a user's password hash is mandatory. The query
MUST return a single row only (any other number of rows is considered to be
"user not found"), and it MUST return a single column only with the PBKDF2
password hash. A single `'$1'` in the query string is replaced by the
username attempt

Tag summary

Content type

Image

Digest

sha256:acd65f856

Size

65.4 MB

Last updated

over 3 years ago

docker pull iegomez/mosquitto-go-auth