Skip to main content

wildburrow

Self-hosted HTTPS tunnels on your own domain, with wildcard names at any depth and certificates issued on demand. It runs on Linux and macOS.

wildburrow 3000 myapp           # https://myapp.tunnel.example.com
wildburrow 3000 '*.foo'         # https://<anything>.foo.tunnel.example.com
wildburrow 3000 '*.bar.foo'     # https://<anything>.bar.foo.tunnel.example.com

A wildcard covers exactly one label, like a TLS wildcard certificate: *.foo serves api.foo but not x.api.foo. An exact name wins over a wildcard. Quote * names so your shell doesn't expand them.

Requests reach your local app unchanged, so websockets, server-sent events and streaming responses work as they do locally. With HTTPS, visitors use wss:// and your app sees plain ws://.

When a name needs a certificate that doesn't exist yet, the server gets *.<parent> from an ACME CA through a DNS-01 challenge while your client waits, then confirms the bind. Later binds under the same parent are instant.

Client

uv tool install wildburrow    # or: pipx install wildburrow
wildburrow login              # asks for the server and how to connect to it
wildburrow 3000 '*.foo'
wildburrow logout             # forgets the saved server

login keeps its answers in ~/.config/wildburrow/config.toml until you log in again or out; nothing in it expires, and every tunnel logs in afresh. Given options, wildburrow login tunnel.example.com --transport websocket, it asks nothing and saves what you passed.

Two transports are available:

Transport How it connects Auth methods
ssh (default) your ssh binary, port 2222, one -R per name key, password
websocket wss://tunnel.example.com on 443 key (signed with ssh-keygen -Y sign), password, token
wildburrow login tunnel.example.com --transport websocket --auth key --identity ~/.ssh/id_ed25519
wildburrow login tunnel.example.com --transport websocket --auth token --token wb_...
WILDBURROW_TOKEN=wb_... wildburrow 3000 app     # token from the environment

The client keeps the tunnel up: when the connection drops, it reconnects and binds its names again, waiting --retry-delay seconds (default 1) and doubling up to --retry-max (default 30). Over websocket it also waits for a server that isn't reachable yet. A refused login or a rejected name ends the client instead, and --no-reconnect makes any disconnect end it. login saves these flags like the others and connect overrides them. Binding a name you already hold moves it to the new connection; other identities are refused.

The websocket transport goes through an HTTP proxy from HTTPS_PROXY (or HTTP_PROXY) unless NO_PROXY excludes the server. The ssh transport follows your SSH config, so a ProxyJump or ProxyCommand there applies.

Plain ssh works without the client:

ssh -T -p 2222 -R '*.foo:80:localhost:3000' tunnel.example.com

Server

uv tool install 'wildburrow[server]'
wildburrow service install

service install asks for the domain, whether to serve HTTPS and through which DNS provider, and which keys may open tunnels, then installs the server as a system service and starts it: systemd on Linux, launchd on macOS. It asks for sudo itself. Point tunnel.example.com and *.tunnel.example.com at the server; DNS wildcards match any depth, so deeper names resolve too.

wildburrow service status       # running or not, and where the logs are
wildburrow service edit         # the same questions, answered with the current settings
wildburrow service uninstall    # stop and remove it, asking whether to keep the certificates

Each of them takes the settings as options instead, and then asks nothing:

wildburrow service install --domain=tunnel.example.com --acme-email=you@example.com \
  --dns=cloudflare --dns-cloudflare-api-token-file=/root/cloudflare-token
wildburrow service edit --http-port=8080
wildburrow service uninstall --yes --purge

Without a DNS provider the server serves plain HTTP on port 80. With one, it gets wildcard certificates by proving it owns the domain through DNS: port 80 redirects bound names to HTTPS, and port 443 terminates TLS, picking the certificate by SNI. A request whose Host isn't covered by the certificate of its connection gets 421. Try --acme-directory=https://acme-staging-v02.api.letsencrypt.org/directory until the first certificate comes through, so a mistake doesn't run into Let's Encrypt's rate limits.

Unless told otherwise, the service lets in the keys in your ~/.ssh/authorized_keys, copied when you install or edit. Its settings live in /etc/wildburrow, readable only by root and the service.

On Linux, an install under a home directory, which is where uv tool install puts it, runs as its owner with that home read-only; one outside any home, like /opt, runs as a throwaway user that systemd makes up. Either way it can bind ports 80 and 443 and nothing more. On macOS it runs as the install's owner, which macOS lets bind those ports.

Foreground

wildburrow serve runs the server in the foreground with the same settings, read from options, an --env-file of KEY=VALUE lines, or the environment:

sudo wildburrow serve --domain=tunnel.example.com

Docker

docker run -d --restart unless-stopped -p 80:80 -p 443:443 -p 2222:2222 -v wildburrow-data:/data \
  -e WILDBURROW_DOMAIN=tunnel.example.com \
  -e WILDBURROW_AUTH_KEY_AUTHORIZED_KEYS="$(cat ~/.ssh/id_ed25519.pub)" \
  -e WILDBURROW_DNS=cloudflare -e WILDBURROW_DNS_CLOUDFLARE_API_TOKEN=... \
  -e WILDBURROW_ACME_EMAIL=you@example.com \
  ghcr.io/isik-kaplan/wildburrow

The image comes in versions matching the package, for amd64 and arm64, and runs on Docker Desktop for macOS too. examples/docker-compose.traefik.yml runs it behind Traefik with TLS passthrough and PROXY protocol, for hosts where another proxy already owns ports 80 and 443.

Settings

