runv-server

server tooling for runv.club
Log | Files | Refs | README

remove_runv_jails.py (4855B)


      1 #!/usr/bin/env python3
      2 """
      3 Remove o modelo antigo de jail SSH runv-jailed de membros existentes.
      4 
      5 Desfaz, de forma idempotente, o que ``runv_jail.ensure_runv_jail_for_user`` aplicava:
      6 bind mount em /srv/jail/<user>/home/<user>, linha em /etc/fstab, grupo runv-jailed
      7 e directório /srv/jail/<user>. Também remove o drop-in SSH global da jail.
      8 
      9 Execute como root no servidor Debian.
     10 """
     11 
     12 from __future__ import annotations
     13 
     14 import argparse
     15 import grp
     16 import logging
     17 import os
     18 import pwd
     19 import subprocess
     20 import sys
     21 from pathlib import Path
     22 
     23 _SCRIPT_DIR = Path(__file__).resolve().parent
     24 if str(_SCRIPT_DIR) not in sys.path:
     25     sys.path.insert(0, str(_SCRIPT_DIR))
     26 
     27 from admin_guard import ensure_admin_cli
     28 import runv_jail
     29 
     30 SSHD_DROPIN = Path("/etc/ssh/sshd_config.d/90-runv-jailed.conf")
     31 
     32 
     33 def setup_logging(verbose: bool) -> logging.Logger:
     34     log = logging.getLogger("remove_runv_jails")
     35     log.setLevel(logging.DEBUG if verbose else logging.INFO)
     36     log.handlers.clear()
     37     h = logging.StreamHandler(sys.stderr)
     38     h.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
     39     log.addHandler(h)
     40     return log
     41 
     42 
     43 def require_root(dry_run: bool, log: logging.Logger) -> None:
     44     if dry_run:
     45         return
     46     if os.geteuid() != 0:
     47         log.error("execute como root (ou use --dry-run)")
     48         raise SystemExit(2)
     49 
     50 
     51 def group_members() -> list[str]:
     52     try:
     53         g = grp.getgrnam(runv_jail.RUNV_JAILED_GROUP)
     54     except KeyError:
     55         return []
     56     names = set(g.gr_mem)
     57     for pw in pwd.getpwall():
     58         if pw.pw_gid == g.gr_gid:
     59             names.add(pw.pw_name)
     60     return sorted(n for n in names if not runv_jail.jail_skip_username(n))
     61 
     62 
     63 def remove_sshd_dropin(*, dry_run: bool, log: logging.Logger) -> None:
     64     if not SSHD_DROPIN.exists():
     65         log.info("drop-in SSH jail ausente: %s", SSHD_DROPIN)
     66         return
     67     if dry_run:
     68         log.info("[dry-run] removeria %s e recarregaria ssh", SSHD_DROPIN)
     69         return
     70     old_body = SSHD_DROPIN.read_bytes()
     71     SSHD_DROPIN.unlink()
     72     log.info("removido drop-in SSH jail: %s", SSHD_DROPIN)
     73     test = subprocess.run(["sshd", "-t"], capture_output=True, text=True, timeout=30)
     74     if test.returncode != 0:
     75         SSHD_DROPIN.write_bytes(old_body)
     76         err = (test.stderr or test.stdout or "").strip()
     77         raise RuntimeError(
     78             f"sshd -t falhou após remover {SSHD_DROPIN}; ficheiro restaurado: {err}"
     79         )
     80     for unit in ("ssh", "sshd"):
     81         r = subprocess.run(["systemctl", "reload", unit], capture_output=True, text=True, timeout=60)
     82         if r.returncode == 0:
     83             log.info("systemctl reload %s concluído", unit)
     84             return
     85     log.warning("não foi possível recarregar ssh/sshd automaticamente; recarregue manualmente")
     86 
     87 
     88 def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
     89     p = argparse.ArgumentParser(description="Remove runv-jailed e /srv/jail de membros existentes.")
     90     p.add_argument("--dry-run", action="store_true", help="mostra sem alterar")
     91     p.add_argument("--verbose", "-v", action="store_true", help="log detalhado")
     92     p.add_argument("--user", metavar="USER", help="remove jail apenas deste utilizador")
     93     p.add_argument(
     94         "--keep-sshd-dropin",
     95         action="store_true",
     96         help="não remover /etc/ssh/sshd_config.d/90-runv-jailed.conf",
     97     )
     98     return p.parse_args(argv)
     99 
    100 
    101 def main(argv: list[str] | None = None) -> int:
    102     args = parse_args(argv)
    103     ensure_admin_cli(script_name=Path(__file__).name, dry_run=bool(args.dry_run))
    104     log = setup_logging(args.verbose)
    105     require_root(bool(args.dry_run), log)
    106 
    107     users = [args.user.strip()] if args.user else group_members()
    108     users = [u for u in users if u and not runv_jail.jail_skip_username(u)]
    109     failures = 0
    110     if not users:
    111         log.info("nenhum membro em %s", runv_jail.RUNV_JAILED_GROUP)
    112     for username in users:
    113         try:
    114             pw = pwd.getpwnam(username)
    115         except KeyError:
    116             log.warning("%s não existe em passwd; ignorado", username)
    117             continue
    118         log.info("--- removendo jail de %s", username)
    119         try:
    120             runv_jail.teardown_runv_jail_for_user(
    121                 username,
    122                 Path(pw.pw_dir),
    123                 log,
    124                 dry_run=bool(args.dry_run),
    125             )
    126         except Exception as e:
    127             failures += 1
    128             log.error("%s: falha ao remover jail: %s", username, e)
    129             continue
    130 
    131     if not args.keep_sshd_dropin:
    132         try:
    133             remove_sshd_dropin(dry_run=bool(args.dry_run), log=log)
    134         except RuntimeError as e:
    135             log.error("%s", e)
    136             return 1
    137 
    138     if failures:
    139         log.error("concluído com %d falha(s)", failures)
    140         return 1
    141     log.info("concluído")
    142     return 0
    143 
    144 
    145 if __name__ == "__main__":
    146     raise SystemExit(main())