luci-app-vhost-manager: migrate to vhosts config

This commit is contained in:
CyberMind-FR 2025-12-29 16:31:18 +01:00
parent 40e937a919
commit 9cdbb21a99
10 changed files with 749 additions and 410 deletions

View File

@ -11,7 +11,7 @@
"test_config" "test_config"
] ]
}, },
"uci": ["vhost_manager", "nginx"], "uci": ["vhosts", "vhost_manager", "nginx"],
"file": { "file": {
"/etc/nginx/conf.d/*": ["read"], "/etc/nginx/conf.d/*": ["read"],
"/etc/acme/*": ["read"] "/etc/acme/*": ["read"]
@ -26,7 +26,7 @@
"reload_nginx" "reload_nginx"
] ]
}, },
"uci": ["vhost_manager", "nginx"], "uci": ["vhosts", "vhost_manager", "nginx"],
"file": { "file": {
"/etc/nginx/conf.d/*": ["write"] "/etc/nginx/conf.d/*": ["write"]
} }

View File

@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-vhost-manager PKG_NAME:=luci-app-vhost-manager
PKG_VERSION:=0.4.1 PKG_VERSION:=0.4.1
PKG_RELEASE:=2 PKG_RELEASE:=3
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr> PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
@ -14,6 +14,11 @@ LUCI_DESCRIPTION:=Nginx reverse proxy manager with Let's Encrypt SSL certificate
LUCI_DEPENDS:=+luci-base +rpcd +nginx-ssl +acme +curl LUCI_DEPENDS:=+luci-base +rpcd +nginx-ssl +acme +curl
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/conffiles
/etc/config/vhost_manager
/etc/config/vhosts
endef
# File permissions (CRITICAL: RPCD scripts MUST be executable 755) # File permissions (CRITICAL: RPCD scripts MUST be executable 755)
# Format: path:owner:group:mode # Format: path:owner:group:mode
@ -21,7 +26,8 @@ LUCI_PKGARCH:=all
# - Helper scripts: 755 (if executable) # - Helper scripts: 755 (if executable)
# - Config files: 644 (readable by all, writable by root) # - Config files: 644 (readable by all, writable by root)
# - CSS/JS files: 644 (set automatically by luci.mk) # - CSS/JS files: 644 (set automatically by luci.mk)
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.vhost-manager:root:root:755 PKG_FILE_MODES:=/usr/libexec/rpcd/luci.vhost-manager:root:root:755 \
/etc/uci-defaults/50-luci-app-vhost-manager-migrate:root:root:755
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk

View File

