fix(ssh): graceful failure if password-disable throws post-switch; document privateKey-on-failure contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>feat/v1-foundation
parent
b847b80ab6
commit
6f8f5dbc2b
|
|
@ -22,6 +22,10 @@ class SshKeyProvisioner
|
|||
) {}
|
||||
|
||||
/**
|
||||
* `privateKey` may be present even on `ok:false` — when password auth failed to disable AFTER the
|
||||
* credential was already switched to the verified key. Callers MUST display it to the operator and
|
||||
* MUST NOT log or persist it (it is the plaintext private key).
|
||||
*
|
||||
* @return array{ok: bool, privateKey?: string, publicKey?: string, error?: string}
|
||||
*/
|
||||
public function enableKeyOnlyAccess(Server $server): array
|
||||
|
|
@ -72,11 +76,20 @@ class SshKeyProvisioner
|
|||
}
|
||||
|
||||
// 4) Disable password auth (the hardening guard passes now: credential is key + key installed).
|
||||
$res = $this->hardening->apply($server, 'ssh_password', false);
|
||||
// The credential is already the verified key, so any failure here still leaves the operator
|
||||
// with working key access — never a lockout.
|
||||
try {
|
||||
$res = $this->hardening->apply($server, 'ssh_password', false);
|
||||
} catch (Throwable $e) {
|
||||
return array_filter([
|
||||
'ok' => false, 'error' => $e->getMessage(), 'privateKey' => $privateKey, 'publicKey' => $publicKey,
|
||||
], fn ($v) => $v !== null);
|
||||
}
|
||||
|
||||
if (! $res['ok']) {
|
||||
// The credential is already switched, so key access remains — just report the failure.
|
||||
return ['ok' => false, 'error' => $res['output'], 'privateKey' => $privateKey, 'publicKey' => $publicKey];
|
||||
return array_filter([
|
||||
'ok' => false, 'error' => $res['output'], 'privateKey' => $privateKey, 'publicKey' => $publicKey,
|
||||
], fn ($v) => $v !== null);
|
||||
}
|
||||
|
||||
return array_filter([
|
||||
|
|
|
|||
|
|
@ -102,4 +102,23 @@ class SshKeyProvisionerTest extends TestCase
|
|||
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function test_apply_failure_after_switch_keeps_key_credential_and_returns_the_private_key(): void
|
||||
{
|
||||
$server = $this->passwordServer();
|
||||
|
||||
$fleet = Mockery::mock(FleetService::class);
|
||||
$fleet->shouldReceive('addAuthorizedKey')->once();
|
||||
$fleet->shouldReceive('testConnection')->once()->andReturn(['ok' => true, 'error' => null]);
|
||||
|
||||
$hardening = Mockery::mock(HardeningService::class);
|
||||
$hardening->shouldReceive('apply')->once()->andReturn(['ok' => false, 'output' => 'sshd reload failed']);
|
||||
|
||||
$result = (new SshKeyProvisioner($fleet, $hardening))->enableKeyOnlyAccess($server);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['privateKey']); // UI can still reveal it
|
||||
// Credential stays the verified key — operator keeps access (NOT rolled back).
|
||||
$this->assertSame('key', $server->fresh('credential')->credential->auth_type);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue