Skip to content

Configuration

The Localport YAML reference. Run several tunnels and fleet devices from one file, and forward fleet device ports with an access config.

Summary

One file lists every tunnel and every fleet device this machine runs, and localport connect --config brings them all up. Tokens stay in environment variables, referenced as ${VAR}. A second file forwards ports from fleet devices to this machine, and localport access --config reads it.

When to use a config file#

One service needs no file. localport http 3000 --token YOUR_TOKEN is the whole command. A file earns its place when one command stops being enough.

  • A web app and its database, each its own tunnel, brought up together.
  • A machine that joins a fleet as several devices, such as a gateway fronting equipment that cannot run an agent itself.
  • A homelab whose services should all come back after a reboot.
  • A workstation that forwards ports from more than one fleet device.

There are two formats, read by two commands.

FileCommandHolds
Agent configlocalport connect --configtunnels to publish, fleets to join
Access configlocalport access --configaccess entries forwarding device ports here

Both start with version: 1. A file with any other version is refused by name, before anything connects.

How to run several tunnels from one file#

Each tunnel is one entry under tunnels, carrying its own name, its own token, and the local address it forwards to.

version: 1

tunnels:
  - name: api
    token: ${LOCALPORT_TOKEN_API}
    upstream: http://localhost:8080

  - name: database
    token: ${LOCALPORT_TOKEN_DB}
    upstream: tcp://localhost:5432

Run it:

localport connect --config localport.yaml

A token belongs to one tunnel, so two tunnels are two tokens. Each entry opens its own connection and reconnects on its own, so one tunnel failing leaves the others serving.

How to join a fleet from a config file#

A fleet is a private group of machines reached by identity, not by address. Devices on one fleet share one token, because the token identifies the fleet and the name identifies the device.

version: 1

fleets:
  - token: ${LOCALPORT_TOKEN_FLEET}
    devices:
      - name: gw-01
        host: 192.168.1.10
      - name: gw-02
        host: 192.168.1.11

host is the address this device forwards to, and it defaults to localhost. Naming another address turns one agent into a hub for equipment on its network.

The ports each device serves are set in the dashboard, not here. A device forwards to host on whichever ports the dashboard has opened on it, and a caller reaches one with localport access gw-01-<fleet>.<region>.localport.dev -L 5020:502. Nothing on a fleet is reachable by address alone: every caller presents a certificate, and a grant decides which devices that identity may open.

Device names matter more than usual, because the name is the address and the thing a grant is written against.

  • Names are unique within a fleet. Two devices with the same name in one file are refused on load, and the comparison ignores case.
  • The address drops characters a DNS label cannot carry, so gw.01 is reachable as gw01-... while still being granted as gw.01. Lowercase letters, digits, and dashes keep the two identical.

Field reference#

One file may carry tunnels, fleets, or both. A file with neither is refused.

Tunnel fields#

FieldRequiredWhat it does
nameYesNames this tunnel in the agent's output.
tokenYesTunnel token from the dashboard. Reference it as ${VAR}.
upstreamYesThe local service to forward to, and the protocol to forward it as.

Fleet fields#

FieldRequiredWhat it does
tokenYesThe fleet token. Every device in this entry presents it.
devicesYesOne entry per device this machine runs. An empty list is refused.

Device fields#

FieldRequiredDefaultWhat it does
nameYesThe device name, its address, and what a grant is written against.
hostNolocalhostWhere this device forwards to. An address only: no scheme, no port.

A host carrying a scheme or a port is refused on load, because the protocol and the port numbers are properties of the ports opened in the dashboard.

How upstream sets the protocol#

Written asForwards toAs
upstream: 3000localhost:3000http
upstream: localhost:3000localhost:3000http
upstream: tcp://192.168.1.50:5432192.168.1.50:5432tcp
upstream: tls://localhost:8443localhost:8443tls

A scheme sets the protocol, https is read as http, and a value with no scheme is forwarded as HTTP. Anything other than http, https, tcp, or tls is refused by name, so a typo such as htpp fails on load.

A bare number is the only value expanded for you. upstream: 3000 becomes localhost:3000. Every other value needs the port written out, so http://192.168.10.21 fails where http://192.168.10.21:80 works.

How to keep tokens out of the file#

Environment references are substituted before the YAML is parsed, so a file you commit holds no secrets.