Every setting is an option of serve, service install and service edit, and an environment variable for serve: --http-port 8080 is WILDBURROW_HTTP_PORT=8080, and an option wins over the variable. Adding -file to an option, or _FILE to a variable, reads the value from a file, for secrets and multi-line values: --auth-key-authorized-keys-file=~/keys. wildburrow serve --help lists them all, plugins included. A service has no --data-dir; its service manager decides where its data goes.

Variable Default
WILDBURROW_DOMAIN required Base domain tunnels live under
WILDBURROW_TRANSPORTS ssh Transport plugins: ssh, websocket
WILDBURROW_AUTH key Auth plugins, tried in order: key, password, token
WILDBURROW_DATA_DIR ~/.local/state/wildburrow Host key, certificates, ACME account; /data in the image
WILDBURROW_HTTP_HOST / _HTTP_PORT 0.0.0.0 / 80 Plain HTTP listener
WILDBURROW_HTTPS_HOST / _HTTPS_PORT 0.0.0.0 / 443 TLS listener, when WILDBURROW_DNS is set
WILDBURROW_HTTP_PROXY_PROTOCOL / _HTTPS_PROXY_PROTOCOL false Require a PROXY v1/v2 header
WILDBURROW_PROXY_TRUSTED private ranges CIDRs allowed to send PROXY headers
WILDBURROW_MAX_DEPTH 5 Maximum labels in a name, * included
WILDBURROW_MAX_HEADER_BYTES 65536 Largest accepted request head
WILDBURROW_HEADER_TIMEOUT 30 Seconds for the TLS handshake and request head
WILDBURROW_DNS DNS plugin for DNS-01: cloudflare
WILDBURROW_DNS_MANAGE_RECORDS false Create a CNAME for nested wildcards
WILDBURROW_ACME_DIRECTORY Let's Encrypt ACME directory URL (staging, ZeroSSL, ...)
WILDBURROW_ACME_EMAIL ACME account contact
WILDBURROW_ACME_PROPAGATION_SECONDS 30 Wait after publishing a challenge record
WILDBURROW_RENEW_BEFORE_DAYS 30 Renew certificates in use this close to expiry
WILDBURROW_SSH_PORT 2222 SSH transport port (_HOST, _LOGIN_TIMEOUT too)
WILDBURROW_WEBSOCKET_OPEN_TIMEOUT 10 Seconds a client has to open a visitor stream
WILDBURROW_AUTH_KEY_AUTHORIZED_KEYS ~/.ssh/authorized_keys OpenSSH public keys, one per line
WILDBURROW_AUTH_PASSWORD_HASH argon2 hash from wildburrow hash-password
WILDBURROW_AUTH_PASSWORD_MAX_FAILURES 5 Failed passwords per address before blocking
WILDBURROW_AUTH_PASSWORD_BLOCK_SECONDS 900 How long failures count against an address
WILDBURROW_AUTH_TOKEN_HASHES name:sha256 lines from wildburrow token NAME
WILDBURROW_DNS_CLOUDFLARE_API_TOKEN Token with DNS Edit and Zone Read

Plugins

Transports, auth methods and DNS providers are plugins registered through entry points: wildburrow.transports, wildburrow.auth and wildburrow.dns. A plugin class has a Settings dataclass, filled from WILDBURROW_<NAME>_* (transports), WILDBURROW_AUTH_<NAME>_* or WILDBURROW_DNS_<NAME>_*, and is constructed with it. Its fields become serve options the same way. A DNS plugin implements create_txt, delete_txt, ensure_cname and aclose; see wildburrow/dns/base.py. Install a package that registers an entry point and select it by name.

Development

uv sync --all-extras
uv run pytest -n auto                                # needs Docker for the Pebble ACME tests
uv run pytest -n auto -m "not acme and not system"   # without Docker
uv run pytest -m system --no-cov tests/system        # installs the real service; CI runs it on Linux and macOS
uv run ruff check . && uv run ruff format --check .

Coverage stays at 100%, and every mutant has to be killed. The mutation run re-tests only what changed since the last run in mutants/, both source and tests; CI keeps that directory cached between runs.

uv run python -m scripts.mutation run          # re-test what changed
uv run python -m scripts.mutation run --full   # re-test everything
uv run python -m scripts.mutation run --shard commands   # one shard: commands, service, client, ...

CI runs each shard as a job of its own with its own cache, so a cold run takes minutes and a red shard says where to look. There is one pass: each mutant runs against the tests traced to it, the test that killed it last time and those that killed its siblings first, and a survivor is a test to write. mutmut leaves decorated functions alone, so a decorated function only hands off to a plain one.

Every push to master runs the tests and the mutation run, then publishes to PyPI and ghcr.io when the version in pyproject.toml isn't on PyPI yet.

Metadata

Release files for wildburrow 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wildburrow 0.1.0
File Size Uploaded Provenance
wildburrow-0.1.0.tar.gz 106.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for wildburrow 0.1.0
File Interpreter ABI Platform
wildburrow-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 165.5 kB

Release files / wildburrow-0.1.0.tar.gz

Download URL wildburrow-0.1.0.tar.gz
Size 106.5 kB
Tags Source
SHA-256 checksum
How to use checksums
fcd97f6042245ced1d148a1a09e37c3ead9a4b151fe9f50b7b4518967b04cfbc
BLAKE2b-256 checksum
How to use checksums
603248c94bdaee8c46a7f65e9a75f02cbf20acbf809f4403a0b10d5356f24f9c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release files / wildburrow-0.1.0-py3-none-any.whl

Download URL wildburrow-0.1.0-py3-none-any.whl
Size 59.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dd7aaeb449ccba404ee1e4a41ba6dcb33315910eb8af77039df93f8f2f397537
BLAKE2b-256 checksum
How to use checksums
ba270e9f9a033c867da838c40640d66974207c144f64cb45a4192b246a00e28a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Security for wildburrow 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for wildburrow-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page