Skip to main content

tailnet-guard

A tiny, pure, dependency-free trust barrier for services fronted by a Tailscale tailnet. It answers one question — may I serve this peer? — from facts you supply (the peer's real socket address, never a forwarded header, plus the capability token it presented), and it makes "listen on a public interface" hard to do by accident.

Extracted from a security-reviewed pattern shared by two projects (easy-podcast + antibody-analysis) so the posture lives in one place to audit and fix. Stdlib only (ipaddress, hmac); every check is fail-closed.

Install

pip install tailnet-guard            # once published
pip install git+https://github.com/falahat/tailnet-guard.git   # from source

Use

from tailnet_guard import PeerRequest, GuardOutcome, evaluate_peer, resolve_bind_host

# 1. Refuse to bind anything but a tailnet/loopback interface.
host = resolve_bind_host(args.bind)                 # raises BindError otherwise
#    allow_any=True permits 0.0.0.0 ONLY behind an outer reachability constraint
#    (e.g. a container whose ports are published on a tailnet address).

# 2. Gate each request on the REAL socket address + the capability token.
outcome = evaluate_peer(
    PeerRequest(remote_addr=request.client.host, token=presented_token),
    expected_token=worker_secret,
)
if outcome is not GuardOutcome.ALLOW:
    raise Forbidden()        # flat 403 — don't leak which check failed

Two network policies

The membership predicate is a parameter, so each caller keeps its own policy:

  • is_tailnet_or_loopback (default) — strict: only the Tailscale CGNAT range (100.64.0.0/10) or loopback. For a server that should never trust the physical LAN.
  • is_local — broad: any non-public address (RFC1918 private, link-local, loopback, tailnet). For a host that also trusts LAN peers.
evaluate_peer(req, secret, membership=is_local)         # admit LAN peers too
resolve_bind_host("192.168.1.10", membership=is_local)  # allow a LAN bind

What's in the box

Symbol Purpose
evaluate_peer / PeerRequest / GuardOutcome the core guard decision (address → membership → pinned identity → token)
resolve_bind_host / BindError refuse unsafe bind hosts; allow_any for constrained wildcards
tokens_match constant-time token compare; empty never matches
normalize_ip, is_loopback, is_tailnet, is_tailnet_or_loopback, is_local membership predicates (IPv4-mapped-IPv6 aware)
whois / PeerIdentity resolve a peer's real tailnet identity (node/user/tags) via tailscaled — the strong check behind the coarse range gate (the one module that does I/O)
mtls.server_context / mtls.client_context / mtls.peer_identity build mutually-authenticating TLS contexts (certs from e.g. step-ca) for service↔service hops; recover the verified peer's identity from its cert
biscuit.mint / biscuit.attenuate / biscuit.authorize Biscuit capability tokens — issue a least-privilege grant, narrow it, and authorize it offline with the issuer's public key (the biscuit extra)
signing.sign / signing.verify RFC 9421 HTTP message signing — bind a request (method/URI/body digest) to an Ed25519 key so it can't be replayed or tampered (the signing extra)
host_ok Host-header anti-DNS-rebinding check
parse_allowlist / peer_allowed optional IP/CIDR allowlist that only ever narrows

Security notes

  • Pass the real socket peer, e.g. request.client.host (ASGI) or self.client_address[0] (http.server) — never X-Forwarded-For / Host, which a client controls.
  • The guard is stateless — it does not provide replay protection. If you need that, add a nonce/freshness layer at your message envelope (Tailscale's WireGuard already authenticates the transport).
  • Turn any non-ALLOW outcome into a flat 403 without echoing the reason.

Metadata

Release files for tailnet-guard 0.6.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 tailnet-guard 0.6.0
File Size Uploaded
tailnet_guard-0.6.0.tar.gz 24.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tailnet-guard 0.6.0
File Interpreter ABI Platform
tailnet_guard-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.2 kB

Release files / tailnet_guard-0.6.0.tar.gz

Download URL tailnet_guard-0.6.0.tar.gz
Size 24.0 kB
Tags Source
SHA-256 checksum
How to use checksums
37c89f83cefe45f17bb4d8014cb038553b4590c02f8029bc1b9da1b9d89a2f2d
BLAKE2b-256 checksum
How to use checksums
4ca87eaaef7f06f4ce696540ee73aa2aaebd3132d1871c336540adf26d8dfb72
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jun 28, 2026.

Transparency log

Release files / tailnet_guard-0.6.0-py3-none-any.whl

Download URL tailnet_guard-0.6.0-py3-none-any.whl
Size 18.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
96c475aecd980f367efbd1ad97193decf8669e3ae7c6d674319791aae6e45057
BLAKE2b-256 checksum
How to use checksums
27e71caa727c33ce645e4cbb448fece72a24577c9eeef081b0a6ec516b68e26e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jun 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

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