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;
bei Start-/HTTP-Fehlern Code, Unit und Daten zurücksetzen. `--web-host` wählt
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
unprivilegiert, Python-/Unitprüfung per runuser als Pi-Benutzer. Passwort und
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;
bei Start-/HTTP-Fehlern Code, Unit und Daten zurücksetzen. `--web-host` wählt
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
unprivilegiert, Python-/Unitprüfung per runuser als Pi-Benutzer. Passwort und
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.
- Offen am Pi: aktualisierten Upload ausführen und tatsächliche Anzahl der
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
Update mit `--apply` ein und prüft den gestarteten Webdienst. Anleitung,
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.
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.
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.
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,
unprivilegierten Aufruf des internen Rootteils und unveränderte Daten prüfen.
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.
"""
import os
import json
import sys
from pathlib import Path
import shutil
import subprocess
@@ -235,5 +237,96 @@ exit 0
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__':
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
dem ursprünglichen Pi-Benutzer zuordnen; bestehende Datenbesitzer beim Kopieren
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='*.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 -a --delay-updates --human-readable --info=stats2
"${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/"
)
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 'Ausgelassen: .git, .venv, data, preview, references, __pycache__, .env, Schlüsseldateien\n'
if ! $apply; then
printf '\nVorschau, noch keine Verbindung und keine Übertragung:\n'
printf ' %q' "${master_cmd[@]}"; printf '\n'
printf ' %q' "${ssh_cmd[@]}"; printf '\n'
printf ' %q' "${rsync_cmd[@]}"; printf '\n'
printf ' %q' "${service_cmd[@]}"; printf '\n'
@@ -108,6 +136,9 @@ if ! $apply; then
exit 0
fi
printf 'SSH-Verbindung einmal anmelden; danach folgt nur noch die sudo-Abfrage auf dem Pi.\n'
"${master_cmd[@]}"
master_started=true
"${ssh_cmd[@]}"
"${rsync_cmd[@]}"
printf 'Übertragung abgeschlossen.\n'