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;
|
||||
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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user