#!/bin/sh # Take Emon's SSH access away again (Linux or macOS). https://bootstrap.ihemon.me # # curl -fsSL https://bootstrap.ihemon.me/revoke.sh | sudo sh # remove Emon's key # curl -fsSL https://bootstrap.ihemon.me/revoke.sh | sudo sh -s -- --stop-ssh # ...and turn the SSH server off # # Removes exactly the Emon-Key line from every user's ~/.ssh/authorized_keys (and root's), wherever # grant.sh or a person put it, and prints what it removed. Every other key stays. Needs no internet. # --stop-ssh also stops the SSH server and keeps it off after reboot (only do that if nobody else # needs SSH on this machine). If this machine joined Emon's Tailscale as a guest (grant.sh --tailscale), it is # logged out of it; a Tailscale that belongs to you (not a guest) is never touched. It writes no files of its # own and leaves nothing behind. set -eu # Emon-Key (SHA256:wVrLsx8kwxTscCD6oAcmcslPxagnZDKl1yaM2rwl39g). Matched by its key blob, so an # Emon-Key line is found whatever its options or comment are. tests/ check blob == fingerprint. EMON_KEY_BLOB="AAAAC3NzaC1lZDI1NTE5AAAAIJQNrt4zH0k6C2G4Y7fZX+4S73gLZvrHodPTl2hELfWk" # Emon's tailnet (MagicDNS suffix): only a guest-* node with tag:guest IN this tailnet is ever logged out. TAILNET_SUFFIX="tailbdfb8.ts.net" say() { printf '%s\n' "$*"; } step() { printf '\n==> %s\n' "$*"; } note() { printf ' %s\n' "$*"; } die() { printf '\nERROR: %s\n' "$*" >&2 exit 1 } have() { command -v "$1" >/dev/null 2>&1; } usage() { say "usage: revoke.sh [--stop-ssh [--force]]" say " --stop-ssh also stop the SSH server and keep it off (refused over SSH unless --force)" } # one home folder per line (paths may contain spaces) homes() { if [ "$(uname -s)" = Darwin ]; then dscl . -list /Users NFSHomeDirectory 2>/dev/null | sed -E 's/^[^[:space:]]+[[:space:]]+//' echo /var/root else if have getent; then getent passwd; else cat /etc/passwd; fi | cut -d: -f6 echo /root fi } # remove_from FILE -> prints how many lines it removed; fails (file untouched) when it can't be sure. # awk under LC_ALL=C works on bytes: a comment in another encoding can't make it skip lines (grep's "binary file"). remove_from() { n=$(LC_ALL=C awk -v b="$EMON_KEY_BLOB" 'index($0, b) {c++} END {print c + 0}' "$1") || return 1 if [ "$n" -gt 0 ]; then all=$(LC_ALL=C awk 'END {print NR}' "$1") || return 1 kept=$(LC_ALL=C awk -v b="$EMON_KEY_BLOB" '!index($0, b) {c++} END {print c + 0}' "$1") || return 1 [ $((kept + n)) -eq "$all" ] || return 1 rest=$(LC_ALL=C awk -v b="$EMON_KEY_BLOB" '!index($0, b)' "$1") || return 1 # Rewrite in place: owner, mode and SELinux label of the file stay as they were. if [ -n "$rest" ]; then printf '%s\n' "$rest" >"$1" || return 1; else : >"$1" || return 1; fi fi printf '%s\n' "$n" } # true when this shell runs inside an SSH login (sudo drops SSH_CONNECTION, so also walk up the parents) over_ssh() { [ -n "${SSH_CONNECTION:-}${SSH_CLIENT:-}" ] && return 0 [ -d /proc/self ] || return 1 pid=$$ i=0 while [ "$pid" -gt 1 ] && [ "$i" -lt 40 ]; do case $(cat "/proc/$pid/comm" 2>/dev/null) in sshd | sshd-session | sshd-auth) return 0 ;; esac # PPid from status, not stat: a process name with a space would shift the stat fields pid=$(awk '$1 == "PPid:" {print $2}' "/proc/$pid/status" 2>/dev/null) || return 1 [ -n "$pid" ] || return 1 i=$((i + 1)) done return 1 } # pids of live sshd processes (Linux /proc; zombies skipped) sshd_pids() { for d in /proc/[0-9]*; do [ "$(cat "$d/comm" 2>/dev/null)" = sshd ] || continue [ "$(awk '{print $3}' "$d/stat" 2>/dev/null)" = Z ] && continue printf '%s ' "${d#/proc/}" done } stop_ssh() { if [ "$1" != force ] && over_ssh; then die "you are connected over SSH: stopping SSH would cut you off. Run it on the machine itself, or add --force." fi if [ "$(uname -s)" = Darwin ]; then systemsetup -f -setremotelogin off >/dev/null 2>&1 || launchctl bootout system/com.openssh.sshd >/dev/null 2>&1 || true launchctl disable system/com.openssh.sshd >/dev/null 2>&1 || true note "Remote Login (SSH) turned off. Check: System Settings > General > Sharing > Remote Login." elif [ -d /run/systemd/system ] && have systemctl; then for u in ssh.socket ssh.service sshd.service; do if systemctl cat "$u" >/dev/null 2>&1; then systemctl disable --now "$u" >/dev/null 2>&1 || true fi done note "SSH server stopped and will not start at boot" elif have rc-service && [ -e /etc/init.d/sshd ]; then rc-service sshd stop >/dev/null 2>&1 || true rc-update del sshd default >/dev/null 2>&1 || true note "SSH server stopped and will not start at boot" else for d in $(sshd_pids); do kill "$d" 2>/dev/null || true; done # Wait until they are really gone, so a grant right after this starts a fresh one. i=0 while [ -n "$(sshd_pids)" ] && [ "$i" -lt 10 ]; do sleep 1 i=$((i + 1)) done if [ -n "$(sshd_pids)" ]; then note "sshd is still running (pids: $(sshd_pids))"; else note "SSH server processes stopped"; fi fi note "(the OpenSSH package itself is left installed)" } ts_cli() { for t in tailscale /Applications/Tailscale.app/Contents/MacOS/Tailscale; do if have "$t"; then printf '%s\n' "$t" return 0 fi done return 1 } # Log out of Emon's tailnet, but only a guest node (tag:guest) that grant.sh --tailscale joined. tailscale_leave() { TS=$(ts_cli) || return 0 st=$("$TS" status --json --peers=false 2>/dev/null | tr -d ' \t\r\n' || true) case $st in *'"Tags":["tag:guest"]'*'"DNSName":"guest-'*".$TAILNET_SUFFIX."* | *'"DNSName":"guest-'*".$TAILNET_SUFFIX."*'"Tags":["tag:guest"]'*) ;; *) case $st in *'"BackendState":"Running"'*) note "Tailscale: this computer is not one of Emon's guests; left as it is" ;; esac return 0 ;; esac step "Tailscale (guest)" if "$TS" logout >/dev/null 2>&1; then note "logged out of Emon's Tailscale (the guest device disappears from it)" else note "could not log out of Tailscale; run: sudo $TS logout" FAILED=yes fi note "Tailscale itself stays installed. To remove it: your package manager (apt-get remove tailscale, dnf remove tailscale, ...)" } main() { stop=no force=no while [ $# -gt 0 ]; do case $1 in --stop-ssh) stop=yes ;; --force) force=yes ;; -h | --help) usage exit 0 ;; *) die "unknown option: $1 (try --help)" ;; esac shift done say "Remove Emon's SSH access (https://bootstrap.ihemon.me)" [ "$(id -u)" -eq 0 ] || die "run it with sudo: curl -fsSL https://bootstrap.ihemon.me/revoke.sh | sudo sh" step "Emon's key" total=0 FAILED=no list=$(homes | sort -u) # one folder per line (spaces allowed); a for loop, not a pipe, so total/FAILED survive it old_ifs=$IFS IFS=' ' set -f for h in $list; do case $h in /*) ;; *) continue ;; esac if [ ! -d "$h/.ssh" ] || [ -L "$h/.ssh" ]; then continue; fi for f in "$h/.ssh/authorized_keys" "$h/.ssh/authorized_keys2"; do if [ ! -f "$f" ] || [ -L "$f" ]; then continue; fi if ! n=$(remove_from "$f"); then note "COULD NOT clean $f (left unchanged): remove the Emon-Key line by hand" FAILED=yes continue fi if [ "$n" -gt 0 ]; then note "removed $n Emon-Key line(s) from $f" total=$((total + n)) fi done done set +f IFS=$old_ifs if [ "$FAILED" = yes ]; then note "NOT finished: Emon-Key may still be in the file(s) above." elif [ "$total" -eq 0 ]; then note "Emon-Key was not found in any authorized_keys. Nothing to remove." else note "done: Emon can no longer log in with his key. Other keys were not touched." note "(an SSH session that is open right now keeps running until it ends or you reboot)" fi tailscale_leave if [ "$stop" = yes ]; then step "SSH server" if [ "$force" = yes ]; then stop_ssh force; else stop_ssh no; fi else say "" say "SSH server left as it is. If you turned it on only for Emon and want it off too:" say " curl -fsSL https://bootstrap.ihemon.me/revoke.sh | sudo sh -s -- --stop-ssh" fi } main "$@"