/ documentation

Outposts: your own hardware

Connect a machine you already own (a raspberry pi, a mac mini, a gpu box) so it appears in your machine list, serves a real HTTPS URL, and is reachable from your machines by name. No port forwarding, no dynamic dns, no reverse proxy of your own.

What an outpost is

An outpost is your hardware, connected to ironwire by a small agent it runs. The agent dials out to us; nothing on the internet, and nothing on your own network, connects to it. It appears in list beside your machines, answers to the same SSH key, and serves the same https://<name>-<handle>.<content-domain> URL scheme.

$ ssh dashboard@ironwire.sh list
NAME       KIND     STATUS              IMAGE    IP         FLAGS  URL
api        vm       running             ubuntu   10.0.0.5   -      https://api-alice.ironwire.xyz
homelab    outpost  online              —        —          -      https://homelab-alice.ironwire.xyz
studio     outpost  offline · 4h ago    —        —          -      https://studio-alice.ironwire.xyz

List them, from wherever you are:

ssh

$ ssh dashboard@ironwire.sh outpost
NAME             STATUS                       URL
homelab          online                       https://homelab-alice.ironwire.xyz
studio           offline · last seen 4 hours ago  https://studio-alice.ironwire.xyz

dashboard

Outposts sit in the machine list with KIND=outpost; open one for its state, URL and uptime.

It is not a machine we run. We own no hypervisor on your hardware, so create, resize, stop, start, pause, snapshot and copy do not apply: they answer <name> is an outpost; it cannot be …. Outposts have three states of their own: pending (a code was issued for a script, and no machine has used it), online, and offline carrying the time it was last seen.

Outposts are included in the price. They draw no cpu, memory or disk from your pool and appear on no invoice line: you are paying for the control plane, not for compute you already own. A per-account limit exists as an abuse control; see Quotas.

Connecting one

On the machine you want to connect, install the agent and run it:

$ curl -sSL https://ironwire.sh/connect | sh
$ sudo ironwire-outpost

It sets itself up as a service that starts at boot, then shows a short code and waits:

⏳ Waiting for approval

    KXRT-4419    expires in 14:32

Approve it from the account that should own this machine:
  • dashboard:  a → Outposts → add → enter the code
  • or run:     ssh dashboard@ironwire.sh outpost approve KXRT-4419

Approve it from your account, whichever way is handy. Whichever account approves the code owns the machine, so if you have more than one ironwire identity, approve from the one you mean (add -i <key> to the ssh command):

ssh

$ ssh dashboard@ironwire.sh outpost approve KXRT-4419
outpost hamids-mac-mini approved

dashboard

Press a for your account, open Outposts, press a to add, and type the code. The name is filled in from the machine's own name; change it if you like, then ↵.

The screen on the machine switches to connected and shows which account it joined. The code works once and expires after 15 minutes; the machine shows a fresh one when it does. Nothing is copied from ironwire to the machine, and the agent already knows how to check it is talking to ironwire.

--name <name>Name it something other than the machine's own name. Names share one namespace with your machines.
--replaceThe name belongs to an existing offline outpost (say, this machine was reinstalled): make this machine that outpost, keeping its URL, custom domain and private address. An outpost that is online must be removed first.

A fresh machine joins sharing nothing: no URL, no ports, no shell. You choose what it shares next, on the machine. Root is needed on the machine and only there: to place the binary and to manage the service.

What the machine shares

Run sudo ironwire-outpost again on the machine and press s to change what it shares. It takes effect straight away, with no new code and no approval. You decide this at the machine; nothing on our side can widen it: not a setting, not support, not us.

web portThe local port your main HTTPS URL fronts. Leave it empty for no public URL.
extra portsPublish more ports as <public>:<local>, reachable at https://<name>-<handle>.<content-domain>:<public>.
shell + filesOffer an SSH shell and the file manager. Off by default: without it the machine runs no shell service at all, so there is nothing to log into.
shell logs in asThe dedicated service account, or you (the login that ran sudo).
files folderThe directory the file manager may see. A guardrail against mistakes, not a security boundary; do not rely on it to contain something determined.

Internal or public

The machine decides what it shares; your account decides who can reach each share. A share is either internal, reachable only by your own machines at <name>.internal:<port>, or public, also on its HTTPS URL or public port. A newly shared port starts internal: nothing reaches it from the internet until you make it public.

ssh

$ ssh dashboard@ironwire.sh outpost port homelab web public
$ ssh dashboard@ironwire.sh outpost port homelab 8443 internal

dashboard

Open the outpost's actions with space, choose ports, and press p on a share to switch it between public and internal.

web is the web port; any other value is a published port's public number, which must be between 8100 and 9999 to be made public. It takes effect at once, without touching the machine. For example, a headless browser's control port can be shared internally so every machine in your account reaches studio.internal:9222 while nothing on the internet can.

The same from a script, with flags (only the ones you pass change anything):

$ sudo ironwire-outpost share --http 30000 --publish 8443:30003
$ sudo ironwire-outpost share --shell --files /srv/data
$ sudo ironwire-outpost share --no-shell

For scripts

Everything the screen does has a command:

sudo ironwire-outpost join [flags]Set the machine up and ask to join; prints the code and waits until it is approved (--no-wait returns at once). Share flags: --http, --publish, --shell, --files, --as-user.
ironwire-outpost status [--json]Where it stands: waiting (with its code), connected (name, account, URL), removed, or why it cannot connect; and what it shares.
sudo ironwire-outpost share …Change what it shares (above).
sudo ironwire-outpost disconnectLeave ironwire and remove everything from this machine, in one step.

