Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
36896bcfc9 | |||
b324110c49 | |||
b38e239fa2 | |||
f60ffd85da | |||
f10d8e4aa9 | |||
b57d1e5a66 | |||
001f0cb9f6 | |||
d2130a24a1 |
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Nginx Proxy Manager
|
# Nginx Proxy Manager
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@ -25,6 +25,11 @@ running at home or otherwise, including free SSL, without having to know too muc
|
|||||||
- Return immediate 404's
|
- Return immediate 404's
|
||||||
|
|
||||||
|
|
||||||
|
## Using [Rancher](https://rancher.com)?
|
||||||
|
|
||||||
|
Easily start an Nginx Proxy Manager Stack by adding [my template catalog](https://github.com/jc21/rancher-templates).
|
||||||
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
### Method 1: Using docker-compose
|
### Method 1: Using docker-compose
|
||||||
@ -101,9 +106,7 @@ I won't go in to too much detail here but here are the basics for someone new to
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- Pass on human readable ssl cert errors to the ui
|
- Pass on human readable ssl cert errors to the ui
|
||||||
- Allow a host to be a redirection to another domain
|
|
||||||
- UI: Allow column sorting on tables
|
- UI: Allow column sorting on tables
|
||||||
- UI: Allow filtering hosts by types
|
- UI: Allow filtering hosts by types
|
||||||
- Advanced option to overwrite the default location block (or regex to do it automatically)
|
- Advanced option to overwrite the default location block (or regex to do it automatically)
|
||||||
- Add nice upstream error pages
|
- Add nice upstream error pages
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nginx-proxy-manager",
|
"name": "nginx-proxy-manager",
|
||||||
"version": "1.0.1",
|
"version": "1.1.1",
|
||||||
"description": "Nginx proxt with built in Web based management",
|
"description": "Nginx proxt with built in Web based management",
|
||||||
"main": "src/backend/index.js",
|
"main": "src/backend/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -44,19 +44,31 @@ const internalHost = {
|
|||||||
*/
|
*/
|
||||||
create: payload => {
|
create: payload => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Enforce lowercase hostnames
|
let existing_host = false;
|
||||||
payload.hostname = payload.hostname.toLowerCase();
|
|
||||||
|
|
||||||
// 1. Check that the hostname doesn't already exist
|
if (payload.type === 'stream') {
|
||||||
let existing_host = db.hosts.findOne({hostname: payload.hostname});
|
// Check that the incoming port doesn't already exist
|
||||||
|
existing_host = db.hosts.findOne({incoming_port: payload.incoming_port});
|
||||||
|
|
||||||
|
if (payload.incoming_port === 80 || payload.incoming_port === 81 || payload.incoming_port === 443) {
|
||||||
|
reject(new error.ConfigurationError('Port ' + payload.incoming_port + ' is reserved'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
payload.hostname = payload.hostname.toLowerCase();
|
||||||
|
|
||||||
|
// Check that the hostname doesn't already exist
|
||||||
|
existing_host = db.hosts.findOne({hostname: payload.hostname});
|
||||||
|
}
|
||||||
|
|
||||||
if (existing_host) {
|
if (existing_host) {
|
||||||
reject(new error.ValidationError('Hostname already exists'));
|
reject(new error.ValidationError('Hostname already exists'));
|
||||||
} else {
|
} else {
|
||||||
// 2. Add host to db
|
// Add host to db
|
||||||
let host = db.hosts.save(payload);
|
let host = db.hosts.save(payload);
|
||||||
|
|
||||||
// 3. Fire the config generation for this host
|
// Fire the config generation for this host
|
||||||
internalHost.configure(host, true)
|
internalHost.configure(host, true)
|
||||||
.then((/*result*/) => {
|
.then((/*result*/) => {
|
||||||
resolve(host);
|
resolve(host);
|
||||||
@ -98,10 +110,16 @@ const internalHost = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check that the hostname doesn't already exist
|
// Check that the hostname doesn't already exist
|
||||||
let other_host = db.hosts.findOne({hostname: payload.hostname});
|
let other_host = false;
|
||||||
|
|
||||||
|
if (typeof payload.incoming_port !== 'undefined') {
|
||||||
|
other_host = db.hosts.findOne({incoming_port: payload.incoming_port});
|
||||||
|
} else {
|
||||||
|
other_host = db.hosts.findOne({hostname: payload.hostname});
|
||||||
|
}
|
||||||
|
|
||||||
if (other_host && other_host._id !== id) {
|
if (other_host && other_host._id !== id) {
|
||||||
reject(new error.ValidationError('Hostname already exists'));
|
reject(new error.ValidationError((other_host.type === 'stream' ? 'Source Stream Port' : 'Hostname') + ' already exists'));
|
||||||
} else {
|
} else {
|
||||||
// 2. Update host
|
// 2. Update host
|
||||||
db.hosts.update({_id: id}, payload, {multi: false, upsert: false});
|
db.hosts.update({_id: id}, payload, {multi: false, upsert: false});
|
||||||
@ -126,17 +144,17 @@ const internalHost = {
|
|||||||
return data;
|
return data;
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (
|
if (data.updated.type !== 'stream') {
|
||||||
(data.original.ssl && !data.updated.ssl) || // ssl was enabled and is now disabled
|
if (
|
||||||
(data.original.ssl && data.original.hostname !== data.updated.hostname) // hostname was changed for a previously ssl-enabled host
|
(data.original.ssl && !data.updated.ssl) || // ssl was enabled and is now disabled
|
||||||
) {
|
(data.original.ssl && data.original.hostname !== data.updated.hostname) // hostname was changed for a previously ssl-enabled host
|
||||||
// SSL was turned off or hostname for ssl has changed so we should remove certs for the original
|
) {
|
||||||
return internalSsl.deleteCerts(data.original)
|
// SSL was turned off or hostname for ssl has changed so we should remove certs for the original
|
||||||
.then(() => {
|
return internalSsl.deleteCerts(data.original)
|
||||||
db.hosts.update({_id: data.updated._id}, {ssl_expires: 0}, {multi: false, upsert: false});
|
.then(() => {
|
||||||
data.updated.ssl_expires = 0;
|
return data;
|
||||||
return data;
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -32,6 +32,10 @@ const internalNginx = {
|
|||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
getConfigName: host => {
|
getConfigName: host => {
|
||||||
|
if (host.type === 'stream') {
|
||||||
|
return '/config/nginx/stream/' + host.incoming_port + '.conf';
|
||||||
|
}
|
||||||
|
|
||||||
return '/config/nginx/' + host.hostname + '.conf';
|
return '/config/nginx/' + host.hostname + '.conf';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const ejs = require('ejs');
|
const ejs = require('ejs');
|
||||||
const timestamp = require('unix-timestamp');
|
const timestamp = require('unix-timestamp');
|
||||||
const batchflow = require('batchflow');
|
|
||||||
const internalNginx = require('./nginx');
|
const internalNginx = require('./nginx');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const db = require('../db');
|
|
||||||
const utils = require('../lib/utils');
|
const utils = require('../lib/utils');
|
||||||
const error = require('../lib/error');
|
const error = require('../lib/error');
|
||||||
|
|
||||||
@ -15,7 +12,7 @@ timestamp.round = true;
|
|||||||
|
|
||||||
const internalSsl = {
|
const internalSsl = {
|
||||||
|
|
||||||
interval_timeout: 60 * 1000,
|
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
|
||||||
interval: null,
|
interval: null,
|
||||||
interval_processing: false,
|
interval_processing: false,
|
||||||
|
|
||||||
@ -28,42 +25,21 @@ const internalSsl = {
|
|||||||
*/
|
*/
|
||||||
processExpiringHosts: () => {
|
processExpiringHosts: () => {
|
||||||
if (!internalSsl.interval_processing) {
|
if (!internalSsl.interval_processing) {
|
||||||
let hosts = db.hosts.find();
|
logger.info('Renewing SSL certs close to expiry...');
|
||||||
|
return utils.exec('/usr/bin/certbot renew --webroot=/config/letsencrypt-acme-challenge')
|
||||||
|
.then(result => {
|
||||||
|
logger.info(result);
|
||||||
|
internalSsl.interval_processing = false;
|
||||||
|
|
||||||
if (hosts && hosts.length) {
|
return internalNginx.reload()
|
||||||
internalSsl.interval_processing = true;
|
.then(() => {
|
||||||
|
return result;
|
||||||
batchflow(hosts).sequential()
|
});
|
||||||
.each((i, host, next) => {
|
})
|
||||||
if ((typeof host.is_deleted === 'undefined' || !host.is_deleted) && host.ssl && typeof host.ssl_expires !== 'undefined' && !internalSsl.hasValidSslCerts(host)) {
|
.catch(err => {
|
||||||
// This host is due to expire in 1 day, time to renew
|
logger.error(err);
|
||||||
logger.info('Host ' + host.hostname + ' is due for SSL renewal');
|
internalSsl.interval_processing = false;
|
||||||
|
});
|
||||||
internalSsl.renewSsl(host)
|
|
||||||
.then(() => {
|
|
||||||
// Certificate was requested ok, update the timestamp on the host
|
|
||||||
db.hosts.update({_id: host._id}, {ssl_expires: timestamp.now('+90d')}, {
|
|
||||||
multi: false,
|
|
||||||
upsert: false
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(next)
|
|
||||||
.catch(err => {
|
|
||||||
logger.error(err);
|
|
||||||
next(err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.error(err => {
|
|
||||||
logger.error(err);
|
|
||||||
internalSsl.interval_processing = false;
|
|
||||||
})
|
|
||||||
.end((/*results*/) => {
|
|
||||||
internalSsl.interval_processing = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -73,8 +49,7 @@ const internalSsl = {
|
|||||||
*/
|
*/
|
||||||
hasValidSslCerts: host => {
|
hasValidSslCerts: host => {
|
||||||
return fs.existsSync('/etc/letsencrypt/live/' + host.hostname + '/fullchain.pem') &&
|
return fs.existsSync('/etc/letsencrypt/live/' + host.hostname + '/fullchain.pem') &&
|
||||||
fs.existsSync('/etc/letsencrypt/live/' + host.hostname + '/privkey.pem') &&
|
fs.existsSync('/etc/letsencrypt/live/' + host.hostname + '/privkey.pem');
|
||||||
host.ssl_expires > timestamp.now('+1d');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,7 +59,7 @@ const internalSsl = {
|
|||||||
requestSsl: host => {
|
requestSsl: host => {
|
||||||
logger.info('Requesting SSL certificates for ' + host.hostname);
|
logger.info('Requesting SSL certificates for ' + host.hostname);
|
||||||
|
|
||||||
return utils.exec('/usr/bin/letsencrypt certonly --agree-tos --email "' + host.letsencrypt_email + '" -n -a webroot --webroot-path=' + host.root_path + ' -d "' + host.hostname + '"')
|
return utils.exec('/usr/bin/letsencrypt certonly --agree-tos --email "' + host.letsencrypt_email + '" -n -a webroot --webroot-path=/config/letsencrypt-acme-challenge -d "' + host.hostname + '"')
|
||||||
.then(result => {
|
.then(result => {
|
||||||
logger.info(result);
|
logger.info(result);
|
||||||
return result;
|
return result;
|
||||||
@ -98,7 +73,7 @@ const internalSsl = {
|
|||||||
renewSsl: host => {
|
renewSsl: host => {
|
||||||
logger.info('Renewing SSL certificates for ' + host.hostname);
|
logger.info('Renewing SSL certificates for ' + host.hostname);
|
||||||
|
|
||||||
return utils.exec('/usr/bin/letsencrypt renew --force-renewal --disable-hook-validation --cert-name "' + host.hostname + '"')
|
return utils.exec('/usr/bin/certbot renew --force-renewal --disable-hook-validation --webroot-path=/config/letsencrypt-acme-challenge --cert-name "' + host.hostname + '"')
|
||||||
.then(result => {
|
.then(result => {
|
||||||
logger.info(result);
|
logger.info(result);
|
||||||
return result;
|
return result;
|
||||||
@ -112,7 +87,7 @@ const internalSsl = {
|
|||||||
deleteCerts: host => {
|
deleteCerts: host => {
|
||||||
logger.info('Deleting SSL certificates for ' + host.hostname);
|
logger.info('Deleting SSL certificates for ' + host.hostname);
|
||||||
|
|
||||||
return utils.exec('/usr/bin/letsencrypt delete -n --cert-name "' + host.hostname + '"')
|
return utils.exec('/usr/bin/certbot delete -n --cert-name "' + host.hostname + '"')
|
||||||
.then(result => {
|
.then(result => {
|
||||||
logger.info(result);
|
logger.info(result);
|
||||||
})
|
})
|
||||||
@ -130,20 +105,17 @@ const internalSsl = {
|
|||||||
let filename = internalNginx.getConfigName(host);
|
let filename = internalNginx.getConfigName(host);
|
||||||
let template_data = host;
|
let template_data = host;
|
||||||
|
|
||||||
template_data.root_path = '/tmp/' + host.hostname;
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
template = fs.readFileSync(__dirname + '/../templates/letsencrypt.conf.ejs', {encoding: 'utf8'});
|
||||||
|
let config_text = ejs.render(template, template_data);
|
||||||
|
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
|
||||||
|
|
||||||
return utils.exec('mkdir -p ' + template_data.root_path)
|
resolve(template_data);
|
||||||
.then(() => {
|
} catch (err) {
|
||||||
try {
|
reject(new error.ConfigurationError(err.message));
|
||||||
template = fs.readFileSync(__dirname + '/../templates/letsencrypt.conf.ejs', {encoding: 'utf8'});
|
}
|
||||||
let config_text = ejs.render(template, template_data);
|
});
|
||||||
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
|
|
||||||
|
|
||||||
return template_data;
|
|
||||||
} catch (err) {
|
|
||||||
throw new error.ConfigurationError(err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,10 +129,6 @@ const internalSsl = {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return internalSsl.requestSsl(data);
|
return internalSsl.requestSsl(data);
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// Certificate was requested ok, update the timestamp on the host
|
|
||||||
db.hosts.update({_id: host._id}, {ssl_expires: timestamp.now('+90d')}, {multi: false, upsert: false});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -30,7 +30,7 @@ function apiValidator (schema, payload/*, description*/) {
|
|||||||
resolve(payload);
|
resolve(payload);
|
||||||
} else {
|
} else {
|
||||||
let message = ajv.errorsText(validate.errors);
|
let message = ajv.errorsText(validate.errors);
|
||||||
//debug(validate.errors);
|
//console.log(validate.errors);
|
||||||
|
|
||||||
let err = new error.ValidationError(message);
|
let err = new error.ValidationError(message);
|
||||||
err.debug = [validate.errors, payload];
|
err.debug = [validate.errors, payload];
|
||||||
|
@ -152,38 +152,4 @@ router
|
|||||||
.catch(next);
|
.catch(next);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Renew Host Action
|
|
||||||
*
|
|
||||||
* /api/hosts/123/renew
|
|
||||||
*/
|
|
||||||
router
|
|
||||||
.route('/:host_id/renew')
|
|
||||||
.options((req, res) => {
|
|
||||||
res.sendStatus(204);
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/hosts/123/renew
|
|
||||||
*/
|
|
||||||
.post((req, res, next) => {
|
|
||||||
validator({
|
|
||||||
required: ['host_id'],
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
host_id: {
|
|
||||||
$ref: 'definitions#/definitions/_id'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, req.params)
|
|
||||||
.then(data => {
|
|
||||||
return internalHost.renew(data.host_id);
|
|
||||||
})
|
|
||||||
.then(result => {
|
|
||||||
res.status(200)
|
|
||||||
.send(result);
|
|
||||||
})
|
|
||||||
.catch(next);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^(proxy|redirection|404)$"
|
"pattern": "^(proxy|redirection|404|stream)$"
|
||||||
},
|
},
|
||||||
"hostname": {
|
"hostname": {
|
||||||
"$ref": "../definitions.json#/definitions/hostname"
|
"$ref": "../definitions.json#/definitions/hostname"
|
||||||
@ -38,11 +38,6 @@
|
|||||||
"ssl": {
|
"ssl": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"ssl_expires": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 0,
|
|
||||||
"readonly": true
|
|
||||||
},
|
|
||||||
"letsencrypt_email": {
|
"letsencrypt_email": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "email"
|
"format": "email"
|
||||||
@ -59,6 +54,17 @@
|
|||||||
"access_list": {
|
"access_list": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"readonly": true
|
"readonly": true
|
||||||
|
},
|
||||||
|
"incoming_port": {
|
||||||
|
"type": "integer",
|
||||||
|
"minumum": 1,
|
||||||
|
"maxiumum": 65535
|
||||||
|
},
|
||||||
|
"protocols": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"links": [
|
"links": [
|
||||||
@ -86,8 +92,7 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type"
|
||||||
"hostname"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
@ -125,6 +130,12 @@
|
|||||||
},
|
},
|
||||||
"access_list_id": {
|
"access_list_id": {
|
||||||
"$ref": "#/definitions/access_list_id"
|
"$ref": "#/definitions/access_list_id"
|
||||||
|
},
|
||||||
|
"incoming_port": {
|
||||||
|
"$ref": "#/definitions/incoming_port"
|
||||||
|
},
|
||||||
|
"protocols": {
|
||||||
|
"$ref": "#/definitions/protocols"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -146,6 +157,9 @@
|
|||||||
"required": [],
|
"required": [],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/definitions/type"
|
||||||
|
},
|
||||||
"hostname": {
|
"hostname": {
|
||||||
"$ref": "#/definitions/hostname"
|
"$ref": "#/definitions/hostname"
|
||||||
},
|
},
|
||||||
@ -178,6 +192,12 @@
|
|||||||
},
|
},
|
||||||
"access_list_id": {
|
"access_list_id": {
|
||||||
"$ref": "#/definitions/access_list_id"
|
"$ref": "#/definitions/access_list_id"
|
||||||
|
},
|
||||||
|
"incoming_port": {
|
||||||
|
"$ref": "#/definitions/incoming_port"
|
||||||
|
},
|
||||||
|
"protocols": {
|
||||||
|
"$ref": "#/definitions/protocols"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -227,9 +247,6 @@
|
|||||||
"ssl": {
|
"ssl": {
|
||||||
"$ref": "#/definitions/ssl"
|
"$ref": "#/definitions/ssl"
|
||||||
},
|
},
|
||||||
"ssl_expires": {
|
|
||||||
"$ref": "#/definitions/ssl_expires"
|
|
||||||
},
|
|
||||||
"letsencrypt_email": {
|
"letsencrypt_email": {
|
||||||
"$ref": "#/definitions/letsencrypt_email"
|
"$ref": "#/definitions/letsencrypt_email"
|
||||||
},
|
},
|
||||||
@ -244,6 +261,12 @@
|
|||||||
},
|
},
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"$ref": "#/definitions/advanced"
|
"$ref": "#/definitions/advanced"
|
||||||
|
},
|
||||||
|
"incoming_port": {
|
||||||
|
"$ref": "#/definitions/incoming_port"
|
||||||
|
},
|
||||||
|
"protocols": {
|
||||||
|
"$ref": "#/definitions/protocols"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,6 @@ server {
|
|||||||
access_log /config/logs/letsencrypt.log proxy;
|
access_log /config/logs/letsencrypt.log proxy;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root <%- root_path %>;
|
root /config/letsencrypt-acme-challenge;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,19 +14,19 @@ server {
|
|||||||
<%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %>
|
<%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %>
|
||||||
|
|
||||||
<% if (typeof ssl !== 'undefined' && ssl) { -%>
|
<% if (typeof ssl !== 'undefined' && ssl) { -%>
|
||||||
|
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||||
include conf.d/include/ssl-ciphers.conf;
|
include conf.d/include/ssl-ciphers.conf;
|
||||||
ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem;
|
||||||
<% } -%>
|
<% } -%>
|
||||||
|
|
||||||
<% if (typeof access_list_id !== 'undefined' && access_list_id) { -%>
|
|
||||||
auth_basic "Authorization required";
|
|
||||||
auth_basic_user_file /config/access/<%- access_list_id %>;
|
|
||||||
<% } -%>
|
|
||||||
|
|
||||||
<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %>
|
<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %>
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
<% if (typeof access_list_id !== 'undefined' && access_list_id) { -%>
|
||||||
|
auth_basic "Authorization required";
|
||||||
|
auth_basic_user_file /config/access/<%- access_list_id %>;
|
||||||
|
<% } -%>
|
||||||
<%- typeof force_ssl !== 'undefined' && force_ssl ? 'include conf.d/include/force-ssl.conf;' : '' %>
|
<%- typeof force_ssl !== 'undefined' && force_ssl ? 'include conf.d/include/force-ssl.conf;' : '' %>
|
||||||
include conf.d/include/proxy.conf;
|
include conf.d/include/proxy.conf;
|
||||||
}
|
}
|
||||||
|
11
manager/src/backend/templates/stream.conf.ejs
Normal file
11
manager/src/backend/templates/stream.conf.ejs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# <%- incoming_port %> - <%- protocols.join(',').toUpperCase() %>
|
||||||
|
<%
|
||||||
|
protocols.forEach(function (protocol) {
|
||||||
|
%>
|
||||||
|
server {
|
||||||
|
listen <%- incoming_port %> <%- protocol === 'tcp' ? '' : protocol %>;
|
||||||
|
proxy_pass <%- forward_server %>:<%- forward_port %>;
|
||||||
|
}
|
||||||
|
<%
|
||||||
|
});
|
||||||
|
%>
|
@ -118,14 +118,6 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
reconfigure: function (_id) {
|
reconfigure: function (_id) {
|
||||||
return fetch('post', 'hosts/' + _id + '/reconfigure');
|
return fetch('post', 'hosts/' + _id + '/reconfigure');
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {String} _id
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
renew: function (_id) {
|
|
||||||
return fetch('post', 'hosts/' + _id + '/renew');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -86,6 +86,17 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Stream Host Form
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
*/
|
||||||
|
showStreamHostForm: function (model) {
|
||||||
|
require(['./main', './host/stream_form'], function (App, View) {
|
||||||
|
App.UI.showModalDialog(new View({model: model}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Delete Host Confirmation
|
* Show Delete Host Confirmation
|
||||||
*
|
*
|
||||||
@ -108,17 +119,6 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Show Renew Host
|
|
||||||
*
|
|
||||||
* @param model
|
|
||||||
*/
|
|
||||||
showRenewHost: function (model) {
|
|
||||||
require(['./main', './host/renew'], function (App, View) {
|
|
||||||
App.UI.showModalDialog(new View({model: model}));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Advanced Host
|
* Show Advanced Host
|
||||||
*
|
*
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<table class="table table-condensed table-striped">
|
<table class="table table-condensed table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Hostname</th>
|
<th>Source</th>
|
||||||
<th>Destination</th>
|
<th>Destination</th>
|
||||||
<th>SSL</th>
|
<th>SSL</th>
|
||||||
<th>Access List</th>
|
<th>Access List</th>
|
||||||
@ -13,6 +13,7 @@
|
|||||||
<li><a href="#" class="new-proxy">Proxy Host</a></li>
|
<li><a href="#" class="new-proxy">Proxy Host</a></li>
|
||||||
<li><a href="#" class="new-redirection">Redirection Host</a></li>
|
<li><a href="#" class="new-redirection">Redirection Host</a></li>
|
||||||
<li><a href="#" class="new-404">404 Host</a></li>
|
<li><a href="#" class="new-404">404 Host</a></li>
|
||||||
|
<li><a href="#" class="new-stream">Stream Host</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
@ -28,7 +28,8 @@ module.exports = Mn.View.extend({
|
|||||||
ui: {
|
ui: {
|
||||||
new_proxy: 'th .new-proxy',
|
new_proxy: 'th .new-proxy',
|
||||||
new_redirection: 'th .new-redirection',
|
new_redirection: 'th .new-redirection',
|
||||||
new_404: 'th .new-404'
|
new_404: 'th .new-404',
|
||||||
|
new_stream: 'th .new-stream'
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
@ -45,6 +46,11 @@ module.exports = Mn.View.extend({
|
|||||||
'click @ui.new_404': function (e) {
|
'click @ui.new_404': function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
Controller.show404HostForm(new HostModel.Model);
|
Controller.show404HostForm(new HostModel.Model);
|
||||||
|
},
|
||||||
|
|
||||||
|
'click @ui.new_stream': function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Controller.showStreamHostForm(new HostModel.Model);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
<td><a href="<%- ssl ? 'https' : 'http' %>://<%- hostname %>" target="_blank"><%- hostname %></a></td>
|
<td>
|
||||||
|
<% if (type === 'stream') { %>
|
||||||
|
<%- incoming_port %>
|
||||||
|
<%- protocols.join(', ').toUpperCase() %>
|
||||||
|
<% } else { %>
|
||||||
|
<a href="<%- ssl ? 'https' : 'http' %>://<%- hostname %>" target="_blank"><%- hostname %></a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="monospace">
|
<span class="monospace">
|
||||||
<% if (type === 'proxy') { %>
|
<% if (type === 'proxy' || type === 'stream') { %>
|
||||||
<%- forward_server %>:<%- forward_port %>
|
<%- forward_server %>:<%- forward_port %>
|
||||||
<% } else if (type === 'redirection') { %>
|
<% } else if (type === 'redirection') { %>
|
||||||
<%- forward_host %>
|
<%- forward_host %>
|
||||||
@ -11,27 +18,32 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<% if (ssl && force_ssl) { %>
|
<% if (type === 'stream') { %>
|
||||||
Forced
|
-
|
||||||
<% } else if (ssl) { %>
|
|
||||||
Enabled
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
No
|
<% if (ssl && force_ssl) { %>
|
||||||
|
Forced
|
||||||
|
<% } else if (ssl) { %>
|
||||||
|
Enabled
|
||||||
|
<% } else { %>
|
||||||
|
No
|
||||||
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<% if (access_list) { %>
|
<% if (type === 'stream') { %>
|
||||||
<a href="#" class="access_list"><%- access_list.name %></a>
|
-
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<em>None</em>
|
<% if (access_list) { %>
|
||||||
|
<a href="#" class="access_list"><%- access_list.name %></a>
|
||||||
|
<% } else { %>
|
||||||
|
<em>None</em>
|
||||||
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<% if (ssl) { %>
|
|
||||||
<button type="button" class="btn btn-default btn-xs renew" title="Renew SSL"><i class="fa fa-shield" aria-hidden="true"></i></button>
|
|
||||||
<% } %>
|
|
||||||
<button type="button" class="btn btn-default btn-xs reconfigure" title="Reconfigure Nginx"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
<button type="button" class="btn btn-default btn-xs reconfigure" title="Reconfigure Nginx"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||||
<button type="button" class="btn btn-default btn-xs advanced" title="Advanced Configuration"><i class="fa fa-code" aria-hidden="true"></i></button>
|
<button type="button" class="btn btn-default btn-xs advanced" title="Advanced Configuration"<%- type === 'stream' ? ' disabled' : '' %>><i class="fa fa-code" aria-hidden="true"></i></button>
|
||||||
<button type="button" class="btn btn-warning btn-xs edit" title="Edit"><i class="fa fa-pencil" aria-hidden="true"></i></button>
|
<button type="button" class="btn btn-warning btn-xs edit" title="Edit"><i class="fa fa-pencil" aria-hidden="true"></i></button>
|
||||||
<button type="button" class="btn btn-danger btn-xs delete" title="Delete"><i class="fa fa-times" aria-hidden="true"></i></button>
|
<button type="button" class="btn btn-danger btn-xs delete" title="Delete"><i class="fa fa-times" aria-hidden="true"></i></button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -15,7 +15,6 @@ module.exports = Mn.View.extend({
|
|||||||
delete: 'button.delete',
|
delete: 'button.delete',
|
||||||
access_list: 'a.access_list',
|
access_list: 'a.access_list',
|
||||||
reconfigure: 'button.reconfigure',
|
reconfigure: 'button.reconfigure',
|
||||||
renew: 'button.renew',
|
|
||||||
advanced: 'button.advanced'
|
advanced: 'button.advanced'
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -32,6 +31,9 @@ module.exports = Mn.View.extend({
|
|||||||
case '404':
|
case '404':
|
||||||
Controller.show404HostForm(this.model);
|
Controller.show404HostForm(this.model);
|
||||||
break;
|
break;
|
||||||
|
case 'stream':
|
||||||
|
Controller.showStreamHostForm(this.model);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -50,11 +52,6 @@ module.exports = Mn.View.extend({
|
|||||||
Controller.showReconfigureHost(this.model);
|
Controller.showReconfigureHost(this.model);
|
||||||
},
|
},
|
||||||
|
|
||||||
'click @ui.renew': function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
Controller.showRenewHost(this.model);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click @ui.advanced': function (e) {
|
'click @ui.advanced': function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
Controller.showAdvancedHost(this.model);
|
Controller.showAdvancedHost(this.model);
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form class="form-horizontal">
|
|
||||||
<div class="modal-header text-left">
|
|
||||||
<h4 class="modal-title">Renew SSL Certificates</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>This will renew the SSL Certificates for the host. This normally happens automatically however if you notice
|
|
||||||
SSL working incorrectly, this may fix it.</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
|
||||||
<button type="submit" class="btn btn-success renew">Renew SSL</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,33 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
import Mn from 'backbone.marionette';
|
|
||||||
|
|
||||||
const template = require('./renew.ejs');
|
|
||||||
const Api = require('../api');
|
|
||||||
const App = require('../main');
|
|
||||||
|
|
||||||
module.exports = Mn.View.extend({
|
|
||||||
template: template,
|
|
||||||
|
|
||||||
ui: {
|
|
||||||
buttons: 'form button',
|
|
||||||
renew: 'button.renew'
|
|
||||||
},
|
|
||||||
|
|
||||||
events: {
|
|
||||||
'click @ui.renew': function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
|
|
||||||
|
|
||||||
Api.Hosts.renew(this.model.get('_id'))
|
|
||||||
.then((/*result*/) => {
|
|
||||||
App.UI.closeModal();
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
alert(err.message);
|
|
||||||
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
55
manager/src/frontend/js/app/host/stream_form.ejs
Normal file
55
manager/src/frontend/js/app/host/stream_form.ejs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="modal-header text-left">
|
||||||
|
<h4 class="modal-title"><% if (typeof _id !== 'undefined') { %>Edit<% } else { %>Create<% } %> Stream Host</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
A Stream Host will forward a TCP/UDP connection directly to a another server on your network. <strong>There is no authentication.</strong>
|
||||||
|
Note you will also have to open the incoming port in your docker configuration for this to work.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
You will not be able to use port <strong>80</strong>, <strong>81</strong> or <strong>443</strong> or any other previously configured Stream Host incoming port.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-4 control-label">Incoming Port</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input type="number" minimum="1" maximum="65535" class="form-control" placeholder="" name="incoming_port" value="<%- incoming_port ? incoming_port : '' %>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-4 control-label">Forwarding IP</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input type="text" class="form-control" placeholder="192.168.0.1" name="forward_server" value="<%- forward_server %>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-4 control-label">Forwarding Port</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input type="number" minimum="1" maximum="65535" class="form-control" placeholder="" name="forward_port" value="<%- typeof _id === 'undefined' ? '' : forward_port %>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-4 col-sm-8">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="protocols[]" value="tcp"<%- typeof _id === 'undefined' || hasStreamProtocol('tcp') ? ' checked' : '' %>> TCP Forwarding
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="protocols[]" value="udp"<%- hasStreamProtocol('udp') ? ' checked' : '' %>> UDP Forwarding
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-success save">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
63
manager/src/frontend/js/app/host/stream_form.js
Normal file
63
manager/src/frontend/js/app/host/stream_form.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import Mn from 'backbone.marionette';
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
const template = require('./stream_form.ejs');
|
||||||
|
const Controller = require('../controller');
|
||||||
|
const Api = require('../api');
|
||||||
|
const App = require('../main');
|
||||||
|
|
||||||
|
require('jquery-serializejson');
|
||||||
|
|
||||||
|
module.exports = Mn.View.extend({
|
||||||
|
template: template,
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
form: 'form',
|
||||||
|
buttons: 'form button'
|
||||||
|
},
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'submit @ui.form': function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let data = _.extend({}, this.ui.form.serializeJSON());
|
||||||
|
|
||||||
|
data.type = 'stream';
|
||||||
|
|
||||||
|
// Ports are integers
|
||||||
|
data.incoming_port = parseInt(data.incoming_port, 10);
|
||||||
|
data.forward_port = parseInt(data.forward_port, 10);
|
||||||
|
|
||||||
|
if (typeof data.protocols === 'undefined' || !data.protocols.length) {
|
||||||
|
alert('You must select one or more Protocols');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
|
||||||
|
let method = Api.Hosts.create;
|
||||||
|
|
||||||
|
if (this.model.get('_id')) {
|
||||||
|
// edit
|
||||||
|
method = Api.Hosts.update;
|
||||||
|
data._id = this.model.get('_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
method(data)
|
||||||
|
.then((/*result*/) => {
|
||||||
|
App.UI.closeModal();
|
||||||
|
Controller.showDashboard();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
alert(err.message);
|
||||||
|
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
templateContext: {
|
||||||
|
hasStreamProtocol: function (protocol) {
|
||||||
|
return this.protocols.indexOf(protocol) !== -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -20,7 +20,9 @@ const model = Backbone.Model.extend({
|
|||||||
letsencrypt_email: '',
|
letsencrypt_email: '',
|
||||||
accept_tos: false,
|
accept_tos: false,
|
||||||
access_list_id: '',
|
access_list_id: '',
|
||||||
advanced: ''
|
advanced: '',
|
||||||
|
incoming_port: 0,
|
||||||
|
protocols: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)
|
||||||
|
# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel
|
||||||
|
# other regex checks, because in our other config files have regex rule that denies access to files with dotted names.
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
auth_basic off;
|
||||||
|
|
||||||
|
# Set correct content type. According to this:
|
||||||
|
# https://community.letsencrypt.org/t/using-the-webroot-domain-verification-method/1445/29
|
||||||
|
# Current specification requires "text/plain" or no content header at all.
|
||||||
|
# It seems that "text/plain" is a safe option.
|
||||||
|
default_type "text/plain";
|
||||||
|
|
||||||
|
# This directory must be the same as in /etc/letsencrypt/cli.ini
|
||||||
|
# as "webroot-path" parameter. Also don't forget to set "authenticator" parameter
|
||||||
|
# there to "webroot".
|
||||||
|
# Do NOT use alias, use root! Target directory is located here:
|
||||||
|
# /var/www/common/letsencrypt/.well-known/acme-challenge/
|
||||||
|
root /config/letsencrypt-acme-challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hide /acme-challenge subdirectory and return 404 on all requests.
|
||||||
|
# It is somewhat more secure than letting Nginx return 403.
|
||||||
|
# Ending slash is important!
|
||||||
|
location = /.well-known/acme-challenge/ {
|
||||||
|
return 404;
|
||||||
|
}
|
@ -53,3 +53,7 @@ http {
|
|||||||
include /etc/nginx/conf.d/*.conf;
|
include /etc/nginx/conf.d/*.conf;
|
||||||
include /config/nginx/*.conf;
|
include /config/nginx/*.conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream {
|
||||||
|
include /config/nginx/stream/*.conf;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/with-contenv bash
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
|
mkdir -p /config/letsencrypt-acme-challenge
|
||||||
|
|
||||||
cd /srv/manager
|
cd /srv/manager
|
||||||
node --abort_on_uncaught_exception --max_old_space_size=250 /srv/manager/src/backend/index.js
|
node --abort_on_uncaught_exception --max_old_space_size=250 /srv/manager/src/backend/index.js
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/with-contenv bash
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
mkdir -p /tmp/nginx /config/{nginx,logs,access} /var/lib/nginx/cache/{public,private}
|
mkdir -p /tmp/nginx /config/{nginx,logs,access} /config/nginx/stream /var/lib/nginx/cache/{public,private}
|
||||||
chown root /tmp/nginx
|
chown root /tmp/nginx
|
||||||
exec nginx
|
exec nginx
|
||||||
|
4
rootfs/root/.config/letsencrypt/cli.ini
Normal file
4
rootfs/root/.config/letsencrypt/cli.ini
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
text = True
|
||||||
|
non-interactive = True
|
||||||
|
authenticator = webroot
|
||||||
|
webroot-path = /config/letsencrypt-acme-challenge
|
Reference in New Issue
Block a user