SPB Git forge
29commits 1branches 0releases
684.0 KBsize
maindefault branch
2 days agolast push
Python 95.8% Shell 4.2%
6.9 KB · 125 lines python
Raw Blame History
1"""`mld harden <nœud>|--all` : un Mac du cluster doit revenir TOUT SEUL après une coupure de courant ou une mise à jour.23Ce que ça règle (idempotent, sudo via ~/dispatch/.sudo*) :4  1. alimentation : redémarrage automatique après coupure (autorestart) et après gel (restartfreeze), jamais de veille5     (sleep/disksleep 0), réveil réseau (womp) ;6  2. mises à jour macOS + App Store : aucune installation automatique (un redémarrage surprise couperait les apps) ;7  3. Tailscale : `tailscale set --auto-update=false` + Sparkle désactivé — une MAJ relance tailscaled et le nœud disparaît8     de *.maclustr.io (les noms résolvent vers l'IP Tailscale) même si le LAN et le tunnel vont bien ;9  4. ouverture de session automatique (kcpassword + autoLoginUser) : sans session graphique, les LaunchAgents (pm2 startup,10     gardiens Ka, pousseur, admin-ka) ne démarrent jamais — c'est ce qui a laissé les apps mortes le 2026-09-10.11     Impossible si FileVault est actif (le disque reste chiffré jusqu'à la saisie du mot de passe) : signalé ;12  5. vérifie que pm2 startup (LaunchAgent ou LaunchDaemon) et le LaunchDaemon WireGuard wg1 sont en place.13Les nœuds loués (REMOTE_NODES) reçoivent 1, 2, 5 mais pas l'auto-login (session du fournisseur) sauf --autologin.14"""15import os16import shlex17from concurrent.futures import ThreadPoolExecutor18from . import config, nodes, ssh1920KC_KEY = [0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F]212223def kcpassword(pw):24    """Encodage /etc/kcpassword (XOR avec la clé Apple, complété à un multiple de 12 octets)."""25    b = list(pw.encode("utf-8")) + [0]26    while len(b) % 12:27        b.append(0)28    return bytes(x ^ KC_KEY[i % len(KC_KEY)] for i, x in enumerate(b))293031def _sudo_pw(alias):32    pwf = config.sudo_pw_file(alias)33    return open(pwf).read().strip() if os.path.exists(pwf) else None343536def script_for(alias, autologin):37    pw = _sudo_pw(alias)38    if not pw:39        return None40    user = config.user_of(alias)41    kc = "".join("\\x%02x" % x for x in kcpassword(pw))   # même mot de passe pour sudo et l'ouverture de session42    s = r'''43S(){ echo %(pw)s | sudo -S -p '' "$@"; }44echo "== alimentation"45S pmset -a sleep 0 disksleep 0 womp 1 >/dev/null 2>&146S pmset -a autorestart 1 >/dev/null 2>&1 || echo "  autorestart non supporté (portable)"47S systemsetup -setrestartfreeze on >/dev/null 2>&148S systemsetup -setrestartpowerfailure on >/dev/null 2>&149echo "  $(pmset -g | grep -E '^ *(autorestart|sleep|disksleep|womp) ' | awk '{print $1"="$2}' | tr '\n' ' ')"50echo "== mises à jour macOS / App Store : désactivées"51for k in AutomaticCheckEnabled AutomaticDownload AutomaticallyInstallMacOSUpdates ConfigDataInstall CriticalUpdateInstall; do S defaults write /Library/Preferences/com.apple.SoftwareUpdate $k -bool false; done52S defaults write /Library/Preferences/com.apple.commerce AutoUpdate -bool false53S defaults write /Library/Preferences/com.apple.commerce AutoUpdateRestartRequired -bool false54echo "  SoftwareUpdate: $(defaults read /Library/Preferences/com.apple.SoftwareUpdate 2>/dev/null | grep -E 'Automatic|Install' | tr -d ' ;' | tr '\n' ' ')"55T=/Applications/Tailscale.app/Contents/MacOS/Tailscale56if [ -x "$T" ]; then57  echo "== Tailscale $($T version 2>/dev/null | head -1) : pas de mise à jour automatique"58  "$T" set --auto-update=false >/dev/null 2>&1 && echo "  auto-update off" || echo "  ! tailscale set a échoué (session ?)"59  defaults write io.tailscale.ipn.macsys SUEnableAutomaticChecks -bool false 2>/dev/null60  defaults write io.tailscale.ipn.macsys SUAutomaticallyUpdate -bool false 2>/dev/null61  defaults write io.tailscale.ipn.macos SUEnableAutomaticChecks -bool false 2>/dev/null62else63  echo "== Tailscale : absent"64fi65echo "== ouverture de session automatique (%(user)s)"66if fdesetup status 2>/dev/null | grep -q 'On'; then67  echo "  ! FILEVAULT ACTIF : auto-login impossible, le Mac attend un mot de passe au démarrage (fdesetup disable pour lever ça)"68elif [ "%(autologin)s" = "1" ]; then69  printf '%(kc)s' > /tmp/.kc.$$ && S install -m 600 -o root -g wheel /tmp/.kc.$$ /etc/kcpassword; rm -f /tmp/.kc.$$70  S defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser -string %(user)s71  echo "  autoLoginUser=$(defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null) kcpassword=$(S stat -f %%z /etc/kcpassword 2>/dev/null) octets"72else73  echo "  non touché (nœud loué : session du fournisseur) — autoLoginUser=$(defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null || echo aucun)"74fi75echo "== démarrage des services"76P=$(ls ~/Library/LaunchAgents/pm2.*.plist /Library/LaunchDaemons/pm2.*.plist 2>/dev/null | head -1)77[ -n "$P" ] && echo "  pm2 startup : $P" || echo "  ! pm2 startup ABSENT → mld prepare %(alias)s"78if [ -f /Library/LaunchDaemons/io.maclustr.wireguard-wg1.plist ]; then79  ifconfig 2>/dev/null | grep -q 'inet 10\.67\.0\.' && echo "  wg1 (tunnel BHS64) : monté" || { echo "  wg1 : plist présent mais interface absente → relance"; S launchctl kickstart -k system/io.maclustr.wireguard-wg1; }80else81  echo "  wg1 : non raccordé au MacLustr Tunnel (mld tunnel peer %(alias)s si le nœud doit exposer un site)"82fi83echo "  session console : $(who | awk '/console/{print $1" depuis "$3" "$4}' | head -1)"84''' % {"pw": shlex.quote(pw), "user": user, "kc": kc, "autologin": "1" if autologin else "0", "alias": alias}85    return s868788def harden(alias, autologin=None, verbose=True):89    ip = nodes.ip_of(alias, rediscover=False) or nodes.ip_of(alias)90    if not ip:91        return alias, False, "injoignable"92    if autologin is None:93        autologin = alias not in config.REMOTE_NODES94    s = script_for(alias, autologin)95    if not s:96        return alias, False, "mot de passe sudo absent (%s)" % config.sudo_pw_file(alias)97    rc, out, err = ssh.run(ip, s, timeout=240)98    if verbose:99        print("### %s (%s)" % (alias, ip))100        print(out.rstrip())101        if err.strip():102            print("  stderr: " + err.strip()[-200:])103    flags = []104    if "FILEVAULT" in out:105        flags.append("FileVault")106    if "pm2 startup ABSENT" in out:107        flags.append("pm2-startup")108    if "tailscale set a échoué" in out:109        flags.append("tailscale")110    return alias, rc == 0, ", ".join(flags) if flags else "ok"111112113def harden_all(aliases=None, autologin=None):114    lan = nodes.load_lan()["ips"] or nodes.discover(verbose=False)115    targets = [a for a in config.NODES if a in lan and (not aliases or a in aliases)]116    with ThreadPoolExecutor(max_workers=12) as ex:117        results = list(ex.map(lambda a: harden(a, autologin=autologin, verbose=False), targets))118    print("%-8s %-6s %s" % ("nœud", "état", "à surveiller"))119    for a, ok, note in results:120        print("%-8s %-6s %s" % (a, "ok" if ok else "ÉCHEC", note))121    missing = [a for a in config.NODES if a not in lan]122    if missing:123        print("non joignables (éteints ?) : %s" % ", ".join(missing))124    return results125