To link with no one at a keyboard, issue a one-time code from your account and pass it to the machine:

$ ssh dashboard@ironwire.sh outpost add homelab
$ # on the machine:
$ sudo ironwire-outpost link <code> --http 3000

That code works once and expires in 30 minutes. A code always (re)links the machine it is typed on, even one that was linked before.

Living with it

outpost info shows its state, URL, published ports, whether a shell was offered, the machine it runs on, and uptime over 24 hours / 7 days / 30 days:

$ ssh dashboard@ironwire.sh outpost info homelab
outpost:  homelab
status:   online
url:      https://homelab-alice.ironwire.xyz
port:     https://homelab-alice.ironwire.xyz:8443 (local 30003)
port:     internal — homelab.internal:9222 (local 9222, your machines only)
access:   shell offered, files under /srv/data
machine:  linux/arm64 raspberrypi (agent 1.0.0)
uptime:   99.8% 24h · 99.1% 7d · 98.7% 30d
current:  up 6d 4h

With a shell shared, ssh homelab@ironwire.sh gives you a shell on your own machine, and the file manager opens a pane on it. Without it, there is nothing listening and the connection is refused saying so.

Running a command on it (ssh homelab@ironwire.sh '<cmd>') and copying files with a tar stream over SSH work on it too, exactly as they do on a machine (the same forwarded exit code, the same two-way copy), so a script never has to know which kind of thing it was handed. Both need the shell shared: without one there is no worker process on that hardware to run anything, and you are told so rather than left waiting.

The dashboard opens it too: press ↵ on the outpost's row for a shell in the same framed window your machines use, with the same tabs, F2 to detach and F3 to switch between open shells. If the machine goes to sleep while you are in it, you land back on the dashboard and are told it went offline.

Press space on the row for its actions. An outpost is offered only what its hardware can do: a shell, its published ports, a custom domain, whether it takes orders from your machines, and removing it. There is nothing to start, stop, restart, resize or copy, because we run no hypervisor on your machine. The file manager is f from the list, the same as for a machine.

Your machines reach it privately by name at <name>.internal on every port it shares, internal or public; see Networking. Machines in other accounts cannot reach it at all.

Reaching its shell, files, run or cp from inside one of your machines is a different question: that needs fleet access on the machine doing the reaching, and it stops entirely if you turn takes orders off for the outpost (b in its actions popup). Your own access never changes, and neither does its URL or its ports.

Attach your own domain exactly as you would for a machine: outpost domain homelab app.example.com, then point a CNAME at the target it prints.

When it is offline

A machine at home sleeps, loses power, or loses its uplink. That is normal, not a fault.

Its status becomes offline with the time it was last seen and, when we know it, why: stopped answering (it went quiet, usually asleep or unplugged), disconnected (the connection dropped), or refused: … with the reason, for example a lapsed account. The same reason is in list, outpost info and the offline_reason field of list --json.

$ ssh dashboard@ironwire.sh outpost
NAME             STATUS                                            URL
studio           offline · stopped answering · last seen 4 hours ago  https://studio-alice.ironwire.xyz

Its URL serves a page saying the outpost is offline and when it was last connected, not a blank gateway error, so you can tell "my pi is asleep" from "ironwire is down". Nothing is retried or escalated by us: the agent reconnects on its own when the machine comes back, including after a reboot, with no login and nothing to do. On the machine, sudo ironwire-outpost shows what it sees; if it cannot reach ironwire at all, it says so, and notes when outbound UDP port 7823 may be blocked on that network.

Removing it

From the machine: sudo ironwire-outpost disconnect (or d on its screen) removes it from your account and from the machine in one step. Nothing is left behind. If ironwire cannot be reached at that moment, the machine is still cleaned up, and the outpost stays in your account as offline until you remove it there.

From your account: outpost rm homelab drops it, cuts the tunnel, and frees the name and URL. We do not run destructive commands on hardware we do not own, so the agent stays on the machine. Its screen says it was removed and offers to join again (j), or you can remove it from there with disconnect.

$ ssh dashboard@ironwire.sh outpost rm homelab
outpost homelab removed. The machine will show that it was removed, and can join again with `sudo ironwire-outpost` — or leave for good with `sudo ironwire-outpost disconnect`.

The dashboard's delete action on an outpost does the same thing and says so before you confirm: it forgets the outpost here and leaves your machine running.

A machine that was reinstalled or wiped simply runs sudo ironwire-outpost again and shows a new code; approve it with --replace to keep the old outpost's name, URL and domain.

Linux and macOS are not equal

Both are supported and both survive reboot. The difference is what the operating system can enforce.

On Linux, the services run under kernel-enforced restrictions: the tunnel process cannot read any of your files, the shell service can only see the directory you named, and neither can reach the rest of your network.

On macOS there is no equivalent. You get a dedicated, hidden service account (created for you during setup) and the system's own permission prompts, and nothing more.

macOS also guards ~/Documents, ~/Desktop, ~/Downloads and Photos, and a background service cannot be asked for consent; there is no session to show the prompt in. Those paths do not fail, they hang. Reading a file's extended attributes there blocks forever, so ls -l@, eza -l and anything else that reads them appears to freeze (in your home directory too, since it contains those folders). Plain stat and directory listing are unaffected, which is why ls -la looks fine.

To make them readable, grant Full Disk Access to /usr/local/bin/ironwire-outpost in System Settings › Privacy & Security, then restart the shell service with sudo launchctl kickstart -k system/sh.ironwire.outpost-shell.

See the command reference for full outpost syntax, and fleet access & orders for reaching an outpost from inside one of your machines.