Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/src/Controller/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,24 @@ private function getActionsOverviewContainerVars(): array
Task::STATUS_FINISHED => 'Set Browser Dev Environment password',
Task::STATUS_ERROR => 'Failed to set Browser Dev Environment password',
],
'generate_ssh_key' => [
Task::STATUS_CREATED => 'Waiting to generate SSH key pair',
Task::STATUS_EXECUTING => 'Generating SSH key pair',
Task::STATUS_FINISHED => 'Generated SSH key pair',
Task::STATUS_ERROR => 'Failed to generate SSH key pair',
],
'revoke_ssh_key' => [
Task::STATUS_CREATED => 'Waiting to revoke SSH key pair',
Task::STATUS_EXECUTING => 'Revoking SSH key pair',
Task::STATUS_FINISHED => 'Revoked SSH key pair',
Task::STATUS_ERROR => 'Failed to revoke SSH key pair',
],
'get_ssh_key' => [
Task::STATUS_CREATED => 'Waiting to read SSH key pair',
Task::STATUS_EXECUTING => 'Reading SSH key pair',
Task::STATUS_FINISHED => 'Downloaded SSH private key',
Task::STATUS_ERROR => 'Failed to read SSH key pair',
],
];

$unknown_action_descriptions = [
Expand All @@ -281,6 +299,9 @@ private function getActionsOverviewContainerVars(): array
'disable_public_dashboard' => 'ui-04',
'activate_browserdev' => 'button-play',
'deactivate_browserdev' => 'button-pause',
'generate_ssh_key' => 'key-25',
'revoke_ssh_key' => 'key-25',
'get_ssh_key' => 'key-25',
'unknown_action' => 'ui-04',
];

Expand Down
30 changes: 30 additions & 0 deletions src/src/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class SettingsController extends BaseController
'enable_public_dashboard' => 'Enabling Online Access',
'disable_public_dashboard' => 'Disabling Online Access',
'update_system' => 'Updating System',
'generate_ssh_key' => 'Generating SSH Key',
'get_ssh_key' => 'Getting SSH Key',
'revoke_ssh_key' => 'Revoking SSH Key',
];

