Add safe repository password rotation
This commit is contained in:
+28
-3
@@ -59,6 +59,8 @@
|
||||
'new-repo': 'Legt ein neues, isoliertes Restic-Speicherziel an.',
|
||||
'existing-repo': 'Bindet ein bereits initialisiertes Restic-Repository mit seinem ursprünglichen Passwort ein. URBM initialisiert oder überschreibt es nicht.',
|
||||
'submit-repo': 'Speichert Ziel und Zugangsdaten. Das Repository wird dadurch noch nicht automatisch initialisiert.',
|
||||
'submit-repo-password': 'Erstellt und prüft einen neuen Restic-Zugangs-Key, entfernt danach den bisherigen Key und aktualisiert das geschützte URBM-Secret.',
|
||||
'adopt-repo-password': 'Prüft ein bereits gültiges Repository-Passwort und speichert es nur auf diesem URBM-Server, ohne Restic-Keys zu verändern.',
|
||||
'init-repo': 'Initialisiert am Ziel einmalig ein neues, leeres Restic-Repository. Bei einem vorhandenen Repository nur das ursprüngliche Passwort eintragen und Test verwenden.',
|
||||
'unlock-repo': 'Entfernt ausschließlich von Restic als veraltet erkannte Repository-Sperren. Aktive Sperren werden nicht gelöscht.',
|
||||
'load-snapshots': 'Lädt alle Sicherungsstände des ausgewählten Repositorys.',
|
||||
@@ -150,6 +152,7 @@
|
||||
'repo-snapshots': 'Wechselt zu den Sicherungsständen dieses Repositorys.',
|
||||
'maint': action.includes(':check:') ? 'Prüft Struktur und Integrität des Restic-Repositorys.' : 'Entfernt nach Retention nicht mehr benötigte Datenblöcke und gibt Speicher frei.',
|
||||
'edit-repo': 'Öffnet Ziel-, Mount- und Zugangseinstellungen dieses Repositorys.',
|
||||
'change-repo-password': 'Ändert den Restic-Key im Repository und aktualisiert danach das geschützte URBM-Secret. Vorhandene Snapshots bleiben unverändert.',
|
||||
'delete-repo': 'Löscht nur die URBM-Konfiguration. Die Backupdaten am Speicherziel werden nicht gelöscht.',
|
||||
'browse': 'Öffnet den Dateibaum dieses Snapshots, um einzelne Dateien oder Ordner auszuwählen.',
|
||||
'open-dir': 'Öffnet dieses Verzeichnis innerhalb des schreibgeschützten Snapshots.',
|
||||
@@ -492,7 +495,7 @@
|
||||
function renderRepositories() {
|
||||
const rows = state.config.repositories.map(r => {
|
||||
const initialize = volatileLocation(r) ? disabledButton('Initialisieren','Dieses Ziel liegt im flüchtigen RAM. Zuerst über Bearbeiten einen dauerhaften Pfad wie /mnt/user/Backups/HomeServer eintragen.') : button('Initialisieren',`init-repo:${r.id}`,'primary');
|
||||
const actions = initialize+button('Testen',`test-repo:${r.id}`)+button('Entsperren',`unlock-repo:${r.id}`)+button('Snapshots',`repo-snapshots:${r.id}`)+button('Prüfen',`maint:check:${r.id}`)+button('Bereinigen',`maint:prune:${r.id}`)+button('Bearbeiten',`edit-repo:${r.id}`)+button('Löschen',`delete-repo:${r.id}`,'danger');
|
||||
const actions = initialize+button('Testen',`test-repo:${r.id}`)+button('Passwort ändern',`change-repo-password:${r.id}`)+button('Entsperren',`unlock-repo:${r.id}`)+button('Snapshots',`repo-snapshots:${r.id}`)+button('Prüfen',`maint:check:${r.id}`)+button('Bereinigen',`maint:prune:${r.id}`)+button('Bearbeiten',`edit-repo:${r.id}`)+button('Löschen',`delete-repo:${r.id}`,'danger');
|
||||
return `<tr><td><strong>${esc(r.name)}</strong><br><span class="bu-muted">${esc(r.id)}</span></td><td>${esc(typeLabels[r.type]||r.type)}</td><td>${esc(r.location)}${volatileLocation(r)?'<span class="bu-warning">Flüchtiger Pfad: Die Daten gehen beim Neustart verloren. Verwende /mnt/user/...</span>':''}</td><td>${r.mount?.managed?'Durch URBM':'Direkt / extern'}</td><td>${actionGroup(actions)}</td></tr>`;
|
||||
}).join('');
|
||||
return `<div class="bu-toolbar"><div><h2>Repositorys</h2><div class="bu-muted">Lokale, SMB-, NFS- und SFTP-Ziele.</div></div><div class="bu-actions">${button('Vorhandenes einbinden','existing-repo')}${button('Neues Repository','new-repo','primary')}</div></div><section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Name</th><th>Typ</th><th>Speicherort</th><th>Einbindung</th><th>Aktionen</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">Keine Repositorys konfiguriert.</div>'}</section>`;
|
||||
@@ -506,12 +509,17 @@
|
||||
${existingRepository?'<div class="wide bu-inline-info">Wähle den exakten Ordner des vorhandenen Restic-Repositorys. Darin müssen direkt die Einträge <strong>config</strong>, <strong>data</strong>, <strong>index</strong>, <strong>keys</strong> und <strong>snapshots</strong> liegen. Verwende das ursprüngliche Repository-Passwort. Es wird nichts initialisiert.</div>':''}
|
||||
<label>Name<input name="name" required value="${esc(repo.name||'')}"></label><label>Typ<select name="type">${['local','smb','nfs','sftp'].map(v=>`<option value="${v}" ${v===repo.type?'selected':''}>${esc(typeLabels[v])}</option>`).join('')}</select></label>
|
||||
<div id="bu-repository-specific" class="wide"></div>
|
||||
<fieldset class="wide bu-fieldset"><legend>Restic-Verschlüsselung</legend><div class="bu-form-grid"><label>Repository-Passwort<input name="password" type="password" autocomplete="new-password" ${repo.id?'':'required'} placeholder="${repo.id?'Leer lassen, um das vorhandene Passwort zu behalten':'Ein starkes Repository-Passwort eingeben'}"></label><label>Repository-Passwort wiederholen<input name="passwordConfirm" type="password" autocomplete="new-password" ${repo.id?'':'required'} placeholder="${repo.id?'Bei einer Passwortänderung erneut eingeben':'Repository-Passwort erneut eingeben'}"></label></div><p class="bu-muted">Das Passwort wird von URBM geschützt gespeichert. ${repo.id?'Beide Felder leer lassen, um das vorhandene Passwort zu behalten.':'Bewahre dieses Passwort sicher auf; ohne das Passwort können Backups nicht wiederhergestellt werden.'}</p></fieldset>
|
||||
${repo.id?`<fieldset class="wide bu-fieldset"><legend>Restic-Verschlüsselung</legend><p>Das Repository-Passwort wird getrennt von den Ziel- und Mount-Einstellungen geändert.</p><div class="bu-actions">${button('Repository-Passwort ändern',`change-repo-password:${repo.id}`)}</div></fieldset>`:`<fieldset class="wide bu-fieldset"><legend>Restic-Verschlüsselung</legend><div class="bu-form-grid"><label>Repository-Passwort<input name="password" type="password" autocomplete="new-password" required placeholder="Ein starkes Repository-Passwort eingeben"></label><label>Repository-Passwort wiederholen<input name="passwordConfirm" type="password" autocomplete="new-password" required placeholder="Repository-Passwort erneut eingeben"></label></div><p class="bu-muted">Das Passwort wird von URBM geschützt gespeichert. Bewahre dieses Passwort zusätzlich sicher auf; ohne das Passwort können Backups nicht wiederhergestellt werden.</p></fieldset>`}
|
||||
<div class="wide bu-actions">${button(existingRepository?'Einbinden und testen':'Speichern','submit-repo','primary')}${button('Abbrechen','cancel-form')}</div></form></section>`;
|
||||
enhanceTooltips(content);
|
||||
renderRepositorySpecific(repo.type || 'local', repo);
|
||||
}
|
||||
|
||||
function repositoryPasswordForm(repo) {
|
||||
content.innerHTML=`<section class="bu-card"><h2>Repository-Passwort verwalten</h2><div class="bu-inline-info">Repository: <strong>${esc(repo.name)}</strong><br><strong>Passwort sicher ändern</strong> erstellt einen neuen Restic-Key und entfernt den bisherigen. Snapshots und Backupdaten werden nicht neu verschlüsselt oder kopiert.<br><strong>Vorhandenes Passwort übernehmen</strong> ist für weitere URBM-Server gedacht, nachdem das Passwort bereits auf einem anderen Server geändert wurde.</div><form id="bu-repo-password-form" class="bu-form"><input type="hidden" name="id" value="${esc(repo.id)}"><label>Neues oder bereits geändertes Passwort<input name="password" type="password" minlength="8" required autocomplete="new-password"></label><label>Passwort wiederholen<input name="passwordConfirm" type="password" minlength="8" required autocomplete="new-password"></label><div class="wide bu-warning">Der Vorgang ist nur möglich, wenn keine URBM-Aufgabe wartet, läuft oder pausiert ist. Nach einer echten Passwortänderung müssen weitere Server mit „Vorhandenes Passwort übernehmen“ auf das neue Passwort umgestellt werden.</div><div class="wide bu-actions">${button('Passwort sicher ändern','submit-repo-password','primary')}${button('Vorhandenes Passwort übernehmen','adopt-repo-password')}${button('Abbrechen','cancel-form')}</div></form></section>`;
|
||||
enhanceTooltips(content);
|
||||
}
|
||||
|
||||
function repositoryLocationField(label, value, placeholder) {
|
||||
return `<label class="wide">${label}<input name="location" required placeholder="${esc(placeholder)}" value="${esc(value||'')}"></label>`;
|
||||
}
|
||||
@@ -631,6 +639,7 @@
|
||||
if (name === 'new-repo') return repositoryForm();
|
||||
if (name === 'existing-repo') return repositoryForm({}, true);
|
||||
if (name === 'edit-repo') return repositoryForm(state.config.repositories.find(r=>r.id===a));
|
||||
if (name === 'change-repo-password') return repositoryPasswordForm(state.config.repositories.find(r=>r.id===a));
|
||||
if (name === 'cancel-form') return render();
|
||||
if (name === 'refresh') return load();
|
||||
if (name === 'run-job') { await api(`/v1/jobs/${a}/run`,{method:'POST'}); notify('Backup wurde eingereiht'); return load(); }
|
||||
@@ -661,6 +670,7 @@
|
||||
if (name === 'delete-job') { if(confirm('Diesen Job löschen?')) { state.config.jobs=state.config.jobs.filter(j=>j.id!==a); await saveConfig(); render(); } return; }
|
||||
if (name === 'duplicate-job') { const source=state.config.jobs.find(j=>j.id===a); const copy=structuredClone(source); copy.id=id('job'); copy.name += ' Kopie'; copy.enabled=false; state.config.jobs.push(copy); await saveConfig(); return render(); }
|
||||
if (name === 'delete-repo') { if(state.config.jobs.some(j=>j.repositoryId===a)) throw new Error('Das Repository wird noch von einem Job verwendet.'); if(confirm('Dieses Repository aus der URBM-Konfiguration löschen? Die Backup-Daten am Ziel bleiben erhalten.')) { state.config.repositories=state.config.repositories.filter(r=>r.id!==a); await saveConfig(); render(); } return; }
|
||||
if (name === 'adopt-repo-password') { await submitRepositoryPassword(true); return; }
|
||||
if (name.startsWith('submit-')) { await submit(name); return; }
|
||||
} catch (error) { notify(error.message, true); }
|
||||
}
|
||||
@@ -707,12 +717,27 @@
|
||||
render(); notify(`Repository gespeichert: ${saved.location}`);
|
||||
}
|
||||
}
|
||||
if (kind === 'submit-repo-password') {
|
||||
await submitRepositoryPassword(false);
|
||||
}
|
||||
if (kind === 'submit-restore') { const f=new FormData(document.getElementById('bu-restore-form')); await api('/v1/restores',{method:'POST',body:JSON.stringify({schemaVersion:1,id:'',repositoryId:f.get('repositoryId'),snapshotId:f.get('snapshotId'),includes:lines(f.get('includes')),target:f.get('target'),inPlace:f.get('inPlace')==='on',confirmed:f.get('confirmed')==='on'})}); notify('Wiederherstellung wurde eingereiht'); state.view='activity'; render(); }
|
||||
if (kind === 'submit-settings') { const f=new FormData(document.getElementById('bu-settings-form')); state.config.settings={resticPath:f.get('resticPath'),rsyncPath:state.config.settings.rsyncPath||'/usr/bin/rsync',restoreRoot:f.get('restoreRoot'),persistentLogDir:f.get('persistentLogDir'),showLiveLog:f.get('showLiveLog')==='on',catchUpWindowHours:Number(f.get('catchUp')),checkCron:f.get('checkCron'),pruneCron:f.get('pruneCron')}; await saveConfig(); render(); }
|
||||
if (kind === 'submit-unraid-notify') { const f=new FormData(document.getElementById('bu-unraid-notify-form')); const target={schemaVersion:1,id:f.get('id')||'unraid',name:'Unraid',type:'unraid',enabled:f.get('enabled')==='on',events:f.getAll('events')}; if(target.enabled&&!target.events.length) throw new Error('Wähle mindestens ein Ereignis für Unraid-Benachrichtigungen aus.'); state.config.notifications=state.config.notifications.filter(n=>n.type!=='unraid'&&n.type!=='ntfy').concat(target); await saveConfig(); render(); }
|
||||
if (kind === 'submit-gotify-notify') { const f=new FormData(document.getElementById('bu-gotify-notify-form')); const existing=state.config.notifications.find(n=>n.type==='gotify'); const target={schemaVersion:1,id:f.get('id')||'gotify',name:'Gotify',type:'gotify',enabled:f.get('enabled')==='on',url:String(f.get('url')||'').trim(),tokenRef:String(f.get('tokenRef')||'').trim(),events:f.getAll('events')}; if(target.enabled&&!target.url) throw new Error('Die Gotify-Server-URL ist erforderlich.'); if(target.enabled&&!target.events.length) throw new Error('Wähle mindestens ein Ereignis für Gotify-Benachrichtigungen aus.'); if(target.enabled&&!existing&&!f.get('token')) throw new Error('Ein Gotify-Anwendungs-Token ist erforderlich.'); if(f.get('token')) await api(`/v1/secrets/${encodeURIComponent(target.tokenRef)}`,{method:'PUT',body:JSON.stringify({type:'gotify-token',value:f.get('token')})}); state.config.notifications=state.config.notifications.filter(n=>n.type!=='gotify'&&n.type!=='ntfy'); if(target.enabled||target.url) state.config.notifications.push(target); await saveConfig(); render(); }
|
||||
}
|
||||
|
||||
async function submitRepositoryPassword(adopt) {
|
||||
const f=new FormData(document.getElementById('bu-repo-password-form'));
|
||||
const password=String(f.get('password')||'');
|
||||
if(password.length<8) throw new Error('Das Repository-Passwort muss mindestens 8 Zeichen lang sein.');
|
||||
if(password!==String(f.get('passwordConfirm')||'')) throw new Error('Die beiden Repository-Passwörter stimmen nicht überein.');
|
||||
const prompt=adopt?'Dieses bereits gültige Passwort prüfen und nur auf diesem URBM-Server übernehmen?':'Repository-Passwort jetzt wirklich ändern?\n\nDas bisherige Passwort funktioniert danach für den bisherigen Restic-Key nicht mehr. Weitere Server müssen anschließend auf das neue Passwort umgestellt werden.';
|
||||
if(!confirm(prompt)) return;
|
||||
const suffix=adopt?'/password/adopt':'/password';
|
||||
await api(`/v1/repositories/${encodeURIComponent(f.get('id'))}${suffix}`,{method:'POST',body:JSON.stringify({password})});
|
||||
notify(adopt?'Repository-Passwort wurde geprüft und auf diesem Server übernommen':'Repository-Passwort wurde sicher geändert'); render();
|
||||
}
|
||||
|
||||
document.querySelector('.bu-tabs').addEventListener('click', e => { const button=e.target.closest('[data-view]'); if(!button)return; state.view=button.dataset.view; document.querySelectorAll('.bu-tabs button').forEach(x=>x.classList.toggle('active',x===button)); render(); if(state.view==='dashboard')loadRepositoryStats(); });
|
||||
content.addEventListener('click', e => { const target=e.target.closest('[data-action]'); if(!target)return; e.preventDefault(); handle(target.dataset.action,target); });
|
||||
content.addEventListener('change', e => {
|
||||
@@ -754,7 +779,7 @@
|
||||
const form = e.target.closest('form');
|
||||
if (!form) return;
|
||||
e.preventDefault();
|
||||
const action = form.id === 'bu-repo-form' ? 'submit-repo' : form.id === 'bu-job-form' ? 'submit-job' : form.id === 'bu-restore-form' ? 'submit-restore' : form.id === 'bu-settings-form' ? 'submit-settings' : form.id === 'bu-unraid-notify-form' ? 'submit-unraid-notify' : form.id === 'bu-gotify-notify-form' ? 'submit-gotify-notify' : '';
|
||||
const action = form.id === 'bu-repo-form' ? 'submit-repo' : form.id === 'bu-repo-password-form' ? 'submit-repo-password' : form.id === 'bu-job-form' ? 'submit-job' : form.id === 'bu-restore-form' ? 'submit-restore' : form.id === 'bu-settings-form' ? 'submit-settings' : form.id === 'bu-unraid-notify-form' ? 'submit-unraid-notify' : form.id === 'bu-gotify-notify-form' ? 'submit-gotify-notify' : '';
|
||||
if (action) handle(action);
|
||||
});
|
||||
content.addEventListener('toggle', e => {
|
||||
|
||||
Reference in New Issue
Block a user