aq.dev / docs/private-networks

Connecting to a private network (Tailscale and similar)

Sometimes an agent needs to reach a machine that is not on the internet: a test rig, a lab device, a staging database behind your VPN. AQ does not install or manage Tailscale, ZeroTier or any other overlay network. You run your own, from your repository's setup command, as your own user on the machine, with no sudo. This page shows a safe way to do that with Tailscale; the same rules apply to any similar tool.

What AQ does and does not do

  • Nothing is preinstalled. The setup below downloads Tailscale's official static build into your own directories.
  • Everything runs as your OS user, in userspace networking mode. No root, no network interface, no change to the machine's routing.
  • On AQ's machines, ordinary users cannot reach the cloud metadata server, and AQ's cloud machines sit on networks that hold none of AQ's internal services, whatever tool runs on them. Your tunnel reaches your network and the public internet.
  • AQ watches for tunnels and proxies on its machines. A node that advertises routes or offers itself as an exit node, Tailscale SSH, or a proxy that listens where other users can reach it is flagged for review. On a Free sandbox, a node that advertises routes or an exit node is stopped. On paid machines AQ only reports; it does not stop your processes.

Safe settings

  • Ephemeral, pre-authorized, tagged nodes. Each machine joins as an ephemeral node with its own tag, for example tag:aq. Use a tag only for AQ; never reuse one another provider's machines already carry.
  • Shields up. Start with --shields-up, so nothing on your tailnet can open a connection to the AQ machine.
  • No --ssh, --advertise-routes or --advertise-exit-node. The AQ machine only connects out. AQ flags all three.
  • Userspace networking, no TCP proxy. Run tailscaled --tun=userspace-networking. Reach hosts with tailscale nc or tailscale ssh, which talk to the daemon over its unix socket. Do not start --socks5-server or --outbound-http-proxy-listen on a machine others use: those listen on a TCP port, and every other user on the machine could send traffic through your tailnet. If a tool needs a proxy, put it on a unix socket inside a directory only you can read (mode 0700).
  • State in your own directory. Keep the daemon's socket and state in a 0700 directory under your home, never in /tmp or a shared path.

An OAuth client, saved as an AQ secret

In the Tailscale admin console, create an OAuth client with the auth_keys scope and select only your AQ tag. It can then mint auth keys for that tag and nothing else. Save its secret in AQ as a personal or organization secret, for example TS_OAUTH_CLIENT_SECRET, limited to the repository that needs it. The setup passes it to tailscale up with ephemeral=true&preauthorized=true, so the node joins without approval and leaves when it goes away.

Access controls

Give the tag only what it needs. In your tailnet policy file, declare the tag and allow it to reach the rigs on port 22 and nothing more. With no rule pointing at tag:aq, nothing on your tailnet can start a connection to it.

{
  "tagOwners": {
    "tag:aq": ["autogroup:admin"],
    "tag:rig": ["autogroup:admin"]
  },
  "acls": [
    { "action": "accept", "src": ["tag:aq"], "proto": "tcp", "dst": ["tag:rig:22"] }
  ]
}

Example setup

Add this to the repository's setup command (or a script it calls). It downloads a pinned release, checks it against Tailscale's published checksum, and starts the daemon in your home. The variables at the top are the only things to change.

#!/usr/bin/env bash
set -euo pipefail
TS_VERSION=1.104.7
TS_TAG=tag:aq
ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
TS_DIR="$HOME/.aq-tailscale"
PKG="tailscale_$TS_VERSION"_"$ARCH"
install -d -m 0700 "$TS_DIR"
cd "$TS_DIR"

if [ ! -x "$PKG/tailscaled" ]; then
  url="https://pkgs.tailscale.com/stable/$PKG.tgz"
  curl -fsSLo ts.tgz "$url"
  echo "$(curl -fsSL "$url.sha256")  ts.tgz" | sha256sum -c -
  tar -xzf ts.tgz
  rm ts.tgz
fi
TS="$TS_DIR/$PKG"
SOCK="$TS_DIR/tailscaled.sock"

if ! "$TS/tailscale" --socket="$SOCK" status >/dev/null 2>&1; then
  nohup "$TS/tailscaled" --tun=userspace-networking \
    --statedir="$TS_DIR/state" --socket="$SOCK" >"$TS_DIR/tailscaled.log" 2>&1 &
  sleep 2
fi

"$TS/tailscale" --socket="$SOCK" up \
  --auth-key="$TS_OAUTH_CLIENT_SECRET?ephemeral=true&preauthorized=true" \
  --advertise-tags="$TS_TAG" --shields-up --hostname="aq-$(hostname -s)"

Then reach a rig over SSH through the daemon, with no proxy port:

ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
TS="$HOME/.aq-tailscale/tailscale_1.104.7_$ARCH"
SOCK="$HOME/.aq-tailscale/tailscaled.sock"
ssh -o ProxyCommand="$TS/tailscale --socket=$SOCK nc %h %p" robot@rig-01

The same ProxyCommand line works in ~/.ssh/config, so git, scp and rsync over SSH use it too.

Teardown

Ephemeral nodes remove themselves from your tailnet 30 to 60 minutes after they go quiet, so a work item that ends, or a machine that sleeps, leaves nothing behind. To remove a node at once, run tailscale --socket="$SOCK" logout before stopping the daemon. Revoking the OAuth client stops every new machine from joining.

Other tools

ZeroTier, WireGuard in userspace, cloudflared and similar tools follow the same rules: run them as your user from the setup, connect out only, keep any proxy on a unix socket in a 0700 directory, and never advertise routes or offer an exit node from an AQ machine. More on how machines are isolated: security and isolation.