(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:
@@ -21,6 +21,7 @@ from .forms import (
|
||||
HostConfigForm,
|
||||
ManualBackupForm,
|
||||
RetentionApplyForm,
|
||||
SshCredentialGenerateForm,
|
||||
ScheduleConfigForm,
|
||||
SshCredentialForm,
|
||||
)
|
||||
@@ -29,6 +30,7 @@ from .models import BackupRun, GlobalConfig, HostConfig, ScheduleConfig, Snapsho
|
||||
from .retention import run_sql_retention_apply, run_sql_retention_plan
|
||||
from .self_check import collect_self_checks, summarize_self_checks
|
||||
from .snapshot_discovery import discover_snapshots, inspect_snapshot_discovery
|
||||
from .ssh_keys import SshKeyError, delete_generated_key_files, generate_ssh_key
|
||||
|
||||
|
||||
@staff_member_required
|
||||
@@ -110,6 +112,42 @@ def create_ssh_credential(request):
|
||||
)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def generate_ssh_credential(request):
|
||||
if request.method == "POST":
|
||||
form = SshCredentialGenerateForm(request.POST)
|
||||
if form.is_valid():
|
||||
credential = SshCredential.objects.create(
|
||||
name=form.cleaned_data["name"],
|
||||
key_type=form.cleaned_data["key_type"],
|
||||
known_hosts=form.cleaned_data["known_hosts"],
|
||||
notes=form.cleaned_data["notes"],
|
||||
)
|
||||
try:
|
||||
credential = generate_ssh_key(credential, key_type=form.cleaned_data["key_type"])
|
||||
except SshKeyError as exc:
|
||||
credential.delete()
|
||||
form.add_error(None, str(exc))
|
||||
else:
|
||||
if form.cleaned_data["set_global_default"]:
|
||||
global_config = GlobalConfig.objects.filter(name="default").first()
|
||||
if global_config is not None:
|
||||
global_config.default_ssh_credential = credential
|
||||
global_config.save(update_fields=["default_ssh_credential", "updated_at"])
|
||||
messages.success(request, f"SSH key generated for {credential.name}.")
|
||||
return redirect("ssh_credentials")
|
||||
else:
|
||||
form = SshCredentialGenerateForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"pobsync_backend/ssh_credential_generate.html",
|
||||
{
|
||||
"form": form,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def edit_ssh_credential(request, credential_id: int):
|
||||
credential = get_object_or_404(SshCredential, id=credential_id)
|
||||
@@ -132,6 +170,27 @@ def edit_ssh_credential(request, credential_id: int):
|
||||
)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
@require_POST
|
||||
def delete_ssh_credential(request, credential_id: int):
|
||||
credential = get_object_or_404(SshCredential, id=credential_id)
|
||||
if credential.hosts.exists() or credential.global_configs.exists():
|
||||
messages.error(request, f"SSH key {credential.name} is still in use and cannot be deleted.")
|
||||
return redirect("edit_ssh_credential", credential_id=credential.id)
|
||||
|
||||
name = credential.name
|
||||
try:
|
||||
if credential.generated or credential.key_path:
|
||||
delete_generated_key_files(credential)
|
||||
except SshKeyError as exc:
|
||||
messages.error(request, f"Could not delete SSH key files for {name}: {exc}")
|
||||
return redirect("edit_ssh_credential", credential_id=credential.id)
|
||||
|
||||
credential.delete()
|
||||
messages.success(request, f"SSH key deleted: {name}.")
|
||||
return redirect("ssh_credentials")
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def edit_global_config(request):
|
||||
global_config = GlobalConfig.objects.filter(name="default").first()
|
||||
@@ -434,6 +493,7 @@ def _default_schedule_initial() -> dict[str, object]:
|
||||
def _default_global_initial() -> dict[str, object]:
|
||||
return {
|
||||
"name": "default",
|
||||
"default_ssh_credential": SshCredential.objects.order_by("name").first(),
|
||||
"ssh_user": "root",
|
||||
"ssh_port": 22,
|
||||
"rsync_binary": "rsync",
|
||||
|
||||
Reference in New Issue
Block a user