Compare commits

..

23 Commits

Author SHA1 Message Date
Girish Ramakrishnan eeddc233dd more changes 2022-03-16 09:05:41 -07:00
Girish Ramakrishnan f48690ee11 dyndns: fix typo 2022-03-15 09:53:54 -07:00
Girish Ramakrishnan 3b0bdd9807 support: send the server IPv4 when remote support enabled 2022-03-14 21:30:54 -07:00
Girish Ramakrishnan 6dc5c4f13b ldap: add dummy apps search route for directus 2022-03-14 09:17:49 -07:00
Girish Ramakrishnan 9bb5096f1c nginx: enable underscores in headers
chatwoot requires this

https://www.chatwoot.com/docs/self-hosted/deployment/caprover#api-requests-failing-with-you-need-to-sign-in-or-sign-up-before-continuing

They are apparently disabled by default since they conflict with some CGI headers:

https://stackoverflow.com/questions/22856136/why-do-http-servers-forbid-underscores-in-http-header-names
https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/?highlight=disappearing%20http%20headers#missing-disappearing-http-headers
2022-03-13 23:04:34 -07:00
Girish Ramakrishnan af42008fd3 Enable IPv6 on new interfaces with net_admin cap 2022-03-12 09:14:37 -08:00
Johannes Zellner d6875d4949 Add test coverage support 2022-03-11 00:52:41 +01:00
Girish Ramakrishnan 4396bd3ea7 wildcard: handle ENODATA 2022-03-08 17:14:42 -08:00
Girish Ramakrishnan db03053e05 cloudflare: remove async 2022-03-08 14:30:27 -08:00
Girish Ramakrishnan 193dff8c30 Better log 2022-03-03 10:08:34 -08:00
Girish Ramakrishnan 59582d081a port25check: log the error message 2022-03-03 09:58:58 -08:00
Girish Ramakrishnan ef684d32a2 port25checker: Use random tick to not bombard our checker service 2022-03-03 09:57:41 -08:00
Girish Ramakrishnan fc2a326332 mysql: Fix default collation
https://github.com/mattermost/mattermost-server/issues/19602#issuecomment-1057360142

> SELECT @@character_set_database, @@collation_database;

This will show utf8mb4 and utf8mb4_0900_ai_ci (was utf8mb4_unicode_ci)

To see the table schemas:

> SELECT table_schema, table_name, table_collation FROM information_schema.tables;
2022-03-02 22:34:30 -08:00
Girish Ramakrishnan e66a804012 ufw may not be installed 2022-03-02 19:36:32 -08:00
Girish Ramakrishnan 5afa7345a5 route53: check permissions to perform route53:ListResourceRecordSets
otherwise, at install time we see "DNS credentials for xx are invalid. Update it in Domains & Certs view"

the exact error from route 53 is:

