underpost 3.2.90 → 3.3.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.
Files changed (71) hide show
  1. package/.github/workflows/ghpkg.ci.yml +7 -1
  2. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  3. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  4. package/.github/workflows/release.cd.yml +1 -9
  5. package/CHANGELOG.md +110 -1
  6. package/CLI-HELP.md +139 -9
  7. package/README.md +5 -2
  8. package/bin/build.js +7 -5
  9. package/bin/deploy.js +1 -1
  10. package/deploy/lib/logging.sh +96 -0
  11. package/deploy/pwa-microservices-template/deploy.sh +72 -0
  12. package/deploy/release/deploy.sh +62 -0
  13. package/docker-compose.yml +1 -1
  14. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
  15. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  16. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/package.json +5 -5
  19. package/scripts/audit-selinux.sh +64 -0
  20. package/scripts/coverall-test.sh +24 -0
  21. package/scripts/gpu-diag.sh +0 -0
  22. package/scripts/ip-info.sh +0 -0
  23. package/scripts/k3s-node-setup.sh +18 -15
  24. package/scripts/kubeadm-node-setup.sh +12 -23
  25. package/scripts/link-local-underpost-cli.sh +0 -0
  26. package/scripts/lxd-vm-setup.sh +0 -0
  27. package/scripts/maas-nat-firewalld.sh +0 -0
  28. package/scripts/nat-iptables.sh +2 -0
  29. package/scripts/rhel-grpc-setup.sh +0 -0
  30. package/scripts/rocky-kickstart.sh +25 -9
  31. package/scripts/test-monitor.sh +1 -1
  32. package/src/cli/baremetal.js +1 -2
  33. package/src/cli/cloud-init.js +1 -1
  34. package/src/cli/cluster.js +73 -68
  35. package/src/cli/db.js +9 -2
  36. package/src/cli/deploy.js +21 -5
  37. package/src/cli/docker-compose.js +1 -1
  38. package/src/cli/env.js +1 -1
  39. package/src/cli/image.js +0 -1
  40. package/src/cli/index.js +121 -9
  41. package/src/cli/lxd.js +1 -1
  42. package/src/cli/monitor.js +1 -1
  43. package/src/cli/release.js +57 -22
  44. package/src/cli/repository.js +11 -9
  45. package/src/cli/run.js +36 -9
  46. package/src/cli/ssh.js +198 -77
  47. package/src/cli/system.js +26 -13
  48. package/src/cli/test.js +1 -1
  49. package/src/cli/vultr.js +583 -0
  50. package/src/cli/wireguard.js +2125 -0
  51. package/src/client-builder/client-build.js +20 -14
  52. package/src/db/mongo/MongooseDB.js +4 -0
  53. package/src/index.js +25 -1
  54. package/src/projects/underpost/catalog-underpost.js +4 -1
  55. package/src/server/backup.js +1 -1
  56. package/src/server/conf.js +18 -108
  57. package/src/server/cron.js +249 -51
  58. package/src/server/dns.js +100 -6
  59. package/src/server/environment.js +98 -0
  60. package/src/server/forward-proxy.js +549 -0
  61. package/src/server/middlewares.js +56 -1
  62. package/src/server/process.js +0 -1
  63. package/src/server/selinux.js +185 -0
  64. package/src/server/systemd.js +205 -0
  65. package/src/server/underpost-compression.js +186 -0
  66. package/src/server/underpost-gateway.js +20 -10
  67. package/src/server/underpost-ingress.js +18 -2
  68. package/test/selinux.test.js +71 -0
  69. package/test/underpost-gateway.test.js +41 -0
  70. package/test/underpost-ingress.test.js +52 -0
  71. package/test/wireguard-edge.test.js +1177 -0
package/src/cli/ssh.js CHANGED
@@ -8,6 +8,14 @@ import { generateRandomPasswordSelection } from '../client/components/core/Commo
8
8
  import { pbcopy, shellExec } from '../server/process.js';
9
9
  import { loggerFactory } from '../server/logger.js';
10
10
  import { waitForPort } from '../server/conf.js';
11
+ import {
12
+ runSELinuxCommands,
13
+ selinuxPackagesCommandFactory,
14
+ selinuxRestoreconCommandFactory,
15
+ selinuxSshContextCommandsFactory,
16
+ selinuxSshPortCommandsFactory,
17
+ shellArgumentFactory,
18
+ } from '../server/selinux.js';
11
19
  import fs from 'fs-extra';