/**
Expand All @@ -55,6 +58,9 @@ class SettingsController extends BaseController
'enable_public_dashboard' => 'createEnablePublicDashboardTask',
'disable_public_dashboard' => 'createDisablePublicDashboardTask',
'update_system' => 'createApplyUpdatesTask',
'generate_ssh_key' => 'createGenerateSshKeyTask',
'get_ssh_key' => 'createGetSshKeyTask',
'revoke_ssh_key' => 'createRevokeSshKeyTask',
];

public function __construct(
Expand Down Expand Up @@ -317,6 +323,9 @@ public function index(Request $request): Response

$browserdev_status = $this->browserDevHelper->getBrowserDevStatus(true);

$ssh_public_key = $this->optionRepository->findOneBy(['name' => 'SSH_PUBLIC_KEY']) ?? new Option();
$ssh_fingerprint = $this->optionRepository->findOneBy(['name' => 'SSH_KEY_FINGERPRINT']) ?? new Option();

return $this->render('pages/settings/index.html.twig', [
'controller_title' => 'Settings',
'controller_subtitle' => 'Features & Security',
Expand All @@ -340,6 +349,8 @@ public function index(Request $request): Response
'dashboard_settings' => $this->dashboardHelper->getSettings(),
'updates_status' => $updates_status,
'browserdev_status' => $browserdev_status,
'ssh_public_key' => $ssh_public_key->getValue(),
'ssh_fingerprint' => $ssh_fingerprint->getValue(),
]);
}

Expand All @@ -356,6 +367,25 @@ public function logout(): Response
return $this->redirectToRoute('settings');
}

#[Route('/settings/download/private_key', name: 'settings_download_private_key')]
public function downloadPrivateKey(): Response
{
$privateKey = $this->optionRepository->findOneBy(['name' => 'SSH_PRIVATE_KEY']);

if (null === $privateKey || empty($privateKey->getValue())) {
// No private key available. Redirect to settings with an alert.
return $this->redirectToRoute('settings');
}

$keyContent = $privateKey->getValue();
$response = new Response($keyContent);
$response->headers->set('Content-Type', 'application/octet-stream');
$response->headers->set('Content-Disposition', 'attachment; filename="id_ed25519"');
$response->headers->set('Content-Length', (string) strlen($keyContent));

return $response;
}

#[Route('/settings/{action}', name: 'settings_action')]
public function action(string $action): Response
{
Expand Down
27 changes: 27 additions & 0 deletions src/src/Factory/TaskFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class TaskFactory
public const APPLY_UPDATES = 'apply_updates';
public const ENABLE_BROWSERDEV = 'activate_browserdev';
public const DISABLE_BROWSERDEV = 'deactivate_browserdev';
public const GENERATE_SSH_KEY = 'generate_ssh_key';
public const GET_SSH_KEY = 'get_ssh_key';
public const REVOKE_SSH_KEY = 'revoke_ssh_key';
public const SET_BROWSERDEV_PASSWORD = 'set_browserdev_password';

private OptionRepository $optionRepository;
Expand Down Expand Up @@ -299,4 +302,28 @@ public function createSetBrowserDevPasswordTask(string $password): Task

return $task;
}

public function createGenerateSshKeyTask(): Task
{
$task = new Task();
$task->setTask(self::GENERATE_SSH_KEY);

return $task;
}

public function createGetSshKeyTask(): Task
{
$task = new Task();
$task->setTask(self::GET_SSH_KEY);

return $task;
}

public function createRevokeSshKeyTask(): Task
{
$task = new Task();
$task->setTask(self::REVOKE_SSH_KEY);

return $task;
}
}
9 changes: 9 additions & 0 deletions src/templates/pages/settings/action.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
{% if action == "update_system" %}
<h2>Updating Edgebox</h2><p>This can take a minute...</p>
{% endif %}
{% if action == "generate_ssh_key" %}
<h2>Generating SSH Key Pair</h2><p>Generating a new ED25519 key pair...</p>
{% endif %}
{% if action == "revoke_ssh_key" %}
<h2>Revoking SSH Key Pair</h2><p>Removing the SSH key pair from this Edgebox...</p>
{% endif %}
{% if action == "get_ssh_key" %}
<h2>Fetching SSH Key</h2><p>Reading the SSH key pair...</p>
{% endif %}
</b></p>

<script>
Expand Down
114 changes: 114 additions & 0 deletions src/templates/pages/settings/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,119 @@
</p>
{% endblock %}
{% endembed %}
{% embed 'blocks/_collapse_card.html.twig' with {
'title': 'SSH Key Access',
'collapse_id': 'collapseSshKeyOptions',
'is_collapsed': not ssh_public_key or app.request.query.get('option_changed') != 'ssh_key',
} %}
{% block card_header %}
<p class="text-sm opacity-8">
<b>Connect to this Edgebox via SSH and manage your instance from a terminal.</b> Generate an ED25519 key pair, download the private key, and use any SSH client to log in as root. Ideal for advanced administration, debugging, and scripting that goes beyond what the web dashboard offers.
</p>
{% endblock %}
{% block card_content %}
{% if ssh_public_key %}
<div class="row mb-4">
<div class="col-12">
<h6 class="text-sm text-uppercase text-muted mb-2">Step 1 &mdash; Download your private key</h6>
<p class="text-sm opacity-8 mb-3">Click below to download the private key file <code>id_ed25519</code>. You will use it to authenticate your SSH connections to this Edgebox.</p>
<a href="{{ path('settings_download_private_key') }}" class="btn btn-sm btn-outline-dark"><i class="fa fa-download mr-2"></i>Download Private Key</a>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<h6 class="text-sm text-uppercase text-muted mb-2">Step 2 &mdash; Connect from your computer</h6>
<div class="mb-3">
<button type="button" class="btn btn-sm btn-primary ssh-tab-btn active" id="ssh-tab-mac-btn" onclick="showSshTab('mac');">macOS / Linux</button>
<button type="button" class="btn btn-sm btn-outline-secondary ssh-tab-btn" id="ssh-tab-win-btn" onclick="showSshTab('win');">Windows</button>
</div>
<div id="ssh-tab-mac-content">
<ol class="text-sm opacity-8 pl-3">
<li>Open <b>Terminal</b> and place the downloaded key:
<pre class="mt-1 mb-0"><code>mv ~/Downloads/id_ed25519 ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519</code></pre></li>
<li>Connect to your Edgebox:
<pre class="mt-1 mb-0"><code>ssh -i ~/.ssh/id_ed25519 root@{{ ip_address }}</code></pre></li>
<li>Accept the host fingerprint prompt if this is your first connection.</li>
</ol>
</div>
<div id="ssh-tab-win-content" style="display:none;">
<ol class="text-sm opacity-8 pl-3">
<li>If <b>OpenSSH</b> is not installed, run this in PowerShell as Administrator:
<pre class="mt-1 mb-0"><code>Add-WindowsCapability -Online -Name OpenSSH.Client*</code></pre></li>
<li>Save the downloaded <code>id_ed25519</code> file to your <code>.ssh</code> folder (create it if needed). Then connect from PowerShell:
<pre class="mt-1 mb-0"><code>ssh -i %USERPROFILE%\.ssh\id_ed25519 root@{{ ip_address }}</code></pre></li>
<li>Accept the host fingerprint prompt if this is your first connection.</li>
</ol>
<p class="text-xs opacity-8 mt-2"><i class="fa fa-info-circle mr-1"></i>Alternatively use <a href="https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html" target="_blank">PuTTY</a>: convert the private key with <b>PuTTYgen</b> (Conversions &rarr; Import, save as <code>.ppk</code>), then load it in <b>Pageant</b> or the <b>Auth</b> section of PuTTY's session settings.</p>
</div>
</div>
</div>
<hr class="my-4">
<h6 class="text-xs text-uppercase text-muted mb-3">Advanced</h6>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label">Public Key</label>
<div class="input-group">
<textarea class="form-control" id="ssh-pub-key" rows="3" readonly>{{ ssh_public_key }}</textarea>
<div class="input-group-append">
<button class="btn btn-outline-secondary" onclick="navigator.clipboard.writeText(document.getElementById('ssh-pub-key').value); this.textContent='Copied!';">Copy</button>
</div>
</div>
</div>
{% if ssh_fingerprint %}
<div class="form-group">
<label class="form-control-label">Fingerprint</label>
<p class="text-sm"><code>{{ ssh_fingerprint }}</code></p>
</div>
{% endif %}
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label">Connection Command</label>
<div class="input-group">
<input type="text" class="form-control" id="ssh-command" value="ssh -i ~/.ssh/id_ed25519 root@{{ ip_address }}" readonly>
<div class="input-group-append">
<button class="btn btn-outline-secondary" onclick="navigator.clipboard.writeText(document.getElementById('ssh-command').value); this.textContent='Copied!';">Copy</button>
</div>
</div>
</div>
<div class="form-group">
<a href="{{ path('settings_action', {'action': 'generate_ssh_key'}) }}" class="btn btn-sm btn-outline-warning mr-2" onclick="return confirm('Regenerating keys will overwrite the existing pair. Any clients using the current private key will need to download the new one. Continue?');"><i class="fa fa-refresh mr-2"></i>Regenerate</a>
<a href="{{ path('settings_action', {'action': 'revoke_ssh_key'}) }}" class="btn btn-sm btn-outline-danger" onclick="return confirm('Revoking the SSH key pair permanently deletes it. You will not be able to SSH into this Edgebox with the old keys. Continue?');"><i class="fa fa-trash mr-2"></i>Revoke</a>
</div>
</div>
</div>
{% else %}
<div class="alert alert-info" role="alert">
<span class="alert-icon"><i class="ni ni-notification-70 text-white"></i></span>
<span class="alert-text text-xs text-white"><b>No SSH key pair exists yet.</b> Generate one below to enable direct SSH access to this Edgebox.</span>
</div>
<a href="{{ path('settings_action', {'action': 'generate_ssh_key'}) }}" class="btn btn-outline-dark">Generate SSH Key</a>
{% endif %}
{% endblock %}
{% endembed %}
<script>
function showSshTab(which) {
var mac = document.getElementById('ssh-tab-mac-content');
var win = document.getElementById('ssh-tab-win-content');
var macBtn = document.getElementById('ssh-tab-mac-btn');
var winBtn = document.getElementById('ssh-tab-win-btn');
if (which === 'win') {
win.style.display = 'block';
mac.style.display = 'none';
winBtn.className = 'btn btn-sm btn-primary ssh-tab-btn active';
macBtn.className = 'btn btn-sm btn-outline-secondary ssh-tab-btn';
} else {
mac.style.display = 'block';
win.style.display = 'none';
macBtn.className = 'btn btn-sm btn-primary ssh-tab-btn active';
winBtn.className = 'btn btn-sm btn-outline-secondary ssh-tab-btn';
}
}
</script>

{% embed 'blocks/_collapse_card.html.twig' with {
'title': 'Browser Dev Environment',
'collapse_id': 'collapseBrowserDevOptions',
Expand Down Expand Up @@ -602,3 +715,4 @@


{% endblock content %}

Loading