target = ExportTarget::create([ 'name' => 'Übergabe', 'driver' => ExportTarget::LOCAL, 'path' => '/opt/clupilot/storage/archive', 'layout' => ExportTarget::BY_DAY, 'active' => true, ]); }); it('asks the host for a key, and names the directory it may read', function () { Livewire::actingAs(operator('Owner'), 'operator') ->test(Finance::class) ->call('createPullAccess', $this->target->uuid) ->assertSet('waitingForKey', true); $request = json_decode(File::get(storage_path('app/deploy/update-request.json')), true); expect($request['kind'])->toBe('archive-key') // The path travels with the request: the panel is the only side that // knows which destination the button was pressed on. ->and($request['archive_path'])->toBe('/opt/clupilot/storage/archive'); }); it('refuses for a destination that is not on this host', function () { // Only a directory here has a home directory and an authorized_keys to put // anything into. Anywhere else is somebody else's machine. $remote = ExportTarget::create([ 'name' => 'Anbieter', 'driver' => ExportTarget::SFTP, 'path' => '/rechnungen', 'host' => 'backup.example.com', 'port' => 22, 'username' => 'u1', 'active' => true, ]); Livewire::actingAs(operator('Owner'), 'operator') ->test(Finance::class) ->call('createPullAccess', $remote->uuid) ->assertSet('waitingForKey', false); expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse(); }); it('shows the minted key once and takes it off the disk in the same breath', function () { // A private key that stays on disk after somebody has seen it is a private // key on disk. File::ensureDirectoryExists(storage_path('app/deploy')); File::put(storage_path('app/deploy/archive-key.json'), json_encode([ 'created_at' => now()->toIso8601String(), 'label' => 'clupilot-archiv', 'path' => '/opt/clupilot/storage/archive', 'private_key' => "-----BEGIN OPENSSH PRIVATE KEY-----\nAAAA\n-----END OPENSSH PRIVATE KEY-----", ])); Livewire::actingAs(operator('Owner'), 'operator') ->test(Finance::class) ->set('waitingForKey', true) ->call('collectArchiveKey') ->assertSet('waitingForKey', false) ->assertSee('BEGIN OPENSSH PRIVATE KEY'); expect(File::exists(storage_path('app/deploy/archive-key.json')))->toBeFalse(); }); it('never writes the key into the database', function () { // It is shown to be copied into a NAS and nowhere else. Storing it "for // convenience" would put a working credential into every backup of this // database. File::ensureDirectoryExists(storage_path('app/deploy')); File::put(storage_path('app/deploy/archive-key.json'), json_encode([ 'created_at' => now()->toIso8601String(), 'label' => 'x', 'path' => '/opt/clupilot/storage/archive', 'private_key' => 'GEHEIM-XYZ', ])); Livewire::actingAs(operator('Owner'), 'operator') ->test(Finance::class) ->set('waitingForKey', true) ->call('collectArchiveKey'); foreach (ExportTarget::all() as $target) { expect(json_encode($target->getAttributes()))->not->toContain('GEHEIM-XYZ'); } expect(json_encode(App\Models\ExportTarget::query()->get()->toArray()))->not->toContain('GEHEIM-XYZ'); }); it('locks the key to one directory, read-only, with no shell', function () { // The agent's half, which no PHP test can reach. rrsync rather than pinning // the exact rsync option string: that string differs between rsync versions // and breaks silently, as a refusal with no reason given. $agent = File::get(base_path('deploy/update-agent.sh')); expect($agent)->toContain('archive-key') ->and($agent)->toContain('/usr/bin/rrsync -ro') ->and($agent)->toContain(',restrict ') // Refuses rather than issuing an UNrestricted key when rrsync is absent. ->and($agent)->toContain('rrsync_missing'); // And the installer puts rsync on the host, because sshd starts // `rsync --server` HERE when the NAS connects. expect(File::get(base_path('deploy/install-agent.sh')))->toContain('install -y -qq rsync'); });