@ -5,215 +5,543 @@ with lib;
let
cfg = config . services . mosquitto ;
listenerConf = optionalString cfg . ssl . enable ''
listener $ { toString cfg . ssl . port } $ { cfg . ssl . host }
cafile $ { cfg . ssl . cafile }
certfile $ { cfg . ssl . certfile }
keyfile $ { cfg . ssl . keyfile }
'' ;
passwordConf = optionalString cfg . checkPasswords ''
password_file $ { cfg . dataDir } /passwd
'' ;
mosquittoConf = pkgs . writeText " m o s q u i t t o . c o n f " ''
acl_file $ { aclFile }
persistence true
allow_anonymous $ { boolToString cfg . allowAnonymous }
listener $ { toString cfg . port } $ { cfg . host }
$ { passwordConf }
$ { listenerConf }
$ { cfg . extraConf }
'' ;
userAcl = ( concatStringsSep " \n \n " ( mapAttrsToList ( n : c :
" u s e r ${ n } \n " + ( concatStringsSep " \n " c . acl ) ) cfg . users
) ) ;
aclFile = pkgs . writeText " m o s q u i t t o . a c l " ''
$ { cfg . aclExtraConf }
$ { userAcl }
'' ;
in
{
# note that mosquitto config parsing is very simplistic as of may 2021.
# often times they'll e.g. strtok() a line, check the first two tokens, and ignore the rest.
# there's no escaping available either, so we have to prevent any being necessary.
str = types . strMatching " [ ^ \r \n ] * " // {
description = " s i n g l e - l i n e s t r i n g " ;
} ;
path = types . addCheck types . path ( p : str . check " ${ p } " ) ;
configKey = types . strMatching " [ ^ \r \n \t ] + " ;
optionType = with types ; oneOf [ str path bool int ] // {
description = " s t r i n g , p a t h , b o o l , o r i n t e g e r " ;
} ;
optionToString = v :
if isBool v then boolToString v
else if path . check v then " ${ v } "
else toString v ;
assertKeysValid = prefix : valid : config :
mapAttrsToList
( n : _ : {
assertion = valid ? ${ n } ;
message = " I n v a l i d c o n f i g k e y ${ prefix } . ${ n } . " ;
} )
config ;
formatFreeform = { prefix ? " " }: mapAttrsToList ( n : v : " ${ prefix } ${ n } ${ optionToString v } " ) ;
userOptions = with types ; submodule {
options = {
password = mkOption {
type = uniq ( nullOr str ) ;
default = null ;
description = ''
Specifies the ( clear text ) password for the MQTT User .
'' ;
} ;
###### Interface
passwordFile = mkOption {
type = uniq ( nullOr types . path ) ;
example = " / p a t h / t o / f i l e " ;
default = null ;
description = ''
Specifies the path to a file containing the
clear text password for the MQTT user .
'' ;
} ;
options = {
services . mosquitto = {
enable = mkEnableOption " t h e M Q T T M o s q u i t t o b r o k e r " ;
hashedPassword = mkOption {
type = uniq ( nullOr str ) ;
default = null ;
description = ''
Specifies the hashed password for the MQTT User .
To generate hashed password install <literal> mosquitto < /literal >
package and use <literal> mosquitto_passwd < /literal > .
'' ;
} ;
host = mkOption {
default = " 1 2 7 . 0 . 0 . 1 " ;
example = " 0 . 0 . 0 . 0 " ;
type = types . str ;
hashedPasswordFile = mkOption {
type = uniq ( nullOr types . path ) ;
example = " / p a t h / t o / f i l e " ;
default = null ;
description = ''
Host to listen on without SSL .
Specifies the path to a file containing the
hashed password for the MQTT user .
To generate hashed password install <literal> mosquitto < /literal >
package and use <literal> mosquitto_passwd < /literal > .
'' ;
} ;
port = mkOption {
default = 1883 ;
type = types . int ;
acl = mkOption {
type = listOf str ;
example = [ " r e a d A / B " " r e a d w r i t e A / # " ] ;
default = [ ] ;
description = ''
Port on which to listen without SSL .
Control client access to topics on the broker .
'' ;
} ;
} ;
} ;
ssl = {
enable = mkEnableOption " S S L l i s t e n e r " ;
userAsserts = prefix : users :
mapAttrsToList
( n : _ : {
assertion = builtins . match " [ ^ : \r \n ] + " n != null ;
message = " I n v a l i d u s e r n a m e ${ n } i n ${ prefix } " ;
} )
users
++ mapAttrsToList
( n : u : {
assertion = count ( s : s != null ) [
u . password u . passwordFile u . hashedPassword u . hashedPasswordFile
] <= 1 ;
message = " C a n n o t s e t m o r e t h a n o n e p a s s w o r d o p t i o n f o r u s e r ${ n } i n ${ prefix } " ;
} ) users ;
makePasswordFile = users : path :
let
makeLines = store : file :
mapAttrsToList
( n : u : " a d d L i n e ${ escapeShellArg n } ${ escapeShellArg u . ${ store } } " )
( filterAttrs ( _ : u : u . ${ store } != null ) users )
++ mapAttrsToList
( n : u : " a d d F i l e ${ escapeShellArg n } ${ escapeShellArg " ${ u . ${ file } } " } " )
( filterAttrs ( _ : u : u . ${ file } != null ) users ) ;
plainLines = makeLines " p a s s w o r d " " p a s s w o r d F i l e " ;
hashedLines = makeLines " h a s h e d P a s s w o r d " " h a s h e d P a s s w o r d F i l e " ;
in
pkgs . writeScript " m a k e - m o s q u i t t o - p a s s w d "
( ''
#! ${pkgs.runtimeShell}
set - eu
file = $ { escapeShellArg path }
rm - f " $ f i l e "
touch " $ f i l e "
addLine ( ) {
echo " $ 1 : $ 2 " > > " $ f i l e "
}
addFile ( ) {
if [ $ ( wc - l < " $ 2 " ) - gt 1 ] ; then
echo " i n v a l i d m o s q u i t t o p a s s w o r d f i l e $ 2 " > & 2
return 1
fi
echo " $ 1 : $ ( c a t " $ 2 " ) " > > " $ f i l e "
}
''
+ concatStringsSep " \n "
( plainLines
++ optional ( plainLines != [ ] ) ''
$ { pkgs . mosquitto } /bin/mosquitto_passwd - U " $ f i l e "
''
++ hashedLines ) ) ;
makeACLFile = idx : users : supplement :
pkgs . writeText " m o s q u i t t o - a c l - ${ toString idx } . c o n f "
( concatStringsSep
" \n "
( flatten [
supplement
( mapAttrsToList
( n : u : [ " u s e r ${ n } " ] ++ map ( t : " t o p i c ${ t } " ) u . acl )
users )
] ) ) ;
authPluginOptions = with types ; submodule {
options = {
plugin = mkOption {
type = path ;
description = ''
Plugin path to load , should be a <literal> . so < /literal > file .
'' ;
} ;
cafile = mkOption {
type = types . nullOr types . path ;
default = null ;
description = " P a t h t o P E M e n c o d e d C A c e r t i f i c a t e s . " ;
} ;
denySpecialChars = mkOption {
type = bool ;
description = ''
Automatically disallow all clients using <literal> #</literal>
or <literal> + < /literal > in their name/id.
'' ;
default = true ;
} ;
certfile = mkOption {
type = types . nullOr types . path ;
default = null ;
description = " P a t h t o P E M e n c o d e d s e r v e r c e r t i f i c a t e . " ;
} ;
options = mkOption {
type = attrsOf optionType ;
description = ''
Options for the auth plugin . Each key turns into a <literal> auth_opt_ * < /literal >
line in the config .
'' ;
default = { } ;
} ;
} ;
} ;
keyfile = mkOption {
type = types . nullOr types . path ;
default = null ;
description = " P a t h t o P E M e n c o d e d s e r v e r k e y . " ;
} ;
authAsserts = prefix : auth :
mapAttrsToList
( n : _ : {
assertion = configKey . check n ;
message = " I n v a l i d a u t h p l u g i n k e y ${ prefix } . ${ n } " ;
} )
auth ;
formatAuthPlugin = plugin :
[
" a u t h _ p l u g i n ${ plugin . plugin } "
" a u t h _ p l u g i n _ d e n y _ s p e c i a l _ c h a r s ${ optionToString plugin . denySpecialChars } "
]
++ formatFreeform { prefix = " a u t h _ o p t _ " ; } plugin . options ;
freeformListenerKeys = {
allow_anonymous = 1 ;
allow_zero_length_clientid = 1 ;
auto_id_prefix = 1 ;
cafile = 1 ;
capath = 1 ;
certfile = 1 ;
ciphers = 1 ;
" c i p h e r s _ t l s 1 . 3 " = 1 ;
crlfile = 1 ;
dhparamfile = 1 ;
http_dir = 1 ;
keyfile = 1 ;
max_connections = 1 ;
max_qos = 1 ;
max_topic_alias = 1 ;
mount_point = 1 ;
protocol = 1 ;
psk_file = 1 ;
psk_hint = 1 ;
require_certificate = 1 ;
socket_domain = 1 ;
tls_engine = 1 ;
tls_engine_kpass_sha1 = 1 ;
tls_keyform = 1 ;
tls_version = 1 ;
use_identity_as_username = 1 ;
use_subject_as_username = 1 ;
use_username_as_clientid = 1 ;
} ;
host = mkOption {
default = " 0 . 0 . 0 . 0 " ;
example = " l o c a l h o s t " ;
type = types . str ;
description = ''
Host to listen on with SSL .
'' ;
} ;
listenerOptions = with types ; submodule {
options = {
port = mkOption {
type = port ;
description = ''
Port to listen on . Must be set to 0 to listen on a unix domain socket .
'' ;
default = 1883 ;
} ;
port = mkOption {
default = 8883 ;
type = types . int ;
description = ''
Port on which to listen with SSL .
'' ;
} ;
address = mkOption {
type = nullOr str ;
description = ''
Address to listen on . Listen on <literal> 0 .0 .0 .0 < /literal > / <literal> : : < /literal >
when unset .
'' ;
default = null ;
} ;
dataDir = mkOption {
default = " / v a r / l i b / m o s q u i t t o " ;
type = types . path ;
authPlugins = mkOption {
type = listOf authPluginOptions ;
description = ''
The data directory .
Authentication plugin to attach to this listener .
Refer to the < link xlink:href= " h t t p s : / / m o s q u i t t o . o r g / m a n / m o s q u i t t o - c o n f - 5 . h t m l " >
mosquitto . conf documentation < /link > for details on authentication plugins .
'' ;
default = [ ] ;
} ;
users = mkOption {
type = types . attrsOf ( types . submodule {
options = {
password = mkOption {
type = with types ; uniq ( nullOr str ) ;
default = null ;
description = ''
Specifies the ( clear text ) password for the MQTT User .
'' ;
} ;
type = attrsOf userOptions ;
example = { john = { password = " 1 2 3 4 5 6 " ; acl = [ " t o p i c r e a d w r i t e j o h n / # " ] ; } ; } ;
description = ''
A set of users and their passwords and ACLs .
'' ;
default = { } ;
} ;
passwordFile = mkOption {
type = with types ; uniq ( nullOr str ) ;
example = " / p a t h / t o / f i l e " ;
default = null ;
description = ''
Specifies the path to a file containing the
clear text password for the MQTT user .
'' ;
} ;
acl = mkOption {
type = listOf str ;
description = ''
Additional ACL items to prepend to the generated ACL file .
'' ;
default = [ ] ;
} ;
hashedPassword = mkOption {
type = with types ; uniq ( nullOr str ) ;
default = null ;
description = ''
Specifies the hashed password for the MQTT User .
To generate hashed password install <literal> mosquitto < /literal >
package and use <literal> mosquitto_passwd < /literal > .
'' ;
} ;
settings = mkOption {
type = submodule {
freeformType = attrsOf optionType ;
} ;
description = ''
Additional settings for this listener .
'' ;
default = { } ;
} ;
} ;
} ;
hashedPasswordFile = mkOption {
type = with types ; uniq ( nullOr str ) ;
example = " / p a t h / t o / f i l e " ;
default = null ;
listenerAsserts = prefix : listener :
assertKeysValid prefix freeformListenerKeys listener . settings
++ userAsserts prefix listener . users
++ imap0
( i : v : authAsserts " ${ prefix } . a u t h P l u g i n s . ${ toString i } " v )
listener . authPlugins ;
formatListener = idx : listener :
[
" l i s t e n e r ${ toString listener . port } ${ toString listener . address } "
" p a s s w o r d _ f i l e ${ cfg . dataDir } / p a s s w d - ${ toString idx } "
" a c l _ f i l e ${ makeACLFile idx listener . users listener . acl } "
]
++ formatFreeform { } listener . settings
++ concatMap formatAuthPlugin listener . authPlugins ;
freeformBridgeKeys = {
bridge_alpn = 1 ;
bridge_attempt_unsubscribe = 1 ;
bridge_bind_address = 1 ;
bridge_cafile = 1 ;
bridge_capath = 1 ;
bridge_certfile = 1 ;
bridge_identity = 1 ;
bridge_insecure = 1 ;
bridge_keyfile = 1 ;
bridge_max_packet_size = 1 ;
bridge_outgoing_retain = 1 ;
bridge_protocol_version = 1 ;
bridge_psk = 1 ;
bridge_require_ocsp = 1 ;
bridge_tls_version = 1 ;
cleansession = 1 ;
idle_timeout = 1 ;
keepalive_interval = 1 ;
local_cleansession = 1 ;
local_clientid = 1 ;
local_password = 1 ;
local_username = 1 ;
notification_topic = 1 ;
notifications = 1 ;
notifications_local_only = 1 ;
remote_clientid = 1 ;
remote_password = 1 ;
remote_username = 1 ;
restart_timeout = 1 ;
round_robin = 1 ;
start_type = 1 ;
threshold = 1 ;
try_private = 1 ;
} ;
bridgeOptions = with types ; submodule {
options = {
addresses = mkOption {
type = listOf ( submodule {
options = {
address = mkOption {
type = str ;
description = ''
Specifies the path to a file containing the
hashed password for the MQTT user .
To generate hashed password install <literal> mosquitto < /literal >
package and use <literal> mosquitto_passwd < /literal > .
Address of the remote MQTT broker .
'' ;
} ;
acl = mkOption {
type = types . listOf types . str ;
example = [ " t o p i c r e a d A / B " " t o p i c A / # " ] ;
port = mkOption {
type = port ;
description = ''
Control client access to topics on the broker .
Port of the remote MQTT broker .
'' ;
default = 1883 ;
} ;
} ;
} ) ;
example = { john = { password = " 1 2 3 4 5 6 " ; acl = [ " t o p i c r e a d w r i t e j o h n / # " ] ; } ; } ;
default = [ ] ;
description = ''
A set of users and their passwords and ACLs .
Remote endpoints for the bridge .
'' ;
} ;
allowAnonymous = mkOption {
default = false ;
type = types . bool ;
topics = mkOption {
type = listOf str ;
description = ''
Allow clients to connect without authentication .
Topic patterns to be shared between the two brokers .
Refer to the < link xlink:href= " h t t p s : / / m o s q u i t t o . o r g / m a n / m o s q u i t t o - c o n f - 5 . h t m l " >
mosquitto . conf documentation < /link > for details on the format .
'' ;
default = [ ] ;
example = [ " # b o t h 2 l o c a l / t o p i c / r e m o t e / t o p i c / " ] ;
} ;
checkPasswords = mkOption {
default = false ;
example = true ;
type = types . bool ;
setting s = mkOption {
type = submodule {
freeformType = attrsOf optionTyp e ;
} ;
description = ''
Refuse connection when clients provide incorrect passwords .
Additional settings for this bridge .
'' ;
default = { } ;
} ;
} ;
} ;
extraConf = mkOption {
default = " " ;
type = types . lines ;
description = ''
Extra config to append to ` mosquitto . conf ` file .
'' ;
} ;
bridgeAsserts = prefix : bridge :
assertKeysValid prefix freeformBridgeKeys bridge . settings
++ [ {
assertion = length bridge . addresses > 0 ;
message = " B r i d g e ${ prefix } n e e d s r e m o t e b r o k e r a d d r e s s e s " ;
} ] ;
formatBridge = name : bridge :
[
" c o n n e c t i o n ${ name } "
" a d d r e s s e s ${ concatMapStringsSep " " ( a : " ${ a . address } : ${ toString a . port } " ) bridge . addresses } "
]
++ map ( t : " t o p i c ${ t } " ) bridge . topics
++ formatFreeform { } bridge . settings ;
freeformGlobalKeys = {
allow_duplicate_messages = 1 ;
autosave_interval = 1 ;
autosave_on_changes = 1 ;
check_retain_source = 1 ;
connection_messages = 1 ;
log_facility = 1 ;
log_timestamp = 1 ;
log_timestamp_format = 1 ;
max_inflight_bytes = 1 ;
max_inflight_messages = 1 ;
max_keepalive = 1 ;
max_packet_size = 1 ;
max_queued_bytes = 1 ;
max_queued_messages = 1 ;
memory_limit = 1 ;
message_size_limit = 1 ;
persistence_file = 1 ;
persistence_location = 1 ;
persistent_client_expiration = 1 ;
pid_file = 1 ;
queue_qos0_messages = 1 ;
retain_available = 1 ;
set_tcp_nodelay = 1 ;
sys_interval = 1 ;
upgrade_outgoing_qos = 1 ;
websockets_headers_size = 1 ;
websockets_log_level = 1 ;
} ;
aclExtraConf = mkOption {
default = " " ;
type = types . lines ;
description = ''
Extra config to prepend to the ACL file .
'' ;
} ;
globalOptions = with types ; {
enable = mkEnableOption " t h e M Q T T M o s q u i t t o b r o k e r " ;
bridges = mkOption {
type = attrsOf bridgeOptions ;
default = { } ;
description = ''
Bridges to build to other MQTT brokers .
'' ;
} ;
listeners = mkOption {
type = listOf listenerOptions ;
default = { } ;
description = ''
Listeners to configure on this broker .
'' ;
} ;
includeDirs = mkOption {
type = listOf path ;
description = ''
Directories to be scanned for further config files to include .
Directories will processed in the order given ,
<literal> * . conf < /literal > files in the directory will be
read in case-sensistive alphabetical order .
'' ;
default = [ ] ;
} ;
logDest = mkOption {
type = listOf ( either path ( enum [ " s t d o u t " " s t d e r r " " s y s l o g " " t o p i c " " d l t " ] ) ) ;
description = ''
Destinations to send log messages to .
'' ;
default = [ " s t d e r r " ] ;
} ;
logType = mkOption {
type = listOf ( enum [ " d e b u g " " e r r o r " " w a r n i n g " " n o t i c e " " i n f o r m a t i o n "
" s u b s c r i b e " " u n s u b s c r i b e " " w e b s o c k e t s " " n o n e " " a l l " ] ) ;
description = ''
Types of messages to log .
'' ;
default = [ ] ;
} ;
persistence = mkOption {
type = bool ;
description = ''
Enable persistent storage of subscriptions and messages .
'' ;
default = true ;
} ;
dataDir = mkOption {
default = " / v a r / l i b / m o s q u i t t o " ;
type = types . path ;
description = ''
The data directory .
'' ;
} ;
settings = mkOption {
type = submodule {
freeformType = attrsOf optionType ;
} ;
description = ''
Global configuration options for the mosquitto broker .
'' ;
default = { } ;
} ;
} ;
globalAsserts = prefix : cfg :
flatten [
( assertKeysValid prefix freeformGlobalKeys cfg . settings )
( imap0 ( n : l : listenerAsserts " ${ prefix } . l i s t e n e r . ${ toString n } " l ) cfg . listeners )
( mapAttrsToList ( n : b : bridgeAsserts " ${ prefix } . b r i d g e . ${ n } " b ) cfg . bridges )
] ;
formatGlobal = cfg :
[
" p e r _ l i s t e n e r _ s e t t i n g s t r u e "
" p e r s i s t e n c e ${ optionToString cfg . persistence } "
]
++ map
( d : if path . check d then " l o g _ d e s t f i l e ${ d } " else " l o g _ d e s t ${ d } " )
cfg . logDest
++ map ( t : " l o g _ t y p e ${ t } " ) cfg . logType
++ formatFreeform { } cfg . settings
++ concatLists ( imap0 formatListener cfg . listeners )
++ concatLists ( mapAttrsToList formatBridge cfg . bridges )
++ map ( d : " i n c l u d e _ d i r ${ d } " ) cfg . includeDirs ;
configFile = pkgs . writeText " m o s q u i t t o . c o n f "
( concatStringsSep " \n " ( formatGlobal cfg ) ) ;
in
{
###### Interface
options . services . mosquitto = globalOptions ;
###### Implementation
config = mkIf cfg . enable {
assertions = mapAttrsToList ( name : cfg : {
assertion = length ( filter ( s : s != null ) ( with cfg ; [
password passwordFile hashedPassword hashedPasswordFile
] ) ) <= 1 ;
message = " C a n n o t s e t m o r e t h a n o n e p a s s w o r d o p t i o n " ;
} ) cfg . users ;
assertions = globalAsserts " s e r v i c e s . m o s q u i t t o " cfg ;
systemd . services . mosquitto = {
description = " M o s q u i t t o M Q T T B r o k e r D a e m o n " ;
@ -227,7 +555,7 @@ in
RuntimeDirectory = " m o s q u i t t o " ;
WorkingDirectory = cfg . dataDir ;
Restart = " o n - f a i l u r e " ;
ExecStart = " ${ pkgs . mosquitto } / b i n / m o s q u i t t o - c ${ mosquittoConf } " ;
ExecStart = " ${ pkgs . mosquitto } / b i n / m o s q u i t t o - c ${ configFile } " ;
ExecReload = " ${ pkgs . coreutils } / b i n / k i l l - H U P $ M A I N P I D " ;
# Hardening
@ -252,12 +580,34 @@ in
ReadWritePaths = [
cfg . dataDir
" / t m p " # mosquitto_passwd creates files in /tmp before moving them
] ;
ReadOnlyPaths = with cfg . ssl ; lib . optionals ( enable ) [
certfile
keyfile
cafile
] ;
] ++ filter path . check cfg . logDest ;
ReadOnlyPaths =
map ( p : " ${ p } " )
( cfg . includeDirs
++ filter
( v : v != null )
( flatten [
( map
( l : [
( l . settings . psk_file or null )
( l . settings . http_dir or null )
( l . settings . cafile or null )
( l . settings . capath or null )
( l . settings . certfile or null )
( l . settings . crlfile or null )
( l . settings . dhparamfile or null )
( l . settings . keyfile or null )
] )
cfg . listeners )
( mapAttrsToList
( _ : b : [
( b . settings . bridge_cafile or null )
( b . settings . bridge_capath or null )
( b . settings . bridge_certfile or null )
( b . settings . bridge_keyfile or null )
] )
cfg . bridges )
] ) ) ;
RemoveIPC = true ;
RestrictAddressFamilies = [
" A F _ U N I X " # for sd_notify() call
@ -275,20 +625,12 @@ in
] ;
UMask = " 0 0 7 7 " ;
} ;
preStart = ''
rm - f $ { cfg . dataDir } /passwd
touch $ { cfg . dataDir } /passwd
'' + c o n c a t S t r i n g s S e p " \ n " (
mapAttrsToList ( n : c :
if c . hashedPasswordFile != null then
" e c h o ' ${ n } : ' $ ( c a t ' ${ c . hashedPasswordFile } ' ) > > ${ cfg . dataDir } / p a s s w d "
else if c . passwordFile != null then
" ${ pkgs . mosquitto } / b i n / m o s q u i t t o _ p a s s w d - b ${ cfg . dataDir } / p a s s w d ${ n } $ ( c a t ' ${ c . passwordFile } ' ) "
else if c . hashedPassword != null then
" e c h o ' ${ n } : ${ c . hashedPassword } ' > > ${ cfg . dataDir } / p a s s w d "
else optionalString ( c . password != null )
" ${ pkgs . mosquitto } / b i n / m o s q u i t t o _ p a s s w d - b ${ cfg . dataDir } / p a s s w d ${ n } ' ${ c . password } ' "
) cfg . users ) ;
preStart =
concatStringsSep
" \n "
( imap0
( idx : listener : makePasswordFile listener . users " ${ cfg . dataDir } / p a s s w d - ${ toString idx } " )
cfg . listeners ) ;
} ;
users . users . mosquitto = {
@ -302,4 +644,6 @@ in
users . groups . mosquitto . gid = config . ids . gids . mosquitto ;
} ;
meta . maintainers = with lib . maintainers ; [ pennae ] ;
}