From 998d9f603d1247ccfcc3c238210076f3177a6f86 Mon Sep 17 00:00:00 2001 From: abrown38 Date: Wed, 30 Sep 2026 17:47:41 +0000 Subject: [PATCH] simplify remotes --- CHANGELOG.md | 2 +- bin/terractl | 9 +- completions/bash/terractl | 4 +- docs/guide.md | 50 +++++----- lib/terractl-remote | 203 +++++++++++++++++++------------------- systemd/terractld.service | 2 +- 6 files changed, 137 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4e81c..30058f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - `list` has a new `BOOT` column showing whether each server starts at boot (`enabled`, `disabled`, or e.g. `masked`). - Manage other hosts, like LXD/Incus remotes: - `sudo terractl daemon enable` starts `terractld.service`, a daemon other hosts connect to (HTTPS, TCP port 7776; `TERRACTL_LISTEN` in `/etc/default/terractl` changes it). It's off until enabled. `daemon disable` and `daemon status` turn it off and show its state. - - `sudo terractl daemon trust add ` creates a one-time token for a computer, valid for 24 hours; `daemon trust list` and `daemon trust remove ` show and revoke access. On that computer, `terractl remote add ` adds the host; `remote list` and `remote remove` manage remotes, which are stored per user. + - `daemon enable` prints a command with a one-time token (valid for 24 hours); running it on another computer, `terractl remote add `, adds the host as a remote named after its hostname (or `remote add `). `sudo terractl daemon token` makes tokens for more computers, `daemon status` lists the trusted ones, and `daemon revoke ` removes one. Adding a host again replaces the old remote, e.g. after a revoke. `remote list` and `remote remove` manage remotes, which are stored per user. - `terractl --host ` runs any command on a remote, including `attach`, `watch` and `export`/`import`/`mods add` with files from your computer. The daemon runs it as root there, with messages adjusted to `terractl --host ...`. - `terractl copy` and `terractl move` copy, move or rename instances, on one host or between hosts (`[:]`). Paths and ports follow the destination's settings, and a missing server zip is copied along. `move` stops the source first, starts it again if the transfer fails, and starts the moved instance where it was running before. - Security: the token carries the daemon's certificate fingerprint, which the client checks on every connection; each computer gets its own key, and the daemon stores only hashes. The daemon runs as root with systemd sandboxing. diff --git a/bin/terractl b/bin/terractl index 62c7582..eef42b1 100755 --- a/bin/terractl +++ b/bin/terractl @@ -96,13 +96,12 @@ Other hosts: copy an instance, on this host or between hosts move [:] [:][] [--with-server] move (or rename) an instance; stops it while moving - remote add add a host to manage (token from 'daemon trust add') - remote add
[] ... when the token's addresses don't work from here + remote add [] [--address [:]] + add a host to manage (token from its 'daemon enable') remote list | remote remove show or forget remotes daemon enable|disable|status let other hosts manage this one (terractld.service) - daemon trust add create a one-time token for a computer to join - daemon trust list | trust remove - show or revoke computers that may manage this host + daemon token a one-time token for another computer to add this host + daemon revoke take a computer's access away Used by terraria@.service: journal-access let the terraria group read the servers' journal diff --git a/completions/bash/terractl b/completions/bash/terractl index 02fcfbf..28b7fb0 100644 --- a/completions/bash/terractl +++ b/completions/bash/terractl @@ -153,9 +153,7 @@ _terractl() { return ;; daemon) if (( cword == 2 )); then - _terractl_reply "enable disable status trust" - elif (( cword == 3 )) && [[ ${words[2]} == trust ]]; then - _terractl_reply "add list remove" + _terractl_reply "enable disable status token revoke" fi return ;; esac diff --git a/docs/guide.md b/docs/guide.md index 1ccab34..1d40ea7 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -670,7 +670,7 @@ sudo terractl move calamity nas: # this host → nas terractl copy nas:calamity pi:test # nas → pi, as 'test' ``` -### Turning on the daemon +### Setting it up On the host to be managed: @@ -678,29 +678,31 @@ On the host to be managed: sudo terractl daemon enable ``` -This starts `terractld.service` and makes it start at boot. It listens on **TCP port 7776**, on all addresses. If a firewall is active, allow that port (for example `sudo ufw allow 7776/tcp`), ideally only from the computers that will manage the host. To listen on one address or on another port, set `TERRACTL_LISTEN` in `/etc/default/terractl` (e.g. `TERRACTL_LISTEN=10.0.10.77:7776`) and run `sudo systemctl restart terractld`. +It prints a command with a one-time token. Run that command on the computer that should manage the host: -`sudo terractl daemon disable` turns it off again. `terractl daemon status` shows whether it's running, where it listens and its certificate's fingerprint, and with `sudo`, which computers it trusts. +```bash +terractl remote add tctl1_eyJ2IjoxLCJmcCI6... +``` + +That's all: the host is now a remote called by its hostname (say `nas`), and `terractl --host nas list` works. To call it something else, put a name before the token: `terractl remote add nas tctl1_...`. The host, in turn, knows the computer by the computer's hostname. + +For each further computer, run `sudo terractl daemon token` on the host for a new token. A token works **once**, **within 24 hours**. + +### The daemon + +`daemon enable` starts `terractld.service` and makes it start at boot. It listens on **TCP port 7776**, on all addresses. If a firewall is active, allow that port (for example `sudo ufw allow 7776/tcp`), ideally only from the computers that will manage the host. To listen on one address or on another port, set `TERRACTL_LISTEN` in `/etc/default/terractl` (e.g. `TERRACTL_LISTEN=10.0.10.77:7776`) and run `sudo systemctl restart terractld`. + +`sudo terractl daemon disable` turns it off again. `terractl daemon status` shows whether it's running, where it listens and its certificate's fingerprint, and with `sudo`, which computers it trusts. `sudo terractl daemon revoke ` takes a computer's access away. The daemon is an ordinary systemd service: systemd starts it at boot, restarts it if it crashes, and keeps its log. `journalctl -u terractld` lists every command it ran and for which computer. -### Adding a remote +### Remotes -Each computer that should manage the host needs a one-time token. On the host with the daemon: +The token contains the host's addresses, so usually `remote add` needs nothing else. If none of them works from the computer (behind NAT, for example), add one that does: `terractl remote add tctl1_... --address 203.0.113.5`, or `--address nas.example.com:7800` for another port. -```bash -sudo terractl daemon trust add laptop -``` +Adding a host that's already a remote replaces that remote, keeping its name unless you give a new one. So if a remote stops working because its access was revoked, just get a new token and add it again. -`laptop` is a name for the computer, used in the host's log and to revoke it later. The command prints what to run on that computer: - -```bash -terractl remote add nas tctl1_eyJ2IjoxLCJmcCI6... -``` - -`nas` is the name you'll use for the host there; choose any. The token contains the host's addresses, so usually nothing else is needed. If none of them works from the computer (behind NAT, for example), add the address: `terractl remote add nas 203.0.113.5 tctl1_...`, or `nas.example.com:7800` for another port. - -A token works **once**, **within 24 hours**. Remotes are kept per user in `~/.config/terractl/remotes.json`, readable only by you. Under `sudo`, the remotes of the person who ran sudo are used. +Remotes are kept per user in `~/.config/terractl/remotes.json`, readable only by you. Under `sudo`, the remotes of the person who ran sudo are used. ```bash terractl remote list # your remotes @@ -757,7 +759,7 @@ On this host, copying *from* it works for members of the `terraria` group. Copyi - **Encrypted and pinned.** Traffic is HTTPS (TLS 1.2 or newer). The daemon's certificate is self-signed, the token carries its fingerprint, and every connection checks it, so nothing in between can pose as the daemon. If the fingerprint ever changes, `terractl` refuses to connect and says so. - **Keys, not passwords.** Joining trades the token for a random key for that computer. The daemon keeps only hashes of keys and tokens, in `/var/lib/terractl/trust.json` (root only). - **A trusted computer is an admin of the host's servers.** It can do what `sudo terractl` can: create, delete, import, start and stop instances, and install mods or server files, which run as the `terraria` user. It can't change the daemon or run anything other than `terractl`. Give tokens only to computers you'd trust with `sudo terractl`. -- **Revoking.** `sudo terractl daemon trust list` shows the trusted computers, and `sudo terractl daemon trust remove laptop` takes one's access away immediately. +- **Revoking.** `sudo terractl daemon status` shows the trusted computers, and `sudo terractl daemon revoke laptop` takes one's access away immediately. - **Firewall.** The daemon doesn't need to be reachable from the internet. Limit port 7776 to your LAN or VPN, or set `TERRACTL_LISTEN` to a LAN or VPN address. The daemon runs as root, because it creates and removes instances and starts services, but with systemd's protections it can't write to `/usr`, `/boot`, `/etc` or home folders. Its certificate, key and trust list are in `/var/lib/terractl/`. Files in transit are kept in `/var/lib/terractl/transfers/`, and removed once used, after 6 hours unused, or when the daemon restarts. @@ -798,7 +800,7 @@ The servers always run as the `terraria` user. Administration is simplest with ` | `import` | root (or `terraria`) | Run as root, it gives the new instance to `terraria`. | | `prepare`, `run` | Normally only the service | Run as root, they automatically switch to the `terraria` user, so no files end up owned by root. Any other user is refused. | | `service-stop` | Only the service | Needs the server's process ID from systemd. | -| `daemon enable`, `daemon disable`, `daemon trust ...` | root only | | +| `daemon enable`, `disable`, `token`, `revoke` | root only | | | `daemon status` | anyone | Shows the trusted computers only with `sudo`. | | `remote ...`, `--host ...` | anyone | Uses your own remotes. On the remote, the daemon runs the command as root. | | `copy`, `move` | depends on this host's part | Copying from this host: the group is enough. Copying to it, or moving away from it: root. | @@ -918,7 +920,7 @@ The daemon isn't running on that host (check with `terractl daemon status` there The daemon has a different certificate than when you added it. That's expected if `/var/lib/terractl` was deleted there, or the package was purged and installed again; remove the remote and add it again. If nothing like that happened, don't work around it: something may be intercepting the connection. **`doesn't trust this computer (any more)`.** -Its access was revoked with `daemon trust remove`, or that computer was added again under the same name elsewhere. Get a new token. +Its access was revoked with `daemon revoke`, or the host's `/var/lib/terractl` was reset. Run `sudo terractl daemon token` there and `terractl remote add ` here; that replaces the old remote. --- @@ -972,12 +974,12 @@ terractl copy [:] [:][] [--with-server] Copy an instance, here or between hosts terractl move [:] [:][] [--with-server] Move or rename an instance -terractl remote add Add a remote (token from 'daemon trust add') -terractl remote add
[] ... at an address the token doesn't list +terractl remote add [] [--address [:]] + Add (or replace) a remote terractl remote list | remove Show or forget remotes terractl daemon enable|disable|status Let other hosts manage this one -terractl daemon trust add|remove Create a token for a computer, or revoke one -terractl daemon trust list Show trusted computers and unused tokens +terractl daemon token A one-time token for another computer +terractl daemon revoke Take a computer's access away Used by the service: terractl prepare Create dirs/configs, unpack zip if needed diff --git a/lib/terractl-remote b/lib/terractl-remote index 7b1b6e5..3f61514 100755 --- a/lib/terractl-remote +++ b/lib/terractl-remote @@ -14,7 +14,7 @@ # the instance to the destination's folders and free ports. # # Security: the daemon's certificate is self-signed. A computer joins with a -# one-time token from 'terractl daemon trust add', which holds the certificate's +# one-time token from 'terractl daemon enable' or 'daemon token', which holds the certificate's # fingerprint and a secret. The client checks that fingerprint on every # connection, so nothing in between can pose as the daemon, and trades the # secret for a random key of its own, sent with each request. The daemon stores @@ -27,7 +27,6 @@ import base64 import codecs import errno import fcntl -import getpass import hashlib import hmac import http.client @@ -212,7 +211,7 @@ def open_state(cmdline): class StateLock: - """Serialises changes to trust.json between the daemon and 'terractl daemon trust'.""" + """Serialises changes to trust.json between the daemon and 'terractl daemon token/revoke'.""" def __enter__(self): self.fd = os.open(state_path(".lock"), os.O_RDWR | os.O_CREAT, 0o600) @@ -427,6 +426,15 @@ def extract_upload(tar_path, dest): raise HttpError(400, f"the files sent aren't a valid archive ({e})") +def unique_client_name(wanted, clients): + base = wanted[:60] if isinstance(wanted, str) and CLIENT_RE.fullmatch(wanted[:60]) else "computer" + taken = {c.get("name") for c in clients} + name, n = base, 2 + while name in taken: + name, n = f"{base}-{n}", n + 1 + return name + + class Handler(http.server.BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" server_version = "terractld" @@ -579,7 +587,8 @@ class Handler(http.server.BaseHTTPRequestHandler): # ---- endpoints def join(self): - secret = self.read_json().get("secret") + req = self.read_json() + secret, wanted, old_key = req.get("secret"), req.get("name"), req.get("old_key") if not isinstance(secret, str): raise HttpError(400, "bad request") digest = sha256(secret) @@ -593,11 +602,20 @@ class Handler(http.server.BaseHTTPRequestHandler): save_trust(trust) dlog(f"{self.client_address[0]}: refused a join (unknown, used or expired token)") raise HttpError(403, "this token is not valid (it may have been used already, or " - "expired); create a new one there: sudo terractl daemon trust add ") + "expired); create a new one there: sudo terractl daemon token") trust["tokens"].remove(token) key = secrets.token_urlsafe(32) - name = token["name"] - trust["clients"] = [c for c in trust["clients"] if c.get("name") != name] + # A computer that's trusted already (it sends its old key) keeps its + # entry and name; a new one is named after its hostname + old = None + if isinstance(old_key, str): + old = next((c for c in trust["clients"] + if hmac.compare_digest(c.get("key_sha256", ""), sha256(old_key))), None) + if old: + name = old["name"] + trust["clients"].remove(old) + else: + name = unique_client_name(wanted, trust["clients"]) trust["clients"].append({"name": name, "key_sha256": sha256(key), "added": time.strftime("%Y-%m-%d %H:%M"), "from": self.client_address[0]}) @@ -969,11 +987,12 @@ def daemon_enable(): if not wait_listening(host, port, 10): die(f"terractld isn't listening on {describe_listen(host, port)}; see: journalctl -u terractld") print(f"terractld is running and starts at boot, listening on {describe_listen(host, port)}.") - print(f"Certificate fingerprint (SHA-256): {cert_fingerprint()}") - print() - print("To let a computer manage this host, create a token for it:") - print(" sudo terractl daemon trust add ") print(f"If a firewall is active, allow TCP port {port} (e.g. sudo ufw allow {port}/tcp).") + print() + if load_trust()["clients"]: + print("To add another computer: sudo terractl daemon token") + else: + print_token() return 0 @@ -983,7 +1002,7 @@ def daemon_disable(): if proc.returncode != 0: die(f"stopping terractld failed: {proc.stderr.strip()}") print("terractld is stopped and won't start at boot, so other hosts can't manage this one.") - print("The computers it trusts are kept for next time (terractl daemon trust list).") + print("The computers it trusts are kept for next time (sudo terractl daemon status).") return 0 @@ -1001,76 +1020,54 @@ def daemon_status(): except PermissionError: print("trusted: (run with sudo to see which computers)") trust = None - if trust is not None: - names = ", ".join(c["name"] for c in trust["clients"]) or "none yet" - print(f"trusted: {names}") - waiting = [t["name"] for t in trust["tokens"] if t["expires"] > time.time()] - if waiting: - print(f"tokens for: {', '.join(waiting)} (not used yet)") + if trust is not None and not trust["clients"]: + print("trusted: none yet (add one: sudo terractl daemon token)") + elif trust is not None: + print() + print(f"{'TRUSTED':<24} {'ADDED':<17} FROM") + for c in trust["clients"]: + print(f"{c['name']:<24} {c.get('added', '-'):<17} {c.get('from', '-')}") + unused = [t for t in trust["tokens"] if t["expires"] > time.time()] + if unused: + print(f"(and {len(unused)} unused token{'s' if len(unused) > 1 else ''})") if active != "active": print("Start it with: sudo terractl daemon enable") return 0 -def trust_add(name): - if not CLIENT_RE.fullmatch(name): - die(f"invalid name '{name}' (letters, digits and _ . - @, up to 64)") - open_state(f"daemon trust add {name}") +def print_token(): + """Creates a one-time token and prints the command that uses it.""" + open_state("daemon token") ensure_cert() secret = secrets.token_hex(32) with StateLock(): trust = load_trust() now = time.time() - trust["tokens"] = [t for t in trust["tokens"] if t["expires"] > now and t["name"] != name] - trust["tokens"].append({"name": name, "secret_sha256": sha256(secret), - "expires": int(now + TOKEN_HOURS * 3600)}) + trust["tokens"] = [t for t in trust["tokens"] if t["expires"] > now] + trust["tokens"].append({"secret_sha256": sha256(secret), "expires": int(now + TOKEN_HOURS * 3600)}) save_trust(trust) - known = any(c["name"] == name for c in trust["clients"]) host, port = listen_setting() - token = make_token({"v": 1, "fp": cert_fingerprint(), "s": secret, "a": host_addresses(host, port)}) + fields = {"v": 1, "fp": cert_fingerprint(), "s": secret, "a": host_addresses(host, port)} here = (socket.gethostname() or "").split(".")[0] - if not NAME_RE.fullmatch(here): - here = "" - print(f"On '{name}', run this to add this host as a remote called '{here}':") + if NAME_RE.fullmatch(here): + fields["n"] = here # the remote's name, unless the other computer picks one + print(f"To manage this host from another computer, run this there (works once, within {TOKEN_HOURS} hours):") print() - print(f" terractl remote add {here} {token}") + print(f" terractl remote add {make_token(fields)}") print() - print(f"The token works once, within {TOKEN_HOURS} hours.") - if known: - print(f"'{name}' is trusted already; joining again gives it a new key and the old one stops working.") if systemctl("is-active", DAEMON_UNIT).stdout.strip() != "active": print("The daemon isn't running yet; start it with: sudo terractl daemon enable") return 0 -def trust_list(): - try: - trust = load_trust() - except PermissionError: - die("this needs root: sudo terractl daemon trust list") - if not trust["clients"]: - print("No computers are trusted yet. Add one with: sudo terractl daemon trust add ") - else: - print(f"{'NAME':<24} {'ADDED':<17} FROM") - for c in trust["clients"]: - print(f"{c['name']:<24} {c.get('added', '-'):<17} {c.get('from', '-')}") - now = time.time() - for t in trust["tokens"]: - if t["expires"] > now: - hours = max(1, round((t["expires"] - now) / 3600)) - print(f"Token for '{t['name']}' not used yet (expires in {hours} h)") - return 0 - - -def trust_remove(name): - open_state(f"daemon trust remove {name}") +def daemon_revoke(name): + open_state(f"daemon revoke {name}") with StateLock(): trust = load_trust() clients = [c for c in trust["clients"] if c["name"] != name] - tokens = [t for t in trust["tokens"] if t["name"] != name] - if len(clients) == len(trust["clients"]) and len(tokens) == len(trust["tokens"]): - die(f"'{name}' isn't trusted (see: terractl daemon trust list)") - trust["clients"], trust["tokens"] = clients, tokens + if len(clients) == len(trust["clients"]): + die(f"no computer called '{name}' is trusted (see: sudo terractl daemon status)") + trust["clients"] = clients save_trust(trust) print(f"'{name}' can't manage this host any more.") return 0 @@ -1078,20 +1075,13 @@ def trust_remove(name): def daemon_main(argv): sub, rest = (argv[0], argv[1:]) if argv else ("", []) - if sub == "trust": - what = rest[0] if rest else "list" - if what == "add" and len(rest) == 2: - return trust_add(rest[1]) - if what == "list" and len(rest) <= 1: - return trust_list() - if what == "remove" and len(rest) == 2: - return trust_remove(rest[1]) - die("usage: terractl daemon trust add | trust list | trust remove ") + if sub == "revoke" and len(rest) == 1: + return daemon_revoke(rest[0]) commands = {"enable": daemon_enable, "disable": daemon_disable, "status": daemon_status, - "serve": daemon_serve} + "token": print_token, "serve": daemon_serve} if sub in commands and not rest: return commands[sub]() - die("usage: terractl daemon enable | disable | status | trust ...") + die("usage: terractl daemon enable | disable | status | token | revoke ") # --------------------------------------------------------------- client ---- @@ -1244,9 +1234,8 @@ class Remote: except (ValueError, KeyError, TypeError): message = f"HTTP error {resp.status}" if resp.status == 401: - die(f"{self.label} doesn't trust this computer (any more). On {self.label}, run " - f"'sudo terractl daemon trust add ', then here: terractl " - f"remote remove {self.name} && terractl remote add {self.name} {self.addr} ") + die(f"{self.label} doesn't trust this computer (any more). Run 'sudo terractl daemon " + f"token' there, then here: terractl remote add ") die(f"{self.label}: {message}") return resp @@ -1384,40 +1373,50 @@ def find_remote(spec): if host and conf["address"].lower() == host.lower() and int(conf["port"]) == port: return Remote(name, conf, via=f"--host {spec}") die(f"'{spec}' isn't one of your remotes (see: terractl remote list). To add it, run " - f"'sudo terractl daemon trust add ' there, then here: " - f"terractl remote add ") + f"'sudo terractl daemon token' there, then here: terractl remote add ") def remote_add(args): - if len(args) not in (2, 3): - die("usage: terractl remote add , or: terractl remote add
[]") - name = args[0] + usage = "usage: terractl remote add [] [--address [:]]" + address, rest = None, [] + args = iter(args) + for arg in args: + if arg == "--address": + address = next(args, None) or die(usage) + elif arg.startswith("--address="): + address = arg.split("=", 1)[1] + elif arg.startswith("-"): + die(f"unknown option '{arg}'") + else: + rest.append(arg) + if len(rest) not in (1, 2): + die(usage) + name, token = (None, rest[0]) if len(rest) == 1 else rest + fields = read_token(token) + remotes = load_remotes() + # Adding a host that's a remote already (same certificate) replaces that + # remote: after its access was revoked, say, or to rename it + old = next((n for n, c in sorted(remotes.items()) if c["fingerprint"] == fields["fp"]), None) + name = name or old or fields.get("n") + if not name: + die("give the remote a name: terractl remote add ") if not NAME_RE.fullmatch(name): die(f"invalid remote name '{name}' (letters, digits and _ . -, not starting with . or -)") - remotes = load_remotes() - if name in remotes: - die(f"there's a remote called '{name}' already (to replace it: terractl remote remove {name})") - if len(args) == 3: - address, token = args[1], args[2] - elif args[1].startswith(TOKEN_PREFIX): - address, token = None, args[1] - else: - address = args[1] - if not sys.stdin.isatty(): - die("no token given") - token = getpass.getpass("Token (from 'sudo terractl daemon trust add' on that host): ") - fields = read_token(token) + if name in remotes and name != old: + die(f"'{name}' is already the name of another remote; choose another: terractl remote add ") candidates = [split_hostport(address)] if address else [split_hostport(a) for a in fields.get("a", [])] if not candidates: - die("the token doesn't say where the host is; give its address: " - "terractl remote add
") + die("the token doesn't say where the host is; add --address ") + join = {"secret": fields["s"], "name": (socket.gethostname() or "").split(".")[0]} + if old: + join["old_key"] = remotes[old].get("key", "") problems = [] for host, port in candidates: remote = Remote(name, {"address": host, "port": port, "fingerprint": fields["fp"]}) remote.joining = True remote.label = remote.addr try: - reply = remote.call("POST", "/1.0/trust/join", {"secret": fields["s"]}, auth=False, timeout=5) + reply = remote.call("POST", "/1.0/trust/join", join, auth=False, timeout=5) break except Unreachable as e: problems.append(str(e)) @@ -1425,7 +1424,9 @@ def remote_add(args): if len(problems) == 1: die(problems[0]) die("couldn't reach the host at any address in the token:\n " + "\n ".join(problems) + - "\nGive an address that works from here: terractl remote add
") + "\nGive an address that works from here: terractl remote add --address ") + if old: + del remotes[old] remotes[name] = {"address": host, "port": port, "fingerprint": fields["fp"], "key": reply["key"], "client_name": reply["name"], "added": time.strftime("%Y-%m-%d %H:%M")} save_remotes(remotes) @@ -1437,7 +1438,7 @@ def remote_add(args): about = f"{info['hostname']} at {remote.addr}, terractl {info['version']}" except Fail: about = remote.addr - print(f"added remote '{name}' ({about}); it knows this computer as '{reply['name']}'") + print(f"{'updated' if old else 'added'} remote '{name}' ({about}); it knows this computer as '{reply['name']}'") print(f"Try: terractl --host {name} list") return 0 @@ -1447,7 +1448,7 @@ def remote_list(args): die("usage: terractl remote list") remotes = load_remotes() if not remotes: - print("No remotes yet. Add one with: terractl remote add ") + print("No remotes yet. Run 'sudo terractl daemon token' on a host, then here: terractl remote add ") return 0 print(f"{'NAME':<20} {'ADDRESS':<30} FINGERPRINT") for name, conf in sorted(remotes.items()): @@ -1463,8 +1464,8 @@ def remote_remove(args): if conf is None: die(f"there's no remote called '{args[0]}' (see: terractl remote list)") save_remotes(remotes) - print(f"removed remote '{args[0]}'. It still lists this computer as '{conf.get('client_name', '?')}'; " - f"to revoke that too, run there: sudo terractl daemon trust remove {conf.get('client_name', '')}") + print(f"removed remote '{args[0]}'. It still trusts this computer; to revoke that too, run there: " + f"sudo terractl daemon revoke {conf.get('client_name', '')}") return 0 @@ -1940,6 +1941,10 @@ if __name__ == "__main__": except Fail as e: print(f"terractl: {e}", file=sys.stderr) sys.exit(1) + except BrokenPipeError: + # Output went into something that stopped reading (e.g. head) + os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) + sys.exit(1) except OSError as e: where = f": {e.filename}" if e.filename else "" print(f"terractl: {e.strerror or e}{where}", file=sys.stderr) diff --git a/systemd/terractld.service b/systemd/terractld.service index d1ac0ce..1023e32 100644 --- a/systemd/terractld.service +++ b/systemd/terractld.service @@ -8,7 +8,7 @@ Wants=network-online.target Type=simple # Listens on TERRACTL_LISTEN from /etc/default/terractl (default: all addresses, # port 7776). Its certificate and the list of trusted computers are kept in -# /var/lib/terractl. Manage it with: terractl daemon enable|disable|status|trust +# /var/lib/terractl. Manage it with: terractl daemon enable|disable|status|token|revoke ExecStart=@BINDIR@/terractl daemon serve Restart=on-failure RestartSec=5