From 7cf973c40a684b54af82cd0c5a296110625cf5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Sat, 9 Mar 2024 23:08:34 -0500 Subject: [PATCH] TOR class to allow remote SSH access over deep networks This class configures an onion service to access the SSH server over Tor. This is useful when you have a box behind NAT or some firewall that is broken or unknown, and you need to get a rescue shell on the host. With this, you give an operator a thumb drive, who only needs to figure out how to boot into GRML, and then after a while you get a shell, pretty much regardless of where the box is. This is not enabled by default, naturally, otherwise the secret key would leak in default GRML builds: this is solely designed to be run in an ad-hoc, one-time fashion. It also generates the SSH keys for the same reason: those are shown in the build logs and can be used to authenticate the remote host (a redundant measure to the onion service name, of course). I also enable `DEFAULT_BOOT_OPTIONS=ssh` in my builds, but that hasn't been done here (although maybe it's possible to enable that in the class? to be investigated). Finally, another shim is required here to inject a valid SSH public key in the image, so you can login over SSH. In my case, I have an extra CLASS that only has this one script which does: gpg --export-ssh-key anarcat@debian.org | tee -a $target/root/.ssh/authorized_keys This could also be folded in the TOR class, but I'm not sure how to do variables yet, so that's not yet standardized. --- etc/grml/fai/config/files/etc/tor/torrc/TOR | 4 ++ etc/grml/fai/config/package_config/TOR | 4 ++ etc/grml/fai/config/scripts/TOR/10-gen_hs | 67 +++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 etc/grml/fai/config/files/etc/tor/torrc/TOR create mode 100644 etc/grml/fai/config/package_config/TOR create mode 100755 etc/grml/fai/config/scripts/TOR/10-gen_hs diff --git a/etc/grml/fai/config/files/etc/tor/torrc/TOR b/etc/grml/fai/config/files/etc/tor/torrc/TOR new file mode 100644 index 00000000..8e7d6eb6 --- /dev/null +++ b/etc/grml/fai/config/files/etc/tor/torrc/TOR @@ -0,0 +1,4 @@ +SocksPort 0 +Log notice syslog +HiddenServiceDir /var/lib/tor/ssh_onion_service +HiddenServicePort 22 127.0.0.1:22 diff --git a/etc/grml/fai/config/package_config/TOR b/etc/grml/fai/config/package_config/TOR new file mode 100644 index 00000000..09cc9fb4 --- /dev/null +++ b/etc/grml/fai/config/package_config/TOR @@ -0,0 +1,4 @@ +PACKAGES install + +tor + diff --git a/etc/grml/fai/config/scripts/TOR/10-gen_hs b/etc/grml/fai/config/scripts/TOR/10-gen_hs new file mode 100755 index 00000000..cc772607 --- /dev/null +++ b/etc/grml/fai/config/scripts/TOR/10-gen_hs @@ -0,0 +1,67 @@ +#!/usr/bin/python3 + +from os import environ +from pathlib import Path +from subprocess import run, Popen, PIPE +from time import sleep + +target = environ.get("target") +assert target, "no $target set in the environment, aborting" +assert isinstance(target, str), "$target environment variable must be a string" + +wants_dir = Path(target) / "etc/systemd/system/grml-boot.target.wants/" +print("enabling tor.service in", str(wants_dir)) +assert not str(wants_dir).startswith("/etc"), "wants dir starts with /etc" +wants_dir.mkdir(exist_ok=True) +(wants_dir / "tor.service").symlink_to("/lib/systemd/system/tor.service") + +print("deploying minimal torrc config for SSH onion service") +run(["fcopy", "-i", "-B", "-v", "/etc/tor/torrc"], check=True) + +cmd = [ + "chroot", target, + "runuser", "-u", "debian-tor", "--", + "tor", "--DisableNetwork", "1", + "--Runasdaemon", "0", + "--SocksPort", "0", + "--HiddenServiceDir", "/var/lib/tor/ssh_onion_service", + "--HiddenServicePort", "22 127.0.0.1:22", +] +print("starting tor to create the private keys and hostname with", str(cmd)) +process = Popen( + cmd, + stdout=PIPE, + text=True, +) + +assert process.stdout, "no output from tor command?" + +hostname_path = Path(target) / "var/lib/tor/ssh_onion_service/hostname" +for line in process.stdout: + print(line.strip()) + if "DisableNetwork is set." in line: + count = 0 + while count < 10 and not hostname_path.exists(): + print(f"path {hostname_path} doesn't exist, sleeping") + sleep(1) + count += 1 + process.terminate() + +with hostname_path.open() as hn: + print("Tor onion service name:", hn.read().strip()) + +print("generating SSH keys in", target) +run(["chroot", target, "ssh-keygen", "-A"], check=True) + +for path in [str(x) for x in (Path(target) / "etc/ssh").iterdir()]: + if path.startswith("ssh_host_") and path.endswith("_key.pub"): + run( + [ + "chroot", + target, + "ssh-keygen", + "-l", + "-f", + path.removeprefix(target), + ] + )