OpenRung
A free, privacy-first relay network that helps you reach the open internet. Open source and nonprofit-run.
翻訳待ち
Runtime network egress control for Python. One function call to restrict which hosts your code can connect to.
翻訳待ち
Loading the full README.
Read the full README ↗tethered checks every connection made through Python's socket layer against your allow list, from inside the process, before the connection is opened. activate() sets the ceiling for the whole process; scope() narrows a single code path.
import tethered
tethered.activate(allow=["*.stripe.com", "db.internal"])
import urllib.request
urllib.request.urlopen("https://api.stripe.com/v1/charges") # allowed: matches *.stripe.com
urllib.request.urlopen("https://evil.test/exfil") # raises tethered.EgressBlocked
Your code, your dependencies and AI-generated code share one Python process, and any of them can open an outbound connection you never intended. Firewalls, network policies and proxies can stop that, but they take a platform team, a separate service or admin rights. tethered takes one line of Python. It complements those controls and does not replace them.
| Supply-chain defense | AI agent guardrails | Least privilege |
|---|---|---|
| A dependency that phones home or exfiltrates data through Python's networking is stopped: it reaches the hosts you listed and nothing else. | Code an agent wrote runs under the same allow list as the rest of the process, in production and in your test suite. | scope() holds a request handler, a background job or a library call to the destinations it needs. |
The check adds about 25 µs to a connect, and tethered looks each allowed host name up a second time, which costs one more DNS round trip where no cache answers it (measurements).
uv add tethered
Or pip install tethered. Python 3.11+ on Linux, macOS and Windows, with no runtime dependencies. Wheels are prebuilt; a source install needs a C compiler.
Upgrading from 0.5.x? 1.0 refuses to run where 0.5.x went on without enforcing, so a deployment can stop at start-up after the upgrade. Read the migration guide first.
# main.py, wsgi.py, manage.py: your entry point
import tethered
tethered.activate(allow=["*.stripe.com:443", "db.internal:5432"])
from myapp import create_app # import the application afterwards
app = create_app()
Activate first, before your application and its dependencies are imported. Connections that are already open keep working, and processes started earlier are not reached, multiprocessing's fork server included: the workers it forks afterwards hold no policy either.
import httpx
import tethered
@tethered.scope(allow=["*.stripe.com:443"])
def charge(amount: int, token: str) -> dict:
# this function, and everything it calls, reaches *.stripe.com:443 only
return httpx.post("https://api.stripe.com/v1/charges", ...).json()
A scope is a decorator or a with block, for sync and async code, and it works without activate(). With both in place a connection must be allowed by both: a scope narrows the policy and never widens it.
Writing a library? Use tethered in your tests, and leave both activate() and scope() to the application, which owns the process (guide for package authors).
Python children started with the same interpreter inherit the policy as they start: multiprocessing and ProcessPoolExecutor workers, gunicorn and uvicorn workers, subprocess.run([sys.executable, ...]). A child launched through subprocess inside a scope inherits the scope too.
Every other launch (curl, another interpreter, python -S) is external: logged by default, refused with external_subprocess_policy="block". The rules are on the subprocess page.
tethered.activate(
allow=["api.stripe.com:443", "db.internal:5432"],
locked=True, # replacing or switching off the policy takes the token
lock_token=object(), # ... and nobody keeps it
fail_closed=True, # an error inside the check blocks instead of allowing
external_subprocess_policy="block", # no shelling out to a program that does the egress
)
Under a lock, monkey-patching tethered's functions or classes, or swapping its active policy, is detected and fails closed, and a Python child that cannot establish the locked policy refuses to start. Activate before importing third-party code: whatever runs earlier is not covered.
"block" also stops the helper programs libraries run, such as file for platform.architecture(), and code that expects OSError there fails (details). Locked mode is not a sandbox: the security model lists what it checks and what it does not stop.
python -m tethered check, run where the application runs, lists what tethered would refuse there, what it would not see and what needs configuration. It changes nothing (the check).conftest.py (example). A test that reaches a host you did not list fails, and production is not touched.log_only=True: every connection the policy would refuse is logged, and none is refused. The audit hook is installed all the same, each allowed host name is looked up a second time, and Python children inherit the policy. What tethered does not see is not logged (log-only mode).log_only.This is the beginning of the README. Read the rest at the original. Read the full README ↗