Private
Public Access
only 2 PW
This commit is contained in:
1 parent
b0f35bf3cc
commit
044ad2afbe
8 files changed
+175
-5
No files matched your search
@@ -216,6 +216,9 @@ Versorgung und Sicherheit im Plan.
|
|||||||
gemeinsames `data/` über `HALLOWEEN_STATE_DIR`. Nach Dienststopp Daten sichern;
|
gemeinsames `data/` über `HALLOWEEN_STATE_DIR`. Nach Dienststopp Daten sichern;
|
||||||
bei Start-/HTTP-Fehlern Code, Unit und Daten zurücksetzen. `--web-host` wählt
|
bei Start-/HTTP-Fehlern Code, Unit und Daten zurücksetzen. `--web-host` wählt
|
||||||
Loopback oder ausdrücklich Heimnetz; gespeicherte Wahl bleibt erhalten.
|
Loopback oder ausdrücklich Heimnetz; gespeicherte Wahl bleibt erhalten.
|
||||||
|
Upload teilt eine SSH-Verbindung über privaten temporären Socket; ohne
|
||||||
|
Schlüssel typischerweise einmal SSH- und einmal sudo-Passwort. Aufräumen bei
|
||||||
|
Erfolg/Fehler, keine neue Passwortabfrage durch Slave-Verbindungen.
|
||||||
Update mit genau einem sudo-Aufruf für Umschaltung und Rückfall; Paketsetup
|
Update mit genau einem sudo-Aufruf für Umschaltung und Rückfall; Paketsetup
|
||||||
unprivilegiert, Python-/Unitprüfung per runuser als Pi-Benutzer. Passwort und
|
unprivilegiert, Python-/Unitprüfung per runuser als Pi-Benutzer. Passwort und
|
||||||
sudoers/Zeitstempel bleiben unverändert; echte Passwortabfrage noch prüfen.
|
sudoers/Zeitstempel bleiben unverändert; echte Passwortabfrage noch prüfen.
|
||||||
|
|||||||
@@ -216,6 +216,9 @@ Versorgung und Sicherheit im Plan.
|
|||||||
gemeinsames `data/` über `HALLOWEEN_STATE_DIR`. Nach Dienststopp Daten sichern;
|
gemeinsames `data/` über `HALLOWEEN_STATE_DIR`. Nach Dienststopp Daten sichern;
|
||||||
bei Start-/HTTP-Fehlern Code, Unit und Daten zurücksetzen. `--web-host` wählt
|
bei Start-/HTTP-Fehlern Code, Unit und Daten zurücksetzen. `--web-host` wählt
|
||||||
Loopback oder ausdrücklich Heimnetz; gespeicherte Wahl bleibt erhalten.
|
Loopback oder ausdrücklich Heimnetz; gespeicherte Wahl bleibt erhalten.
|
||||||
|
Upload teilt eine SSH-Verbindung über privaten temporären Socket; ohne
|
||||||
|
Schlüssel typischerweise einmal SSH- und einmal sudo-Passwort. Aufräumen bei
|
||||||
|
Erfolg/Fehler, keine neue Passwortabfrage durch Slave-Verbindungen.
|
||||||
Update mit genau einem sudo-Aufruf für Umschaltung und Rückfall; Paketsetup
|
Update mit genau einem sudo-Aufruf für Umschaltung und Rückfall; Paketsetup
|
||||||
unprivilegiert, Python-/Unitprüfung per runuser als Pi-Benutzer. Passwort und
|
unprivilegiert, Python-/Unitprüfung per runuser als Pi-Benutzer. Passwort und
|
||||||
sudoers/Zeitstempel bleiben unverändert; echte Passwortabfrage noch prüfen.
|
sudoers/Zeitstempel bleiben unverändert; echte Passwortabfrage noch prüfen.
|
||||||
@@ -317,3 +320,14 @@ Bildsequenzen eines Blender-Schnittprojekts bei Weitergabe mitnehmen.
|
|||||||
zweiten sudo-Aufrufs, erfolgreichem Rückfall und abgelehnter sudo-Anmeldung.
|
zweiten sudo-Aufrufs, erfolgreichem Rückfall und abgelehnter sudo-Anmeldung.
|
||||||
- Offen am Pi: aktualisierten Upload ausführen und tatsächliche Anzahl der
|
- Offen am Pi: aktualisierten Upload ausführen und tatsächliche Anzahl der
|
||||||
Passwortabfragen sowie Dienst/Python-Benutzer prüfen; kein echter PAM-Test.
|
Passwortabfragen sowie Dienst/Python-Benutzer prüfen; kein echter PAM-Test.
|
||||||
|
|
||||||
|
## Mehrfache Passwortabfragen: gemeinsame SSH-Verbindung
|
||||||
|
|
||||||
|
- Nutzer meldet weiterhin 4–5 Passwortabfragen. Skriptprüfung zeigt zusätzlich
|
||||||
|
zum einzigen sudo-Aufruf bislang drei separate SSH-Verbindungen. Die genaue
|
||||||
|
Beschriftung der gemeldeten Dialoge wurde nicht beobachtet.
|
||||||
|
- Erledigt: einmalige SSH-Master-Verbindung mit privatem temporärem Socket,
|
||||||
|
gemeinsame Nutzung durch mkdir/rsync/Aktivierung, BatchMode für Slaves und
|
||||||
|
Aufräumen bei Erfolg/Fehler. Ohne Schlüssel sind SSH und sudo zwei Anmeldungen.
|
||||||
|
- Lokal verifiziert: 15 Update-/Transport-Simulationstests bestanden, keine
|
||||||
|
Verbindung/Socketerzeugung bei Vorschau. Echte Passwortanzahl am Pi noch offen.
|
||||||
+2
-1
@@ -34,7 +34,8 @@ direkt im WLAN sichtbar.
|
|||||||
Das versionierte Uploadskript richtet den systemd-Autostart jetzt bei jedem
|
Das versionierte Uploadskript richtet den systemd-Autostart jetzt bei jedem
|
||||||
Update mit `--apply` ein und prüft den gestarteten Webdienst. Anleitung,
|
Update mit `--apply` ein und prüft den gestarteten Webdienst. Anleitung,
|
||||||
Rückfall und Datensicherung stehen in [UPLOAD.md](UPLOAD.md). Der Updateablauf
|
Rückfall und Datensicherung stehen in [UPLOAD.md](UPLOAD.md). Der Updateablauf
|
||||||
verwendet einen einzigen sudo-Aufruf für Umschaltung und möglichen Rückfall;
|
verwendet eine gemeinsame SSH-Verbindung und einen einzigen sudo-Aufruf
|
||||||
|
für Umschaltung und möglichen Rückfall;
|
||||||
das Passwort wird nicht gespeichert. Python-Prüfungen laufen als Pi-Benutzer.
|
das Passwort wird nicht gespeichert. Python-Prüfungen laufen als Pi-Benutzer.
|
||||||
|
|
||||||
Der Dienst nutzt `current/.venv`, startet die WebUI beim Hochfahren und verweist
|
Der Dienst nutzt `current/.venv`, startet die WebUI beim Hochfahren und verweist
|
||||||
|
|||||||
@@ -47,7 +47,22 @@ auf dem Laptop wird dadurch nicht automatisch geöffnet.
|
|||||||
beenden. Ein bestehender, vom Projekt verwalteter Dienst wird selbst gestoppt.
|
beenden. Ein bestehender, vom Projekt verwalteter Dienst wird selbst gestoppt.
|
||||||
Eine fremde gleichnamige systemd-Unit wird abgelehnt.
|
Eine fremde gleichnamige systemd-Unit wird abgelehnt.
|
||||||
|
|
||||||
## Einmalige sudo-Abfrage beim Update
|
## Passwortabfragen beim Update
|
||||||
|
|
||||||
|
Alle Upload-Schritte nutzen nun **eine gemeinsame SSH-Verbindung**. Mit korrekter
|
||||||
|
Passwortanmeldung ohne SSH-Schlüssel sind normalerweise insgesamt zwei Abfragen
|
||||||
|
nötig: einmal das SSH-Passwort für die Verbindung, einmal das sudo-Passwort für
|
||||||
|
den privilegierten Updateaufruf. Bei Schlüssel-/Agent-Anmeldung entfällt die
|
||||||
|
SSH-Passwortabfrage; bei gültigem sudo-Zeitstempel kann auch diese entfallen.
|
||||||
|
Ein erster Hostschlüssel-Dialog ist keine Passwortabfrage.
|
||||||
|
|
||||||
|
Der Verbindungssocket liegt in einem privaten temporären Ordner (0700) unter
|
||||||
|
`/tmp` auf dem Laptop. Ordneranlage, rsync und Aktivierung verwenden denselben
|
||||||
|
Socket. Am Ende oder bei Fehlern wird die Master-Verbindung geschlossen und der
|
||||||
|
Socketordner entfernt. Bei Verbindungsverlust fragen spätere Schritte nicht
|
||||||
|
erneut nach einem Passwort (`BatchMode=yes`), sondern brechen bei fehlender
|
||||||
|
Anmeldemöglichkeit ab. SSH-Konfiguration, Schlüssel und Passwörter werden nicht
|
||||||
|
neu gespeichert oder geändert. Die Vorschau öffnet keine Verbindung.
|
||||||
|
|
||||||
Das Uploadskript bereitet die neue Python-Umgebung als normaler Pi-Benutzer vor.
|
Das Uploadskript bereitet die neue Python-Umgebung als normaler Pi-Benutzer vor.
|
||||||
Danach läuft die Umschaltung mit **einem einzigen sudo-Aufruf**: Unitprüfung,
|
Danach läuft die Umschaltung mit **einem einzigen sudo-Aufruf**: Unitprüfung,
|
||||||
|
|||||||
@@ -59,3 +59,8 @@ Automatische Tests prüfen die aktuelle Software ohne Pi-Hardware.
|
|||||||
Rückfall unabhängig von Passwort-Tickets geprüft werden. Abgelehnte Anmeldung,
|
Rückfall unabhängig von Passwort-Tickets geprüft werden. Abgelehnte Anmeldung,
|
||||||
unprivilegierten Aufruf des internen Rootteils und unveränderte Daten prüfen.
|
unprivilegierten Aufruf des internen Rootteils und unveränderte Daten prüfen.
|
||||||
Kein echter root-/PAM-/systemd- oder Pi-Nachweis aus diesen Tests.
|
Kein echter root-/PAM-/systemd- oder Pi-Nachweis aus diesen Tests.
|
||||||
|
|
||||||
|
- UploadConnectionTest simuliert ssh/rsync und prüft genau einen Master, denselben
|
||||||
|
privaten Socket für alle Schritte, BatchMode auf Slaves, TTY für sudo sowie
|
||||||
|
Aufräumen bei Erfolg/Verbindungs-/Transfer-/Aktivierungsfehlern und pure Vorschau.
|
||||||
|
Dies bestätigt keine reale SSH-/Passwortanmeldung am Pi.
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
No SSH, package downloads, real services or Pi hardware are used.
|
No SSH, package downloads, real services or Pi hardware are used.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -235,5 +237,96 @@ exit 0
|
|||||||
self.assertEqual((self.root / 'current').resolve(), self.old)
|
self.assertEqual((self.root / 'current').resolve(), self.old)
|
||||||
|
|
||||||
|
|
||||||
|
class UploadConnectionTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory(prefix='halloween-ssh-test-')
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.base = Path(self.temp.name)
|
||||||
|
self.bin = self.base / 'bin'
|
||||||
|
self.bin.mkdir()
|
||||||
|
self.env = dict(os.environ, PATH=f'{self.bin}:{os.environ["PATH"]}', TEST_SSH_BASE=str(self.base))
|
||||||
|
ssh = self.bin / 'ssh'
|
||||||
|
ssh.write_text('#!' + sys.executable + '\n' + '''
|
||||||
|
import json, os, pathlib, sys
|
||||||
|
base = pathlib.Path(os.environ['TEST_SSH_BASE'])
|
||||||
|
args = sys.argv[1:]
|
||||||
|
control = next((arg.split('=',1)[1] for arg in args if arg.startswith('ControlPath=')), None)
|
||||||
|
if '-S' in args: control = args[args.index('-S')+1]
|
||||||
|
with (base / 'calls.jsonl').open('a') as log:
|
||||||
|
log.write(json.dumps({'tool':'ssh','args':args,'control':control})+'\\n')
|
||||||
|
path = pathlib.Path(control)
|
||||||
|
if '-M' in args:
|
||||||
|
assert path.parent.stat().st_mode & 0o777 == 0o700
|
||||||
|
if (base / 'fail-master').exists(): sys.exit(7)
|
||||||
|
path.touch()
|
||||||
|
elif '-O' in args:
|
||||||
|
assert args[args.index('-O')+1] == 'exit'
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
else:
|
||||||
|
assert 'BatchMode=yes' in args
|
||||||
|
assert path.exists(), 'Shared connection must exist'
|
||||||
|
if '-tt' in args and (base / 'fail-activation').exists(): sys.exit(7)
|
||||||
|
''')
|
||||||
|
ssh.chmod(0o755)
|
||||||
|
rsync = self.bin / 'rsync'
|
||||||
|
rsync.write_text('#!' + sys.executable + '\n' + '''
|
||||||
|
import json, os, pathlib, shlex, sys
|
||||||
|
base = pathlib.Path(os.environ['TEST_SSH_BASE'])
|
||||||
|
args = sys.argv[1:]
|
||||||
|
transport = shlex.split(args[args.index('-e')+1])
|
||||||
|
control = next(arg.split('=',1)[1] for arg in transport if arg.startswith('ControlPath='))
|
||||||
|
assert 'BatchMode=yes' in transport
|
||||||
|
assert pathlib.Path(control).exists()
|
||||||
|
with (base / 'calls.jsonl').open('a') as log:
|
||||||
|
log.write(json.dumps({'tool':'rsync','args':args,'control':control})+'\\n')
|
||||||
|
if (base / 'fail-transfer').exists(): sys.exit(7)
|
||||||
|
''')
|
||||||
|
rsync.chmod(0o755)
|
||||||
|
|
||||||
|
def upload(self, apply=True):
|
||||||
|
command = ['bash', str(PROJECT / 'tools/upload_to_pi.sh'), '--user', 'piuser',
|
||||||
|
'--host', 'pi.local', '--remote-dir', '/home/piuser/halloween']
|
||||||
|
if apply: command.append('--apply')
|
||||||
|
return subprocess.run(command, env=self.env, capture_output=True, text=True)
|
||||||
|
|
||||||
|
def calls(self):
|
||||||
|
return [json.loads(line) for line in (self.base / 'calls.jsonl').read_text().splitlines()]
|
||||||
|
|
||||||
|
def test_one_authenticated_connection_for_all_steps_and_cleanup(self):
|
||||||
|
result = self.upload()
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
calls = self.calls()
|
||||||
|
self.assertEqual(sum('-M' in call['args'] for call in calls), 1)
|
||||||
|
self.assertEqual([call['tool'] for call in calls], ['ssh','ssh','rsync','ssh','ssh'])
|
||||||
|
self.assertEqual(len({call['control'] for call in calls}), 1)
|
||||||
|
self.assertIn('-tt', calls[3]['args'])
|
||||||
|
self.assertIn('-O', calls[-1]['args'])
|
||||||
|
self.assertFalse(Path(calls[0]['control']).parent.exists())
|
||||||
|
|
||||||
|
def test_failure_closes_connection_and_stops_following_steps(self):
|
||||||
|
for failure in ('fail-master', 'fail-transfer', 'fail-activation'):
|
||||||
|
with self.subTest(failure=failure):
|
||||||
|
(self.base / failure).touch()
|
||||||
|
result = self.upload()
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
calls = self.calls()
|
||||||
|
self.assertEqual(sum('-M' in call['args'] for call in calls), 1)
|
||||||
|
self.assertFalse(Path(calls[0]['control']).parent.exists())
|
||||||
|
if failure == 'fail-master':
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
else:
|
||||||
|
self.assertIn('-O', calls[-1]['args'])
|
||||||
|
if failure == 'fail-transfer':
|
||||||
|
self.assertFalse(any('-tt' in call['args'] for call in calls))
|
||||||
|
(self.base / failure).unlink()
|
||||||
|
(self.base / 'calls.jsonl').unlink()
|
||||||
|
|
||||||
|
def test_preview_does_not_connect_or_create_session(self):
|
||||||
|
result = self.upload(apply=False)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertFalse((self.base / 'calls.jsonl').exists())
|
||||||
|
self.assertIn('halloween-ssh-VORSCHAU', result.stdout)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -44,3 +44,11 @@ Schritte; Reihenfolge im zugehörigen Vorschau-README dokumentieren.
|
|||||||
- Neu erzeugte Backupordner, Unit-Sicherung, current-Link und service-host
|
- Neu erzeugte Backupordner, Unit-Sicherung, current-Link und service-host
|
||||||
dem ursprünglichen Pi-Benutzer zuordnen; bestehende Datenbesitzer beim Kopieren
|
dem ursprünglichen Pi-Benutzer zuordnen; bestehende Datenbesitzer beim Kopieren
|
||||||
bewahren. Abgelehnte sudo-Anmeldung darf keine Umschaltung/Dienstpause auslösen.
|
bewahren. Abgelehnte sudo-Anmeldung darf keine Umschaltung/Dienstpause auslösen.
|
||||||
|
|
||||||
|
- Upload baut einen SSH-ControlMaster pro angewendetem Aufruf auf; Ordneranlage,
|
||||||
|
rsync und Aktivierung teilen den Socket. Privater temporärer Ordner unter /tmp
|
||||||
|
(0700), ControlPersist=no und EXIT-/Signal-Aufräumen. Hostschlüsselprüfung bleibt
|
||||||
|
aktiv, keine Passwörter/Schlüssel/SSH-Konfiguration neu speichern.
|
||||||
|
- Slave-Verbindungen verwenden BatchMode=yes, damit bei Verlust der gemeinsamen
|
||||||
|
Verbindung keine weiteren Passwortdialoge entstehen. Ohne Schlüssel sind SSH
|
||||||
|
und sudo weiterhin zwei getrennte Anmeldungen. Vorschau erzeugt keinen Socket.
|
||||||
+34
-3
@@ -88,19 +88,47 @@ excludes=(
|
|||||||
--exclude='*.p12'
|
--exclude='*.p12'
|
||||||
--exclude='*.pfx'
|
--exclude='*.pfx'
|
||||||
)
|
)
|
||||||
ssh_cmd=(ssh -o StrictHostKeyChecking=ask "$target" "mkdir -p -- '$remote_dir/releases' && mkdir -- '$remote_dir/releases/$release'")
|
# One SSH authentication for the entire upload. The socket is private and only
|
||||||
|
# lives for this invocation; no persistent key or password is written.
|
||||||
|
control_dir=
|
||||||
|
control_path=/tmp/halloween-ssh-VORSCHAU/control
|
||||||
|
master_started=false
|
||||||
|
cleanup_connection() {
|
||||||
|
if $master_started; then
|
||||||
|
ssh -o BatchMode=yes -S "$control_path" -O exit "$target" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if [[ -n $control_dir ]]; then
|
||||||
|
rm -f -- "$control_path"
|
||||||
|
rmdir -- "$control_dir" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
if $apply; then
|
||||||
|
control_dir=$(mktemp -d /tmp/halloween-ssh-XXXXXXXX)
|
||||||
|
chmod 700 "$control_dir"
|
||||||
|
control_path="$control_dir/control"
|
||||||
|
trap cleanup_connection EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
fi
|
||||||
|
master_cmd=(ssh -M -N -f -o ControlPersist=no -o BatchMode=no
|
||||||
|
-o StrictHostKeyChecking=ask -o "ControlPath=$control_path" "$target")
|
||||||
|
# If the shared connection is lost, refuse password fallback on later steps.
|
||||||
|
ssh_options=(-o StrictHostKeyChecking=ask -o ControlMaster=no
|
||||||
|
-o "ControlPath=$control_path" -o BatchMode=yes)
|
||||||
|
ssh_cmd=(ssh "${ssh_options[@]}" "$target" "mkdir -p -- '$remote_dir/releases' && mkdir -- '$remote_dir/releases/$release'")
|
||||||
rsync_cmd=(
|
rsync_cmd=(
|
||||||
rsync -a --delay-updates --human-readable --info=stats2
|
rsync -a --delay-updates --human-readable --info=stats2
|
||||||
"${excludes[@]}"
|
"${excludes[@]}"
|
||||||
-e 'ssh -o StrictHostKeyChecking=ask'
|
-e "ssh -o StrictHostKeyChecking=ask -o ControlMaster=no -o ControlPath=$control_path -o BatchMode=yes"
|
||||||
-- "$source_dir/" "$target:$remote_dir/releases/$release/"
|
-- "$source_dir/" "$target:$remote_dir/releases/$release/"
|
||||||
)
|
)
|
||||||
service_cmd=(ssh -tt -o StrictHostKeyChecking=ask "$target" "bash '$remote_dir/releases/$release/tools/activate_pi_release.sh' '$remote_dir' '$release' '$bind_host'")
|
service_cmd=(ssh -tt "${ssh_options[@]}" "$target" "bash '$remote_dir/releases/$release/tools/activate_pi_release.sh' '$remote_dir' '$release' '$bind_host'")
|
||||||
|
|
||||||
printf 'Quelle: %s\nZiel: %s:%s/\n' "$source_dir" "$target" "$remote_dir"
|
printf 'Quelle: %s\nZiel: %s:%s/\n' "$source_dir" "$target" "$remote_dir"
|
||||||
printf 'Ausgelassen: .git, .venv, data, preview, references, __pycache__, .env, Schlüsseldateien\n'
|
printf 'Ausgelassen: .git, .venv, data, preview, references, __pycache__, .env, Schlüsseldateien\n'
|
||||||
if ! $apply; then
|
if ! $apply; then
|
||||||
printf '\nVorschau, noch keine Verbindung und keine Übertragung:\n'
|
printf '\nVorschau, noch keine Verbindung und keine Übertragung:\n'
|
||||||
|
printf ' %q' "${master_cmd[@]}"; printf '\n'
|
||||||
printf ' %q' "${ssh_cmd[@]}"; printf '\n'
|
printf ' %q' "${ssh_cmd[@]}"; printf '\n'
|
||||||
printf ' %q' "${rsync_cmd[@]}"; printf '\n'
|
printf ' %q' "${rsync_cmd[@]}"; printf '\n'
|
||||||
printf ' %q' "${service_cmd[@]}"; printf '\n'
|
printf ' %q' "${service_cmd[@]}"; printf '\n'
|
||||||
@@ -108,6 +136,9 @@ if ! $apply; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
printf 'SSH-Verbindung einmal anmelden; danach folgt nur noch die sudo-Abfrage auf dem Pi.\n'
|
||||||
|
"${master_cmd[@]}"
|
||||||
|
master_started=true
|
||||||
"${ssh_cmd[@]}"
|
"${ssh_cmd[@]}"
|
||||||
"${rsync_cmd[@]}"
|
"${rsync_cmd[@]}"
|
||||||
printf 'Übertragung abgeschlossen.\n'
|
printf 'Übertragung abgeschlossen.\n'
|
||||||
|
|||||||
Reference in new issue
Block a user