diff --git a/CHANGES b/CHANGES
index 1eab49298..dade402a9 100644
--- a/CHANGES
+++ b/CHANGES
@@ -2634,4 +2634,5 @@
* redis: update to 7.0.11
* ionos profitbricks: add new regions Berlin and Logrono
* docker: update to 23.0.6
+* network: trusted IPs
diff --git a/dashboard/src/js/client.js b/dashboard/src/js/client.js
index b201d7e04..11935f20f 100644
--- a/dashboard/src/js/client.js
+++ b/dashboard/src/js/client.js
@@ -1128,9 +1128,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
};
Client.prototype.getBlocklist = function (callback) {
- var config = {};
-
- get('/api/v1/network/blocklist', config, function (error, data, status) {
+ get('/api/v1/network/blocklist', null, function (error, data, status) {
if (error) return callback(error);
if (status !== 200) return callback(new ClientError(status, data));
callback(null, data.blocklist);
@@ -1146,6 +1144,23 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
});
};
+ Client.prototype.getTrustedIps = function (callback) {
+ get('/api/v1/network/trusted_ips', null, function (error, data, status) {
+ if (error) return callback(error);
+ if (status !== 200) return callback(new ClientError(status, data));
+ callback(null, data.trustedIps);
+ });
+ };
+
+ Client.prototype.setTrustedIps = function (trustedIps, callback) {
+ post('/api/v1/network/trusted_ips', { trustedIps: trustedIps }, null, function (error, data, status) {
+ if (error) return callback(error);
+ if (status !== 200) return callback(new ClientError(status, data));
+
+ callback(null);
+ });
+ };
+
Client.prototype.setDynamicDnsConfig = function (enabled, callback) {
post('/api/v1/settings/dynamic_dns', { enabled: enabled }, null, function (error, data, status) {
if (error) return callback(error);
diff --git a/dashboard/src/translation/en.json b/dashboard/src/translation/en.json
index ef8d937c3..3d1610e7b 100644
--- a/dashboard/src/translation/en.json
+++ b/dashboard/src/translation/en.json
@@ -806,7 +806,13 @@
},
"configureIpv6": {
"title": "Configure IPv6 Provider"
- }
+ },
+ "trustedIps": {
+ "description": "HTTP headers from matching IP addresses will be trusted",
+ "title": "Configure Trusted IPs",
+ "summary": "{{ trustCount }} IPs trusted"
+ },
+ "trustedIpRanges": "Trusted IPs & Ranges "
},
"services": {
"title": "Services",
diff --git a/dashboard/src/views/network.html b/dashboard/src/views/network.html
index 4621cfeba..49ce72c77 100644
--- a/dashboard/src/views/network.html
+++ b/dashboard/src/views/network.html
@@ -71,6 +71,32 @@
+
+
+
+
+
+
+
+
+
+ {{ 'network.ipv6.description' | tr }}
+
+
+
+
+
+
+ {{ 'network.ip.provider' | tr }}
+
+
+ {{ prettyIpProviderName(ipv6Configure.provider) }}
+
+
+
+
+
+ {{ 'network.ip.address' | tr }}
+
+
+ {{ ipv6Configure.ipv6 }}
+ {{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})
+ {{ ipv6Configure.displayError }}
+
+
+
+
+
+ {{ 'network.ip.interface' | tr }}
+
+
+ {{ ipv6Configure.ifname }}
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- {{ 'network.ipv6.description' | tr }}
-
-
-
-
-
-
- {{ 'network.ip.provider' | tr }}
-
-
- {{ prettyIpProviderName(ipv6Configure.provider) }}
-
-
-
-
-
- {{ 'network.ip.address' | tr }}
-
-
- {{ ipv6Configure.ipv6 }}
- {{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})
- {{ ipv6Configure.displayError }}
-
-
-
-
- {{ 'network.ip.interface' | tr }}
+ {{ 'network.trustedIpRanges' | tr }}
- {{ ipv6Configure.ifname }}
-
-
-
-
-
-
-
-
+
{{ 'network.trustedIps.summary' | tr:{ trustCount: trustedIps.currentTrustedIpsLength } }}
+
diff --git a/dashboard/src/views/network.js b/dashboard/src/views/network.js
index 0c5fb3813..6e6aa742d 100644
--- a/dashboard/src/views/network.js
+++ b/dashboard/src/views/network.js
@@ -192,6 +192,50 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
}
};
+ $scope.trustedIps = {
+ busy: false,
+ error: {},
+ trustedIps: '',
+ currentTrustedIps: '',
+ currentTrustedIpsLength: 0,
+
+ refresh: function () {
+ Client.getTrustedIps(function (error, result) {
+ if (error) return console.error(error);
+
+ $scope.trustedIps.currentTrustedIps = result;
+ $scope.trustedIps.currentTrustedIpsLength = result.split('\n').filter(function (l) { return l.length !== 0 && l[0] !== '#'; }).length;
+ });
+ },
+
+ show: function () {
+ $scope.trustedIps.error = {};
+ $scope.trustedIps.trustedIps = $scope.trustedIps.currentTrustedIps;
+
+ $('#trustedIpsModal').modal('show');
+ },
+
+ submit: function () {
+ $scope.trustedIps.error = {};
+ $scope.trustedIps.busy = true;
+
+ Client.setTrustedIps($scope.trustedIps.trustedIps, function (error) {
+ $scope.trustedIps.busy = false;
+ if (error) {
+ $scope.trustedIps.error.trustedIps = error.message;
+ $scope.trustedIps.error.ip = error.message;
+ $scope.trustedIpsChangeForm.$setPristine();
+ $scope.trustedIpsChangeForm.$setUntouched();
+ return;
+ }
+
+ $scope.trustedIps.refresh();
+
+ $('#trustedIpsModal').modal('hide');
+ });
+ }
+ };
+
$scope.sysinfo = {
busy: false,
error: {},
@@ -276,6 +320,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
$scope.dyndnsConfigure.refresh();
$scope.ipv6Configure.refresh();
+ $scope.trustedIps.refresh();
if ($scope.user.isAtLeastOwner) $scope.blocklist.refresh();
});
diff --git a/setup/start.sh b/setup/start.sh
index b318a12b9..5d7215da8 100755
--- a/setup/start.sh
+++ b/setup/start.sh
@@ -167,6 +167,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/nginx/applications/dashboard"
mkdir -p "${PLATFORM_DATA_DIR}/nginx/cert"
cp "${script_dir}/start/nginx/nginx.conf" "${PLATFORM_DATA_DIR}/nginx/nginx.conf"
cp "${script_dir}/start/nginx/mime.types" "${PLATFORM_DATA_DIR}/nginx/mime.types"
+touch "${PLATFORM_DATA_DIR}/nginx/trusted.ips"
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
# default nginx service file does not restart on crash
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
diff --git a/setup/start/nginx/nginx.conf b/setup/start/nginx/nginx.conf
index ce6803ab2..6d074a4f1 100644
--- a/setup/start/nginx/nginx.conf
+++ b/setup/start/nginx/nginx.conf
@@ -38,6 +38,7 @@ http {
# zones for rate limiting
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
+ include trusted.ips;
include applications/*.conf;
include applications/*/*.conf;
}
diff --git a/src/network.js b/src/network.js
index afb2d8e83..87ab3235d 100644
--- a/src/network.js
+++ b/src/network.js
@@ -2,7 +2,7 @@
exports = module.exports = {
getBlocklist,
- setBlocklist
+ setBlocklist,
};
const assert = require('assert'),
diff --git a/src/paths.js b/src/paths.js
index f41c12fda..d1af78d9e 100644
--- a/src/paths.js
+++ b/src/paths.js
@@ -50,6 +50,7 @@ exports = module.exports = {
FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'platformdata/firewall/blocklist.txt'),
LDAP_ALLOWLIST_FILE: path.join(baseDir(), 'platformdata/firewall/ldap_allowlist.txt'),
REVERSE_PROXY_REBUILD_FILE: path.join(baseDir(), 'platformdata/nginx/rebuild-needed'),
+ NGINX_TRUSTED_IPS_FILE: path.join(baseDir(), 'platformdata/nginx/trusted.ips'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata/box'),
MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'),
diff --git a/src/reverseproxy.js b/src/reverseproxy.js
index d321ba16b..4fdddb4b0 100644
--- a/src/reverseproxy.js
+++ b/src/reverseproxy.js
@@ -27,7 +27,10 @@ exports = module.exports = {
removeAppConfigs,
restoreFallbackCertificates,
- handleCertificateProviderChanged
+ handleCertificateProviderChanged,
+
+ getTrustedIps,
+ setTrustedIps
};
const acme2 = require('./acme2.js'),
@@ -52,7 +55,8 @@ const acme2 = require('./acme2.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
- util = require('util');
+ util = require('util'),
+ validator = require('validator');
const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' });
const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh');
@@ -728,3 +732,25 @@ async function handleCertificateProviderChanged(domain) {
safe.fs.appendFileSync(paths.REVERSE_PROXY_REBUILD_FILE, `${domain}\n`, 'utf8');
}
+
+async function getTrustedIps() {
+ return await settings.getTrustedIps();
+}
+
+async function setTrustedIps(trustedIps) {
+ assert.strictEqual(typeof trustedIps, 'string');
+
+ let trustedIpsConfig = 'real_ip_header X-Forwarded-For;\nreal_ip_recursive on;\n';
+
+ for (const line of trustedIps.split('\n')) {
+ if (!line || line.startsWith('#')) continue;
+ const rangeOrIP = line.trim();
+ // this checks for IPv4 and IPv6
+ if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`);
+ trustedIpsConfig += `set_real_ip_from ${rangeOrIP};\n`;
+ }
+
+ await settings.setTrustedIps(trustedIps);
+ if (!safe.fs.writeFileSync(paths.NGINX_TRUSTED_IPS_FILE, trustedIpsConfig, 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
+ await reload();
+}
diff --git a/src/routes/network.js b/src/routes/network.js
index 070448ac2..fe460a44b 100644
--- a/src/routes/network.js
+++ b/src/routes/network.js
@@ -2,7 +2,10 @@
exports = module.exports = {
getBlocklist,
- setBlocklist
+ setBlocklist,
+
+ getTrustedIps,
+ setTrustedIps
};
const assert = require('assert'),
@@ -11,6 +14,7 @@ const assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
network = require('../network.js'),
+ reverseProxy = require('../reverseproxy.js'),
safe = require('safetydance');
async function getBlocklist(req, res, next) {
@@ -32,3 +36,21 @@ async function setBlocklist(req, res, next) {
next(new HttpSuccess(200, {}));
}
+
+async function getTrustedIps(req, res, next) {
+ const [error, trustedIps] = await safe(reverseProxy.getTrustedIps());
+ if (error) return next(BoxError.toHttpError(error));
+
+ next(new HttpSuccess(200, { trustedIps }));
+}
+
+async function setTrustedIps(req, res, next) {
+ assert.strictEqual(typeof req.body, 'object');
+
+ if (typeof req.body.trustedIps !== 'string') return next(new HttpError(400, 'trustedIps must be a string'));
+
+ const [error] = await safe(reverseProxy.setTrustedIps(req.body.trustedIps, AuditSource.fromRequest(req)));
+ if (error) return next(BoxError.toHttpError(error));
+
+ next(new HttpSuccess(200, {}));
+}
diff --git a/src/server.js b/src/server.js
index d93d6d0e8..70509cf45 100644
--- a/src/server.js
+++ b/src/server.js
@@ -282,8 +282,10 @@ async function initializeExpressSync() {
}, routes.branding.set);
// network routes
- router.get ('/api/v1/network/blocklist', token, authorizeOwner, routes.network.getBlocklist);
- router.post('/api/v1/network/blocklist', json, token, authorizeOwner, routes.network.setBlocklist);
+ router.get ('/api/v1/network/blocklist', token, authorizeOwner, routes.network.getBlocklist);
+ router.post('/api/v1/network/blocklist', json, token, authorizeOwner, routes.network.setBlocklist);
+ router.get ('/api/v1/network/trusted_ips', token, authorizeOwner, routes.network.getTrustedIps);
+ router.post('/api/v1/network/trusted_ips', json, token, authorizeOwner, routes.network.setTrustedIps);
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
router.get ('/api/v1/settings/:setting', token, authorizeAdmin, routes.settings.get);
diff --git a/src/settings.js b/src/settings.js
index 0188bf2cd..41fe76e6c 100644
--- a/src/settings.js
+++ b/src/settings.js
@@ -67,6 +67,9 @@ exports = module.exports = {
getFirewallBlocklist,
setFirewallBlocklist,
+ getTrustedIps,
+ setTrustedIps,
+
getGhosts,
setGhosts,
@@ -119,6 +122,7 @@ exports = module.exports = {
APPSTORE_API_TOKEN_KEY: 'appstore_api_token',
APPSTORE_WEB_TOKEN_KEY: 'appstore_web_token',
FIREWALL_BLOCKLIST_KEY: 'firewall_blocklist',
+ TRUSTED_IPS_KEY: 'trusted_ips_key',
API_SERVER_ORIGIN_KEY: 'api_server_origin',
WEB_SERVER_ORIGIN_KEY: 'web_server_origin',
@@ -216,6 +220,7 @@ const gDefaults = (function () {
result[exports.MAIL_FQDN_KEY] = '';
result[exports.FIREWALL_BLOCKLIST_KEY] = '';
+ result[exports.TRUSTED_IPS_KEY] = '';
result[exports.API_SERVER_ORIGIN_KEY] = 'https://api.cloudron.io';
result[exports.WEB_SERVER_ORIGIN_KEY] = 'https://cloudron.io';
@@ -636,6 +641,19 @@ async function setFirewallBlocklist(blocklist) {
await setBlob(exports.FIREWALL_BLOCKLIST_KEY, Buffer.from(blocklist));
}
+async function getTrustedIps() {
+ const value = await get(exports.TRUSTED_IPS_KEY);
+ if (value === null) return gDefaults[exports.TRUSTED_IPS_KEY];
+
+ return value;
+}
+
+async function setTrustedIps(trustedIps) {
+ assert.strictEqual(typeof trustedIps, 'string');
+
+ await set(exports.TRUSTED_IPS_KEY, trustedIps);
+}
+
async function getGhosts() {
const value = await get(exports.GHOSTS_CONFIG_KEY);
if (value === null) return gDefaults[exports.GHOSTS_CONFIG_KEY];