User: arn:aws:iam::xx:user/yy is not authorized to perform: route53:ListResourceRecordSets on resource: arn:aws:route53:::hostedzone/zz because no identity-based policy allows the route53:ListResourceRecordSets action
2022-03-02 10:44:52 -08:00
Girish Ramakrishnan c100be4131 dns: filter out link local addresses
Unlike IPv4, IPv6 requires a link-local address on every network interface on which the IPv6 protocol is enabled, even when routable addresses are also assigned
2022-03-01 12:13:59 -08:00
Girish Ramakrishnan d326d05ad6 sysinfo: add noop provider 2022-03-01 12:05:01 -08:00
Girish Ramakrishnan eb0662b245 Up the json size to 2mb for block list route
https://forum.cloudron.io/topic/6575/cloudron-7-1-2-firewall-not-ipv6-ready
2022-03-01 11:57:50 -08:00
Johannes Zellner b92641d1b8 Update ldapjs to 2.3.2 2022-03-01 17:36:09 +01:00
Girish Ramakrishnan 7912d521ca 7.1.3 changes 2022-02-28 14:26:37 -08:00
Johannes Zellner 71dac64c4c Only allow impersonation for equal or less powerful roles 2022-02-28 20:42:33 +01:00
Girish Ramakrishnan aab6f222b3 better log 2022-02-28 11:04:44 -08:00
Girish Ramakrishnan 1cb1be321c remove usage of deprecated fs.rmdir 2022-02-25 16:43:20 -08:00
24 changed files with 2070 additions and 86 deletions
+1
View File
@@ -1,5 +1,6 @@
node_modules/
coverage/
.nyc_output/
webadmin/dist/
installer/src/certs/server.key
+14
View File
@@ -2435,3 +2435,17 @@
* eventlog: log event for mailbox alias update
* backups: fix incorrect mountpoint check with managed mounts
[7.1.3]
* Fix security issue where an admin can impersonate an owner
* block list: can upload up to 2MB
* dns: fix issue where link local address was picked up for ipv6
* setup: ufw may not be installed
* mysql: fix default collation of databases
[7.1.4]
* wildcard dns: fix handling of ENODATA
* cloudflare: fix error handling
* openvpn: ipv6 support
* dyndns: fix issue where eventlog was getting filled with empty entries
* mandatory 2fa: Fix typo in 2FA check
+1 -1
View File
@@ -185,7 +185,7 @@ systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
# on vultr, ufw is enabled by default. we have our own firewall
ufw disable
ufw disable || true
# we need unbound to work as this is required for installer.sh to do any DNS requests
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: no" > /etc/unbound/unbound.conf.d/cloudron-network.conf
+1963 -57
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -37,7 +37,7 @@
"js-yaml": "^4.1.0",
"json": "^11.0.0",
"jsonwebtoken": "^8.5.1",
"ldapjs": "^2.3.1",
"ldapjs": "^2.3.2",
"lodash": "^4.17.21",
"lodash.chunk": "^4.2.0",
"moment": "^2.29.1",
@@ -77,6 +77,7 @@
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^13.2.1",
"node-sass": "^7.0.1",
"nyc": "^15.1.0",
"recursive-readdir": "^2.2.2"
},
"scripts": {
+7 -2
View File
@@ -79,10 +79,15 @@ echo "=> Run database migrations"
cd "${source_dir}"
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
echo "=> Run tests with mocha"
TESTS=${DEFAULT_TESTS}
if [[ $# -gt 0 ]]; then
TESTS="$*"
fi
BOX_ENV=test ./node_modules/mocha/bin/_mocha --bail --no-timeouts --exit -R spec ${TESTS}
if [[ -z ${COVERAGE+x} ]]; then
echo "=> Run tests with mocha"
BOX_ENV=test ./node_modules/.bin/mocha --bail --no-timeouts --exit -R spec ${TESTS}
else
echo "=> Run tests with mocha and coverage"
BOX_ENV=test ./node_modules/.bin/nyc --reporter=html ./node_modules/.bin/mocha --no-timeouts --exit -R spec ${TESTS}
fi
+1 -1
View File
@@ -324,7 +324,7 @@ Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFile
if (!csrDer) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(csrFilePath, csrDer)) throw new BoxError(BoxError.FS_ERROR, safe.error); // bookkeeping. inspect with openssl req -text -noout -in hostname.csr -inform der
await safe(fs.promises.rmdir(tmpdir, { recursive: true }));
await safe(fs.promises.rm(tmpdir, { recursive: true, force: true }));
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFilePath);
+2
View File
@@ -34,6 +34,7 @@ const apps = require('./apps.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
superagent = require('superagent'),
support = require('./support.js'),
util = require('util');
@@ -379,6 +380,7 @@ async function createTicket(info, auditSource) {
if (info.enableSshSupport) {
await safe(support.enableRemoteSupport(true, auditSource));
info.ipv4 = await sysinfo.getServerIPv4();
}
info.app = info.appId ? await apps.get(info.appId) : null;
+1 -1
View File
@@ -1042,7 +1042,7 @@ async function fullBackup(options, progressCallback) {
percent += step;
if (!app.enableBackup) {
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length})`);
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length}) since automatic backup disabled`);
continue; // nothing to backup
}
+1 -1
View File
@@ -66,7 +66,7 @@ async function startJobs() {
const randomTick = Math.floor(60*Math.random());
gJobs.systemChecks = new CronJob({
cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
cronTime: `${randomTick} ${randomTick} 2 * * *`, // once a day. if you change this interval, change the notification messages with correct duration
onTick: async () => await safe(cloudron.runSystemChecks(), { debug }),
start: true
});
+2 -3
View File
@@ -34,8 +34,7 @@ async function resolve(hostname, rrtype, options) {
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
if (error) throw error;
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array. for TXT records, result is 2d array of strings
// when you query a random record, it errors with ENOTFOUND. But, if you query a record which has a different type
// we sometimes get empty array and sometimes ENODATA. for TXT records, result is 2d array of strings
return result;
}
+1 -1
View File
@@ -34,7 +34,7 @@ function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
async function translateRequestError(result) {
function translateRequestError(result) {
assert.strictEqual(typeof result, 'object');
if (result.statusCode === 404) return new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist'));
+3
View File
@@ -255,6 +255,9 @@ async function verifyDomainConfig(domainObject) {
await upsert(newDomainObject, location, 'A', [ ip ]);
debug('verifyDomainConfig: Test A record added');
await get(newDomainObject, location, 'A');
debug('verifyDomainConfig: Can list record sets');
await del(newDomainObject, location, 'A', [ ip ]);
debug('verifyDomainConfig: Test A record removed again');
+6 -4
View File
@@ -80,8 +80,9 @@ async function verifyDomainConfig(domainObject) {
const fqdn = dns.fqdn(location, domainObject);
const [ipv4Error, ipv4Result] = await safe(dig.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }));
if (ipv4Error && ipv4Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}. Please check if you have set up *.${domainObject.domain} to point to this server's IP`);
if (ipv4Error || !ipv4Result) throw new BoxError(BoxError.BAD_FIELD, ipv4Error ? ipv4Error.message : `Unable to resolve IPv4 of ${fqdn}`);
if (ipv4Error && (ipv4Error.code === 'ENOTFOUND' || ipv4Error.code === 'ENODATA')) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}. Please check if you have set up *.${domainObject.domain} to point to this server's IP`);
if (ipv4Error) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}: ${ipv4Error.message}`);
if (!ipv4Result) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}`);
const ipv4 = await sysinfo.getServerIPv4();
if (ipv4Result.length !== 1 || ipv4 !== ipv4Result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(ipv4Result)} instead of IPv4 ${ipv4}`);
@@ -89,8 +90,9 @@ async function verifyDomainConfig(domainObject) {
const ipv6 = await sysinfo.getServerIPv6(); // both should be RFC 5952 format
if (ipv6) {
const [ipv6Error, ipv6Result] = await safe(dig.resolve(fqdn, 'AAAA', { server: '127.0.0.1', timeout: 5000 }));
if (ipv6Error && ipv6Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`);
if (ipv6Error || !ipv6Result) throw new BoxError(BoxError.BAD_FIELD, ipv6Error ? ipv6Error.message : `Unable to resolve IPv6 of ${fqdn}`);
if (ipv6Error && (ipv6Error.code === 'ENOTFOUND' || ipv6Error.code === 'ENODATA')) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`);
if (ipv6Error) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}: ${ipv6Error.message}`);
if (!ipv6Result) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`);
if (ipv6Result.length !== 1 || ipv6 !== ipv6Result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(ipv6Result)} instead of IPv6 ${ipv6}`);
}
+10 -4
View File
@@ -247,12 +247,12 @@ function getAddresses() {
const addresses = [];
for (const phy of physicalDevices) {
const inet = safe.JSON.parse(safe.child_process.execSync(`ip -f inet -j addr show ${phy.name}`, { encoding: 'utf8' }));
const inet = safe.JSON.parse(safe.child_process.execSync(`ip -f inet -j addr show dev ${phy.name} scope global`, { encoding: 'utf8' }));
for (const r of inet) {
const address = safe.query(r, 'addr_info[0].local');
if (address) addresses.push(address);
}
const inet6 = safe.JSON.parse(safe.child_process.execSync(`ip -f inet6 -j addr show ${phy.name}`, { encoding: 'utf8' }));
const inet6 = safe.JSON.parse(safe.child_process.execSync(`ip -f inet6 -j addr show dev ${phy.name} scope global`, { encoding: 'utf8' }));
for (const r of inet6) {
const address = safe.query(r, 'addr_info[0].local');
if (address) addresses.push(address);
@@ -355,7 +355,8 @@ async function createSubcontainer(app, name, cmd, options) {
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
CapDrop: [],
Sysctls: {}
}
};
@@ -388,7 +389,12 @@ async function createSubcontainer(app, name, cmd, options) {
const capabilities = manifest.capabilities || [];
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('net_admin')) {
containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
// ipv6 for new interfaces is disabled in the container. this prevents the openvpn tun device having ipv6
// See https://github.com/moby/moby/issues/20569 and https://github.com/moby/moby/issues/33099
containerOptions.HostConfig.Sysctls['net.ipv6.conf.all.disable_ipv6'] = '0';
}
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
+2 -2
View File
@@ -27,7 +27,7 @@ async function sync(auditSource) {
info.ipv4 = info.ip;
delete info.ip;
}
const ipv4Changed = info.ip !== ipv4;
const ipv4Changed = info.ipv4 !== ipv4;
const ipv6Changed = ipv6 && info.ipv6 !== ipv6; // both should be RFC 5952 format
if (!ipv4Changed && !ipv6Changed) {
@@ -35,7 +35,7 @@ async function sync(auditSource) {
return;
}
debug(`refreshDNS: updating IP from ${info.ip} to ipv4: ${ipv4} (changed: ${ipv4Changed}) ipv6: ${ipv6} (changed: ${ipv6Changed})`);
debug(`refreshDNS: updating IP from ${info.ipv4} to ipv4: ${ipv4} (changed: ${ipv4Changed}) ipv6: ${ipv6} (changed: ${ipv6Changed})`);
if (ipv4Changed) await dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ipv4 ]);
if (ipv6Changed) await dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'AAAA', [ ipv6 ]);
+1 -1
View File
@@ -16,7 +16,7 @@ exports = module.exports = {
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
'images': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.4.0@sha256:45817f1631992391d585f171498d257487d872480fd5646723a2b956cc4ef15d' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:3.2.0@sha256:c70d0a1ff7ce8a27dcf7d6f8d1718ddba82ef254079fee5d20fdf074f28cd009' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:3.2.1@sha256:75cef64ba4917ba9ec68bc0c9d9ba3a9eeae00a70173cd6d81cc6118038737d9' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:4.3.0@sha256:9f0cfe83310a6f67885c21804c01e73c8d256606217f44dcefb3e05a5402b2c9' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:4.2.0@sha256:c8ebdbe2663b26fcd58b1e6b97906b62565adbe4a06256ba0f86114f78b37e6b' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.2.1@sha256:30a5550c41c3be3a01dbf457497ba2f3c05f3121c595a17ef1aacbef931b6114' },
+8
View File
@@ -687,6 +687,14 @@ async function start() {
res.end();
});
// directus looks for the "DN" of the bind user
gServer.search('ou=apps,dc=cloudron', function(req, res, next) {
const obj = {
dn: req.dn.toString(),
};
finalSend([obj], req, res, next);
});
// just log that an attempt was made to unknown route, this helps a lot during app packaging
gServer.use(function(req, res, next) {
debug('not handled: dn %s, scope %s, filter %s (from %s)', req.dn ? req.dn.toString() : '-', req.scope, req.filter ? req.filter.toString() : '-', req.connection.ldap.id);
+2 -2
View File
@@ -197,7 +197,7 @@ async function checkOutboundPort25() {
relay.status = false;
relay.value = `Connect to ${constants.PORT25_CHECK_SERVER} failed: ${error.message}. Check if port 25 (outbound) is blocked`;
client.destroy();
relay.errorMessage = `Connect to ${constants.PORT25_CHECK_SERVER} failed.`;
relay.errorMessage = `Connect to ${constants.PORT25_CHECK_SERVER} failed: ${error.message}`;
resolve(relay);
});
});
@@ -516,7 +516,7 @@ async function checkRblStatus(domain) {
blacklistedServers.push(result);
}
debug(`checkRblStatus: ${domain} (ip: ${ip}) servers: ${JSON.stringify(blacklistedServers)})`);
debug(`checkRblStatus: ${domain} (ip: ${ip}) blacklistedServers: ${JSON.stringify(blacklistedServers)})`);
return { status: blacklistedServers.length === 0, ip, servers: blacklistedServers };
}
+4 -1
View File
@@ -88,6 +88,9 @@ server {
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256;
ssl_prefer_server_ciphers off;
# some apps have underscores in headers. this is apparently disabled by default because of some legacy CGI compat
underscores_in_headers on;
<% if (endpoint !== 'ip' && endpoint !== 'setup') { -%>
# dhparams is generated only after dns setup
ssl_dhparam /home/yellowtent/platformdata/dhparams.pem;
@@ -215,7 +218,7 @@ server {
<% if ( endpoint === 'dashboard' || endpoint === 'setup' ) { %>
location /api/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
client_max_body_size 2m;
}
location ~ ^/api/v1/cloudron/login$ {
+1
View File
@@ -180,6 +180,7 @@ async function setGhost(req, res, next) {
if (typeof req.body.password !== 'string' || !req.body.password) return next(new HttpError(400, 'password must be non-empty string'));
if ('expiresAt' in req.body && typeof req.body.password !== 'number') return next(new HttpError(400, 'expiresAt must be a number'));
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
const [error] = await safe(users.setGhost(req.resource, req.body.password, req.body.expiresAt || 0));
if (error) return next(BoxError.toHttpError(error));
+1 -1
View File
@@ -29,7 +29,7 @@ function initializeExpressSync() {
const wsServer = new ws.Server({ noServer: true }); // in noServer mode, we have to handle 'upgrade' and call handleUpgrade
const QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
const QUERY_LIMIT = '2mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
const REQUEST_TIMEOUT = 20000; // timeout for all requests (see also setTimeout on the httpServer)
+2 -3
View File
@@ -18,6 +18,7 @@ function api(provider) {
assert.strictEqual(typeof provider, 'string');
switch (provider) {
case 'noop': return require('./sysinfo/noop.js');
case 'fixed': return require('./sysinfo/fixed.js');
case 'network-interface': return require('./sysinfo/network-interface.js');
default: return require('./sysinfo/generic.js');
@@ -33,10 +34,8 @@ async function getServerIPv4() {
// returns RFC 5952 formatted address (https://datatracker.ietf.org/doc/html/rfc5952)
async function getServerIPv6() {
const config = await settings.getIPv6Config();
if (config.provider === 'noop') return null;
const result = await api(config.provider).getServerIPv6(config);
if (!result) return null;
return ipaddr.parse(result).toRFC5952String();
}
+34
View File
@@ -0,0 +1,34 @@
'use strict';
exports = module.exports = {
getServerIPv4,
getServerIPv6,
testIPv4Config,
testIPv6Config
};
const assert = require('assert');
async function getServerIPv4(config) {
assert.strictEqual(typeof config, 'object');
return null;
}
async function getServerIPv6(config) {
assert.strictEqual(typeof config, 'object');
return null;
}
async function testIPv4Config(config) {
assert.strictEqual(typeof config, 'object');
return null;
}
async function testIPv6Config(config) {
assert.strictEqual(typeof config, 'object');
return null;
}