diff options
8 files changed, 409 insertions, 95 deletions
diff --git a/applications/luci-app-fwknopd/Makefile b/applications/luci-app-fwknopd/Makefile index 73a9e6864..ba7a8568e 100644 --- a/applications/luci-app-fwknopd/Makefile +++ b/applications/luci-app-fwknopd/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=Fwknopd config - web config for the firewall knock daemon -LUCI_DEPENDS:=+luci-compat +fwknopd +qrencode +LUCI_DEPENDS:=+fwknopd +qrencode PKG_LICENSE:=GPLv2 PKG_MAINTAINER:=Jonathan Bennett <JBennett@incomsystems.biz> include ../../luci.mk diff --git a/applications/luci-app-fwknopd/htdocs/luci-static/resources/view/fwknopd.js b/applications/luci-app-fwknopd/htdocs/luci-static/resources/view/fwknopd.js new file mode 100644 index 000000000..b88e11980 --- /dev/null +++ b/applications/luci-app-fwknopd/htdocs/luci-static/resources/view/fwknopd.js @@ -0,0 +1,379 @@ +'use strict'; +'require fs'; +'require dom'; +'require view'; +'require form'; +'require ui'; +'require tools.widgets as widgets'; + +var domparser = new DOMParser(); +var QRCODE_VARIABLES = ['KEY_BASE64', 'KEY', 'HMAC_KEY_BASE64', 'HMAC_KEY']; +var INVALID_KEYS = ['__CHANGEME__', 'CHANGEME']; + +function setOptionValue(map, section_id, option, value) { + var option = L.toArray(map.lookupOption(option, section_id))[0]; + var uiEl = option ? option.getUIElement(section_id) : null; + if (uiEl) + uiEl.setValue(value); +} + +function lines(content) { + return content.split(/\r?\n/); +} + +function parseKeys(content) { + var l = lines(content); + var keys = {}; + for (var i = 0; i < l.length; i++) { + var p = l[i].split(/:(.*)/, 2); + if (p.length == 2) + keys[p[0].trim()] = p[1].trim(); + } + return keys; +} + +var KeyTypeValue = form.ListValue.extend({ + __init__: function() { + this.super('__init__', arguments); + }, + + cfgvalue: function(section_id) { + for (var i = 0; i < this.keylist.length; i++) { + var value = this.map.data.get( + this.uciconfig || this.section.uciconfig || this.map.config, + this.ucisection || section_id, + this.keylist[i] + ); + if (value) + return this.keylist[i]; + } + return this.keylist[0]; + }, + + remove: function() { + // Ignore + }, + + write: function() { + // Ignore + }, +}); + +var YNValue = form.Flag.extend({ + __init__: function() { + this.super('__init__', arguments); + this.enabled = 'Y'; + this.disabled = 'N'; + this.default = 'N'; + }, + + cfgvalue: function(section_id) { + var value = this.super('cfgvalue', arguments); + return value ? String(value).toUpperCase() : value; + }, + + parse: function(section_id) { + var active = this.isActive(section_id), + cval = this.cfgvalue(section_id), + fval = active ? this.formvalue(section_id) : null; + + if (String(fval).toUpperCase() != cval) { + if (fval == 'Y') + return Promise.resolve(this.write(section_id, fval)); + else if (cval !== undefined) + return Promise.resolve(this.remove(section_id)); + } + }, +}); + +var QrCodeValue = form.DummyValue.extend({ + __init__: function() { + this.super('__init__', arguments); + this.needsRefresh = {}; + + this.components = []; + QRCODE_VARIABLES.forEach(L.bind(function(option) { + this.components.push(option); + var dep = {}; + dep[option] = /.+/; + this.depends(dep); + }, this)); + }, + + cfgQrCode: function(section_id) { + var qr = []; + for (var i = 0; i < this.components.length; i++) { + var value = this.map.data.get( + this.uciconfig || this.section.uciconfig || this.map.config, + this.ucisection || section_id, + this.components[i] + ); + + if (value) + qr.push(this.components[i] + ':' + value); + } + return qr ? qr.join(' ') : null; + }, + + formQrCode: function(section_id) { + var qr = []; + for (var i = 0; i < this.components.length; i++) { + var value = null; + + var uiEl = L.toArray(this.map.lookupOption(this.components[i], section_id))[0]; + if (uiEl) { + if (uiEl.isActive(section_id)) + value = uiEl.formvalue(section_id); + } + + if (value) + qr.push(this.components[i] + ':' + value); + } + return qr ? qr.join(' ') : null; + }, + + onchange: function(ev, section_id) { + if (this.needsRefresh[section_id] !== undefined) + this.needsRefresh[section_id] = true; + else { + this.refresh(section_id); + } + }, + + refresh: function(section_id) { + var qrcode = this.formQrCode(section_id); + var formvalue = this.formvalue(section_id); + if (formvalue != qrcode) { + this.getUIElement(section_id).setValue(qrcode); + var uiEl = document.getElementById(this.cbid(section_id)); + if (uiEl) { + var contentEl = uiEl.nextSibling; + if (contentEl.childNodes.length == 1) { + dom.append(contentEl, E('em', { 'class': 'spinning', }, [ _('Loading…') ])); + } + + this.needsRefresh[section_id] = false; + + // Render QR code + return this.renderSvg(qrcode) + .then(L.bind(function(svgEl) { + dom.content(contentEl, svgEl || E('div')); + + var needsAnotherRefresh = this.needsRefresh[section_id]; + delete this.needsRefresh[section_id]; + + if (needsAnotherRefresh) { + this.refresh(section_id); + } + }, this)).finally(L.bind(function() { + if (this.needsRefresh[section_id] === undefined) { + if (contentEl.childNodes.length == 2) + contentEl.removeChild(contentEl.lastChild); + delete this.needsRefresh[section_id]; + } + }, this)).catch(L.error); + } + } + // Nothing to render + return Promise.resolve(null); + }, + + renderWidget: function(section_id) { + var qrcode = this.cfgQrCode(section_id); + return this.renderSvg(qrcode) + .then(L.bind(function(svgEl) { + var uiEl = new ui.Hiddenfield(qrcode, { id: this.cbid(section_id) }); + return E([ + uiEl.render(), + E('div', {}, svgEl || E('div')) + ]); + }, this)); + }, + + qrEncodeSvg: function(qrcode) { + return fs.exec('/usr/bin/qrencode', ['--type', 'svg', '--inline', '-o', '-', qrcode]) + .then(function(response) { + return response.stdout; + }); + }, + + renderSvg: function(qrcode) { + if (qrcode) + return this.qrEncodeSvg(qrcode) + .then(function(rawsvg) { + return domparser.parseFromString(rawsvg, 'image/svg+xml') + .querySelector('svg'); + }); + else + return Promise.resolve(null); + }, +}); + +var GenerateButton = form.Button.extend({ + __init__: function() { + this.super('__init__', arguments); + this.onclick = L.bind(this.generateKeys, this); + this.keytypes = {}; + }, + + keytype: function(key, regex) { + this.keytypes[key] = regex; + }, + + qrcode: function(option) { + this.qrcode = option; + }, + + generateKeys: function(ev, section_id) { + return fs.exec('/usr/sbin/fwknopd', ['--key-gen']) + .then(function(response) { return parseKeys(response.stdout); }) + .then(L.bind(this.applyKeys, this, section_id)) + .catch(L.error); + }, + + applyKeys: function(section_id, keys) { + for (var key in keys) { + setOptionValue(this.map, section_id, key, keys[key]); + for (var type in this.keytypes) { + if (this.keytypes[type].test(key)) + setOptionValue(this.map, section_id, type, key); + } + } + + // Force update of dependencies (element visibility) + this.map.checkDepends(); + + // Refresh QR code + var option = L.toArray(this.map.lookupOption(this.qrcode, section_id))[0]; + if (option) + return option.refresh(section_id); + else + return Promise.resolve(null); + }, + +}); + +return view.extend({ + + render: function() { + var m, s, o; + + m = new form.Map('fwknopd', _('Firewall Knock Operator Daemon')); + + s = m.section(form.TypedSection, 'global', _('Enable Uci/Luci control')); + s.anonymous = true; + s.option(form.Flag, 'uci_enabled', _('Enable config overwrite'), _('When unchecked, the config files in /etc/fwknopd will be used as is, ignoring any settings here.')); + + s = m.section(form.TypedSection, 'network', _('Network configuration')); + s.anonymous = true; + o = s.option(widgets.NetworkSelect, 'network', _('Network'), _('The network on which the daemon listens. The daemon \ + is automatically started when the network is up-and-running. This option \ + has precedence over “PCAP_INTF” option.')); + o.unpecified = true; + o.nocreate = true; + o.rmempty = true; + + // set the access.conf settings + s = m.section(form.TypedSection, 'access', _('access.conf stanzas')); + s.anonymous = true; + s.addremove = true; + + var qrCode = s.option(QrCodeValue, 'qr', _('QR code'), ('QR code to configure fwknopd Android application.')); + + o = s.option(form.Value, 'SOURCE', 'SOURCE', _('The source address from which the SPA packet will be accepted. The string “ANY” is \ + also accepted if a valid SPA packet should be honored from any source IP. \ + Networks should be specified in CIDR notation (e.g. “192.168.10.0/24”), \ + and individual IP addresses can be specified as well. Multiple entries \ + are comma-separated.')); + o.validate = function(section_id, value) { + return String(value).length > 0 ? true : _('The source address has to be specified.'); + } + + s.option(form.Value, 'DESTINATION', 'DESTINATION', _('The destination address for which the SPA packet will be accepted. The \ + string “ANY” is also accepted if a valid SPA packet should be honored to any \ + destination IP. Networks should be specified in CIDR notation \ + (e.g. “192.168.10.0/24”), and individual IP addresses can be specified as well. \ + Multiple entries are comma-separated.')); + + o = s.option(GenerateButton, 'keys', _('Generate keys'), _('Generates the symmetric key used for decrypting an incoming \ + SPA packet, that is encrypted by the fwknop client with Rijndael block cipher, \ + and HMAC authentication key used to verify the authenticity of the incoming SPA \ + packet before the packet is decrypted.')); + o.inputtitle = _("Generate Keys"); + o.keytype('keytype', /^KEY/); + o.keytype('hkeytype', /^HMAC_KEY/); + o.qrcode('qr'); + + o = s.option(form.Value, 'KEY', 'KEY', _('Define the symmetric key used for decrypting an incoming SPA \ + packet that is encrypted by the fwknop client with Rijndael.')); + o.depends('keytype', 'KEY'); + o.onchange = L.bind(qrCode.onchange, qrCode); + o.validate = function(section_id, value) { + return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.'); + } + + o = s.option(form.Value, 'KEY_BASE64', 'KEY_BASE64', _('Define the symmetric key (in Base64 encoding) used for \ + decrypting an incoming SPA packet that is encrypted by the fwknop client \ + with Rijndael.')); + o.depends('keytype', 'KEY_BASE64'); + o.onchange = L.bind(qrCode.onchange, qrCode); + o.validate = function(section_id, value) { + return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.'); + } + + o = s.option(KeyTypeValue, 'keytype', _('Key type')); + o.value('KEY', _('Normal key')); + o.value('KEY_BASE64', _('Base64 key')); + o.onchange = L.bind(qrCode.onchange, qrCode); + + o = s.option(form.Value, 'HMAC_KEY', 'HMAC_KEY', _('Define the HMAC authentication key used for verifying \ + the authenticity of the SPA packet before the packet is decrypted.')); + o.depends('hkeytype', 'HMAC_KEY'); + o.onchange = L.bind(qrCode.onchange, qrCode); + o.validate = function(section_id, value) { + return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.'); + } + + o = s.option(form.Value, 'HMAC_KEY_BASE64', 'HMAC_KEY_BASE64', _('Define the HMAC authentication key \ + (in Base64 encoding) used for verifying the authenticity of the SPA \ + packet before the packet is decrypted.')); + o.depends('hkeytype', 'HMAC_KEY_BASE64'); + o.onchange = L.bind(qrCode.onchange, qrCode); + o.validate = function(section_id, value) { + return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.'); + } + + o = s.option(KeyTypeValue, 'hkeytype', _('HMAC key type')); + o.value('HMAC_KEY', _('Normal key')); + o.value('HMAC_KEY_BASE64', _('Base64 key')); + o.onchange = L.bind(qrCode.onchange, qrCode); + + o = s.option(form.Value, 'OPEN_PORTS', 'OPEN_PORTS', _('Define a set of ports and protocols (tcp or udp) that will be opened if a valid knock sequence is seen. \ + If this entry is not set, fwknopd will attempt to honor any proto/port request specified in the SPA data \ + (unless of it matches any “RESTRICT_PORTS” entries). Multiple entries are comma-separated.')); + o.placeholder = "protocol/port,..."; + + o = s.option(form.Value, 'RESTRICT_PORTS', 'RESTRICT_PORTS', _('Define a set of ports and protocols (tcp or udp) that are explicitly not allowed \ + regardless of the validity of the incoming SPA packet. Multiple entries are comma-separated.')); + o.placeholder = "protocol/port,..."; + + o = s.option(form.Value, 'FW_ACCESS_TIMEOUT', 'FW_ACCESS_TIMEOUT', _('Define the length of time access will be granted by fwknopd through the firewall after a \ + valid knock sequence from a source IP address. If “FW_ACCESS_TIMEOUT” is not set then the default \ + timeout of 30 seconds will automatically be set.')); + o.placeholder = "30"; + + s.option(YNValue, 'REQUIRE_SOURCE_ADDRESS', 'REQUIRE_SOURCE_ADDRESS', _('Force all SPA packets to contain a real IP address within the encrypted data. \ + This makes it impossible to use the -s command line argument on the fwknop client command line, so either -R \ + has to be used to automatically resolve the external address (if the client behind a NAT) or the client must \ + know the external IP and set it via the -a argument.')); + + s = m.section(form.TypedSection, 'config', _('fwknopd.conf config options')); + s.anonymous=true; + s.option(form.Value, 'MAX_SPA_PACKET_AGE', 'MAX_SPA_PACKET_AGE', _('Maximum age in seconds that an SPA packet will be accepted. Defaults to 120 seconds.')); + s.option(form.Value, 'PCAP_INTF', 'PCAP_INTF', _('Specify the ethernet interface on which fwknopd will sniff packets.')); + s.option(YNValue, 'ENABLE_IPT_FORWARDING', 'ENABLE_IPT_FORWARDING', _('Allow SPA clients to request access to services through an iptables firewall instead of just to it.')); + s.option(YNValue, 'ENABLE_NAT_DNS', 'ENABLE_NAT_DNS', _('Allow SPA clients to request forwarding destination by DNS name.')); + + return m.render(); + } +}); diff --git a/applications/luci-app-fwknopd/luasrc/model/cbi/fwknopd.lua b/applications/luci-app-fwknopd/luasrc/model/cbi/fwknopd.lua deleted file mode 100644 index 9ae754cb9..000000000 --- a/applications/luci-app-fwknopd/luasrc/model/cbi/fwknopd.lua +++ /dev/null @@ -1,52 +0,0 @@ --- Copyright 2015 Jonathan Bennett <jbennett@incomsystems.biz> --- Licensed to the public under the GNU General Public License v2. -tmp = 0 -m = Map("fwknopd", translate("Firewall Knock Operator")) - -s = m:section(TypedSection, "global", translate("Enable Uci/Luci control")) -- Set uci control on or off -s.anonymous=true -s:option(Flag, "uci_enabled", translate("Enable config overwrite"), translate("When unchecked, the config files in /etc/fwknopd will be used as is, ignoring any settings here.")) - -s = m:section(TypedSection, "access", translate("access.conf stanzas")) -- set the access.conf settings -s.anonymous=true -s.addremove=true -qr = s:option(DummyValue, "note0", "dummy") -qr.tmp = tmp -qr.template = "fwknopd-qr" -qr:depends("uci_enabled", "1") -s:option(Value, "SOURCE", "SOURCE", translate("Use ANY for any source IP")) -k1 = s:option(Value, "KEY", "KEY", translate("Define the symmetric key used for decrypting an incoming SPA packet that is encrypted by the fwknop client with Rijndael.")) -k1:depends("keytype", translate("Normal Key")) -k2 = s:option(Value, "KEY_BASE64", "KEY_BASE64", translate("Define the symmetric key used for decrypting an incoming SPA \ - packet that is encrypted by the fwknop client with Rijndael.")) -k2:depends("keytype", translate("Base64 key")) -l1 = s:option(ListValue, "keytype", "Key type") -l1:value("Normal Key", "Normal Key") -l1:value("Base64 key", "Base64 key") -k3 = s:option(Value, "HMAC_KEY", "HMAC_KEY", "The hmac key") -k3:depends("hkeytype", "Normal Key") -k4 = s:option(Value, "HMAC_KEY_BASE64", "HMAC_KEY_BASE64", translate("The base64 hmac key")) -k4:depends("hkeytype", "Base64 key") -l2 = s:option(ListValue, "hkeytype", "HMAC Key type") -l2:value("Normal Key", "Normal Key") -l2:value("Base64 key", "Base64 key") -s:option(Value, "OPEN_PORTS", "OPEN_PORTS", translate("Define a set of ports and protocols (tcp or udp) that will be opened if a valid knock sequence is seen. \ - If this entry is not set, fwknopd will attempt to honor any proto/port request specified in the SPA data \ - (unless of it matches any “RESTRICT_PORTS” entries). Multiple entries are comma-separated.")) -s:option(Value, "FW_ACCESS_TIMEOUT", "FW_ACCESS_TIMEOUT", translate("Define the length of time access will be granted by fwknopd through the firewall after a \ - valid knock sequence from a source IP address. If “FW_ACCESS_TIMEOUT” is not set then the default \ - timeout of 30 seconds will automatically be set.")) -s:option(Value, "REQUIRE_SOURCE_ADDRESS", "REQUIRE_SOURCE_ADDRESS", translate("Force all SPA packets to contain a real IP address within the encrypted data. \ - This makes it impossible to use the -s command line argument on the fwknop client command line, so either -R \ - has to be used to automatically resolve the external address (if the client behind a NAT) or the client must \ - know the external IP and set it via the -a argument.")) - -s = m:section(TypedSection, "config", translate("fwknopd.conf config options")) -s.anonymous=true -s:option(Value, "MAX_SPA_PACKET_AGE", "MAX_SPA_PACKET_AGE", translate("Maximum age in seconds that an SPA packet will be accepted. Defaults to 120 seconds.")) -s:option(Value, "PCAP_INTF", "PCAP_INTF", translate("Specify the ethernet interface on which fwknopd will sniff packets.")) -s:option(Value, "ENABLE_IPT_FORWARDING", "ENABLE_IPT_FORWARDING", translate("Allow SPA clients to request access to services through an iptables firewall instead of just to it.")) -s:option(Value, "ENABLE_NAT_DNS", "ENABLE_NAT_DNS", translate("Allow SPA clients to request forwarding destination by DNS name.")) - -return m - diff --git a/applications/luci-app-fwknopd/luasrc/view/fwknopd-qr.htm b/applications/luci-app-fwknopd/luasrc/view/fwknopd-qr.htm deleted file mode 100644 index 5773f523e..000000000 --- a/applications/luci-app-fwknopd/luasrc/view/fwknopd-qr.htm +++ /dev/null @@ -1,2 +0,0 @@ -<% print(luci.sys.exec("sh /usr/sbin/gen-qr.sh " .. self.tmp)) %> -<% self.tmp = self.tmp + 1 %> diff --git a/applications/luci-app-fwknopd/root/etc/uci-defaults/40_luci-fwknopd b/applications/luci-app-fwknopd/root/etc/uci-defaults/40_luci-fwknopd index 7cecf2746..00d721e06 100644 --- a/applications/luci-app-fwknopd/root/etc/uci-defaults/40_luci-fwknopd +++ b/applications/luci-app-fwknopd/root/etc/uci-defaults/40_luci-fwknopd @@ -3,16 +3,24 @@ #-- Licensed to the public under the GNU General Public License v2. . /lib/functions/network.sh -[ "$(uci -q get fwknopd.@access[0].KEY)" != "CHANGEME" ] && exit 0 +# Clean-up - keytype/hkeytype is unnecessary now +if uci -q show fwknopd | grep \\.h\\?keytype > /dev/null; then + for keytype in $(uci -q show fwknopd | grep \\.h\\?keytype= | cut -d= -f1); do + uci delete $keytype + done + uci commit fwknopd +fi -uci delete fwknopd.@access[0].KEY -uci delete fwknopd.@access[0].HMAC_KEY -uci set fwknopd.@access[0].keytype='Base64 key' -uci set fwknopd.@access[0].hkeytype='Base64 key' -uci set fwknopd.@access[0].KEY_BASE64=`fwknopd --key-gen | awk '/^KEY/ {print $2;}'` -uci set fwknopd.@access[0].HMAC_KEY_BASE64=`fwknopd --key-gen | awk '/^HMAC/ {print $2;}'` -uci set fwknopd.@config[0].ENABLE_IPT_FORWARDING='y' -uci set fwknopd.@config[0].ENABLE_NAT_DNS='y' +# Generate valid keys +if [ "$(uci -q get fwknopd.@access[0].KEY)" = "CHANGEME" ]; then + uci delete fwknopd.@access[0].KEY + uci delete fwknopd.@access[0].HMAC_KEY + uci set fwknopd.@access[0].KEY_BASE64=`fwknopd --key-gen | awk '/^KEY/ {print $2;}'` + uci set fwknopd.@access[0].HMAC_KEY_BASE64=`fwknopd --key-gen | awk '/^HMAC/ {print $2;}'` + uci set fwknopd.@config[0].ENABLE_IPT_FORWARDING='y' + uci set fwknopd.@config[0].ENABLE_NAT_DNS='y' + + uci commit fwknopd +fi -uci commit fwknopd exit 0 diff --git a/applications/luci-app-fwknopd/root/usr/sbin/gen-qr.sh b/applications/luci-app-fwknopd/root/usr/sbin/gen-qr.sh deleted file mode 100644 index 48850bd36..000000000 --- a/applications/luci-app-fwknopd/root/usr/sbin/gen-qr.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -entry_num=0 -if [ "$1" != "" ]; then -entry_num=$1 -fi - -key_base64=$(uci -q get fwknopd.@access[$entry_num].KEY_BASE64) -key=$(uci -q get fwknopd.@access[$entry_num].KEY) -hmac_key_base64=$(uci -q get fwknopd.@access[$entry_num].HMAC_KEY_BASE64) -hmac_key=$(uci -q get fwknopd.@access[$entry_num].HMAC_KEY) - -if [ "$key_base64" != "" ]; then -qr="KEY_BASE64:$key_base64" -fi -if [ "$key" != "" ]; then -qr="$qr KEY:$key" - -fi -if [ "$hmac_key_base64" != "" ]; then -qr="$qr HMAC_KEY_BASE64:$hmac_key_base64" -fi -if [ "$hmac_key" != "" ]; then -qr="$qr HMAC_KEY:$hmac_key" -fi - -qrencode -t svg -I -o - "$qr" diff --git a/applications/luci-app-fwknopd/root/usr/share/luci/menu.d/luci-app-fwknopd.json b/applications/luci-app-fwknopd/root/usr/share/luci/menu.d/luci-app-fwknopd.json index 85486b997..e3ada68d7 100644 --- a/applications/luci-app-fwknopd/root/usr/share/luci/menu.d/luci-app-fwknopd.json +++ b/applications/luci-app-fwknopd/root/usr/share/luci/menu.d/luci-app-fwknopd.json @@ -2,12 +2,15 @@ "admin/services/fwknopd": { "title": "Firewall Knock Daemon", "action": { - "type": "cbi", - "path": "fwknopd", - "post": { "cbi.submit": true } + "type": "view", + "path": "fwknopd" }, "depends": { "acl": [ "luci-app-fwknopd" ], + "fs": { + "/usr/bin/qrencode": "executable", + "/usr/sbin/fwknopd": "executable" + }, "uci": { "fwknopd": true } } } diff --git a/applications/luci-app-fwknopd/root/usr/share/rpcd/acl.d/luci-app-fwknopd.json b/applications/luci-app-fwknopd/root/usr/share/rpcd/acl.d/luci-app-fwknopd.json index 3877f8752..15d7975bd 100644 --- a/applications/luci-app-fwknopd/root/usr/share/rpcd/acl.d/luci-app-fwknopd.json +++ b/applications/luci-app-fwknopd/root/usr/share/rpcd/acl.d/luci-app-fwknopd.json @@ -2,7 +2,11 @@ "luci-app-fwknopd": { "description": "Grant UCI access for luci-app-fwknopd", "read": { - "uci": [ "fwknopd" ] + "uci": [ "fwknopd" ], + "file": { + "/usr/bin/qrencode": [ "exec" ], + "/usr/sbin/fwknopd --key-gen": [ "exec" ] + } }, "write": { "uci": [ "fwknopd" ] |