(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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user