---
title: "Remote machines"
url: "https://docs.phux.sh/remote"
source: "https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/remote.md"
projectVersion: "0.112.0"
protocolVersion: "1.0.0"
stability: "stable"
---

# Remote machines

phux has no accounts and no hosted service. Each machine runs its own server, and you tell one machine how to reach another once, over the SSH access you already have. After that, you attach to the other machine's sessions directly, from the terminal or the desktop app.

```sh
phux host add me@mini
phux attach mini
```

**Access to a phux server is access to everything it can do**, including
starting programs as the user it runs as. A server listens only on a local
socket until you set it up for remote use, and every remote connection must
present credentials from pairing.

## Adding a machine

Run this on your own machine:

```sh
phux host add me@mini --install
```

Over `ssh me@mini`, this:

1. Checks that `phux` is on the remote `PATH`. With `--install`, it installs
   phux there with the [install script](/installation#install-script) when
   it's missing, or runs `phux update` there when it's older than yours,
   both on your release channel. Without `--install`, a missing phux is an
   error.
2. Installs a service there, so the server survives logouts and reboots.
3. Sets the remote server's `[policy] mode` to `"paired"` and opens a QUIC
   listener on UDP port 8788.
4. Issues a certificate and token for this machine.
5. Saves the whole thing under the name `mini` in your config.

There is nothing to copy by hand. Run `phux host add me@mini` again whenever
the host stops answering: it checks the setup and repairs what broke.

If a non-interactive SSH shell doesn't find `phux` on the remote machine, as
with some Homebrew or Nix installs, say where it is:

```sh
phux host add me@mini --remote-phux /opt/homebrew/bin/phux
```

The machine's entry remembers that path, and where `--install` put phux, so
later attaches, server restarts, and `phux host renew` run the same one.

| Option | Does |
| --- | --- |
| `--name NAME` | Saves the machine under another name. |
| `--install` | Installs phux on the machine, or updates an older one, before setting it up. |
| `--accept-new-host-key` | Trusts the SSH host key of a machine you have never connected to; a changed key is still refused. |
| `--quic-port PORT` | Uses another UDP port than 8788. |
| `--endpoint HOST:PORT` | Registers this address instead of the ones found on the host. |
| `--no-service` | Starts the server without a service, so it won't come back after a reboot. |
| `--ssh-only` | Registers an `ssh://` entry without contacting the host. |
| `--session NAME` | Attaches to this session on arrival. |

## Machines on your tailnet

With [Tailscale](https://tailscale.com), phux lists the machines on your
tailnet so you don't have to type their names:

```sh
phux host discover            # every Mac and Linux machine on the tailnet
phux host discover --login    # sign in to Tailscale, or reconnect it, first
```

It reads the Tailscale client on this machine and shows each machine's
`MagicDNS` name, whether it's online, and whether you already added it. Add
one by that name:

```sh
phux host add mini.tail1234.ts.net --name mini --install --accept-new-host-key
```

`--accept-new-host-key` is safe here because the tailnet already vouches for
the address. Machines that run Tailscale SSH need no SSH key.

`--login` opens the Tailscale app if it isn't running, reconnects a client
that's signed in but disconnected, or starts a sign-in and prints its link.
It doesn't wait for the browser: run `phux host discover` again afterwards.

The desktop app does all of this from **Add Machine…** (in the File menu,
the palette, and Settings > Hosts & connections): it lists your tailnet's
machines, signs you in to Tailscale in the browser, and shows each setup
step as it happens.

## Using a machine

```sh
phux attach mini                 # QUIC straight to the server; SSH is out of the path
phux ls --all                    # sessions here and on every registered machine
phux new --remote mini scratch   # create a session there
phux host ls                     # registered machines
```

`ls`, `new`, `kill`, `rename`, `detach`, `upload`, `download`, and `whoami`
take `--remote NAME`. Pane commands such as `snapshot`, `send-keys`, `run`,
and `wait` act on the server they connect to. To script panes on another
machine, run `phux` there over SSH, or [link the machines](#linking-machines)
so its panes show up here as `mini/@7`.

Several terminals and the desktop app can attach to the same remote session
at once.

## Who can connect

A server's `[policy] mode` decides who can connect:

| Mode | Lets in |
| --- | --- |
| unset or `local` | Only programs on this machine, through the local socket. Configuring any network listener or relay makes the server refuse to start. |
| `paired` | The local socket, plus network connections that present an enrolled client certificate and a token. |

`phux host add` sets `paired` for you.

A remote connection needs two things from pairing: a client certificate
issued by the server's own certificate authority, and a token. The client
pins that certificate authority when it pairs and refuses to connect if it
changes. A token alone never gets anyone in.

Certificates from `phux host add` last 90 days. Attaching warns, and
`phux doctor` reports, when one is within two weeks of expiring.
`phux host renew mini` replaces it.

## Who can do what

**A machine you add with `phux host add` can do everything you can do on the
remote server**, including typing into panes. Only add machines you control.

A CI runner, or a person who should list sessions but not type, gets a
separate certificate with a narrower scope from `phux workload add-key`. The
steps, expiry, rotation, and revocation are in
[Host-scoped read keys](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/runbooks/host-observe-keys.md).

Programs on the server's own machine are different. **The local socket gives
full access to any program running as the same user.** phux can't tell those
programs apart.

## Attaching over SSH

`phux attach --ssh` needs no pairing and no service:

```sh
phux attach --ssh me@box
```

SSH logs you in (password and 2FA prompts work), starts the server on the
remote machine if needed, and opens a QUIC (UDP) listener for this attach
alone.
Then SSH exits and the session runs directly over QUIC, so it survives
network changes and draws locally. The listener and its token live only in
memory and go away shortly after you detach.

The remote server's policy must allow the listener, and UDP must reach it.
If it can't, phux says why and falls back to `ssh -t box phux attach`: the
terminal UI runs on the remote machine and SSH carries its screen. An SSH
login failure, or a remote machine without phux, is an error, not a
fallback.

`--udp-ports 60000-61000` keeps the listener in a range one firewall rule
can cover.

## Pairing with a link

When you can't SSH to the server, pair with a link instead. On the server
machine:

```sh
phux pair
```

This prints a pairing link and a QR code. On the client:

```sh
phux attach --remote mini --code 'https://phux.sh/connect?...'
```

**The link is a secret.** Anyone who has it can connect. The token in it
works until you revoke it, or until `--expires` if you set one. The
enrollment ticket that comes with a QR code works once and expires after ten
minutes. A printed or piped link needs `--enroll` to include one.

`phux pair --qr` shows only the QR code, for pairing a phone without showing
the token on screen. `phux pair --secret-file PATH` writes the link to a
private file instead of printing it.

## Relays

When neither machine can reach the other directly, run a relay somewhere both
can reach:

```sh
phux relay pair --route home     # on the relay machine: prints a token and a fingerprint
phux relay run --listen 0.0.0.0:4433
```

Give the server a `[[connector]]` entry with the relay's address, the token
file, and the relay's fingerprint, then mint the client's link on the server:

```sh
phux pair --relay-route home
```

The server dials out to the relay, so it needs no open port. **The relay sees
only ciphertext.** Every connection runs TLS end to end with the server,
inside the relayed stream. `--max-conns` caps how many connections a relay
accepts (64 by default).

## Linking machines

A **hub** is a server that also lists and routes to other servers, its
**satellites**. A satellite's panes show up on the hub with targets like
`edge/@7`.

```sh
phux host add me@edge --role satellite    # on the hub machine
phux service install --hub                # run this machine's server as a hub
```

A satellite that can't accept connections, such as a sandbox behind NAT,
dials out to the hub instead:

```sh
phux host ticket sandbox     # on the hub: prints a code good for ten minutes
phux host join -             # on the sandbox: paste the code
```

**Joining a hub gives the hub full control of the satellite.** The hub can do
anything on the satellite that you can, as the user the satellite's server
runs as. Join only a hub you own and trust. The hub must run in `paired` mode;
the satellite can stay `local`.

An expired code stops new joins but doesn't undo one. To cut off a joined
satellite, revoke its credential on the hub with `phux workload revoke`, and
remove it with `phux host rm sandbox`.

## Managing credentials

```sh
phux pair ls                     # tokens this server has issued
phux pair revoke ID
phux pair prune --unused-for 30d
phux workload list               # enrolled client certificates
phux workload revoke sha256:...
phux host renew mini             # replace this machine's certificate
```

Revoking a credential refuses new connections and closes the live ones it
let in, without a restart. It doesn't stop any program running in a pane.
Revoke a credential with these commands, not by editing files.

Removing a machine from your list (`phux host rm`) only forgets it here. It
doesn't revoke anything on the server.

[File locations](/reference/files) lists every credential file on both
sides.

## Troubleshooting

`phux doctor` on the server machine checks the listener, the certificate,
and whether the advertised address is reachable.

**`could not install phux`.** The install script failed on the machine.
Release builds exist for Apple silicon macOS and glibc Linux on x86_64 and
arm64. Run the script there yourself to see all of its output.

**`Host key verification failed`.** SSH has never connected to that machine.
Connect once with `ssh` to check and save its key, or pass
`--accept-new-host-key` when something else, such as a tailnet, vouches for
the address.

**`no answer within 5s; is UDP filtered?`** `phux host add` couldn't reach
UDP 8788, so it registered an `ssh://` route instead. Attaching over it
tries direct QUIC and falls back to `ssh -t` running the terminal UI
there. Open the port, or use a relay.

**The token was revoked or the server's certificate authority changed.**
The error names the machine and says to run `phux host add NAME` again.

**`incompatible protocol`.** The two machines run releases too far apart. The
error names both versions. `ssh HOST phux update` updates the remote binary,
and `ssh HOST phux upgrade` moves its running server onto the binary already
installed there.

More in [Troubleshooting](/troubleshooting).