(bugfix) Validate Django-managed SSH private keys

Validate uploaded SSH private keys with ssh-keygen before saving them so
invalid, malformed, or unsupported key material is rejected in the
control panel instead of failing later during rsync.

Auto-populate the public key when it is omitted, add an edit flow for
existing SSH credentials, and cover create, update, and invalid-key
paths with view tests.
This commit is contained in:
2026-05-19 15:22:40 +02:00
parent e65537c6de
commit c018011e83
6 changed files with 134 additions and 18 deletions

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings
@@ -85,24 +86,66 @@ class ViewTests(TestCase):
def test_ssh_credentials_view_creates_key(self) -> None:
self.client.force_login(self.staff_user)
response = self.client.post(
reverse("create_ssh_credential"),
{
"name": "backup-key",
"private_key": "PRIVATE KEY",
"public_key": "PUBLIC KEY",
"known_hosts": "web-01.example.test ssh-ed25519 AAAATEST",
"notes": "production backup key",
},
follow=True,
)
with patch("pobsync_backend.forms.validate_ssh_private_key", return_value="DERIVED PUBLIC KEY"):
response = self.client.post(
reverse("create_ssh_credential"),
{
"name": "backup-key",
"private_key": "PRIVATE KEY",
"public_key": "",
"known_hosts": "web-01.example.test ssh-ed25519 AAAATEST",
"notes": "production backup key",
},
follow=True,
)
self.assertRedirects(response, reverse("ssh_credentials"))
self.assertContains(response, "SSH credential saved for backup-key.")
self.assertContains(response, "backup-key")
credential = SshCredential.objects.get(name="backup-key")
self.assertEqual(credential.private_key, "PRIVATE KEY")
self.assertEqual(credential.public_key, "PUBLIC KEY")
self.assertEqual(credential.private_key, "PRIVATE KEY\n")
self.assertEqual(credential.public_key, "DERIVED PUBLIC KEY")
def test_ssh_credentials_view_rejects_invalid_key(self) -> None:
self.client.force_login(self.staff_user)
response = self.client.post(
reverse("create_ssh_credential"),
{
"name": "bad-key",
"private_key": "not a private key",
"public_key": "",
"known_hosts": "",
"notes": "",
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Invalid SSH private key")
self.assertFalse(SshCredential.objects.exists())
def test_ssh_credentials_view_updates_existing_key(self) -> None:
self.client.force_login(self.staff_user)
credential = SshCredential.objects.create(name="backup-key", private_key="OLD KEY")
with patch("pobsync_backend.forms.validate_ssh_private_key", return_value="UPDATED PUBLIC KEY"):
response = self.client.post(
reverse("edit_ssh_credential", args=[credential.id]),
{
"name": "backup-key",
"private_key": "UPDATED KEY",
"public_key": "",
"known_hosts": "",
"notes": "rotated",
},
follow=True,
)
self.assertRedirects(response, reverse("ssh_credentials"))
credential.refresh_from_db()
self.assertEqual(credential.private_key, "UPDATED KEY\n")
self.assertEqual(credential.public_key, "UPDATED PUBLIC KEY")
self.assertEqual(credential.notes, "rotated")
def test_global_config_form_creates_default_config(self) -> None:
self.client.force_login(self.staff_user)