mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-25 19:03:14 +01:00
server:install received the root username and password as positional arguments, visible in ps and /proc/<pid>/cmdline for the whole install, and LbInstallFlow/ProxyInstallFlow saved them in a plaintext bin/install/<id>.json for reinstalls. Any SSH host key was accepted. - The panel writes a 0600 bin/install/<id>.cred and runs `server:install <type> <id> <port> - - … --cred-file=<path>`; the command reads and deletes the file before connecting (InstallCredentials). Only <id>.cred files in bin/install/ are accepted. - <id>.json keeps the non-secret parameters only; reinstall_server now needs root_password in the request. cron:cleanup (MAIN) scrubs root_password from files written by earlier releases. - The node's SSH host key (SHA-1) must match the optional Expected SSH Host Key (hex or `ssh-keygen -l -E sha1` output), else the key stored by the first install (new servers.ssh_hostkey_sha1, migration 026), else it is trusted on first use and stored. - New install-form field and strings in all eight languages; cli-tools docs. Phase 0 stop-gap of the MAIN <-> LB API plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Es8UhyPS8MNjuYTagqfGWi
106 lines
5.4 KiB
PHP
106 lines
5.4 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Domain\Server\InstallCredentials;
|
|
|
|
/**
|
|
* SSH credentials for server:install no longer travel on argv (visible in
|
|
* ps and /proc) or sit in a plaintext bin/install/<id>.json: the panel writes
|
|
* a 0600 <id>.cred that the command reads and deletes. The node's SSH host key
|
|
* is pinned: an expected fingerprint, else the one stored at first install.
|
|
*/
|
|
final class InstallCredentialsTest extends TestCase {
|
|
|
|
private string $rDir;
|
|
|
|
protected function setUp(): void {
|
|
$this->rDir = sys_get_temp_dir() . '/xcvm_install_' . uniqid('', true) . '/';
|
|
mkdir($this->rDir, 0750, true);
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
foreach (glob($this->rDir . '*') ?: [] as $rFile) {
|
|
@unlink($rFile);
|
|
}
|
|
@rmdir($this->rDir);
|
|
}
|
|
|
|
public function testCredFileIsPrivateAndReadOnce(): void {
|
|
$rPath = InstallCredentials::write(7, 'root', "p'a\"ss w0rd", $this->rDir);
|
|
$this->assertSame($this->rDir . '7.cred', $rPath);
|
|
$this->assertSame(0600, fileperms($rPath) & 0777);
|
|
|
|
$this->assertSame(['username' => 'root', 'password' => "p'a\"ss w0rd"], InstallCredentials::consume($rPath, $this->rDir));
|
|
$this->assertFileDoesNotExist($rPath, 'deleted as soon as it is read');
|
|
$this->assertNull(InstallCredentials::consume($rPath, $this->rDir));
|
|
}
|
|
|
|
public function testConsumeOnlyAcceptsCredFilesInTheInstallDir(): void {
|
|
$rOther = sys_get_temp_dir() . '/xcvm_' . uniqid('', true) . '.cred';
|
|
file_put_contents($rOther, json_encode(['u' => 'x', 'p' => 'y']));
|
|
$this->assertNull(InstallCredentials::consume($rOther, $this->rDir), 'outside the install dir');
|
|
$this->assertFileExists($rOther, 'and not deleted either');
|
|
@unlink($rOther);
|
|
|
|
file_put_contents($this->rDir . '7.json', json_encode(['u' => 'x', 'p' => 'y']));
|
|
$this->assertNull(InstallCredentials::consume($this->rDir . '7.json', $this->rDir), 'not a .cred');
|
|
$this->assertNull(InstallCredentials::consume($this->rDir . '../' . basename($this->rDir) . '/nope.cred', $this->rDir));
|
|
}
|
|
|
|
public function testCommandCarriesNoPassword(): void {
|
|
$rCommand = InstallCredentials::command(2, 9, 22, 'root', 'S3cret!pw', ['80', '443', '1'], 'SHA1:' . rtrim(base64_encode(sha1('k', true)), '='));
|
|
$this->assertStringNotContainsString('S3cret!pw', $rCommand);
|
|
$this->assertStringContainsString('server:install 2 9 22 - - 80 443 1 --cred-file=', $rCommand);
|
|
$this->assertStringContainsString('--expect-hostkey=' . sha1('k'), $rCommand);
|
|
$rCred = InstallCredentials::dir() . '9.cred';
|
|
$this->assertFileExists($rCred);
|
|
$this->assertSame('S3cret!pw', InstallCredentials::consume($rCred)['password']);
|
|
}
|
|
|
|
public function testSplitOptions(): void {
|
|
[$rArgs, $rOpts] = InstallCredentials::splitOptions(['2', '9', '22', '-', '-', '--cred-file=/x/9.cred', '80', '--expect-hostkey=ab']);
|
|
$this->assertSame(['2', '9', '22', '-', '-', '80'], $rArgs);
|
|
$this->assertSame(['cred-file' => '/x/9.cred', 'expect-hostkey' => 'ab'], $rOpts);
|
|
}
|
|
|
|
public function testHostKeyFormats(): void {
|
|
$rHex = sha1('hostkey');
|
|
$this->assertSame($rHex, InstallCredentials::normalizeHostKey(strtoupper($rHex)));
|
|
$this->assertSame($rHex, InstallCredentials::normalizeHostKey(implode(':', str_split($rHex, 2))));
|
|
$rKeygen = '256 SHA1:' . rtrim(base64_encode(sha1('hostkey', true)), '=') . ' root@lb (ED25519)';
|
|
$this->assertSame($rHex, InstallCredentials::normalizeHostKey($rKeygen));
|
|
$this->assertNull(InstallCredentials::normalizeHostKey(''));
|
|
$this->assertNull(InstallCredentials::normalizeHostKey('SHA256:abcdef'));
|
|
$this->assertNull(InstallCredentials::normalizeHostKey('abc'));
|
|
}
|
|
|
|
public function testHostKeyPinning(): void {
|
|
$rA = sha1('a');
|
|
$rB = sha1('b');
|
|
$this->assertNull(InstallCredentials::checkHostKey($rA, null, null), 'first install: trust on first use');
|
|
$this->assertNull(InstallCredentials::checkHostKey($rA, null, $rA), 'reinstall, same key');
|
|
$this->assertNotNull(InstallCredentials::checkHostKey($rB, null, $rA), 'reinstall, key changed');
|
|
$this->assertNull(InstallCredentials::checkHostKey($rB, $rB, $rA), 'admin supplied the rebuilt node\'s key');
|
|
$this->assertNotNull(InstallCredentials::checkHostKey($rA, $rB, null), 'expected key not presented');
|
|
$this->assertNotNull(InstallCredentials::checkHostKey('', null, null), 'no key at all');
|
|
}
|
|
|
|
public function testLegacyMetadataIsScrubbed(): void {
|
|
file_put_contents($this->rDir . '3.json', json_encode(['root_username' => 'root', 'root_password' => 'old', 'ssh_port' => 22]));
|
|
file_put_contents($this->rDir . '4.json', json_encode(['root_username' => 'root', 'ssh_port' => 22]));
|
|
$this->assertSame(1, InstallCredentials::scrubLegacyMetadata($this->rDir));
|
|
$this->assertSame(['root_username' => 'root', 'ssh_port' => 22], json_decode(file_get_contents($this->rDir . '3.json'), true));
|
|
}
|
|
|
|
public function testNothingWritesThePasswordToMetadataOrArgv(): void {
|
|
$rRoot = dirname(__DIR__, 2) . '/src/';
|
|
foreach (['Cli/Commands/LbInstallFlow.php', 'Cli/Commands/ProxyInstallFlow.php'] as $rFile) {
|
|
$this->assertStringNotContainsString("'root_password' =>", (string) file_get_contents($rRoot . $rFile), $rFile);
|
|
}
|
|
foreach (['Domain/Server/ServerService.php', 'Public/Controllers/Admin/Ajax/ServerAjaxController.php'] as $rFile) {
|
|
$this->assertStringNotContainsString("escapeshellarg(\$rData['root_password'])", (string) file_get_contents($rRoot . $rFile), $rFile);
|
|
$this->assertStringNotContainsString("escapeshellarg(\$rParams['root_password'])", (string) file_get_contents($rRoot . $rFile), $rFile);
|
|
}
|
|
}
|
|
}
|