only 2 PW

This commit is contained in:
marcinator_2 committed 2026-10-14 18:20:48 +02:00
1 parent b0f35bf3cc
commit 044ad2afbe
8 files changed
+175 -5

No files matched your search

+3
View File
@@ -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.
+14
View File
@@ -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
View File
@@ -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
+16 -1
View File
@@ -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,
+5
View File
@@ -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.
+93
View File
@@ -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()
+8
View File
@@ -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
View File
@@ -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'