(feature) Generate filesystem-backed SSH credentials
Add filesystem-backed SSH credentials for the native systemd deployment path. Generated keys are stored below POBSYNC_HOME with 0600 permissions, while Django keeps the public key, fingerprint, path, and selection metadata. Add a Django SSH key generation view, delete action for unused generated keys, and a management command used by the installer to ensure a default backup key exists. Update runtime config to use generated key paths directly as IdentityFile, extend host checks to verify key readability, and keep legacy uploaded keys available for compatibility.
This commit is contained in:
@@ -186,7 +186,9 @@ class SshCredentialForm(forms.ModelForm):
|
||||
raw_private_key = self.cleaned_data.get("private_key", "")
|
||||
|
||||
if not raw_private_key.strip():
|
||||
raise forms.ValidationError("Paste a private key or upload a private key file.")
|
||||
if self.instance and self.instance.pk and self.instance.key_path:
|
||||
return self.instance.private_key
|
||||
raise forms.ValidationError("Paste a private key, upload a private key file, or generate a key from Django.")
|
||||
|
||||
private_key = normalize_private_key(raw_private_key)
|
||||
public_key = validate_ssh_private_key(private_key)
|
||||
@@ -198,6 +200,8 @@ class SshCredentialForm(forms.ModelForm):
|
||||
provided_public_key = normalize_public_key(cleaned_data.get("public_key", ""))
|
||||
if provided_public_key:
|
||||
cleaned_data["public_key"] = provided_public_key
|
||||
elif self.instance and self.instance.pk and self.instance.key_path:
|
||||
cleaned_data["public_key"] = self.instance.public_key
|
||||
|
||||
if cleaned_data.get("private_key") and provided_public_key and hasattr(self, "derived_public_key"):
|
||||
if public_key_identity(provided_public_key) != public_key_identity(self.derived_public_key):
|
||||
@@ -210,6 +214,32 @@ class SshCredentialForm(forms.ModelForm):
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class SshCredentialGenerateForm(forms.Form):
|
||||
name = forms.CharField(max_length=128)
|
||||
key_type = forms.ChoiceField(
|
||||
choices=(("ed25519", "ed25519"), ("rsa", "rsa")),
|
||||
initial="ed25519",
|
||||
help_text="ed25519 is recommended unless you need RSA for an older target.",
|
||||
)
|
||||
set_global_default = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True,
|
||||
help_text="Use this key as the global default when the default global config exists.",
|
||||
)
|
||||
known_hosts = forms.CharField(
|
||||
widget=forms.Textarea(attrs={"spellcheck": "false", "autocomplete": "off"}),
|
||||
required=False,
|
||||
help_text="Optional known_hosts entries. This can also be filled later.",
|
||||
)
|
||||
notes = forms.CharField(widget=forms.Textarea, required=False)
|
||||
|
||||
def clean_name(self) -> str:
|
||||
name = self.cleaned_data["name"].strip()
|
||||
if SshCredential.objects.filter(name=name).exists():
|
||||
raise forms.ValidationError("An SSH credential with this name already exists.")
|
||||
return name
|
||||
|
||||
|
||||
class RetentionApplyForm(forms.Form):
|
||||
kind = forms.ChoiceField(choices=(("scheduled", "Scheduled"), ("manual", "Manual"), ("all", "All")))
|
||||
protect_bases = forms.BooleanField(required=False)
|
||||
|
||||
Reference in New Issue
Block a user