12
20
  import Underpost from '../index.js';
13
21
 
@@ -57,7 +65,7 @@ class UnderpostSSH {
57
65
  * @returns {boolean} True if user exists, false otherwise
58
66
  */
59
67
  checkUserExists: (username) => {
60
- const result = shellExec(`id -u ${username} 2>/dev/null || echo "not_found"`, {
68
+ const result = shellExec(`id -u ${shellArgumentFactory(username)} 2>/dev/null || echo "not_found"`, {
61
69
  silent: true,
62
70
  stdout: true,
63
71
  }).trim();
@@ -73,7 +81,7 @@ class UnderpostSSH {
73
81
  * @returns {string} User's home directory path
74
82
  */
75
83
  getUserHome: (username) => {
76
- return shellExec(`getent passwd ${username} | cut -d: -f6`, {
84
+ return shellExec(`getent passwd ${shellArgumentFactory(username)} | cut -d: -f6`, {
77
85
  silent: true,
78
86
  stdout: true,
79
87
  }).trim();
@@ -90,11 +98,13 @@ class UnderpostSSH {
90
98
  * @returns {void}
91
99
  */
92
100
  createSystemUser: (username, password, groups) => {
93
- shellExec(`useradd -m -s /bin/bash ${username}`);
94
- shellExec(`echo "${username}:${password}" | chpasswd`);
101
+ if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(username)) throw new TypeError('Invalid system username');
102
+ shellExec(`useradd -m -s /bin/bash ${shellArgumentFactory(username)}`);
103
+ shellExec(`printf '%s\n' ${shellArgumentFactory(`${username}:${password}`)} | chpasswd`, { disableLog: true });
95
104
  if (groups) {
96
105
  for (const group of groups.split(',').map((g) => g.trim())) {
97
- shellExec(`usermod -aG "${group}" "${username}"`);
106
+ if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(group)) throw new TypeError('Invalid system group');
107
+ shellExec(`usermod -aG ${shellArgumentFactory(group)} ${shellArgumentFactory(username)}`);
98
108
  }
99
109
  }
100
110
  },
@@ -109,8 +119,8 @@ class UnderpostSSH {
109
119
  */
110
120
  ensureSSHDirectory: (sshDir) => {
111
121
  if (!fs.existsSync(sshDir)) {
112
- shellExec(`mkdir -p ${sshDir}`);
113
- shellExec(`chmod 700 ${sshDir}`);
122
+ shellExec(`mkdir -p ${shellArgumentFactory(sshDir)}`);
123
+ shellExec(`chmod 700 ${shellArgumentFactory(sshDir)}`);
114
124
  }
115
125
  },
116
126
 
@@ -126,11 +136,12 @@ class UnderpostSSH {
126
136
  * @returns {void}
127
137
  */
128
138
  setSSHFilePermissions: (sshDir, username, keyPath, pubKeyPath) => {
129
- shellExec(`chmod 600 ${sshDir}/authorized_keys`);
130
- shellExec(`chmod 644 ${sshDir}/known_hosts`);
131
- if (keyPath) shellExec(`chmod 600 ${keyPath}`);
132
- if (pubKeyPath) shellExec(`chmod 644 ${pubKeyPath}`);
133
- shellExec(`chown -R ${username}:${username} ${sshDir}`);
139
+ shellExec(`chmod 600 ${shellArgumentFactory(`${sshDir}/authorized_keys`)}`);
140
+ shellExec(`chmod 644 ${shellArgumentFactory(`${sshDir}/known_hosts`)}`);
141
+ if (keyPath) shellExec(`chmod 600 ${shellArgumentFactory(keyPath)}`);
142
+ if (pubKeyPath) shellExec(`chmod 644 ${shellArgumentFactory(pubKeyPath)}`);
143
+ shellExec(`chown -R ${shellArgumentFactory(`${username}:${username}`)} ${shellArgumentFactory(sshDir)}`);
144
+ runSELinuxCommands(selinuxSshContextCommandsFactory({ sshDirectory: sshDir }), { execute: shellExec });
134
145
  },
135
146
 
136
147
  /**
@@ -144,13 +155,12 @@ class UnderpostSSH {
144
155
  * @returns {void}
145
156
  */
146
157
  configureAuthorizedKeys: (sshDir, pubKeyPath, disablePassword) => {
147
- if (disablePassword) {
148
- shellExec(`cat >> ${sshDir}/authorized_keys <<EOF
149
- no-port-forwarding,no-X11-forwarding,no-agent-forwarding ${fs.readFileSync(pubKeyPath, 'utf8')}
150
- EOF`);
151
- } else {
152
- shellExec(`cat ${pubKeyPath} >> ${sshDir}/authorized_keys`);
153
- }
158
+ const key = fs.readFileSync(pubKeyPath, 'utf8').trim();
159
+ const entry = disablePassword ? `no-port-forwarding,no-X11-forwarding,no-agent-forwarding ${key}` : key;
160
+ const authorizedKeysPath = `${sshDir}/authorized_keys`;
161
+ shellExec(
162
+ `grep -qxF ${shellArgumentFactory(entry)} ${shellArgumentFactory(authorizedKeysPath)} || printf '%s\n' ${shellArgumentFactory(entry)} >> ${shellArgumentFactory(authorizedKeysPath)}`,
163
+ );
154
164
  },
155
165
 
156
166
  /**
@@ -164,9 +174,13 @@ EOF`);
164
174
  * @returns {void}
165
175
  */
166
176
  configureKnownHosts: (sshDir, port, host) => {
167
- shellExec(`ssh-keyscan -p ${port} -H localhost >> ${sshDir}/known_hosts`);
168
- shellExec(`ssh-keyscan -p ${port} -H 127.0.0.1 >> ${sshDir}/known_hosts`);
169
- if (host) shellExec(`ssh-keyscan -p ${port} -H ${host} >> ${sshDir}/known_hosts`);
177
+ const sshPort = Number(port);
178
+ if (!Number.isInteger(sshPort) || sshPort < 1 || sshPort > 65535)
179
+ throw new RangeError('SSH port must be 1-65535');
180
+ const knownHostsPath = shellArgumentFactory(`${sshDir}/known_hosts`);
181
+ shellExec(`ssh-keyscan -p ${sshPort} -H localhost >> ${knownHostsPath}`);
182
+ shellExec(`ssh-keyscan -p ${sshPort} -H 127.0.0.1 >> ${knownHostsPath}`);
183
+ if (host) shellExec(`ssh-keyscan -p ${sshPort} -H ${shellArgumentFactory(host)} >> ${knownHostsPath}`);
170
184
  },
171
185
 
172
186
  /**
@@ -181,10 +195,78 @@ EOF`);
181
195
  */
182
196
  configureSudoAccess: (username, password, disablePassword) => {
183
197
  if (disablePassword) {
184
- shellExec(`echo '${username} ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/90_${username}`);
198
+ if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(username)) throw new TypeError('Invalid sudo username');
199
+ const sudoersPath = `/etc/sudoers.d/90_${username}`;
200
+ const temporaryPath = `/tmp/underpost-sudoers-${process.pid}-${username}`;
201
+ const rule = `${username} ALL=(ALL) NOPASSWD: ALL`;
202
+ try {
203
+ shellExec(`printf '%s\n' ${shellArgumentFactory(rule)} > ${shellArgumentFactory(temporaryPath)}`);
204
+ shellExec(`sudo visudo -cf ${shellArgumentFactory(temporaryPath)}`);
205
+ shellExec(
206
+ `sudo install -m 0440 -o root -g root ${shellArgumentFactory(temporaryPath)} ${shellArgumentFactory(sudoersPath)}`,
207
+ );
208
+ runSELinuxCommands([selinuxRestoreconCommandFactory(sudoersPath, { recursive: false })], {
209
+ execute: shellExec,
210
+ });
211
+ } finally {
212
+ fs.removeSync(temporaryPath);
213
+ }
185
214
  } else {
186
- shellExec(`echo "${username}:${password}" | sudo chpasswd`);
215
+ shellExec(`printf '%s\n' ${shellArgumentFactory(`${username}:${password}`)} | sudo chpasswd`, {
216
+ disableLog: true,
217
+ });
218
+ }
219
+ },
220
+
221
+ /**
222
+ * Initializes the default SSH files and credentials for a local SSH service.
223
+ * @method
224
+ * @function initializeDefaultSshConfig
225
+ * @memberof UnderpostSSH
226
+ * @param {object} params - SSH configuration values.
227
+ * @param {string} params.userHome - Home directory of the SSH user.
228
+ * @param {string} params.user - SSH user name.
229
+ * @param {string} params.password - SSH key passphrase and user password.
230
+ * @param {string} params.host - Host used in the SSH key comment.
231
+ * @param {number} params.port - SSH port.
232
+ * @param {boolean} params.disablePassword - Whether to configure passwordless sudo.
233
+ * @param {string} [params.controllerPubKeyPath='./engine-private/deploy/id_rsa.pub'] - Preferred controller public key to authorize.
234
+ * @returns {void}
235
+ */
236
+ initializeDefaultSshConfig: ({
237
+ userHome,
238
+ user,
239
+ password,
240
+ host,
241
+ port,
242
+ disablePassword,
243
+ controllerPubKeyPath = './engine-private/deploy/id_rsa.pub',
244
+ }) => {
245
+ const sshDir = `${userHome}/.ssh`;
246
+ const keyPath = `${sshDir}/id_rsa`;
247
+ const pubKeyPath = `${sshDir}/id_rsa.pub`;
248
+
249
+ Underpost.ssh.ensureSSHDirectory(sshDir);
250
+ fs.ensureFileSync(`${sshDir}/authorized_keys`);
251
+ fs.ensureFileSync(`${sshDir}/known_hosts`);
252
+
253
+ if (!fs.existsSync(keyPath)) {
254
+ shellExec(
255
+ `ssh-keygen -t ed25519 -f ${shellArgumentFactory(keyPath)} -N ${shellArgumentFactory(password)} -q -C ${shellArgumentFactory(`${user}@${host}`)}`,
256
+ { disableLog: true },
257
+ );
187
258
  }
259
+ if (!fs.existsSync(pubKeyPath))
260
+ shellExec(
261
+ `ssh-keygen -y -f ${shellArgumentFactory(keyPath)} -P ${shellArgumentFactory(password)} > ${shellArgumentFactory(pubKeyPath)}`,
262
+ { disableLog: true },
263
+ );
264
+
265
+ const authorizedKeyPath = fs.existsSync(controllerPubKeyPath) ? controllerPubKeyPath : pubKeyPath;
266
+ Underpost.ssh.configureAuthorizedKeys(sshDir, authorizedKeyPath, disablePassword);
267
+ Underpost.ssh.configureKnownHosts(sshDir, port, host);
268
+ Underpost.ssh.configureSudoAccess(user, password, disablePassword);
269
+ Underpost.ssh.setSSHFilePermissions(sshDir, user, keyPath, pubKeyPath);
188
270
  },
189
271
 
190
272
  /**
@@ -489,7 +571,44 @@ EOF`);
489
571
 
490
572
  // Handle start server
491
573
  if (options.start) {
492
- Underpost.ssh.chmod({ user: options.user });
574
+ if (!options.deployId) {
575
+ Underpost.ssh.initializeDefaultSshConfig({
576
+ userHome,
577
+ user: options.user,
578
+ password: options.password,
579
+ host: options.host,
580
+ port: options.port,
581
+ disablePassword: options.disablePassword,
582
+ });
583
+ } else {
584
+ // Hardening sshd on a host where the account does not exist produces a
585
+ // daemon that starts cleanly and refuses the only user it was meant to
586
+ // admit. Nothing downstream reports that, so the run has to.
587
+ if (!Underpost.ssh.checkUserExists(options.user))
588
+ throw new Error(
589
+ `[ssh] user '${options.user}' does not exist on this host; ` +
590
+ `run: node bin ssh --deploy-id ${options.deployId} --user ${options.user} --user-add`,
591
+ );
592
+ Underpost.ssh.chmod({ user: options.user });
593
+ // The registry records the key pair this deploy authenticates with, so
594
+ // a box whose authorized_keys was lost converges here rather than
595
+ // needing the key re-issued — which would invalidate every other host
596
+ // holding it.
597
+ const recordedPubKey = confNode?.users?.[options.user]?.publicKeyCopyPath;
598
+ if (recordedPubKey && fs.existsSync(recordedPubKey)) {
599
+ const sshDir = `${userHome}/.ssh`;
600
+ Underpost.ssh.ensureSSHDirectory(sshDir);
601
+ fs.ensureFileSync(`${sshDir}/authorized_keys`);
602
+ Underpost.ssh.configureAuthorizedKeys(sshDir, recordedPubKey, options.disablePassword);
603
+ Underpost.ssh.chmod({ user: options.user });
604
+ logger.info(`Authorized key present for ${options.user}`, { source: recordedPubKey });
605
+ } else if (recordedPubKey) {
606
+ logger.warn('Recorded public key is missing; authorized_keys left untouched', {
607
+ user: options.user,
608
+ expected: recordedPubKey,
609
+ });
610
+ }
611
+ }
493
612
  Underpost.ssh.initService({ port: options.port });
494
613
  }
495
614
 
@@ -848,12 +967,26 @@ EOF
848
967
  * Sets ownership to specified user for ~/.ssh/ and contents.
849
968
  */
850
969
  chmod: ({ user }) => {
851
- shellExec(`sudo chmod 700 ~/.ssh/`);
852
- shellExec(`sudo chmod 600 ~/.ssh/authorized_keys`);
853
- shellExec(`sudo chmod 644 ~/.ssh/known_hosts`);
854
- shellExec(`sudo chmod 600 ~/.ssh/id_rsa`);
970
+ const sshDirectory = `${Underpost.ssh.getUserHome(user)}/.ssh`;
971
+ shellExec(`sudo chmod 700 ${shellArgumentFactory(sshDirectory)}`);
972
+ shellExec(
973
+ `if [ -f ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)} ]; then sudo chmod 600 ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}; fi`,
974
+ );
975
+ shellExec(
976
+ `if [ -f ${shellArgumentFactory(`${sshDirectory}/known_hosts`)} ]; then sudo chmod 644 ${shellArgumentFactory(`${sshDirectory}/known_hosts`)}; fi`,
977
+ );
978
+ shellExec(
979
+ `if [ -f ${shellArgumentFactory(`${sshDirectory}/id_rsa`)} ]; then sudo chmod 600 ${shellArgumentFactory(`${sshDirectory}/id_rsa`)}; fi`,
980
+ );
855
981
  shellExec(`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`);
856
- shellExec(`chown -R ${user}:${user} ~/.ssh`);
982
+ shellExec(`sudo chown -R ${shellArgumentFactory(`${user}:${user}`)} ${shellArgumentFactory(sshDirectory)}`);
983
+ runSELinuxCommands(
984
+ [
985
+ ...selinuxSshContextCommandsFactory({ sshDirectory }),
986
+ selinuxRestoreconCommandFactory('/etc/ssh/ssh_host_ed25519_key', { recursive: false }),
987
+ ],
988
+ { execute: shellExec },
989
+ );
857
990
  },
858
991
 
859
992
  /**
@@ -866,7 +999,7 @@ EOF
866
999
  * @description
867
1000
  * Configures SSH daemon with hardened security settings:
868
1001
  * - Disables password authentication (key-only)
869
- * - Disables root login
1002
+ * - Allows key-only root login
870
1003
  * - Enables ED25519 host key
871
1004
  * - Disables X11 forwarding and TCP forwarding
872
1005
  * - Sets client alive intervals to prevent ghost connections
@@ -878,69 +1011,57 @@ EOF
878
1011
  * - Displays service status with colored output
879
1012
  */
880
1013
  initService: ({ port }) => {
881
- shellExec(
882
- `sudo tee /etc/ssh/sshd_config <<EOF
883
- # ==============================================================
884
- # RHEL Hardened SSHD Configuration
885
- # ==============================================================
886
-
887
- # --- Network Settings ---
888
- Port ${port ? port : '22'}
889
- # Explicitly listen on all interfaces (IPv4 and IPv6)
890
-
891
- # --- Host Keys ---
892
- # ED25519 is the modern standard (Fast & Secure)
1014
+ const sshPort = Number(port || 22);
1015
+ const portCommands = selinuxSshPortCommandsFactory({ port: sshPort });
1016
+ const temporaryPath = `/tmp/underpost-sshd-config-${process.pid}`;
1017
+ const configuration = `Port ${sshPort}
893
1018
  HostKey /etc/ssh/ssh_host_ed25519_key
894
- # RSA is kept for compatibility with older clients (Optional)
895
- # HostKey /etc/ssh/ssh_host_rsa_key
896
-
897
- # --- Logging ---
898
1019
  SyslogFacility AUTHPRIV
899
- # VERBOSE logs the key fingerprint used for login (Audit trail)
900
1020
  LogLevel VERBOSE
901
-
902
- # --- Authentication & Security ---
903
- # STRICTLY KEY-BASED AUTHENTICATION
904
1021
  PubkeyAuthentication yes
905
1022
  PasswordAuthentication no
906
- ChallengeResponseAuthentication no
1023
+ KbdInteractiveAuthentication no
907
1024
  PermitEmptyPasswords no
908
-
909
- # PREVENT ROOT LOGIN
910
- # Administrators should log in as a standard user and use 'sudo'
911
- PermitRootLogin no
912
-
913
- # Security checks on ownership of ~/.ssh/ files
1025
+ PermitRootLogin prohibit-password
914
1026
  StrictModes yes
915
1027
  MaxAuthTries 3
916
1028
  LoginGraceTime 60
917
-
918
- # --- PAM (Pluggable Authentication Modules) ---
919
- # REQUIRED: Must be 'yes' on RHEL for proper session/SELinux handling.
920
- # Since PasswordAuthentication is 'no', PAM will not ask for passwords.
921
1029
  UsePAM yes
922
-
923
- # --- Session & Network Health ---
924
- # Disconnect idle sessions after 5 minutes (300s * 0) to prevent ghost connections
925
1030
  ClientAliveInterval 300
926
1031
  ClientAliveCountMax 0
927
-
928
- # --- Feature Restrictions ---
929
- # Disable GUI forwarding unless explicitly needed
930
1032
  X11Forwarding no
931
- # Disable DNS checks for faster logins (unless you use Host based auth)
932
1033
  UseDNS no
933
- # Disable tunneling unless needed
934
1034
  PermitTunnel no
935
1035
  AllowTcpForwarding no
936
-
937
- # --- Subsystem ---
1036
+ Include /etc/ssh/sshd_config.d/*.conf
938
1037
  Subsystem sftp /usr/libexec/openssh/sftp-server
939
- EOF`,
940
- { disableLog: true },
941
- );
942
- shellExec(`sudo systemctl enable sshd`);
943
- shellExec(`sudo systemctl restart sshd`);
1038
+ `;
1039
+
1040
+ fs.writeFileSync(temporaryPath, configuration, { mode: 0o600 });
1041
+ try {
1042
+ if (sshPort !== 22) {
1043
+ shellExec(
1044
+ `if command -v dnf >/dev/null 2>&1 && ! command -v semanage >/dev/null 2>&1; then ${selinuxPackagesCommandFactory()}; fi`,
1045
+ );
1046
+ }
1047
+ runSELinuxCommands(portCommands, { execute: shellExec });
1048
+ shellExec(`sudo ssh-keygen -A`);
1049
+ shellExec(`sudo /usr/sbin/sshd -t -f ${shellArgumentFactory(temporaryPath)}`);
1050
+ shellExec(`sudo install -m 0600 -o root -g root ${shellArgumentFactory(temporaryPath)} /etc/ssh/sshd_config`);
1051
+ runSELinuxCommands(
1052
+ [selinuxRestoreconCommandFactory(['/etc/ssh/sshd_config', '/etc/ssh/ssh_host_ed25519_key'])],
1053
+ { execute: shellExec },
1054
+ );
1055
+ if (sshPort !== 22) {
1056
+ shellExec(
1057
+ `if command -v firewall-cmd >/dev/null 2>&1 && sudo firewall-cmd --state >/dev/null 2>&1; then sudo firewall-cmd --permanent --add-port=${sshPort}/tcp && sudo firewall-cmd --reload; fi`,
1058
+ );
1059
+ }
1060
+ shellExec(`sudo systemctl enable sshd`);
1061
+ shellExec(`sudo systemctl restart sshd`);
1062
+ } finally {
1063
+ fs.removeSync(temporaryPath);
1064
+ }
944
1065
 
945
1066
  const status = shellExec(`sudo systemctl status sshd`, { silent: true, stdout: true });
946
1067
  if (status.match('running')) console.log(status.replaceAll(`running`, `running`.green));
package/src/cli/system.js CHANGED
@@ -8,6 +8,12 @@
8
8
  */
9
9
 
10
10
  import fs from 'fs-extra';
11
+ import {
12
+ selinuxEnforcingCommandsFactory,
13
+ selinuxPackagesCommandFactory,
14
+ selinuxSshContextCommandsFactory,
15
+ shellArgumentFactory,
16
+ } from '../server/selinux.js';
11
17
 
12
18
  /**
13
19
  * @class UnderpostSystemProvisionig
@@ -196,6 +202,8 @@ logdir /var/log/chrony
196
202
 
197
203
  // Install essential system tools (avoiding duplicates from container packages)
198
204
  `dnf -y install --allowerasing bzip2 openssh-server nano vim-enhanced less openssl-devel git gnupg2 libnsl perl`,
205
+ selinuxPackagesCommandFactory({ sudo: false }),
206
+ ...selinuxEnforcingCommandsFactory({ sudo: false }),
199
207
  `dnf clean all`,
200
208
 
201
209
  // Install Node.js
@@ -218,19 +226,24 @@ logdir /var/log/chrony
218
226
  * @memberof UnderpostSystemProvisionig
219
227
  * @returns {string[]} An array of shell commands.
220
228
  */
221
- user: () => [
222
- `useradd -m -s /bin/bash -G wheel root`, // Create a root user with bash shell and wheel group (sudo on RHEL)
223
- `echo 'root:root' | chpasswd`, // Set a default password for the root user
224
- `mkdir -p /home/root/.ssh`, // Create .ssh directory for authorized keys
225
- // Add the public SSH key to authorized_keys for passwordless login
226
- `echo '${fs.readFileSync(
227
- `/home/dd/engine/engine-private/deploy/id_rsa.pub`,
228
- 'utf8',
229
- )}' > /home/root/.ssh/authorized_keys`,
230
- `chown -R root:root /home/root/.ssh`, // Set ownership for security
231
- `chmod 700 /home/root/.ssh`, // Set permissions for the .ssh directory
232
- `chmod 600 /home/root/.ssh/authorized_keys`, // Set permissions for authorized_keys
233
- ],
229
+ user: ({ username = 'root', publicKeyPath = '/home/dd/engine/engine-private/deploy/id_rsa.pub' } = {}) => {
230
+ if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(username)) throw new TypeError('Invalid Rocky Linux username');
231
+ const home = username === 'root' ? '/root' : `/home/${username}`;
232
+ const sshDirectory = `${home}/.ssh`;
233
+ const publicKey = fs.readFileSync(publicKeyPath, 'utf8').trim();
234
+ const userCommand =
235
+ username === 'root'
236
+ ? `usermod -s /bin/bash root`
237
+ : `id -u ${shellArgumentFactory(username)} >/dev/null 2>&1 || useradd -m -s /bin/bash -G wheel ${shellArgumentFactory(username)}`;
238
+ return [
239
+ userCommand,
240
+ `install -d -m 0700 -o ${shellArgumentFactory(username)} -g ${shellArgumentFactory(username)} ${shellArgumentFactory(sshDirectory)}`,
241
+ `printf '%s\n' ${shellArgumentFactory(publicKey)} > ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}`,
242
+ `chown ${shellArgumentFactory(`${username}:${username}`)} ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}`,
243
+ `chmod 0600 ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}`,
244
+ ...selinuxSshContextCommandsFactory({ sshDirectory, sudo: false }),
245
+ ];
246
+ },
234
247
  /**
235
248
  * @method timezone
236
249
  * @description Generates shell commands for configuring the system timezone on Rocky Linux.
package/src/cli/test.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import fs from 'fs-extra';
8
8
  import { timer } from '../client/components/core/CommonJs.js';
9
9
  import { MariaDB } from '../db/mariadb/MariaDB.js';
10
- import { getNpmRootPath } from '../server/conf.js';
10
+ import { getNpmRootPath } from '../server/environment.js';
11
11
  import { actionInitLog, loggerFactory, setUpInfo } from '../server/logger.js';
12
12
  import { pbcopy, shellExec } from '../server/process.js';
13
13
  import Underpost from '../index.js';