Written asResolves to
${VAR}The value. Unset or empty stops the run.
${VAR:-default}The value, or default when unset or empty.
${VAR:?message}The value, or a refusal carrying your message.
$$A literal $.
version: 1

tunnels:
  - name: web
    token: ${LOCALPORT_TOKEN:?set it to the tunnel token from the dashboard}
    upstream: 3000
export LOCALPORT_TOKEN=tok_abc123
localport connect --config localport.yaml

A missing variable stops the run before anything connects, and the error names every reference that could not be resolved:

LOCALPORT_TOKEN_API is not set; LOCALPORT_TOKEN_DB is not set

Substitution happens on the file's text, so a value may hold any character, including a colon or a #. Every variable the agent reads, and how to keep a token out of the environment as well as the file, is on Environment Variables.

How to forward ports from several devices at once#

An access config lists devices and the ports to forward from each. It is read by localport access --config, and its fields are different from an agent config.

version: 1

access:
  # No credential named, so the identity this machine already holds is
  # presented and renewed in the background. The intended path.
  - device: gw-01-factory.eu.localport.dev
    forward:
      - 5020:502
      - 8080:80

  # `identity` picks one when the machine holds several credentials.
  - device: db-01-factory.eu.localport.dev
    forward:
      - 5432
    identity: 01kpq7x2/client/deploy-prod

  # A credential Localport did not issue still comes from a file.
  - device: cache-01-factory.eu.localport.dev
    forward:
      - 6379:6379
    p12: certs/client.p12
    p12_pass_env: LOCALPORT_P12_PASSWORD

Every forward for one device runs over a single connection to that device, and every device in the file runs at the same time.

Access fields#

FieldRequiredWhat it does
deviceYesThe device address. A scheme and a path are accepted and dropped.
forwardYesOne or more local:remote pairs, the same values -L takes. An empty list is refused.
identityNoWhich stored credential to present: <identity>, <team>/<identity>, or <team>/<kind>/<identity>.
bundleNoPath to a PEM file holding the client certificate, its key, and the fleet CA.
p12NoPath to a PKCS#12 archive.
p12_passNoArchive password inline. Prefer one of the two below.
p12_pass_fileNoFile to read the archive password from.
p12_pass_envNoEnvironment variable to read the archive password from.

A forward value is written local first, as OpenSSH writes it. 5020:502 listens on 5020 and reaches the device's port 502. A single number, 5432, reaches that device port on a local port the operating system picks and prints.

The PEM field is called bundle, not pem

On the command line the flag is --pem. In an access config the key is bundle. They mean the same file. A config using pem: is ignored, since YAML accepts an unknown key without complaint, and the entry falls back to the machine's stored identity.

Rules the loader enforces#

  • ${VAR} is not substituted in an access config. Only agent configs expand it. Use p12_pass_env or p12_pass_file to keep the archive password out of the file.
  • At most one of bundle or p12. Setting both is refused. Omitting both is the normal case and means "present the stored identity".
  • identity cannot be combined with bundle or p12. One selects a stored credential, the others supply a file.
  • A file named by bundle, p12, or p12_pass_file must exist. Missing paths are listed on load, before any connection is attempted.
  • An ambiguous identity is an error, never a prompt. A config file is not interactive, so a selector matching several credentials fails and prints the full form of each.
  • Local listeners bind 127.0.0.1. --local-addr applies to a single -L run, not to entries in a file.
  • Two entries cannot claim one local port. This is caught on load and names both devices. A bare port number is exempt, since the operating system hands each forward a different one.
  • A local port held by something else on the machine stops that device only. The other devices in the file keep serving, and the failure is printed against the device name.

Validation and error messages#

Both formats are checked in full when the file is read. A file that will fail, fails at once and names the entry at fault.

