Passed
Push — develop ( ab926b...704199 )
by Nikolay
13:25 queued 08:47
created

SSHConf::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2024 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System\Configs;
21
22
23
use MikoPBX\Common\Models\PbxSettings;
24
use MikoPBX\Common\Models\PbxSettingsConstants;
25
use MikoPBX\Core\System\MikoPBXConfig;
26
use MikoPBX\Core\System\Processes;
27
use MikoPBX\Core\System\Util;
28
use Phalcon\Di\Injectable;
29
30
/**
31
 * SSH Configuration Management Class.
32
 *
33
 * Manages SSH configurations including password setup, service restarts, and key management.
34
 *
35
 * @package MikoPBX\Core\System\Configs
36
 */
37
class SSHConf extends Injectable
38
{
39
    // Client keep-alive interval in seconds
40
    private const CLIENT_KEEP_ALIVE_INTERVAL = 60;
41
42
    // Client idle timeout in seconds
43
    private const CLIENT_IDLE_TIMEOUT = 1800;
44
45
46
    private MikoPBXConfig $mikoPBXConfig;
47
48
    /**
49
     * Constructor initializing MikoPBX configuration.
50
     */
51
    public function __construct()
52
    {
53
        $this->mikoPBXConfig = new MikoPBXConfig();
54
    }
55
56
    /**
57
     * Configures SSH settings based on current system settings.
58
     *
59
     * @return bool Returns true if configuration is successful, false otherwise.
60
     */
61
    public function configure(): bool
62
    {
63
        $lofFile = '/var/log/lastlog';
64
        if (!file_exists($lofFile)) {
65
            file_put_contents($lofFile, '');
66
        }
67
        $this->generateDropbearKeys();
68
        $sshLogin = $this->getCreateSshUser();
69
        $sshPort  = escapeshellcmd(PbxSettings::getValueByKey(PbxSettingsConstants::SSH_PORT));
70
71
        // Update root password and restart SSH server
72
        $this->updateShellPassword($sshLogin);
73
74
        // Killing existing Dropbear processes before restart
75
        Processes::killByName('dropbear');
76
        usleep(500000); // Delay to ensure process has stopped
77
78
        $sshPasswordDisabled = PbxSettings::getValueByKey(PbxSettingsConstants::SSH_DISABLE_SSH_PASSWORD) === '1';
79
        $options = $sshPasswordDisabled ? '-s' : '';
80
        $dropbear = Util::which('dropbear');
81
82
        // Adding keep-alive and idle timeout options to Dropbear configuration
83
        $command = sprintf(
84
            "%s -p '%s' %s -K %d -I %d -c /etc/rc/hello > /var/log/dropbear_start.log",
85
            $dropbear,
86
            $sshPort,
87
            $options,
88
            self::CLIENT_KEEP_ALIVE_INTERVAL,
89
            self::CLIENT_IDLE_TIMEOUT
90
        );
91
        $result = Processes::mwExec($command);
92
93
        $this->generateAuthorizedKeys($sshLogin);
94
        $this->fixRights($sshLogin);
95
        return ($result === 0);
96
    }
97
98
    /**
99
     * Generates or retrieves SSH keys, handling their storage and retrieval.
100
     *
101
     * @return void
102
     */
103
    private function generateDropbearKeys(): void
104
    {
105
        $keyTypes = [
106
            "rsa" => PbxSettingsConstants::SSH_RSA_KEY,
107
            "dss" => PbxSettingsConstants::SSH_DSS_KEY,
108
            "ecdsa" => PbxSettingsConstants::SSH_ECDSA_KEY,
109
            "ed25519" => PbxSettingsConstants::SSH_ED25519_KEY
110
        ];
111
112
        $dropBearDir = '/etc/dropbear';
113
        Util::mwMkdir($dropBearDir);
114
115
        $dropbearkey = Util::which('dropbearkey');
116
        // Get keys from DB
117
        foreach ($keyTypes as $keyType => $dbKey) {
118
            $resKeyFilePath = "{$dropBearDir}/dropbear_" . $keyType . "_host_key";
119
            $keyValue = trim(PbxSettings::getValueByKey($dbKey));
120
            if (strlen($keyValue) > 100) {
121
                file_put_contents($resKeyFilePath, base64_decode($keyValue));
122
            } elseif (!file_exists($resKeyFilePath)) {
123
                Processes::mwExec("$dropbearkey -t $keyType -f $resKeyFilePath");
124
                $newKey = base64_encode(file_get_contents($resKeyFilePath));
125
                $this->mikoPBXConfig->setGeneralSettings($dbKey, $newKey);
126
            }
127
        }
128
129
    }
130
131
    /**
132
     * Retrieves the designated SSH login username from settings.
133
     *
134
     * @return string SSH login username.
135
     */
136
    private function getCreateSshUser(): string
137
    {
138
        $cat = Util::which('cat');
139
        $cut = Util::which('cut');
0 ignored issues
show
Unused Code introduced by
The assignment to $cut is dead and can be removed.
Loading history...
140
        $sed = Util::which('sed');
0 ignored issues
show
Unused Code introduced by
The assignment to $sed is dead and can be removed.
Loading history...
141
        $chown = Util::which('chown');
142
        $deluser = Util::which('deluser');
143
        $delgroup = Util::which('delgroup');
144
        $addgroup = Util::which('addgroup');
145
        $adduser = Util::which('adduser');
146
147
        $sshLogin = PbxSettings::getValueByKey(PbxSettingsConstants::SSH_LOGIN);
148
        $homeDir = $this->getUserHomeDir($sshLogin);
149
        $mainUsers = ['root', 'www']; // System users that should not be modified
150
151
        // Clean up non-system users
152
        exec("$cat -f 1 -d ':' < /etc/passwd", $systemUsers);
153
        foreach ($systemUsers as $user){
154
            if($sshLogin === $user || in_array($user, $mainUsers, true)){
155
                continue;
156
            }
157
            // Deleting the user and associated group
158
            shell_exec("$deluser $user");
159
            shell_exec("$delgroup $user");
160
        }
161
162
        // Adding SSH user if not root
163
        if ($sshLogin !== 'root') {
164
            shell_exec("$addgroup $sshLogin");
165
            shell_exec("$adduser -h $homeDir -g 'MikoPBX SSH Admin' -s /bin/bash -G root -D '$sshLogin'");
166
            shell_exec("$addgroup -S '$sshLogin' $sshLogin");
167
            shell_exec("$addgroup -S '$sshLogin' root");
168
            $this->updateUserGroupId($sshLogin);
169
            shell_exec("$chown -R $sshLogin:$sshLogin $homeDir");
170
        }
171
        return $sshLogin;
172
    }
173
174
    /**
175
     * Retrieves or assigns the home directory for a specified username.
176
     *
177
     * @param string $sshLogin SSH login username.
178
     * @return string Home directory path.
179
     */
180
    private function getUserHomeDir(string $sshLogin = 'root'): string
181
    {
182
        $homeDir = ($sshLogin === 'root') ? '/root' : "/home/$sshLogin";
183
        Util::mwMkdir($homeDir);
184
        return $homeDir;
185
    }
186
187
    /**
188
     * Updates the shell password for specified SSH login.
189
     *
190
     * @param string $sshLogin SSH login username.
191
     * @return void
192
     */
193
    private function updateShellPassword(string $sshLogin = 'root'): void
194
    {
195
        $password           = PbxSettings::getValueByKey(PbxSettingsConstants::SSH_PASSWORD);
196
        $hashString         = PbxSettings::getValueByKey(PbxSettingsConstants::SSH_PASSWORD_HASH_STRING);
197
        $disablePassLogin   = PbxSettings::getValueByKey(PbxSettingsConstants::SSH_DISABLE_SSH_PASSWORD);
198
199
        $echo     = Util::which('echo');
200
        $chpasswd = Util::which('chpasswd');
201
        $passwd   = Util::which('passwd');
202
        Processes::mwExec("$passwd -l www");
203
        if ($disablePassLogin === '1') {
204
            Processes::mwExec("$passwd -l $sshLogin");
205
            Processes::mwExec("$passwd -l root");
206
        } elseif($sshLogin === 'root') {
207
            Processes::mwExec("$echo '$sshLogin:$password' | $chpasswd");
208
        } else {
209
            Processes::mwExec("$passwd -l root");
210
            Processes::mwExec("$echo '$sshLogin:$password' | $chpasswd");
211
        }
212
213
        // Security hash check and notification
214
        $currentHash = md5_file('/etc/shadow');
215
        PbxSettings::setValue(PbxSettingsConstants::SSH_PASSWORD_HASH_FILE, $currentHash);
216
        if ($hashString !== md5($password)) {
217
            PbxSettings::setValue(PbxSettingsConstants::SSH_PASSWORD_HASH_STRING, md5($password));
218
        }
219
    }
220
221
    /**
222
     * Generates and stores the authorized_keys based on database settings for a specified SSH login.
223
     *
224
     * @param string $sshLogin SSH login username.
225
     * @return void
226
     */
227
    private function generateAuthorizedKeys(string $sshLogin = 'root'): void
228
    {
229
        $homeDir = $this->getUserHomeDir($sshLogin);
230
        $sshDir = "$homeDir/.ssh";
231
        Util::mwMkdir($sshDir);
232
233
        $authorizedKeys = PbxSettings::getValueByKey(PbxSettingsConstants::SSH_AUTHORIZED_KEYS);
234
        file_put_contents("$sshDir/authorized_keys", $authorizedKeys);
235
    }
236
237
    /**
238
     * Corrects file permissions and ownership for SSH user directories.
239
     *
240
     * Sets appropriate permissions and ownership on the home directory and
241
     * SSH-related files to secure the environment.
242
     *
243
     * @param string $sshLogin SSH login username.
244
     * @return void
245
     */
246
    private function fixRights(string $sshLogin = 'root'): void
247
    {
248
        $homeDir = $this->getUserHomeDir($sshLogin);
249
        $sshDir = "$homeDir/.ssh";
250
        $authorizedKeysFile = "$sshDir/authorized_keys";
251
252
        // Ensure the SSH directory exists before attempting to modify it
253
        if (!file_exists($sshDir) || !file_exists($authorizedKeysFile)) {
254
            return; // Early exit if essential directories or files are missing
255
        }
256
257
        // Commands used for modifying file permissions and ownership
258
        $chmod = Util::which('chmod');
259
        $chown = Util::which('chown');
260
261
        // Set directory permissions securely
262
        Processes::mwExec("$chmod 700 $homeDir"); // Only the user can access the home directory
263
        Processes::mwExec("$chmod 700 $sshDir");  // Only the user can access the .ssh directory
264
265
        // Set file permissions securely
266
        Processes::mwExec("$chmod 600 $authorizedKeysFile"); // Only the user can read/write authorized_keys
267
268
        // Change ownership to the user for both home and SSH directories
269
        Processes::mwExec("$chown -R $sshLogin:$sshLogin $homeDir");
270
    }
271
272
    /**
273
     * Updates the user group ID in /etc/passwd.
274
     *
275
     * @param string $sshLogin SSH login username.
276
     * @return void
277
     */
278
    private function updateUserGroupId(string $sshLogin): void
279
    {
280
        $cat = Util::which('cat');
281
        $cut = Util::which('cut');
282
        $sed = Util::which('sed');
283
        $chown = Util::which('chown');
0 ignored issues
show
Unused Code introduced by
The assignment to $chown is dead and can be removed.
Loading history...
284
285
        $currentGroupId = trim(shell_exec("$cat /etc/passwd | grep '^$sshLogin:' | $cut -f 3 -d ':'"));
286
        shell_exec("$sed -i 's/$sshLogin:x:$currentGroupId:/$sshLogin:x:0:/g' /etc/passwd");
287
    }
288
}