(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

@@ -1,5 +1,10 @@
from __future__ import annotations
import os
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
from django import forms
from django.conf import settings
@@ -157,6 +162,18 @@ class SshCredentialForm(forms.ModelForm):
model = SshCredential
fields = ("name", "private_key", "public_key", "known_hosts", "notes")
def clean_private_key(self) -> str:
private_key = self.cleaned_data["private_key"].strip()
public_key = validate_ssh_private_key(private_key)
self.derived_public_key = public_key
return f"{private_key}\n"
def clean(self):
cleaned_data = super().clean()
if cleaned_data.get("private_key") and not cleaned_data.get("public_key") and hasattr(self, "derived_public_key"):
cleaned_data["public_key"] = self.derived_public_key
return cleaned_data
class RetentionApplyForm(forms.Form):
kind = forms.ChoiceField(choices=(("scheduled", "Scheduled"), ("manual", "Manual"), ("all", "All")))
@@ -201,3 +218,35 @@ class ScheduleConfigForm(forms.ModelForm):
except ValueError as exc:
raise forms.ValidationError(str(exc)) from exc
return cron_expr
def validate_ssh_private_key(private_key: str) -> str:
with TemporaryDirectory() as tmp:
key_path = Path(tmp) / "identity"
key_path.write_text(f"{private_key}\n", encoding="utf-8")
os.chmod(key_path, 0o600)
try:
result = subprocess.run(
["ssh-keygen", "-y", "-f", str(key_path)],
check=False,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=5,
)
except FileNotFoundError as exc:
raise forms.ValidationError("ssh-keygen is not available in this container.") from exc
except subprocess.TimeoutExpired as exc:
raise forms.ValidationError("Could not validate SSH private key before timeout.") from exc
if result.returncode != 0:
message = result.stderr.strip() or "OpenSSH could not read this private key."
if "passphrase" in message.lower():
message = "Encrypted SSH private keys are not supported for unattended backups."
raise forms.ValidationError(f"Invalid SSH private key: {message}")
public_key = result.stdout.strip()
if not public_key:
raise forms.ValidationError("Invalid SSH private key: no public key could be derived.")
return public_key