# Underpass documentation Source: https://docs.getunderpass.com --- # Quickstart URL: https://docs.getunderpass.com/getting-started/quickstart By the end of this page, two of your devices will be in one private network and able to reach each other by name. ## 1. Create an account Go to [app.getunderpass.com](https://app.getunderpass.com) and sign in with **Google** or with a **one-time code** sent to your email. Your workspace, your own private network, is created right away. It's free for up to 6 people and 100 devices. ## 2. Install on your first device - **Windows:** download and run the installer from [getunderpass.com/download](https://getunderpass.com/download). - **macOS or Linux:** run ```bash curl -fsSL https://coord.getunderpass.com/download/install.sh | sh ``` A link opens, or is printed in the terminal. Sign in with the same account. See [Install](/getting-started/install) for details. ## 3. Install on a second device Do the same on another computer, server or Raspberry Pi, and sign in with the same account. For a server you can't open a browser on, use an [auth key](/getting-started/add-a-server) instead. ## 4. Check they can see each other On either device: ```bash underpass status ``` You'll see the other device listed, with its address, its name and how it's connected: ```text 100.64.0.7 laptop.underpass.internal connected (https://coord.getunderpass.com) ADDRESS NAME OS PATH LATENCY HANDSHAKE ROUTES 100.64.0.21 db-prod linux direct 203.0.113.7:41642 11.2 ms 4s ago - ``` ## 5. Connect Use the other device's **name** anywhere you'd use an address: ```bash ping db-prod ssh you@db-prod ``` Or open a web app running on it, like `http://db-prod:3000`. ## Next - [Invite your team](/manage/people) - [Add servers with an auth key](/getting-started/add-a-server) - [Decide who can reach what](/features/access-controls) - [Give an internal app a name, with failover](/features/services) --- # Install URL: https://docs.getunderpass.com/getting-started/install Install the app on every device you want in your network. Each install runs a small **background service** that keeps the device connected, even when nobody is signed in to the computer. Every download is on [getunderpass.com/download](https://getunderpass.com/download). You don't need an account to download, only to connect. | Platform | What you get | |---|---| | Windows 10 and 11 (64-bit) | Installer with the app window, tray icon and command line | | macOS (Apple silicon and Intel) | Command line and background service, plus the Mac app | | Linux (x86-64 and ARM64, including Raspberry Pi) | Command line and a systemd service | ## Windows 1. Download and run **UnderpassSetup.exe**. It asks for administrator rights. 2. When it finishes, the Underpass window opens and your browser opens to sign in. 3. Sign in, and approve the device if you're asked to. The installer sets up: - the background service (**Underpass**, starts automatically) - the **tray icon**, started at every sign-in - the **Underpass** app in the Start menu - the command-line tool `tailnet.exe` in `C:\Program Files\Tailnet` > [!NOTE] > On Windows the command-line tool is called `tailnet`, and its folder isn't added to `PATH`. Run it by its full path, for example `& "C:\Program Files\Tailnet\tailnet.exe" status` in PowerShell. Everything in these docs written as `underpass …` works the same as `tailnet …` there. To update, run the newest installer over the old one. Your device keeps its name and address. ## macOS and Linux Run this in a terminal: ```bash curl -fsSL https://coord.getunderpass.com/download/install.sh | sh ``` It: 1. downloads the build for your system and checks it against the published `SHA256SUMS` 2. installs the command-line tool to `/usr/local/bin/underpass` (with `tailnet` as a second name) 3. installs and starts the background service: - **macOS:** a launchd service, `dev.tailnet.tailnetd` - **Linux:** a systemd service, `tailnetd` (systemd is required) 4. prints a link: open it and sign in to connect the device On Linux, the installer also adds you to the `tailnet` group, so after you log out and back in once you can run `underpass` without `sudo`. To install without connecting right away: ```bash curl -fsSL https://coord.getunderpass.com/download/install.sh | UNDERPASS_NO_UP=1 sh ``` Then connect when you're ready with `sudo underpass up`. To update, run the install command again. ### The Mac app After the install command, download **Underpass-macos.zip**, unzip it and move **Underpass.app** to Applications. The app shows your connection and devices, and lets you sign in and out. It needs the background service from the install command. > [!NOTE] > The Mac app isn't notarized yet. The first time you open it, macOS blocks it: open **System Settings → Privacy & Security** and click **Open Anyway**. ## Check it worked ```bash underpass status ``` The first line shows this device's address and name, and `connected`. Below it, each other device and how it's reached (`direct` or `relay`). See [`status`](/reference/cli#status). ## Servers and headless machines On machines nobody signs in to, join with an auth key instead of a browser. See [Add a server](/getting-started/add-a-server). ## Uninstall ### Windows Use **Settings → Apps → Underpass → Uninstall**. Your device's keys are kept, so reinstalling brings it back as the same device. To remove the keys too: ```powershell & "C:\Program Files\Tailnet\tailnet.exe" uninstall --remove-keys ``` ### macOS ```bash sudo underpass uninstall # keeps this Mac's keys sudo underpass uninstall --remove-keys # removes them too ``` Then delete **Underpass.app** from Applications, and the leftover `/usr/local/bin/tailnet` link. ### Linux ```bash sudo systemctl disable --now tailnetd sudo rm /etc/systemd/system/tailnetd.service /usr/local/bin/underpass /usr/local/bin/tailnet sudo systemctl daemon-reload sudo rm -rf /var/lib/tailnetd # only if you also want to remove this machine's keys ``` To take a device out of your network, also remove it in the dashboard under **Machines**. --- # Add a server URL: https://docs.getunderpass.com/getting-started/add-a-server There are two ways to add a machine nobody sits at: | | Sign-in link | Auth key | |---|---|---| | Best for | A few servers you set up by hand | Scripts, images, CI and machines that come and go | | Runs as | The installed background service | Your own systemd unit | | Needs | A browser on **any** device | A key from the dashboard | ## With a sign-in link The sign-in link doesn't have to be opened on the server itself: open it on your laptop. 1. On the server, install and connect: ```bash curl -fsSL https://coord.getunderpass.com/download/install.sh | sh ``` 2. It prints a link: ```text To add this machine to your tailnet, open: https://app.getunderpass.com/… ``` Open it on any device where you're signed in, and approve the machine. 3. The server prints `Connected as db-prod (100.64.0.21).` It stays connected through reboots, because the service starts at boot. ## With an auth key An **auth key** lets a machine join on its own, with no browser. > [!IMPORTANT] > Auth keys work with the command-line tool running on its own, **not** through the background service the install command sets up. Use the steps below on machines where you haven't run the install command, or uninstall the service first (see [Install](/getting-started/install#uninstall)). ### 1. Create an auth key In the dashboard, open **Auth keys** and create one. Choose: - **Reusable** if several machines will use it; otherwise it works once. - **Ephemeral** for short-lived machines like CI runners: they're removed after 10 minutes offline. - **Pre-approved** so machines skip [device approval](/manage/machines#device-approval). - **Tags**, like `tag:prod`, to give the machines a role in [access rules](/features/access-controls). Tagged machines don't expire. - **Auth key expiration**: 1 to 90 days, 7 by default. The key starts with `tskey-auth-` and is **shown once**. Keep it in your secret manager. See [Auth keys](/manage/auth-keys). ### 2. Download the command-line tool ```bash ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/') sudo curl -fsSLo /usr/local/bin/underpass \ "https://coord.getunderpass.com/download/latest/underpass-linux-$ARCH" sudo chmod +x /usr/local/bin/underpass ``` Check it against `https://coord.getunderpass.com/download/latest/SHA256SUMS` if you like. ### 3. Join with the key ```bash sudo underpass up --auth-key tskey-auth-xxxxxxxxxxxx --hostname db-prod ``` It prints `registered as db-prod (100.64.0.21, …)` and keeps the tunnel up while it runs. `--hostname` is optional; without it, the machine's hostname is used. The registration is saved in `/var/lib/tailnet`. Stop the command with Ctrl+C once it's registered. ### 4. Keep it running Create `/etc/systemd/system/underpass.service`: ```ini [Unit] Description=Underpass After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/local/bin/underpass run Restart=always [Install] WantedBy=multi-user.target ``` Then: ```bash sudo systemctl daemon-reload sudo systemctl enable --now underpass ``` `underpass run` reconnects using the saved registration, so the key isn't needed again. ### In a provisioning script ```bash #!/bin/sh set -e ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/') curl -fsSLo /usr/local/bin/underpass "https://coord.getunderpass.com/download/latest/underpass-linux-$ARCH" chmod +x /usr/local/bin/underpass timeout 30 underpass up --auth-key "$UNDERPASS_AUTH_KEY" || true # registers, then stops after 30s # … write the systemd unit above, then: systemctl enable --now underpass ``` Run it as root, with a reusable (and for short-lived machines, ephemeral) key. ## Windows servers Install with **UnderpassSetup.exe**. When the browser opens for sign-in, finish it from any device, as above. ## If it doesn't connect | Message | What to do | |---|---| | `auth key is invalid, expired, or already used` | Create a new key, or a reusable one | | `Logged in. An admin needs to approve this machine…` | Approve it under **Machines**, or use a pre-approved key | | `this workspace's plan allows N machines` | Remove a machine or [upgrade](/manage/plans) | | `start tunnel (needs root/administrator)` | Run the command with `sudo` | See [Troubleshooting](/reference/troubleshooting) for more. --- # How it works URL: https://docs.getunderpass.com/concepts/how-it-works Underpass has three parts. A small app runs on each of your devices. A **coordination service** tells those devices about each other. A **relay** carries traffic only when two devices can't reach each other directly. Your data travels between your devices, encrypted with [WireGuard](https://www.wireguard.com/), and never through the coordination service. ## The coordination service The coordination service runs at `coord.getunderpass.com`. Each device keeps a connection open to it and is told whenever something in your network changes: a device joins or leaves, an address changes, an access rule is saved. It stores: - each device's **public** WireGuard key and discovery key, its name, address, operating system and app version - your workspace's settings: access rules, DNS settings, services, apps, routes and tags - tokens and keys as **SHA-256 hashes** only, never in plain text It never sees: - your devices' private keys, which never leave the device - the private keys of HTTPS certificates, which are made on the machine - any of your traffic > [!NOTE] > Devices that are already connected directly keep talking to each other even if the coordination service is briefly unreachable. New devices and changes wait until it's back. ## Joining a network 1. You install the app and sign in, or start it with an [auth key](/manage/auth-keys). 2. The device creates its keys and registers its public keys with the coordination service. 3. If [device approval](/manage/machines#device-approval) is on, an admin approves it. 4. The device gets a private address from `100.64.0.0/10` and a name, like `laptop`. 5. From then on it receives the list of the other devices it may reach, and keeps it up to date. A device counts as **online** if it has checked in within the last **90 seconds**. ## Direct connections Devices connect straight to each other whenever possible. To get through home routers, hotel Wi-Fi and most firewalls, each device: 1. **Learns its public address.** It asks a STUN server which address and port it appears as, every 20 seconds and whenever its network changes. 2. **Knocks at the same moment as the other side.** Routers only let in replies to traffic they've seen go out, so both devices send to each other at nearly the same time. Each router sees the other side's packet as a reply and lets it through. This is called hole punching. 3. **Keeps checking the path.** A direct path is checked every 3 seconds. If it stops answering for 8 seconds, traffic moves to the relay. Switching between direct and relay never restarts the connection: calls, SSH sessions and file copies keep going. ## The relay Some networks block every direct path. Then traffic goes through the relay. It: - forwards only **WireGuard ciphertext**: it can't read your data - connects only devices in the **same workspace**, using a signed pass from the coordination service that's valid for **10 minutes** and renewed automatically - limits each device to 2 MiB/s (4 MiB bursts), so one device can't slow down others While a connection is relayed, the devices keep trying for a direct path every 35 to 60 seconds, and move to it as soon as it works. To see how each of your devices is connected right now, run [`underpass status`](/reference/cli#status) and look at the `PATH` column: `direct` or `relay`. ## Addresses and names | What | Range or format | |---|---| | Devices | `100.64.0.0/10`, one address per device, unique across all workspaces | | [Services](/features/services) | `100.100.0.0/16` | | Device names | `.`, and the short `` on its own (see [Easy names](/features/easy-names)) | ## Encryption All traffic between devices is encrypted end to end with WireGuard. Each device has its own key pair, and its private key is stored only on that device. See [Security](/concepts/security) for the details. --- # Security URL: https://docs.getunderpass.com/concepts/security ## Encryption and keys - **Traffic** between devices is encrypted end to end with **WireGuard**. - Each device makes its own **WireGuard key pair** and a separate **discovery key** (X25519) when it first starts. The private keys are stored only in the device's state file, readable by the system only. They're never sent anywhere. - **Discovery messages**, the ones devices use to find a direct path, are encrypted with AES-256-GCM, using a key only the two devices can derive. - **HTTPS certificate keys** (ECDSA P-256) are made on the machine. Only a certificate request leaves it. ## What each part can see | Part | Can see | Can't see | |---|---|---| | Coordination service | Public keys, device names and addresses, your settings, hashed tokens | Private keys, your traffic | | Relay | Encrypted packets, and which two devices they're between | What's inside them | | Your devices | Traffic meant for them | Other workspaces' devices | Auth keys, API tokens and device tokens are stored only as **SHA-256 hashes**. Each key and token is shown to you once, when it's created. ## Workspaces are separate Every workspace is its own network. A device only learns about devices in its own workspace. The relay only forwards between devices of the same workspace; to anyone else, your devices look offline. ## Controls - **[Access controls](/features/access-controls).** Rules decide which people and devices can reach each other. Once a policy is saved, everything it doesn't allow is blocked, on each receiving device. - **[Device approval](/manage/machines#device-approval).** New devices wait for an admin before they can reach anything. - **[Key expiry](/manage/machines#key-expiry).** Devices sign in again after 180 days by default (you can choose 1 to 365, or turn it off). Tagged machines don't expire. - **[Two-factor sign-in](/manage/two-factor).** Accounts can require an authenticator app. A workspace can require it for everyone, and it's enforced by the database as well as the app. - **[Audit log](/manage/logs#audit-log).** Every change, sign-in and refused request is recorded. Each entry includes a fingerprint of the one before it, so editing, removing or reordering entries is detectable. ## Network requirements Devices need **no inbound ports**. They only make outgoing connections: - HTTPS to the coordination service - UDP and HTTPS to the relay and STUN server - UDP directly to other devices, when a direct path is possible ## Data retention | Data | Kept for | |---|---| | Audit log | 90 days | | Network logs (off by default) | 30 days | ## Reporting a vulnerability See [getunderpass.com/security](https://getunderpass.com/security#report) for how to report a security issue. --- # Easy names URL: https://docs.getunderpass.com/features/easy-names Every machine and [service](/features/services) in your network gets a name. Type `db-prod` instead of `100.64.0.21`, anywhere: in a browser, `ssh`, a database client or a config file. This is sometimes called MagicDNS. Names work as soon as a device joins, with nothing to set up. ## Short and full names Each machine answers to two names: | Name | Example | |---|---| | Short | `db-prod` | | Full | `db-prod.underpass.internal`, or `db-prod.acme.` once your workspace has its own network name | Use the short name day to day. The full name is there for tools that want one, and for [HTTPS certificates](/features/https). A machine's name comes from its computer name when it joins: the first part, lowercased, keeping letters, digits and dashes, up to 63 characters. If the name is taken, it gets `-1`, `-2` and so on. Rename a machine any time under **Machines**; the new name works everywhere right away. ## How names are answered Each device answers name lookups itself, on its own private address. Names never leave the device, and only resolve on devices in your network. Answers are cached for 60 seconds. | System | How the app hooks in | |---|---| | Windows | Name resolution rules for your network's domains | | Linux | `systemd-resolved` (`resolvectl`) | | macOS | Files in `/etc/resolver` | > [!NOTE] > On Linux, names need `systemd-resolved`. Without it the app logs `MagicDNS needs systemd-resolved (resolvectl); machine names won't resolve, use 100.x addresses`. ## Your own DNS On the dashboard's **DNS** page: | Setting | Does | Limit | |---|---|---| | **Network name** | Gives your workspace its own domain, like `acme.` | 3 to 40 characters: lowercase letters, digits and `-`, no `--`, unique | | **Nameservers** | Send every lookup from your devices to these DNS servers | 8 | | **Split DNS** | Send one domain, like `corp.internal`, to its own servers | 32 domains, 4 servers each | | **Search domains** | Let short names like `printer` find `printer.office.example` | 16 | | **HTTPS certificates** | Let machines get [certificates](/features/https) for their names | | Your network's own names are always answered first. Split DNS domains can't overlap your network's domain. > [!NOTE] > On macOS, machine names, split DNS and search domains work, but macOS keeps its own nameservers for everything else even when you set **Nameservers**. DNS settings need app version 0.6 or newer on each device. --- # Services URL: https://docs.getunderpass.com/features/services A **service** is an internal app with its own name and address, separate from any one machine. You run it on one or more machines (its **hosts**), and every device reaches it by the service's name. If the host in use goes offline, devices move to the next one. Use it for anything people rely on every day: a wiki, Grafana, an admin tool, a Git server. Services work over **TCP**. ## Set one up 1. **Create the service.** In the dashboard, open **Services** and add one with a name (like `grafana`) and its ports (like `tcp:443`). It gets an address from `100.100.0.0/16` and a name. 2. **Run it on each host.** On every machine that runs the app, tell the client which local port to send the service's traffic to: ```bash underpass serve --service=svc:grafana --tcp=443 127.0.0.1:3000 ``` The target can be a port (`3000`), `127.0.0.1:3000` or `localhost:3000`. 3. **Approve the hosts.** In **Services**, approve each machine. A machine only serves once an admin approves it. People then open `https://grafana` (or the service's full name) from any device in the network. ## Which host is used Every device picks the same host, so everyone sees the same app state: 1. Hosts that are **approved** and currently **serving** are candidates. 2. **Online** hosts come first, then the one that has served the **longest**. 3. Each device uses the first candidate it can reach. This is failover, not load balancing: one host serves at a time, and the others wait as backups. ## Failover timing | What happens | When devices move | |---|---| | You stop serving on a host (`serve … off`) | Right away | | A host loses power or its network | After about **90 seconds**, when it counts as offline | For planned maintenance, stop serving first: ```bash underpass serve --service=svc:grafana --tcp=443 off ``` Then update or restart the machine, and run the `serve` command again. ## See what a machine serves ```bash underpass serve status ``` ## Limits - 1 to 16 ports per service, TCP only - Up to 64 services per host - Service and machine names share one namespace: a service can't have the same name as a machine ## Access Only devices in your network can reach a service. To narrow it further, name it in an [access rule](/features/access-controls) as `svc:grafana`. --- # Subnet routes URL: https://docs.getunderpass.com/features/subnet-routes A **subnet router** is a machine that offers a network it's on (for example `192.168.1.0/24`) to the rest of your workspace. Once an admin approves the route, every device can reach the machines in that network, such as printers, NAS boxes or databases in a VPC, as if it were there. ## Set one up 1. **Advertise the route** on the machine that's on that network: ```bash underpass set --advertise-routes=192.168.1.0/24 ``` Separate several routes with commas: `--advertise-routes=192.168.1.0/24,10.20.0.0/16`. 2. **Approve it.** In the dashboard, open **Routes** and approve the route. Only routes that are both advertised and approved are used. Other devices use approved routes automatically. ## Stop advertising ```bash underpass set --advertise-routes= ``` ## Rules for routes - IPv4 only, written as the network address (`192.168.1.0/24`, not `192.168.1.10/24`) - Up to 64 routes per machine - Not inside `100.64.0.0/10` (your network's own addresses), and not loopback, link-local or multicast - `0.0.0.0/0` means an [exit node](/features/exit-nodes), not a subnet route ## When two machines offer the same network Each device uses the first online machine offering that route. A device never routes a network it's already on itself, so devices in the office keep using the office network directly. ## Turn off using routes on a device ```bash underpass set --accept-routes=false ``` The device then ignores every subnet route in the workspace. Set it back to `true` to use them again. ## Access To allow or limit access to a routed network, name it in an [access rule](/features/access-controls) by its CIDR, for example `192.168.1.0/24`. --- # Exit nodes URL: https://docs.getunderpass.com/features/exit-nodes An **exit node** is a machine other devices can send all their internet traffic through. Websites then see the exit node's public address instead of the device's. Use it on untrusted Wi-Fi, or to browse from your home or office address. ## Offer a machine as an exit node 1. On the machine: ```bash underpass set --advertise-exit-node ``` 2. In the dashboard, open **Routes** and approve it as an exit node. ## Use an exit node On the device whose traffic should go out through it, pass the exit node's name or address: ```bash underpass set --exit-node=home-server ``` To go back to using the device's own internet connection: ```bash underpass set --exit-node= ``` On Windows you can also pick one from the tray icon's **Exit node** menu, and offer the PC as one with **Run as exit node**. Offering an exit node works on Linux, macOS and Windows with the default engine. On Linux it needs `nftables`. ## How it behaves - Traffic only goes through the exit node while it's **online and approved**. If it isn't, the device's traffic goes out directly, and the client says why. - An exit node can't use another exit node. - Your private network keeps working: only internet traffic goes through the exit node. ## Access Exit traffic is named `autogroup:internet` in [access rules](/features/access-controls). A rule with `*` as its destination covers it too. --- # App connectors URL: https://docs.getunderpass.com/features/app-connectors Some services only let in addresses you've approved: a company GitHub, a cloud database, a client's firewall. An **app connector** is a machine in your network that sends traffic for those services' domains out from **its own public address**. You add that one address to the allow-list, and your whole team gets in through it, wherever they are. Only traffic for the domains you choose takes this route. Everything else goes out the normal way. ## Set one up 1. **Pick a connector machine.** It should be always on and have a fixed public address, like an office server or a small cloud VM. Linux, macOS and Windows machines can be connectors. 2. **Create an app.** In the dashboard, open **Apps**, add an app with a name (like `github`), and list its domains: ```text github.com *.github.com ``` `*.github.com` covers the domain and everything under it. 3. **Choose the connector** for the app. You can list several; devices use the first one that's online, in the order you list them. 4. **Add the connector's public address** to the service's allow-list. ## How it works The connector looks up the app's domains every minute and reports the addresses to the coordination service. Devices in your network then send traffic for those addresses to the connector, which forwards it to the internet. Names looked up under a `*.` domain stay routed for an hour. ## Rules and limits - 1 to 50 domains per app, app names up to 64 characters - Up to 512 routed addresses per connector per app - Only public IPv4 addresses are routed: never private, loopback, link-local, multicast or `100.64.0.0/10` addresses - Devices that use an app are automatically allowed to reach the connector on port 53 (DNS) ## Access Name an app in an [access rule](/features/access-controls) as `app:` to decide who can use it. --- # Access controls URL: https://docs.getunderpass.com/features/access-controls Access controls are your workspace's **policy**: a list of rules saying who can reach what. You edit them on the dashboard's **Access controls** page. > [!IMPORTANT] > Until you save a policy, every device in your workspace can reach every other. Once you save one, **only what the rules allow gets through** and everything else is blocked. A policy with no rules blocks all traffic between machines. Write your rules, and a few tests, before you save. ## How rules work Each rule says: these **sources** can reach these **destinations**, on this **protocol** and these **ports**. Rules only ever allow; if no rule allows a connection, it's blocked. ```json { "src": ["group:eng"], "dst": ["tag:prod"], "proto": "tcp", "ports": ["22", "5432"], "note": "Engineers manage production" } ``` ## Selectors | Selector | Means | Use in | |---|---|---| | `*` | Every machine | Sources and destinations | | `group:` | Every machine owned by a member of the group | Sources and destinations | | `user:` | Every machine that person owns | Sources and destinations | | `tag:` | Machines that joined with a key carrying that tag | Sources and destinations | | `machine:` | One machine, by its ID | Sources and destinations | | `svc:` | A [service](/features/services) | Destinations | | `app:` | An [app](/features/app-connectors), through its connectors | Destinations | | `192.168.1.0/24` | A [subnet route](/features/subnet-routes) | Destinations | | `autogroup:internet` | Internet traffic through an [exit node](/features/exit-nodes) | Destinations | **Protocols:** `any`, `tcp`, `udp` or `icmp`. **Ports:** one port (`22`), a range (`8000-8100`), or `*` for all. Ports are ignored for `icmp`. ## The policy A policy has four parts: ```json { "groups": { "group:eng": ["priya@acme.com", "jonas@acme.com"], "group:sales": ["sam@acme.com"] }, "rules": [ { "src": ["group:eng"], "dst": ["tag:prod"], "proto": "tcp", "ports": ["22", "5432"] }, { "src": ["group:sales"], "dst": ["svc:crm"], "proto": "tcp", "ports": ["443"] }, { "src": ["*"], "dst": ["svc:wiki"], "proto": "tcp", "ports": ["443"] } ], "ssh": [ { "src": ["group:eng"], "dst": ["tag:prod"] } ], "tests": [ { "src": "priya@acme.com", "accept": ["tag:prod:22"], "deny": [] }, { "src": "sam@acme.com", "accept": ["svc:crm:443"], "deny": ["tag:prod:22"] } ] } ``` ### Groups Named lists of people's emails, written in lowercase. Use them as `group:` in rules. ### SSH rules SSH rules always mean **TCP port 22**, to **machines** only (not services or apps). Keeping them separate makes it easy to see who can log in where. ### Tests A test names a source and lists what it **must** reach (`accept`) and what it **must not** reach (`deny`), written as `:` over TCP, like `tag:prod:22`. A policy only saves if every test passes, so a change can't silently break access. ## Where rules are enforced Each device enforces the rules for the traffic it **receives**: - It lets in only what the rules allow, plus replies to connections it started itself (replies are remembered for 5 minutes of quiet). - What a device sends out is never blocked. - A rule that can't be applied is skipped, never treated as allow-all. > [!WARNING] > Devices enforce access rules from app version **0.3** onwards. Keep every device up to date so all of them enforce your policy. ## Limits | What | Limit | |---|---| | Rules (including SSH rules) | 200 | | Groups | 100, with up to 500 members each | | Tests | 100, with up to 50 targets each | | Sources and destinations per rule | 1 to 50 | | Notes | 200 characters, one line | | Tag names | `tag:` + letter or digit, then up to 62 of `A-Z a-z 0-9 _ . -` | | Group names | `group:` + lowercase letter or digit, then up to 62 of `a-z 0-9 _ . -` | ## History Every saved change is recorded in the [audit log](/manage/logs#audit-log), with who made it. --- # HTTPS certificates URL: https://docs.getunderpass.com/features/https A machine can get a trusted certificate for its full name (like `grafana-box.acme.`) from Let's Encrypt. Browsers then show a normal padlock, with no warnings and no self-signed certificates to install. ## Before you start On the dashboard's **DNS** page: 1. Give your workspace its own **network name** (for example `acme`). 2. Turn on **HTTPS certificates**. ## Get a certificate On a machine where the app was installed with the install command or the Windows installer, point `cert` at the background service's state folder: ```bash # Linux and macOS sudo underpass cert --state-dir /var/lib/tailnetd ``` ```powershell # Windows, as administrator & "C:\Program Files\Tailnet\tailnet.exe" cert --state-dir "$env:ProgramData\TailnetService" ``` On a machine that runs standalone (`underpass up --auth-key` or `underpass run`), plain `sudo underpass cert` is enough. It takes up to a minute and prints where the files are: ```text Certificate: /var/lib/tailnetd/certs/grafana-box.acme.….crt Key: /var/lib/tailnetd/certs/grafana-box.acme.….key ``` To also copy them where your web server reads them: ```bash sudo underpass cert --state-dir /var/lib/tailnetd \ --cert-file /etc/ssl/grafana.crt --key-file /etc/ssl/grafana.key ``` The private key is made on the machine and never leaves it; only a certificate request is sent. ## Renewal The certificate in the state folder renews on its own while the app runs, 30 days before it expires. Copies made with `--cert-file` and `--key-file` don't update themselves: run the command again, or point your web server at the files in the state folder. ## Things to know > [!WARNING] > Trusted certificates are public record (certificate transparency), so a machine's **full name becomes public** once it has one. Pick names you're happy to share. The machine itself is still only reachable from your network. - A machine can get at most one new certificate per hour, unless its current one is within 30 days of expiring. - The certificate covers exactly the machine's full name. --- # Machines URL: https://docs.getunderpass.com/manage/machines The dashboard's **Machines** page lists every device in your workspace with its address, name, operating system, owner and status. Search by name, address, OS or tag, filter by status (Connected, Offline, Needs approval, Signed out, Key expired), last seen, OS, tag or ephemeral, and **Export CSV**. Open a machine for its details: address, full name, hostname, OS, app version, WireGuard public key, owner, type (ephemeral or persistent), when it was added and last seen, its connections, endpoints and tags. ## Rename a machine Use **Edit machine name…** in the row menu, or **Rename** on the machine's page. The name is also its [easy name](/features/easy-names): lowercase letters, numbers and dashes, unique in the workspace. Other characters are converted. ## Remove a machine **Remove…** takes it out of your network right away. To add it back, it has to join again. ## Device approval With **Device approval** on (in **Settings**), new machines joining with an [auth key](/manage/auth-keys) wait for an owner or admin before they can reach anything. They show as **Needs approval**; approve them from the row menu or the machine's page. - Keys created as **pre-approved** skip it. - Machines added by someone signing in through the browser are approved by that person when they connect them. ## Key expiry Machines sign in again after a while, so a lost or retired device stops working on its own. - The workspace default is **180 days**. Change it in **Settings** to 1 to 365 days, or turn expiry off. - A new length applies at each machine's next sign-in. Turning expiry off or on applies to every machine at once. - On a machine's page, owners and admins can **Disable expiry**, **Enable expiry**, or **Expire now**. - [Tagged](/manage/auth-keys#tags) machines never expire. - Machines within 14 days of expiring are flagged in the list. An expired machine keeps its name and address. Run `underpass up` on it to sign in again. ## Owner A machine is owned by the person who added it. Owners and admins can **Change owner** on untagged machines, or set it to nobody. [Access rules](/features/access-controls) using `user:` and `group:` match machines by owner. ## Tags Tags come from the auth key a machine joined with, like `tag:prod`. A tagged machine belongs to the workspace rather than a person, and never expires. To change a machine's tags, add it again with a different key. ## Labels Machines on Linux inside Windows show **WSL**, and machines in Docker or Podman show **Container**. ## Share a machine Owners and admins can **Share…** a machine with someone outside the workspace, by email or with a link. Each invite works once, within 7 days, up to 50 per machine. The person can connect to the shared machine; it can't reach their devices. Machines shared with you appear under **Shared with you**. ## Who can do what | Action | Who | |---|---| | Rename, remove | Any member | | Approve, change owner, key expiry, share | Owners and admins | ## Network health The **Health** page shows how many machines are online, how many connections are direct, the median latency, and anything that needs attention. --- # People and roles URL: https://docs.getunderpass.com/manage/people People are managed on the dashboard's **Users** page, with **Members** and **Invites** tabs. ## Invite people Owners and admins can invite. Choose: | | **By email** | **By link** | |---|---|---| | Who can use it | Only that email address | Anyone you share it with | | How many times | Once | As often as needed | | How it arrives | Emailed to them | You share it yourself | | Expires | After 7 days | After 7 days | Pick the role (**Member** or **Admin**; only the owner can invite admins), then **Send invite** or **Create link**. A link is shown once. Revoke an invite at any time on the **Invites** tab. ## Approve who joins Accepting an invite doesn't let anyone in on its own. They wait on the **Members** tab until an owner or admin clicks **Approve** (or **Decline**). On a paid plan, the dialog shows the added cost first. On Free, approval stops at the plan's limit of 6 people. ## Roles | | Member | Admin | Owner | |---|---|---|---| | Add their own devices and use the network | ✓ | ✓ | ✓ | | Rename and remove machines, create auth keys | ✓ | ✓ | ✓ | | Invite and approve people, remove people | | ✓ | ✓ | | Approve machines and routes, pre-approved keys | | ✓ | ✓ | | Workspace settings, DNS, access controls, services, apps | | ✓ | ✓ | | API tokens, webhooks, audit and network logs | | ✓ | ✓ | | See billing | | ✓ | ✓ | | Invite admins, change roles | | | ✓ | | Change the plan | | | ✓ | The owner changes roles with **Make admin** and **Make member**. ## Remove someone Owners and admins use **Remove from workspace…** in the person's row menu. The owner can't be removed. Their machines stay in the workspace; remove those under [Machines](/manage/machines) if needed. ## Several workspaces Everyone has a **personal workspace** and can belong to others they're invited to. Switch between them with the workspace switcher at the top of the sidebar; workspaces still waiting for your approval are listed but can't be opened yet. Each workspace is its own separate network. ## Leave a workspace Use **Leave** in **Settings**, or in the workspaces list on your **Account** page. You can't leave a workspace you own, or your personal one. ## Rename the workspace Owners and admins rename it in **Settings** (1 to 64 characters) and can change its logo. ## Delete your account On your **Account** page, use **Delete your account** and type your email to confirm. It removes your account and your personal workspace, with its machines, keys and tokens, and cancels paid plans on workspaces you own. You can't delete it while you own a workspace that has other members. Every invite, approval, removal and role change is recorded in the [audit log](/manage/logs#audit-log). --- # Auth keys URL: https://docs.getunderpass.com/manage/auth-keys An **auth key** lets a machine join your workspace on its own. Use it for servers, cloud images, containers and CI runners. See [Add a server](/getting-started/add-a-server#with-an-auth-key) for how to use one. ## Create a key Open **Auth keys** and click **Generate auth key**, or **Machines → Add device**. Options: | Option | Means | |---|---| | **Reusable** | Use the key for more than one machine. Without it, the key works once | | **Ephemeral** | Machines added with it are removed automatically after 10 minutes offline, or as soon as they sign out | | **Pre-approved** | Machines skip [device approval](/manage/machines#device-approval). Owners and admins only | | **Tags** | Comma-separated, saved as `tag:name`. See [Tags](#tags) | | **Auth key expiration** | 1 to 90 days; 7 by default | The key starts with `tskey-auth-`. **Copy it right away: it's shown only once.** Only a hash of it is stored. > [!TIP] > For Docker and Kubernetes, the dashboard's setup guides make reusable, ephemeral keys by default. ## Tags Tags, like `tag:prod` or `tag:ci`, are given to every machine that joins with the key. A tagged machine: - belongs to the workspace, not to a person - never [expires](/manage/machines#key-expiry) - can be named in [access rules](/features/access-controls) as `tag:prod` > [!WARNING] > Any member can create a key with any tags, and tags can grant access in your rules. Keep that in mind when writing rules for tagged machines. ## Manage keys The **Auth keys** page shows active keys, keys expiring within 3 days, and machines added with keys, on **Active**, **Used & expired** and **All** tabs. A key's row menu has **View details…**, **Copy key ID** and **Create a similar key**. **Revoke** a key to stop it adding machines. Machines that already joined with it stay connected. ## Through the API Create keys with `POST /api/v1/keys` and the body `{ "reusable", "ephemeral", "preApproved", "tags", "expirySeconds" }`. Without an expiry, an API-created key lasts 90 days, the maximum. See [API](/manage/api). --- # Two-factor sign-in URL: https://docs.getunderpass.com/manage/two-factor Accounts sign in with **Google** or a **one-time code by email**. Add an authenticator app for a second step at every sign-in. ## Turn it on 1. On your **Account** page, add an authenticator. 2. Scan the QR code with an app like 1Password, Google Authenticator or Authy. (Or choose **Can't scan? Use a key** and type it in.) 3. Enter the **Code from the app** and click **Turn on**. From then on, after Google or the email code, you're asked for the 6-digit code from the app. ## Add a backup There are no recovery codes. **Add a second authenticator** (another phone, or a password manager) on your Account page, so losing one device doesn't lock you out. If you lose every authenticator, contact support from the email on your account. You can remove an authenticator only from a session that passed two-factor, and not your last one while a workspace you're in requires it. ## Require it for a workspace In **Settings → Sign-in**, owners and admins can turn on **Require two-factor authentication**. You need two-factor on your own account first. The setting shows how many members already use it, and asks you to confirm if some don't. When it's on, members without two-factor can't open the workspace until they set it up; they're taken to setup when they try. It's enforced by the database as well as the dashboard, so it can't be bypassed through another route. ## Sessions On your **Account** page, sign out of **Other devices** (every browser except this one) or **Everywhere**. Two-factor changes are recorded in the [audit log](/manage/logs#audit-log). --- # Logs and webhooks URL: https://docs.getunderpass.com/manage/logs ## Audit log **Logs → Audit log** records every change, sign-in and refused request in your workspace: who did it, when, from which address, and what changed. Owners and admins can see it. - **Filters:** Machines, Access, Apps & services, Keys, People & sign-ins; by person; by time (24 hours, 7 days, 30 days, 90 days); and search. - **Export CSV:** up to 10,000 events. - **Kept:** 90 days. - **Tamper-evident:** each entry includes a fingerprint (SHA-256) of the one before it, so editing, removing or reordering entries breaks the chain. Click **Verify integrity** to check it. Event names look like `machine.approved`, `key.created`, `policy.updated`, `member.removed` and `account.signed_in`. When the API refuses a change, it's recorded as `.failed`. ## Network logs **Logs → Network logs** shows which machines talked to which, and how much. It's **off by default**; an owner or admin turns it on. - **Recorded:** for each connection, the protocol, source, destination, packets and bytes each way, packets blocked by access rules, and whether it was between machines, to a subnet, or to the internet. Contents are **never** recorded. - **Reported:** by the machines themselves, every half minute. Machines on the system WireGuard engine don't report. - **Kept:** 30 days. - **Filters:** 15 minutes to 7 days; machines, subnets or internet; search; auto-refresh; **Export CSV**. Network logs need app version 0.5 or newer. ## Webhooks **Logs → Webhooks** sends events to a URL as they happen. | Setting | Options | |---|---| | Format | JSON, n8n, Slack, Discord, Google Chat | | Events | Everything, or any of Machines, Keys, Members, Invites, Access controls, Apps, Services, Workspace settings, Webhooks, Log streams, Sign-ins | | URL | `https` only; private and internal addresses are refused | A JSON delivery looks like: ```json { "id": "…", "type": "machine.approved", "time": "2026-10-28T10:42:00Z", "tailnet": "…", "actor": "amara@acme.com", "ip": "203.0.113.24", "target": { "type": "machine", "id": "…", "name": "db-prod" }, "details": {}, "summary": "…" } ``` ### Verify deliveries JSON and n8n webhooks are signed with a secret (`whsec_…`), shown once and rotatable. Each request has: - `X-Webhook-Event: ` - `X-Webhook-Signature: t=,v1=`, where `v1` is the HMAC-SHA256 of `"."` with your secret Check the signature, and reject requests older than 5 minutes: ```js import crypto from "node:crypto"; function verify(secret, header, rawBody) { const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("="))); const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; const a = Buffer.from(v1 ?? ""), b = Buffer.from(expected); return fresh && a.length === b.length && crypto.timingSafeEqual(a, b); } ``` **Send test** sends a `test.ping` event. ### Delivery and retries A failed delivery is retried after 2 seconds, 10 seconds, 1 minute and 5 minutes. After 20 failures in a row the webhook is paused (**Paused: failing**). The last 50 deliveries are kept, and you can redeliver any of them. ## Log streaming **Logs → Log streaming** sends your **audit log** or **network logs** to a logging service. | Destination | Authentication | |---|---| | Datadog | `DD-API-KEY` | | Splunk HEC | `Authorization: Splunk ` | | Elastic (bulk API) | `Authorization: ApiKey ` | | Axiom | `Authorization: Bearer ` | | HTTP (JSON) | `Authorization: Bearer ` | Events are sent in batches of 100 (500 for network logs), when a batch fills or 5 seconds after its first event. Network log items have `type: "network.flow"`. Failed batches are retried after 2, 10 and 30 seconds. ## Limits Up to 10 webhooks and 10 log streams per workspace. --- # API URL: https://docs.getunderpass.com/manage/api ## API tokens Owners and admins create tokens under **Keys → API tokens**: - **Scope:** **Read only** or **Read & write** - **Expires:** after 7, 30 or 90 days (90 by default, the maximum) - **What is it for?:** a description, 1 to 100 characters Tokens start with `tsapi-` and are **shown once**. A token only ever acts on the workspace it was created in. Up to 50 per workspace. Actions taken with a token show in the [audit log](/manage/logs#audit-log) as `API token `. ## Making requests The API lives at `/api/v1` on the coordination service. Send the token as a Bearer token: ```bash curl -H "Authorization: Bearer tsapi-xxxxxxxx" \ https://coord.getunderpass.com/api/v1/nodes ``` Request and response bodies are JSON. Read-only tokens can only make `GET` requests. ## Endpoints ### Machines | Method | Path | Does | |---|---|---| | `GET` | `/nodes` | List machines | | `GET` | `/nodes/{id}` | Get a machine | | `PATCH` | `/nodes/{id}` | Change `name`, `user` (owner) or `keyExpiryDisabled` | | `DELETE` | `/nodes/{id}` | Remove a machine | | `POST` | `/nodes/{id}/approve` | Approve a machine waiting for approval | | `POST` | `/nodes/{id}/expire` | Expire its key now | | `POST` | `/nodes/{id}/routes` | Approve routes: `{ "approved": ["192.168.1.0/24"] }` | ### Auth keys | Method | Path | Does | |---|---|---| | `GET` | `/keys` | List auth keys | | `POST` | `/keys` | Create one: `{ "reusable", "ephemeral", "preApproved", "tags", "expirySeconds" }` | | `DELETE` | `/keys/{id}` | Revoke one | ### Access controls | Method | Path | Does | |---|---|---| | `GET` | `/policy` | Get the policy | | `PUT` | `/policy` | Replace it. Refused if a test fails | | `DELETE` | `/policy` | Reset to allow all | | `POST` | `/policy/check` | Check a draft policy and its tests without saving | The policy format is on [Access controls](/features/access-controls#the-policy). Editing the policy as JSON is only possible through the API; the dashboard edits it rule by rule. ### Services and apps | Method | Path | Does | |---|---|---| | `GET`, `POST` | `/services` | List, create | | `DELETE` | `/services/{id}` | Delete | | `POST`, `DELETE` | `/services/{id}/hosts/{node}` | Approve or pause a host, remove a host | | `GET`, `POST` | `/apps` | List, create: `{ "name", "domains", "connectors" }` | | `PUT`, `DELETE` | `/apps/{id}` | Update, delete | ### Settings, DNS and logs | Method | Path | Does | |---|---|---| | `GET`, `PATCH` | `/settings` | `keyExpiryDays`, `keyExpiryOff`, `deviceApproval`, `networkLogs` | | `GET`, `PUT` | `/dns` | DNS settings | | `GET` | `/flows` | Network logs | | `GET` | `/audit` | Audit log | | `GET` | `/audit/verify` | Check the audit log's integrity | | `GET` | `/hooks` | Webhooks and log streams (read only with tokens) | ### What tokens can't do API tokens can't manage other tokens, workspaces, sign-ins or shared machines, can't write the audit log, and can't change webhooks or log streams. Use the dashboard for those. ## Connect an AI agent (MCP) The dashboard runs an MCP server, so an AI assistant can work with your workspace on your behalf. Open **Connect your agent** in the dashboard's sidebar for setup steps. Agents sign in with your account (OAuth), not with an API token. Tools include: - listing workspaces and machines - getting an install command (with an auth key that works once, for 24 hours) and waiting for the machine to join - approving machines, checking a connection, explaining how to connect to a machine - renaming machines, inviting people and sharing machines Approving machines, inviting people and sharing need an owner or admin. To give an agent these docs, point it at [docs.getunderpass.com/llms.txt](/llms.txt). --- # Plans and billing URL: https://docs.getunderpass.com/manage/plans Every feature is on every plan. Plans differ in how many people and devices they cover, and in support. | | Free | Team | Business | |---|---|---|---| | Price | $0 | $5 per person a month ($4 billed yearly) | $12 per person a month ($10 billed yearly) | | People | Up to 6 | Unlimited | Unlimited | | Devices | 100 | 25 per person, at least 100 | 50 per person, at least 250 | | Support | Community | Email | Priority, replies within 1 business day; pay by invoice | On Team and Business, the device limit is shared by the whole workspace: it's the larger of the floor (100 or 250) and the per-person allowance times the number of people. See current prices on [getunderpass.com/pricing](https://getunderpass.com/pricing). ## At a limit - **People:** approving another person is refused on Free once there are 6. On a paid plan, approving someone shows the added cost first. - **Devices:** a new machine is refused with `this workspace's plan allows N machines; remove one or upgrade the plan`. Existing machines keep working and can always sign in again, even if a limit is lowered. The **Billing** page shows a banner when the workspace is at a limit. ## Change your plan Owners and admins can see **Billing**; only the **owner** can change the plan. - Upgrading opens a secure checkout. When it's done you see "Payment received. Your new plan is active." - Changing between paid plans is prorated. - Moving to Free cancels at the end of the current billing period. - Seats follow the number of active members. If a payment fails, the plan shows **Payment due** with an **Update payment method** button. --- # CLI reference URL: https://docs.getunderpass.com/reference/cli The command-line tool is `underpass` on macOS and Linux (with `tailnet` as a second name) and `tailnet.exe` on Windows, in `C:\Program Files\Tailnet`. This page writes it as `underpass`. ```text underpass [flags] ``` Run `underpass -h` for a command's flags. Errors are printed as `tailnet: ` and exit with status 1. ## How commands reach the service When the background service is installed (Windows installer, or the macOS and Linux install command), `up`, `down`, `status`, `logout`, `set` and `serve` talk to it. You don't need `sudo` for that: - **Linux:** after the installer adds you to the `tailnet` group (log out and back in once) - **macOS and Windows:** for the signed-in user Without the service, or when you pass a flag that only a standalone run uses (`--auth-key`, `--state-dir`, `--config-only`, `--interface`, `--port`, `--engine`, `--hostname`), `up` runs the tunnel itself, in the foreground, until you stop it. That needs `sudo` or an administrator prompt. ## Commands | Command | Does | |---|---| | [`up`](#up) | Connect: sign in through the browser, or join with `--auth-key` | | [`status`](#status) | Show this machine and the other devices | | [`down`](#down) | Disconnect; the machine stays registered | | [`logout`](#logout) | Disconnect and forget this machine's login | | [`run`](#run) | Keep the tunnel up using the saved registration, with no service | | [`set`](#set) | Subnet routes and exit nodes | | [`serve`](#serve) | Host a [service](/features/services) port | | [`cert`](#cert) | Get an HTTPS certificate for this machine's name | | [`service`](#service) | Install, remove or check the background service (Windows, macOS) | | [`uninstall`](#uninstall) | Remove the app (Windows, macOS) | | [`daemon`](#daemon) | Run the background service (used by the service itself) | | [`version`](#version) | Print the version | ## up Connects this machine. ```bash underpass up ``` Through the service, a machine that isn't signed in prints a link and waits: ```text To add this machine to your tailnet, open: https://app.getunderpass.com/… Waiting for approval... Connected as laptop (100.64.0.7). ``` If [device approval](/manage/machines#device-approval) is on, it says `Logged in. An admin needs to approve this machine in the dashboard...` until an admin does. | Flag | Default | Description | |---|---|---| | `--auth-key` | `$TAILNET_AUTH_KEY` | Join with an [auth key](/manage/auth-keys) instead of the browser. Runs standalone | | `--hostname` | The machine's hostname | Name to register with | | `--server` | `https://coord.getunderpass.com` | Coordination server URL (or `$TAILNET_SERVER`) | | `--engine` | `userspace` | `userspace`: built-in WireGuard with direct connections and relay fallback. `system`: the OS's WireGuard (Linux, Windows), direct connections only | | `--interface` | `tailnet0` | Tunnel interface name | | `--port` | `41642` | WireGuard listen port | | `--state-dir` | See [State](#state) | Where keys and the login are stored | | `--config-only` | `false` | Only write a WireGuard config file; needs no admin rights | | `--advertise-routes`, `--advertise-exit-node`, `--exit-node`, `--accept-routes` | | Same as [`set`](#set) | ## status ```bash underpass status ``` ```text 100.64.0.7 laptop.underpass.internal connected (https://coord.getunderpass.com) Relay: https://relay… (connected) ADDRESS NAME OS PATH LATENCY HANDSHAKE ROUTES 100.64.0.21 db-prod linux direct 203.0.113.7:41642 11.2 ms 4s ago - 100.64.0.12 pi-garage linux relay 38.6 ms 9s ago 192.168.1.0/24 ``` The first line's state is one of `connected`, `connecting`, `disconnected`, `waiting for login`, `logged out` or `waiting for an admin to approve this machine`. A `Problem:` line explains anything that's wrong. | Column | Shows | |---|---| | `PATH` | `direct `, `relay`, or `offline` | | `LATENCY` | Round trip, like `11.2 ms` | | `HANDSHAKE` | Time since the last WireGuard handshake | | `ROUTES` | What the device offers: `exit node`, subnets, or `-` | ## down ```bash underpass down ``` Prints `Disconnected. Run 'underpass up' to reconnect.` The machine stays in your network. ## logout ```bash underpass logout ``` Signs this machine out and forgets its login. Its keys are kept, so signing in again brings back the same name and address. Prints `Logged out. Run 'underpass up' to log in again.` ## run ```bash sudo underpass run ``` Keeps the tunnel up using a saved registration, without the service, for example under your own systemd unit after `up --auth-key` (see [Add a server](/getting-started/add-a-server#with-an-auth-key)). Takes `--state-dir`, `--engine` and `--config-only`. Without a saved registration it says `not registered: run 'underpass up --auth-key ...' first`. ## set Changes routing. A running client picks changes up within a few seconds. | Flag | Default | Description | |---|---|---| | `--advertise-routes` | none | Subnets to route for your network, comma-separated (`192.168.1.0/24`). `""` stops. An admin approves them. See [Subnet routes](/features/subnet-routes) | | `--advertise-exit-node` | `false` | Offer this machine as an [exit node](/features/exit-nodes) | | `--exit-node` | none | Send internet traffic through this machine (its name or address). `""` goes direct | | `--accept-routes` | `true` | Use subnets other machines route | Advertising routes and exit nodes works on Linux, macOS and Windows with the default engine. With no flags, `set` prints the current settings. ## serve Hosts a port of a [service](/features/services) on this machine. ```bash underpass serve --service=svc:grafana --tcp=443 127.0.0.1:3000 # start underpass serve --service=svc:grafana --tcp=443 off # stop underpass serve status # list ``` | Flag | Description | |---|---| | `--service` | The service, like `svc:web`. It must exist in the dashboard first | | `--tcp` | The service's port, like `443` | The last argument is where the app listens: a port (`3000`, meaning `127.0.0.1:3000`) or `host:port`. The machine serves once an admin approves it under **Services**. ## cert Gets a trusted HTTPS certificate for this machine's full name. See [HTTPS certificates](/features/https). | Flag | Description | |---|---| | `--cert-file` | Also copy the certificate (PEM chain) here | | `--key-file` | Also copy the private key (PEM) here | | `--state-dir` | Where the machine's login is stored | ## service Windows (administrator) and macOS (`sudo`): ```bash underpass service install # install and start the background service underpass service uninstall # remove it; keys are kept underpass service status # installed / running / not installed ``` On Linux the service is the systemd unit `tailnetd`: `sudo systemctl enable --now tailnetd`. ## uninstall ```bash underpass uninstall [--remove-keys] ``` Removes the app on Windows and macOS. `--remove-keys` also deletes this machine's keys. On Linux, see [Install](/getting-started/install#uninstall). ## daemon Runs the background service. You don't run this yourself; the installed service does. | Flag | Default | Description | |---|---|---| | `--state-dir` | | Where keys and the login are stored | | `--listen` | `127.0.0.1:41112` (Windows), `127.0.0.1:41113` (macOS, Linux) | Local API address, localhost only | | `--token-file` | | Where the local API token is written for the apps | ## version ```bash underpass version ``` Prints the name and version, like `underpass 0.7.0`. ## State Each machine keeps its keys and login in a state folder, readable only by the system: | | Background service | Standalone (`up`, `run`, `cert`) | |---|---|---| | Linux, macOS | `/var/lib/tailnetd` | `/var/lib/tailnet` (as root) | | Windows | `%ProgramData%\TailnetService` | `%ProgramData%\tailnet` | ## Environment variables | Variable | Used by | Meaning | |---|---|---| | `TAILNET_AUTH_KEY` | `up` (standalone) | Default for `--auth-key` | | `TAILNET_SERVER` | `up` | Default for `--server` | | `UNDERPASS_NO_UP=1` | Install command | Install without connecting | | `UNDERPASS_DOWNLOAD` | Install command | Download from another base URL | --- # Limits URL: https://docs.getunderpass.com/reference/limits ## Plans | Plan | People | Devices | |---|---|---| | Free | Up to 6 | 100 | | Team | Unlimited | 25 per person, pooled, at least 100 | | Business | Unlimited | 50 per person, pooled, at least 250 | Every feature is on every plan. See [Plans and billing](/manage/plans). ## Machines | What | Limit | |---|---| | Machine name | A DNS label: lowercase `a-z`, `0-9` and `-`, up to 63 characters | | Key expiry | 180 days by default; 1 to 365, or off | | Ephemeral machines | Removed after 10 minutes offline | | Counts as offline | 90 seconds without checking in | ## Access controls | What | Limit | |---|---| | Rules (including SSH rules) | 200 | | Groups | 100, up to 500 members each | | Tests | 100, up to 50 targets each | | Sources and destinations per rule | 1 to 50 | ## DNS | What | Limit | |---|---| | Nameservers | 8 | | Split DNS domains | 32, with up to 4 servers each | | Search domains | 16 | | Network name | 3 to 40 characters: lowercase letters, digits and `-`, no `--` | ## Services, routes and apps | What | Limit | |---|---| | Ports per service | 1 to 16, TCP only | | Services per host | 64 | | Subnet routes per machine | 64, IPv4 only | | Domains per app | 1 to 50 | | Routed addresses per app connector, per app | 512 | ## Keys and tokens | What | Limit | |---|---| | Auth key lifetime | 1 to 90 days; 7 by default in the dashboard, 90 through the API | | API token lifetime | Up to 90 days (the default) | | API tokens per workspace | 50 | ## Logs and webhooks | What | Limit | |---|---| | Audit log retention | 90 days | | Network log retention | 30 days | | Webhooks and log streams | 10 of each per workspace | | A destination is paused after | 20 failed deliveries in a row | ## HTTPS certificates | What | Limit | |---|---| | New certificates per machine | 1 per hour, unless the current one is due | | Renewal | 30 days before expiry | ## Relay | What | Limit | |---|---| | Throughput per device | 2 MiB/s, 4 MiB bursts | | Connections per IP address | 32 | --- # Troubleshooting URL: https://docs.getunderpass.com/reference/troubleshooting Start with: ```bash underpass status ``` The first line says whether this machine is connected, and a `Problem:` line explains what's wrong. The table shows how each other device is reached. ## Messages and what to do | Message | Cause | Fix | |---|---|---| | `the Underpass service isn't running` | The background service is stopped, or you can't reach it | Linux: `sudo systemctl restart tailnetd`. macOS: `sudo underpass service install`. Windows: restart the PC or reinstall. On Linux, log out and back in once after installing, or use `sudo` | | `waiting for an admin to approve this machine` | [Device approval](/manage/machines#device-approval) is on | An admin approves it under **Machines** | | `login link expired before it was approved; start again` | The sign-in link wasn't used in time | Run `underpass up` again | | `this machine's key expired; log in again to reconnect` | [Key expiry](/manage/machines#key-expiry) passed | Run `underpass up` | | `this machine was removed from the network` | It was removed in the dashboard | Join again with `underpass up` | | `auth key is invalid, expired, or already used` | The key is wrong, expired or one-time | Create a new key, or a reusable one | | `this workspace's plan allows N machines` | The workspace is at its device limit | Remove a machine or [upgrade](/manage/plans) | | `start tunnel (needs root/administrator)` | A standalone run without admin rights | Use `sudo`, or an administrator terminal | | `wintun.dll not found next to tailnet.exe` | A broken Windows install | Reinstall with **UnderpassSetup.exe** | | `MagicDNS needs systemd-resolved (resolvectl)` | Linux without systemd-resolved | Install and enable it, or use `100.x` addresses | | `forwarding needs nftables` | A Linux subnet router or exit node without nftables | `sudo apt install nftables` | | `can't reach coordinator: … (retrying in Ns)` | No internet, or HTTPS is blocked | Check the connection; the app keeps retrying | ## A device shows `relay` Some networks block every direct path, and the encrypted relay carries the traffic instead. It still works, just with a little more latency. The devices keep trying for a direct path every 35 to 60 seconds and switch on their own when one works. Networks that often need the relay: strict corporate firewalls, some guest Wi-Fi, and mobile networks behind carrier-grade NAT. ## A name doesn't resolve - Check the device is `connected` in `underpass status`. - Try the full name (`db-prod.underpass.internal`) and the address (`100.64.0.21`). If the address works, it's DNS: - **Linux:** names need `systemd-resolved`. - **macOS:** another VPN may own the resolver file for that domain. The app won't overwrite files it didn't write. - Names are cached for 60 seconds after a rename. ## Can't reach a device, but it's online - Check your [access rules](/features/access-controls). Once a policy is saved, anything it doesn't allow is blocked on the receiving device. - Check the app on the device listens on that port, and its own firewall allows it. ## A subnet route or exit node isn't used - An admin has to approve it on the **Routes** page. - `underpass status` on your device shows `Not using other machines' subnet routes` if `--accept-routes=false`. - An exit node is only used while it's online and approved; otherwise traffic goes out directly, and the client logs why. ## Logs | System | Where | |---|---| | Linux | `/var/lib/tailnetd/daemon.log`, and `journalctl -u tailnetd` | | macOS | `/var/lib/tailnetd/daemon.log` and `/var/log/tailnetd.log` | | Windows | `%ProgramData%\TailnetService\daemon.log` | The log starts fresh once it passes 10 MB. ## Still stuck Write to us from [getunderpass.com/support](https://getunderpass.com/support). Include the output of `underpass status` and the last lines of the log. --- # Glossary URL: https://docs.getunderpass.com/reference/glossary **Workspace.** One private network, with its own devices, people and settings. Everyone has a personal workspace and can be invited to others. In some technical places it's called a *tailnet*. **Machine.** A device in your network: a laptop, server, cloud VM or Raspberry Pi. The dashboard and CLI say *machine*; the website sometimes says *device*. **Owner, admin, member.** The three [roles](/manage/people#roles) a person can have in a workspace. **Tag.** A label like `tag:prod` given to machines through the [auth key](/manage/auth-keys) they join with. Tagged machines belong to the workspace rather than a person, never expire, and can be named in [access rules](/features/access-controls). **Auth key.** A key (`tskey-auth-…`) that lets a machine join without a browser. See [Auth keys](/manage/auth-keys). **API token.** A token (`tsapi-…`) for automating your workspace through the [API](/manage/api). **Ephemeral machine.** A machine joined with an ephemeral auth key. It's removed after 10 minutes offline, or as soon as it signs out. Good for CI runners and short-lived containers. **Device approval.** A workspace setting where new machines wait for an admin before they can reach anything. **Key expiry.** How long a machine stays signed in before it has to sign in again: 180 days by default. **MagicDNS, easy names.** Names for your machines and services, like `db-prod`, answered on each device. See [Easy names](/features/easy-names). **Service.** An internal app with its own name and address, served by one or more machines, with failover. Written `svc:`. See [Services](/features/services). **Subnet router.** A machine that lets the rest of your network reach a whole network it's on, like an office LAN. See [Subnet routes](/features/subnet-routes). **Exit node.** A machine other devices can send their internet traffic through. See [Exit nodes](/features/exit-nodes). **App connector.** A machine that sends traffic for chosen internet domains out from its own public address, so allow-lists need one address. See [App connectors](/features/app-connectors). **Relay.** The fallback that carries encrypted traffic between two devices when they can't connect directly. See [How it works](/concepts/how-it-works#the-relay). **Direct connection.** Two devices talking straight to each other, without the relay. **Access controls, policy.** The rules for which people and devices can reach each other. See [Access controls](/features/access-controls). **Coordination service.** The service your devices check in with to learn about each other. It never carries your traffic.