(feature) Add Django-managed SSH credentials

Add SSH credentials as first-class Django data so backup keys can be
uploaded through the control panel instead of mounted into containers.

Credentials can be selected globally or overridden per host. At runtime
the selected key is materialized inside the container with restrictive
file permissions and injected into the rsync SSH command via IdentityFile.
Known hosts entries are handled the same way when configured.

Add control panel views for creating and listing SSH keys, expose the
fields in config forms and admin, document the workflow, and cover global
and host credential selection with tests.
This commit is contained in:
2026-05-19 14:37:38 +02:00
parent 91ce7ad4c5
commit e65537c6de
14 changed files with 388 additions and 10 deletions

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from django import forms
from django.conf import settings
from .models import GlobalConfig, HostConfig, ScheduleConfig
from .models import GlobalConfig, HostConfig, ScheduleConfig, SshCredential
from .scheduler import parse_cron_expr
@@ -46,6 +46,7 @@ class HostConfigForm(forms.ModelForm):
fields = (
"address",
"enabled",
"ssh_credential",
"ssh_user",
"ssh_port",
"source_root",
@@ -59,6 +60,7 @@ class HostConfigForm(forms.ModelForm):
"retention_yearly",
)
help_texts = {
"ssh_credential": "Optional. Overrides the global SSH credential for this host.",
"ssh_user": "Leave empty to use the global SSH user.",
"ssh_port": "Leave empty to use the global SSH port.",
"source_root": "Leave empty to use the global default source root.",
@@ -84,6 +86,7 @@ class GlobalConfigForm(forms.ModelForm):
model = GlobalConfig
fields = (
"name",
"default_ssh_credential",
"ssh_user",
"ssh_port",
"ssh_options",
@@ -102,6 +105,7 @@ class GlobalConfigForm(forms.ModelForm):
)
help_texts = {
"name": "Usually 'default'. The backup engine currently reads the default config.",
"default_ssh_credential": "Optional. Used by hosts without their own SSH credential.",
"default_source_root": "Used by hosts without a custom source root.",
"default_destination_subdir": "Optional subdirectory below each snapshot.",
}
@@ -136,6 +140,24 @@ class ManualBackupForm(forms.Form):
)
class SshCredentialForm(forms.ModelForm):
private_key = forms.CharField(
widget=forms.Textarea,
help_text="Private key used by the worker container for SSH backups.",
)
public_key = forms.CharField(widget=forms.Textarea, required=False)
known_hosts = forms.CharField(
widget=forms.Textarea,
required=False,
help_text="Optional known_hosts entries. When set, StrictHostKeyChecking can stay enabled.",
)
notes = forms.CharField(widget=forms.Textarea, required=False)
class Meta:
model = SshCredential
fields = ("name", "private_key", "public_key", "known_hosts", "notes")
class RetentionApplyForm(forms.Form):
kind = forms.ChoiceField(choices=(("scheduled", "Scheduled"), ("manual", "Manual"), ("all", "All")))
protect_bases = forms.BooleanField(required=False)