MessageCause
unsupported config version N: this agent reads version 1Missing or wrong version.
the file lists no tunnels and no fleetsAn agent config with neither list.
tunnel N: name is requiredA tunnel entry with no name.
tunnel "api": upstream is requiredA tunnel entry with no upstream.
tunnel "api": upstream scheme "htpp" is not one of http, tcp, tlsAn unrecognised scheme.
fleet N: at least one device is requiredA fleet entry with an empty devices list.
fleet N: device "gw-01" is listed twiceTwo devices sharing a name, ignoring case.
device "gw-01": host takes an address without a port, the dashboard opens the portsA port written on a device host.
LOCALPORT_TOKEN is not setA ${VAR} reference that is unset or empty.
unterminated ${ in configA ${ with no closing brace.
access config: no devices listedAn access config with an empty access list.
device gw-01...: 'forward' needs at least one portAn access entry with no forwards.
devices gw-01... and gw-02... both listen on 127.0.0.1:8080Two access entries claiming one local port.

Worked examples#

An application and its database#

Two tunnels, so two tokens.

version: 1

tunnels:
  - name: api
    token: ${LOCALPORT_TOKEN_API}
    upstream: http://localhost:8080

  - name: postgres
    token: ${LOCALPORT_TOKEN_DB}
    upstream: tcp://localhost:5432

A gateway fronting a production line#

One fleet token, one device per machine on the line. Each device's ports are opened in the dashboard.

version: 1

fleets:
  - token: ${LOCALPORT_TOKEN_FLEET}
    devices:
      - name: line-7-plc
        host: 192.168.1.10
      - name: line-7-hmi
        host: 192.168.1.11
      - name: line-7-camera
        host: 192.168.1.12

A tunnel and a device on one machine#

A build server publishing a status page and joining the fleet as itself.

version: 1

tunnels:
  - name: build-status
    token: ${LOCALPORT_TOKEN_STATUS}
    upstream: 8080

fleets:
  - token: ${LOCALPORT_TOKEN_FLEET}
    devices:
      - name: build-01

A workstation forwarding several devices#

version: 1

access:
  - device: db-01-factory.eu.localport.dev
    forward:
      - 5432:5432
  - device: cache-01-factory.eu.localport.dev
    forward:
      - 6379:6379
  - device: gw-01-factory.eu.localport.dev
    forward:
      - 2222:22
      - 8080:80

With localport setup already run on this machine, no entry names a credential and none needs a file on disk.


Frequently asked questions#

Where should the config file live?

Anywhere you can point --config at. There is no search path and no implicit default, so nothing is picked up unless you name it. Projects usually keep localport.yaml beside the code. A machine running a service keeps it next to the unit file, such as /etc/localport/localport.yaml.

Can I commit my config file to git?

Yes, provided every token is a ${VAR} reference and not the literal value. Each person sets their own token in their environment, and the file itself carries nothing secret.

What happens if an environment variable is missing?

The agent refuses to start and names every reference it could not resolve. An unset variable never becomes an empty string. Write ${VAR:-default} where a fallback is correct, or ${VAR:?message} to say what the operator should set.

Which region does a config file connect to?

The default entry point. --region applies to a single tunnel or device started from the command line, and a config file names no region. A tunnel's own region is a property of the tunnel, so an agent landing at the wrong entry point is redirected to the right one without any change to the file.

Where do I list a device's ports?

In the dashboard, on the device. The agent is told which ports to serve when it registers, so a port added there is usable without editing this file or restarting anything. See Fleets.

Can one file hold both tunnels and fleet devices?

Yes. tunnels and fleets are separate lists in the same agent config, and localport connect --config runs everything in the file. An access config stays separate. localport access reads it, and it describes forwards into this machine instead of services leaving it.

Can two tunnels share one token?

No. A token belongs to one tunnel, and a second agent presenting the same token takes the tunnel over instead of running beside it. Give each tunnel its own token. A fleet is the exception and a different thing: its devices share one token because they are devices on one private fleet, each identified by name.

Does one failing entry bring down the others?

No. Each tunnel and each device holds its own connection and reconnects on its own, so a service that is down or a machine that is unplugged affects only its own entry. Failures that make the whole file invalid, such as a missing token or an unresolved variable, are caught before anything connects.

My access config ignores my pem file. Why?

The key in an access config is bundle, not pem. Only the command line calls it --pem. YAML does not object to an unknown key, so a pem: line is read as nothing and the entry falls back to the machine's stored identity. The connection usually succeeds, as the wrong identity.

Can I use a config file with systemd or Docker?

Yes, and it is the usual way to run more than one tunnel unattended. Point ExecStart= at localport connect --config /etc/localport/localport.yaml, keep the token in a file supplied through LoadCredential=, and read it with LOCALPORT_TOKEN_FILE. See Installation for the hardened unit.