create-wordjs 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/index.js +37 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ in CI — plus a pure-JS *SQLite (legacy / WASM)* fallback for hosts where the n
|
|
|
62
62
|
| `--systemd` | (create, `upgrade`; Linux) Also write a systemd unit, `wordjs.service`, to a private staging directory outside the site and print how to install it. The unit runs WordJS as a dedicated non-root account with **no Linux capabilities**. With create it implies `--no-start`. See [Run it as a service](#run-it-as-a-service-linux). |
|
|
63
63
|
| `--port <n>` | (with `--systemd`) Public port written into the unit (`PORT=<n>`); default: the site's configured port, else `3000`. Below 1024 it also stages `60-wordjs-ports.conf`. |
|
|
64
64
|
| `--service-user <name>` | (with `--systemd`) The account the service runs as (default `wordjs`; `root` is refused). |
|
|
65
|
+
| `--mail` | (with `--systemd`) The site will receive mail (mail-server plugin, SMTP on 25): the drop-in also allows 25. Implied on `upgrade` when that plugin is installed. |
|
|
65
66
|
| `-h`, `--help` | Show usage. |
|
|
66
67
|
|
|
67
68
|
Separate-mode options:
|
|
@@ -108,7 +109,10 @@ boot must run as the service account so that account owns the database and certi
|
|
|
108
109
|
|
|
109
110
|
**Ports below 1024 (80/443).** The unit never grants `CAP_NET_BIND_SERVICE`. For a port below 1024,
|
|
110
111
|
`--systemd` also stages `60-wordjs-ports.conf`, a `sysctl.d` drop-in that sets
|
|
111
|
-
`net.ipv4.ip_unprivileged_port_start`
|
|
112
|
+
`net.ipv4.ip_unprivileged_port_start` low enough for every port the site will bind: that port, `80` too
|
|
113
|
+
for an HTTPS site (Let's Encrypt HTTP-01, turned on later in the admin panel) and `25` with `--mail`
|
|
114
|
+
(incoming mail; implied on `upgrade` when the mail-server plugin is installed). Read its trade-off before
|
|
115
|
+
installing it: it lets
|
|
112
116
|
**every** unprivileged process on the host bind ports from that value up to 1023, not just WordJS. On a
|
|
113
117
|
shared host, prefer a reverse proxy (nginx, Caddy) on 80/443 forwarding to WordJS on a high port.
|
|
114
118
|
|
package/index.js
CHANGED
|
@@ -84,8 +84,13 @@ Options:
|
|
|
84
84
|
the first boot must run as that account.
|
|
85
85
|
--port <n> (with --systemd) Public port, written into the unit as PORT=<n> (default:
|
|
86
86
|
the site's configured port, else 3000). Below 1024 it also stages
|
|
87
|
-
60-wordjs-ports.conf (net.ipv4.ip_unprivileged_port_start), never a capability
|
|
87
|
+
60-wordjs-ports.conf (net.ipv4.ip_unprivileged_port_start), never a capability,
|
|
88
|
+
low enough for every port the site will bind: with HTTPS also 80 (Let's
|
|
89
|
+
Encrypt HTTP-01 from Admin -> Security), with --mail also 25.
|
|
88
90
|
--service-user <name> (with --systemd) The account the service runs as (default: wordjs). Not root.
|
|
91
|
+
--mail (with --systemd) The site will receive mail (the mail-server plugin, SMTP on
|
|
92
|
+
port 25): the drop-in then lowers the floor to 25. Implied on upgrade when the
|
|
93
|
+
mail-server plugin is installed.
|
|
89
94
|
-h, --help Show this help.
|
|
90
95
|
|
|
91
96
|
Examples:
|
|
@@ -126,7 +131,7 @@ function parseArgs(argv) {
|
|
|
126
131
|
const opts = {
|
|
127
132
|
mode: 'create', dir: null, zip: null, version: null, http: false, start: true, yes: false, force: false, install: true,
|
|
128
133
|
role: null, gateway: null, token: null, caHash: null, advertise: null, enrollPort: null, host: null,
|
|
129
|
-
systemd: false, port: null, serviceUser: null,
|
|
134
|
+
systemd: false, port: null, serviceUser: null, mail: false,
|
|
130
135
|
sha256: null,
|
|
131
136
|
};
|
|
132
137
|
// A leading subcommand selects the mode (default is the monolith create flow).
|
|
@@ -159,11 +164,12 @@ function parseArgs(argv) {
|
|
|
159
164
|
else if (a === '--systemd') opts.systemd = true;
|
|
160
165
|
else if (a === '--port') opts.port = argv[++i] || fail('--port needs a value (1-65535).');
|
|
161
166
|
else if (a === '--service-user') opts.serviceUser = argv[++i] || fail('--service-user needs an account name.');
|
|
167
|
+
else if (a === '--mail') opts.mail = true;
|
|
162
168
|
else if (a.startsWith('-')) fail(`Unknown option: ${a}`, 'Run with --help to see the available options.');
|
|
163
169
|
else positionals.push(a);
|
|
164
170
|
}
|
|
165
|
-
if ((opts.port !== null || opts.serviceUser !== null) && !opts.systemd) {
|
|
166
|
-
fail('--port
|
|
171
|
+
if ((opts.port !== null || opts.serviceUser !== null || opts.mail) && !opts.systemd) {
|
|
172
|
+
fail('--port, --service-user and --mail only apply together with --systemd.',
|
|
167
173
|
'Without a unit, the port comes from the PORT environment variable or the site config (default 3000).');
|
|
168
174
|
}
|
|
169
175
|
if (opts.systemd) {
|
|
@@ -789,9 +795,10 @@ function underAny(p, roots) {
|
|
|
789
795
|
* written into the file itself: whoever later finds it in /etc/sysctl.d/ should not have to guess why
|
|
790
796
|
* it is there or what it costs.
|
|
791
797
|
*/
|
|
792
|
-
function sysctlDropIn(start, { http = false } = {}) {
|
|
798
|
+
function sysctlDropIn(start, { http = false, mail = false, ports = [] } = {}) {
|
|
793
799
|
const lines = [
|
|
794
800
|
`# WordJS: allow unprivileged processes to bind ports from ${start} up (written by create-wordjs --systemd).`,
|
|
801
|
+
...ports.map((p) => `# ${p.port}: ${p.why}`),
|
|
795
802
|
'#',
|
|
796
803
|
'# This is NOT a grant to WordJS alone. It lowers the privileged-port floor for EVERY unprivileged',
|
|
797
804
|
`# process in this network namespace, so any local user or service may bind ports ${start}-1023 too, and`,
|
|
@@ -802,6 +809,9 @@ function sysctlDropIn(start, { http = false } = {}) {
|
|
|
802
809
|
if (!http && start > 80) {
|
|
803
810
|
lines.push(`# Let's Encrypt HTTP-01 (acme.http01Port: 80 in wordjs-config.json) needs this lowered to 80.`);
|
|
804
811
|
}
|
|
812
|
+
if (!mail && start > 25) {
|
|
813
|
+
lines.push('# Receiving mail (the mail-server plugin, SMTP on port 25) needs this lowered to 25: re-run with --mail.');
|
|
814
|
+
}
|
|
805
815
|
lines.push(
|
|
806
816
|
'#',
|
|
807
817
|
`# Install: sudo install -o root -g root -m 0644 ${SYSCTL_DROPIN_FILE} /etc/sysctl.d/ && sudo sysctl --system`,
|
|
@@ -821,8 +831,14 @@ function sysctlDropIn(start, { http = false } = {}) {
|
|
|
821
831
|
* configuredPort the port the site's config already sets (gatewayPort), or null.
|
|
822
832
|
* acmeHttp01Port acme.http01Port from the config, or null: monolith.js binds it beside an HTTPS
|
|
823
833
|
* listener, so it counts toward the privileged-port floor.
|
|
834
|
+
* mail the site receives mail: the mail-server plugin listens for SMTP on 25.
|
|
835
|
+
*
|
|
836
|
+
* The floor goes as low as every port the site will bind, not only the ones configured today: the
|
|
837
|
+
* administrator turns on Let's Encrypt (HTTP-01 on 80) and installs the mail server from the panel,
|
|
838
|
+
* and neither may then need a file edited as root. An HTTPS site already on a port below 1024 counts
|
|
839
|
+
* 80 for that reason; a site on a high port gets no drop-in (it sits behind a proxy, or wants none).
|
|
824
840
|
*/
|
|
825
|
-
function buildSystemdFiles({ installDir, nodePath, user = DEFAULT_SERVICE_USER, port = null, configuredPort = null, acmeHttp01Port = null, http = false }) {
|
|
841
|
+
function buildSystemdFiles({ installDir, nodePath, user = DEFAULT_SERVICE_USER, port = null, configuredPort = null, acmeHttp01Port = null, http = false, mail = false }) {
|
|
826
842
|
for (const [what, p] of [['site directory', installDir], ['node binary', nodePath]]) {
|
|
827
843
|
if (!unitSafePath(p)) {
|
|
828
844
|
const err = new Error(`The ${what} path "${p}" cannot go into a systemd unit as is: it must be absolute, without spaces, quotes, backslashes, "%" or "$".`);
|
|
@@ -836,10 +852,14 @@ function buildSystemdFiles({ installDir, nodePath, user = DEFAULT_SERVICE_USER,
|
|
|
836
852
|
if (problem) { const err = new Error(problem.message); err.hint = problem.hint; throw err; }
|
|
837
853
|
|
|
838
854
|
const publicPort = port || configuredPort || 3000;
|
|
839
|
-
const bound = [publicPort];
|
|
840
|
-
if (!http && acmeHttp01Port) bound.push(acmeHttp01Port);
|
|
841
|
-
|
|
842
|
-
|
|
855
|
+
const bound = [{ port: publicPort, why: 'the site (its public port)' }];
|
|
856
|
+
if (!http && acmeHttp01Port) bound.push({ port: acmeHttp01Port, why: "Let's Encrypt HTTP-01 (acme.http01Port)" });
|
|
857
|
+
if (!http && publicPort < PRIVILEGED_PORT_LIMIT && !bound.some((b) => b.port === 80)) {
|
|
858
|
+
bound.push({ port: 80, why: "Let's Encrypt HTTP-01, when it is turned on in Admin -> Security" });
|
|
859
|
+
}
|
|
860
|
+
if (mail) bound.push({ port: 25, why: 'incoming mail (the mail-server plugin, SMTP)' });
|
|
861
|
+
const privileged = bound.filter((b) => b.port < PRIVILEGED_PORT_LIMIT).sort((a, b) => a.port - b.port);
|
|
862
|
+
const sysctlStart = privileged.length ? privileged[0].port : null;
|
|
843
863
|
|
|
844
864
|
// Two hardening directives depend on WHERE things are, and each is dropped (with a warning) rather
|
|
845
865
|
// than shipped in a shape that cannot start: ProtectHome= hides /home, /root and /run/user, which
|
|
@@ -915,7 +935,9 @@ function buildSystemdFiles({ installDir, nodePath, user = DEFAULT_SERVICE_USER,
|
|
|
915
935
|
|
|
916
936
|
return {
|
|
917
937
|
unit,
|
|
918
|
-
sysctl: sysctlStart
|
|
938
|
+
sysctl: sysctlStart
|
|
939
|
+
? { start: sysctlStart, ports: privileged.map((b) => b.port), content: sysctlDropIn(sysctlStart, { http, mail, ports: privileged }) }
|
|
940
|
+
: null,
|
|
919
941
|
publicPort,
|
|
920
942
|
warnings,
|
|
921
943
|
};
|
|
@@ -937,7 +959,7 @@ function systemdInstallSteps({ installDir, user = DEFAULT_SERVICE_USER, built, h
|
|
|
937
959
|
// `install -o root -g root -m 0644` and not `cp`: root takes the staged file's CONTENT into a file it
|
|
938
960
|
// creates itself, root-owned and not writable by anyone else, whatever the staged copy's mode was.
|
|
939
961
|
if (built.sysctl) {
|
|
940
|
-
step(`Let it bind port ${built.
|
|
962
|
+
step(`Let it bind ${built.sysctl.ports.length > 1 ? `ports ${built.sysctl.ports.join(', ')}` : `port ${built.sysctl.start}`} without a capability (read the trade-off in the file first):`,
|
|
941
963
|
`sudo install -o root -g root -m 0644 ${staged(SYSCTL_DROPIN_FILE)} /etc/sysctl.d/${SYSCTL_DROPIN_FILE} && sudo sysctl --system`);
|
|
942
964
|
} else if (upgrade) {
|
|
943
965
|
// A site that moved to a high port (behind a proxy, say) no longer needs the floor lowered, and an
|
|
@@ -1046,12 +1068,14 @@ function emitSystemd(installDir, opts) {
|
|
|
1046
1068
|
configuredPort: Number(appConfig.gatewayPort || gwConfig.gatewayPort) || null,
|
|
1047
1069
|
acmeHttp01Port: Number(appConfig.acme && appConfig.acme.http01Port) || null,
|
|
1048
1070
|
http: opts.http,
|
|
1071
|
+
// An installed mail server (an upgrade) receives on 25 whether or not --mail was given.
|
|
1072
|
+
mail: !!opts.mail || fs.existsSync(path.join(installDir, 'backend', 'plugins', 'mail-server')),
|
|
1049
1073
|
});
|
|
1050
1074
|
const stageDir = createSystemdStagingDir();
|
|
1051
1075
|
const written = writeSystemdFiles(stageDir, built);
|
|
1052
1076
|
console.log('');
|
|
1053
1077
|
console.log(` systemd unit staged: ${written.unitPath}`);
|
|
1054
|
-
if (written.sysctlPath) console.log(` sysctl drop-in for
|
|
1078
|
+
if (written.sysctlPath) console.log(` sysctl drop-in for ports from ${built.sysctl.start} up (${built.sysctl.ports.join(', ')}): ${written.sysctlPath}`);
|
|
1055
1079
|
console.log(' (staged outside the site on purpose: the service account will own the site, and must not be able to edit what root installs)');
|
|
1056
1080
|
for (const w of built.warnings) console.log(` ⚠️ ${w}`);
|
|
1057
1081
|
console.log('');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-wordjs",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Create a WordJS site with one command — the self-hosted CMS where third-party plugins run in an OS-isolated process with per-capability permission grants. SSR/SEO out of the box, SQLite by default, no PHP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jaime Martinez (https://github.com/jaimemartinez)",
|