@ -49,26 +49,30 @@ opkg install luci-app-vhost-manager
## Configuration ## Configuration
### UCI Configuration ### UCI Configuration (`/etc/config/vhosts`)
Edit `/etc/config/vhost_manager`: Virtual hosts now live in `/etc/config/vhosts`, allowing other SecuBox components to declaratively install proxies. A default file is dropped during install; edit it like any other UCI config:
```bash ```bash
config global 'global' config global 'global'
option enabled '1' option enabled '1'
option auto_reload '1' option auto_reload '1'
option log_retention '30'
config vhost 'myapp' config vhost 'myapp'
option domain 'app.example.com' option domain 'app.example.com'
option backend 'http://192.168.1.100:8080' option upstream 'http://127.0.0.1:8080'
option ssl '1' option tls 'acme' # off|acme|manual
option cert_path '/etc/custom/fullchain.pem' # used when tls=manual
option key_path '/etc/custom/privkey.pem'
option auth '1' option auth '1'
option auth_user 'admin' option auth_user 'admin'
option auth_pass 'secretpassword' option auth_pass 'secretpassword'
option websocket '1' option websocket '1'
option enabled '1'
``` ```
> Legacy installations may still ship `/etc/config/vhost_manager` for backwards compatibility, but the RPC backend now generates `/etc/nginx/conf.d/*.conf` exclusively from `/etc/config/vhosts`.
### Options ### Options
#### Global Section #### Global Section
@ -78,12 +82,13 @@ config vhost 'myapp'
#### VHost Section #### VHost Section
- `domain`: Domain name for this virtual host (required) - `domain`: Domain name for this virtual host (required)
- `backend`: Backend URL to proxy to (required, e.g., http://192.168.1.100:8080) - `upstream`: Backend URL to proxy to (required, e.g., http://192.168.1.100:8080)
- `ssl`: Enable HTTPS (default: 0, requires valid SSL certificate) - `tls`: TLS strategy (`off`, `acme`, or `manual`)
- `cert_path` / `key_path`: Required when `tls=manual` to point to PEM files
- `auth`: Enable HTTP Basic Authentication (default: 0) - `auth`: Enable HTTP Basic Authentication (default: 0)
- `auth_user`: Username for authentication (required if auth=1) - `auth_user` / `auth_pass`: Credentials used when `auth=1`
- `auth_pass`: Password for authentication (required if auth=1) - `websocket`: Enable WebSocket headers (default: 0)
- `websocket`: Enable WebSocket support (default: 0) - `enabled`: Disable the vhost without deleting it (default: 1)
## Usage ## Usage
@ -135,9 +140,12 @@ ubus call luci.vhost-manager status
ubus call luci.vhost-manager add_vhost '{ ubus call luci.vhost-manager add_vhost '{
"domain": "app.example.com", "domain": "app.example.com",
"backend": "http://192.168.1.100:8080", "backend": "http://192.168.1.100:8080",
"ssl": true, "tls_mode": "acme",
"auth": false, "auth": true,
"websocket": true "auth_user": "admin",
"auth_pass": "secret",
"websocket": true,
"enabled": true
}' }'
``` ```
@ -181,7 +189,7 @@ ubus call luci.vhost-manager get_access_logs '{
## Nginx Configuration ## Nginx Configuration
VHost Manager generates nginx configuration files in `/etc/nginx/conf.d/vhosts/`. VHost Manager generates nginx configuration files in `/etc/nginx/conf.d/`.
### Example Generated Configuration (HTTP Only) ### Example Generated Configuration (HTTP Only)
@ -307,10 +315,16 @@ List all configured virtual hosts.
{ {
"domain": "app.example.com", "domain": "app.example.com",
"backend": "http://192.168.1.100:8080", "backend": "http://192.168.1.100:8080",
"upstream": "http://192.168.1.100:8080",
"tls_mode": "acme",
"ssl": true, "ssl": true,
"ssl_expires": "2025-03-15", "cert_file": "/etc/acme/app.example.com/fullchain.cer",
"auth": false, "cert_expires": "2025-03-15",
"websocket": true "auth": true,
"auth_user": "admin",
"websocket": true,
"enabled": true,
"config_file": "/etc/nginx/conf.d/app.example.com.conf"
} }
] ]
} }
@ -328,24 +342,30 @@ Get details for a specific virtual host.
{ {
"domain": "app.example.com", "domain": "app.example.com",
"backend": "http://192.168.1.100:8080", "backend": "http://192.168.1.100:8080",
"tls_mode": "acme",
"ssl": true, "ssl": true,
"ssl_expires": "2025-03-15", "cert_expires": "2025-03-15",
"cert_issuer": "R3",
"auth": true, "auth": true,
"auth_user": "admin", "auth_user": "admin",
"websocket": true "websocket": true,
"enabled": true
} }
``` ```
### add_vhost(domain, backend, ssl, auth, websocket) ### add_vhost(payload)
Add a new virtual host. Add a new virtual host.
**Parameters:** **Parameters:**
- `domain`: Domain name (required) - `domain`: Domain name (required)
- `backend`: Backend URL (required) - `backend`: Backend URL (required)
- `ssl`: Enable SSL (boolean) - `tls_mode`: `off`, `acme`, or `manual` (required)
- `auth`: Enable authentication (boolean) - `auth`: Enable authentication (boolean)
- `auth_user` / `auth_pass`: Credentials when auth is enabled
- `websocket`: Enable WebSocket (boolean) - `websocket`: Enable WebSocket (boolean)
- `enabled`: Disable the vhost without deleting (boolean)
- `cert_path` / `key_path`: Required when `tls_mode=manual`
**Returns:** **Returns:**
```json ```json
@ -355,11 +375,11 @@ Add a new virtual host.
} }
``` ```
### update_vhost(domain, backend, ssl, auth, websocket) ### update_vhost(payload)
Update an existing virtual host. Update an existing virtual host.
**Parameters:** Same as add_vhost **Parameters:** Same as `add_vhost`. Omitted fields retain their previous value.
**Returns:** **Returns:**
```json ```json
@ -394,8 +414,9 @@ Test connectivity to a backend server.
**Returns:** **Returns:**
```json ```json
{ {
"backend": "http://192.168.1.100:8080",
"reachable": true, "reachable": true,
"response_time": 45 "status": "Backend is reachable"
} }
``` ```
@ -411,7 +432,7 @@ Request a Let's Encrypt SSL certificate.
```json ```json
{ {
"success": true, "success": true,
"message": "Certificate obtained successfully" "message": "Certificate requested"
} }
``` ```
@ -456,6 +477,7 @@ Get nginx access logs for a domain.
**Returns:** **Returns:**
```json ```json
{ {
"domain": "app.example.com",
"logs": [ "logs": [
"192.168.1.50 - - [24/Dec/2025:10:30:15 +0000] \"GET / HTTP/1.1\" 200 1234", "192.168.1.50 - - [24/Dec/2025:10:30:15 +0000] \"GET / HTTP/1.1\" 200 1234",
"192.168.1.51 - - [24/Dec/2025:10:30:16 +0000] \"GET /api HTTP/1.1\" 200 5678" "192.168.1.51 - - [24/Dec/2025:10:30:16 +0000] \"GET /api HTTP/1.1\" 200 5678"
@ -513,18 +535,24 @@ Ensure:
Check htpasswd file exists: Check htpasswd file exists:
```bash ```bash
ls -l /etc/nginx/htpasswd/{domain} ls -l /etc/nginx/.luci-app-vhost-manager_{domain}
``` ```
Regenerate htpasswd file: Regenerate htpasswd file:
```bash ```bash
# Via UCI # Update UCI entry
uci set vhost_manager.myapp.auth_user='newuser' uci set vhosts.myapp.auth='1'
uci set vhost_manager.myapp.auth_pass='newpass' uci set vhosts.myapp.auth_user='newuser'
uci commit vhost_manager uci set vhosts.myapp.auth_pass='newpass'
uci commit vhosts
# Trigger config regeneration # Trigger config regeneration
ubus call luci.vhost-manager update_vhost '{...}' ubus call luci.vhost-manager update_vhost '{
"domain": "myapp.example.com",
"auth": true,
"auth_user": "newuser",
"auth_pass": "newpass"
}'
``` ```
## Security Considerations ## Security Considerations

View File

@ -24,14 +24,14 @@ var callGetVHost = rpc.declare({
var callAddVHost = rpc.declare({ var callAddVHost = rpc.declare({
object: 'luci.vhost-manager', object: 'luci.vhost-manager',
method: 'add_vhost', method: 'add_vhost',
params: ['domain', 'backend', 'ssl', 'auth', 'websocket'], params: ['domain', 'backend', 'tls_mode', 'auth', 'auth_user', 'auth_pass', 'websocket', 'enabled', 'cert_path', 'key_path'],
expect: { } expect: { }
}); });
var callUpdateVHost = rpc.declare({ var callUpdateVHost = rpc.declare({
object: 'luci.vhost-manager', object: 'luci.vhost-manager',
method: 'update_vhost', method: 'update_vhost',
params: ['domain', 'backend', 'ssl', 'auth', 'websocket'], params: ['domain', 'backend', 'tls_mode', 'auth', 'auth_user', 'auth_pass', 'websocket', 'enabled', 'cert_path', 'key_path'],
expect: { } expect: { }
}); });

View File

@ -29,6 +29,22 @@ function formatDate(value) {
} }
} }
function isEnabled(vhost) {
return !vhost || vhost.enabled !== false;
}
function formatTlsMode(vhost) {
var mode = (vhost && vhost.tls_mode) || (vhost && vhost.ssl ? 'acme' : 'off');
switch (mode) {
case 'acme':
return _('ACME (auto)');
case 'manual':
return _('Manual cert');
default:
return _('Disabled');
}
}
return L.view.extend({ return L.view.extend({
load: function() { load: function() {
return Promise.all([ return Promise.all([
@ -58,7 +74,7 @@ return L.view.extend({
}, },
buildForm: function() { buildForm: function() {
var m = new form.Map('vhost_manager', null, null); var m = new form.Map('vhosts', null, null);
var s = m.section(form.GridSection, 'vhost', _('Virtual Hosts')); var s = m.section(form.GridSection, 'vhost', _('Virtual Hosts'));
s.anonymous = false; s.anonymous = false;
s.addremove = true; s.addremove = true;
@ -105,9 +121,20 @@ return L.view.extend({
return widget; return widget;
}; };
o = s.option(form.Flag, 'ssl', _('Enable SSL')); o = s.option(form.ListValue, 'tls_mode', _('TLS Mode'));
o.default = o.disabled; o.value('off', _('Disabled (HTTP only)'));
o.description = _('Serve HTTPS (requires certificate).'); o.value('acme', _('Automatic (acme.sh)'));
o.value('manual', _('Manual certificate'));
o.default = 'acme';
o.description = _('Select how nginx obtains TLS certificates.');
o = s.option(form.Value, 'cert_path', _('Certificate Path'));
o.placeholder = '/etc/custom/fullchain.pem';
o.depends('tls_mode', 'manual');
o = s.option(form.Value, 'key_path', _('Private Key Path'));
o.placeholder = '/etc/custom/privkey.pem';
o.depends('tls_mode', 'manual');
o = s.option(form.Flag, 'auth', _('Enable Authentication')); o = s.option(form.Flag, 'auth', _('Enable Authentication'));
o.default = o.disabled; o.default = o.disabled;
@ -124,19 +151,49 @@ return L.view.extend({
o.default = o.disabled; o.default = o.disabled;
o.description = _('Forward upgrade headers for WS backends.'); o.description = _('Forward upgrade headers for WS backends.');
o = s.option(form.Flag, 'enabled', _('Enable Virtual Host'));
o.default = '1';
o.description = _('Toggle to disable without deleting configuration.');
s.addModalOptions = function(s, section_id) { s.addModalOptions = function(s, section_id) {
var domain = this.section.formvalue(section_id, 'domain'); var domain = this.section.formvalue(section_id, 'domain');
var backend = this.section.formvalue(section_id, 'backend'); var backend = this.section.formvalue(section_id, 'backend');
var ssl = this.section.formvalue(section_id, 'ssl') === '1'; var tlsMode = this.section.formvalue(section_id, 'tls_mode') || 'off';
var auth = this.section.formvalue(section_id, 'auth') === '1'; var auth = this.section.formvalue(section_id, 'auth') === '1';
var websocket = this.section.formvalue(section_id, 'websocket') === '1'; var websocket = this.section.formvalue(section_id, 'websocket') === '1';
var enabled = this.section.formvalue(section_id, 'enabled') !== '0';
var certPath = this.section.formvalue(section_id, 'cert_path') || '';
var keyPath = this.section.formvalue(section_id, 'key_path') || '';
var authUser = this.section.formvalue(section_id, 'auth_user') || '';
var authPass = this.section.formvalue(section_id, 'auth_pass') || '';
if (!domain || !backend) { if (!domain || !backend) {
ui.addNotification(null, E('p', _('Domain and backend are required')), 'error'); ui.addNotification(null, E('p', _('Domain and backend are required')), 'error');
return; return;
} }
API.addVHost(domain, backend, ssl, auth, websocket).then(function(result) { if (auth && (!authUser || !authPass)) {
ui.addNotification(null, E('p', _('Username and password required for authentication')), 'error');
return;
}
if (tlsMode === 'manual' && (!certPath || !keyPath)) {
ui.addNotification(null, E('p', _('Manual TLS requires certificate and key paths')), 'error');
return;
}
API.addVHost(
domain,
backend,
tlsMode,
auth,
auth ? authUser : null,
auth ? authPass : null,
websocket,
enabled,
tlsMode === 'manual' ? certPath : null,
tlsMode === 'manual' ? keyPath : null
).then(function(result) {
if (result.success) { if (result.success) {
ui.addNotification(null, E('p', _('VHost created successfully')), 'info'); ui.addNotification(null, E('p', _('VHost created successfully')), 'info');
@ -174,9 +231,10 @@ return L.view.extend({
}, },
renderHeader: function(vhosts) { renderHeader: function(vhosts) {
var sslEnabled = vhosts.filter(function(v) { return v.ssl; }).length; var active = vhosts.filter(isEnabled);
var authEnabled = vhosts.filter(function(v) { return v.auth; }).length; var sslEnabled = active.filter(function(v) { return v.ssl; }).length;
var websocketEnabled = vhosts.filter(function(v) { return v.websocket; }).length; var authEnabled = active.filter(function(v) { return v.auth; }).length;
var websocketEnabled = active.filter(function(v) { return v.websocket; }).length;
return E('div', { 'class': 'sh-page-header' }, [ return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [ E('div', {}, [
@ -189,6 +247,7 @@ return L.view.extend({
]), ]),
E('div', { 'class': 'sh-stats-grid' }, [ E('div', { 'class': 'sh-stats-grid' }, [
this.renderStatBadge(vhosts.length, _('Defined')), this.renderStatBadge(vhosts.length, _('Defined')),
this.renderStatBadge(active.length, _('Enabled')),
this.renderStatBadge(sslEnabled, _('TLS')), this.renderStatBadge(sslEnabled, _('TLS')),
this.renderStatBadge(authEnabled, _('Auth')), this.renderStatBadge(authEnabled, _('Auth')),
this.renderStatBadge(websocketEnabled, _('WebSocket')) this.renderStatBadge(websocketEnabled, _('WebSocket'))
@ -225,14 +284,22 @@ return L.view.extend({
renderVhostCard: function(vhost, cert) { renderVhostCard: function(vhost, cert) {
var pills = []; var pills = [];
if (vhost.ssl) pills.push(E('span', { 'class': 'vhost-pill success' }, _('SSL'))); if (!isEnabled(vhost)) {
pills.push(E('span', { 'class': 'vhost-pill danger' }, _('Disabled')));
} else if (vhost.ssl) {
pills.push(E('span', { 'class': 'vhost-pill success' }, _('TLS')));
}
if (vhost.auth) pills.push(E('span', { 'class': 'vhost-pill warn' }, _('Auth'))); if (vhost.auth) pills.push(E('span', { 'class': 'vhost-pill warn' }, _('Auth')));
if (vhost.websocket) pills.push(E('span', { 'class': 'vhost-pill' }, _('WebSocket'))); if (vhost.websocket) pills.push(E('span', { 'class': 'vhost-pill' }, _('WebSocket')));
if (vhost.tls_mode === 'manual') {
pills.push(E('span', { 'class': 'vhost-pill' }, _('Manual cert')));
}
return E('div', { 'class': 'vhost-card' }, [ return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🌐', vhost.domain || _('Unnamed')]), E('div', { 'class': 'vhost-card-title' }, ['🌐', vhost.domain || _('Unnamed')]),
E('div', { 'class': 'vhost-card-meta' }, vhost.backend || _('No backend defined')), E('div', { 'class': 'vhost-card-meta' }, vhost.backend || _('No backend defined')),
pills.length ? E('div', { 'class': 'vhost-filter-tags' }, pills) : '', pills.length ? E('div', { 'class': 'vhost-filter-tags' }, pills) : '',
E('div', { 'class': 'vhost-card-meta' }, _('TLS Mode: %s').format(formatTlsMode(vhost))),
E('div', { 'class': 'vhost-card-meta' }, E('div', { 'class': 'vhost-card-meta' },
cert ? _('Certificate expires %s').format(formatDate(cert.expires)) : _('No certificate detected')) cert ? _('Certificate expires %s').format(formatDate(cert.expires)) : _('No certificate detected'))
]); ]);

View File

@ -0,0 +1,17 @@
config global 'global'
option enabled '1'
option auto_reload '1'
# Example VHost entry
# config vhost 'zigbeeui'
# option domain 'zigbee.example.com'
# option upstream 'http://127.0.0.1:8080'
# option tls 'acme' # off|acme|manual
# option cert_path '/etc/custom/fullchain.pem' # used when tls=manual
# option key_path '/etc/custom/privkey.pem'
# option auth '0'
# option auth_user 'admin'
# option auth_pass 'secret'
# option websocket '1'
# option enabled '1'

View File

@ -0,0 +1,96 @@
#!/bin/sh
. /lib/functions.sh
LEGACY_CFG="/etc/config/vhost_manager"
TARGET_CFG="/etc/config/vhosts"
[ -f "$LEGACY_CFG" ] || exit 0
# If new config already contains vhost entries, assume migration already done.
if uci -q show vhosts 2>/dev/null | grep -q '=vhost'; then
exit 0
fi
# Ensure target config exists
if [ ! -f "$TARGET_CFG" ]; then
cat <<'CFG' > "$TARGET_CFG"
config global 'global'
option enabled '1'
option auto_reload '1'
CFG
fi
ensure_global_section() {
if ! uci -q get vhosts.global >/dev/null; then
local g
g=$(uci add vhosts global)
uci rename vhosts.$g='global'
fi
}
normalize_bool() {
case "$1" in
1|true|on|yes|enabled) echo "1" ;;
*) echo "0" ;;
esac
}
migrate_global() {
local section="$1"
local enabled auto_reload log_retention
config_get enabled "$section" enabled
config_get auto_reload "$section" auto_reload
config_get log_retention "$section" log_retention
ensure_global_section
[ -n "$enabled" ] && uci set vhosts.global.enabled="$enabled"
[ -n "$auto_reload" ] && uci set vhosts.global.auto_reload="$auto_reload"
[ -n "$log_retention" ] && uci set vhosts.global.log_retention="$log_retention"
}
migrate_vhost() {
local section="$1"
local domain backend ssl auth auth_user auth_pass websocket
config_get domain "$section" domain
config_get backend "$section" backend
config_get ssl "$section" ssl
config_get auth "$section" auth
config_get auth_user "$section" auth_user
config_get auth_pass "$section" auth_pass
config_get websocket "$section" websocket
[ -n "$domain" ] || return
[ -n "$backend" ] || return
local tls_mode="off"
if [ "$(normalize_bool "$ssl")" = "1" ]; then
tls_mode="acme"
fi
local s
s=$(uci add vhosts vhost)
uci set vhosts.$s.domain="$domain"
uci set vhosts.$s.upstream="$backend"
uci set vhosts.$s.tls="$tls_mode"
if [ "$(normalize_bool "$auth")" = "1" ]; then
uci set vhosts.$s.auth="1"
[ -n "$auth_user" ] && uci set vhosts.$s.auth_user="$auth_user"
[ -n "$auth_pass" ] && uci set vhosts.$s.auth_pass="$auth_pass"
else
uci set vhosts.$s.auth="0"
fi
uci set vhosts.$s.websocket="$(normalize_bool "$websocket")"
uci set vhosts.$s.enabled="1"
}
config_load vhost_manager
config_foreach migrate_global global
config_foreach migrate_vhost vhost
uci commit vhosts
mv "$LEGACY_CFG" "${LEGACY_CFG}.legacy" 2>/dev/null
exit 0

View File

@ -12,7 +12,7 @@
"get_access_logs" "get_access_logs"
] ]
}, },
"uci": ["vhost_manager", "nginx"], "uci": ["vhosts", "vhost_manager", "nginx"],
"file": { "file": {
"/etc/nginx/conf.d/*": ["read"], "/etc/nginx/conf.d/*": ["read"],
"/etc/acme/*": ["read"], "/etc/acme/*": ["read"],
@ -29,7 +29,7 @@
"reload_nginx" "reload_nginx"
] ]
}, },
"uci": ["vhost_manager", "nginx"], "uci": ["vhosts", "vhost_manager", "nginx"],
"file": { "file": {
"/etc/nginx/conf.d/*": ["write"], "/etc/nginx/conf.d/*": ["write"],
"/etc/nginx/.htpasswd_*": ["write"] "/etc/nginx/.htpasswd_*": ["write"]

View File

@ -11,7 +11,7 @@
"test_config" "test_config"
] ]
}, },
"uci": ["vhost_manager", "nginx"], "uci": ["vhosts", "vhost_manager", "nginx"],
"file": { "file": {
"/etc/nginx/conf.d/*": ["read"], "/etc/nginx/conf.d/*": ["read"],
"/etc/acme/*": ["read"] "/etc/acme/*": ["read"]
@ -26,7 +26,7 @@
"reload_nginx" "reload_nginx"
] ]
}, },
"uci": ["vhost_manager", "nginx"], "uci": ["vhosts", "vhost_manager", "nginx"],
"file": { "file": {
"/etc/nginx/conf.d/*": ["write"] "/etc/nginx/conf.d/*": ["write"]
} }