eth0ZTS / v0.1.2 · PRE-RELEASE
Run anything.
Anywhere.
Trust no one.
ZTS gives any unmodified container only the network paths its profile declares. By name, by port, by transport. No proxy, no SDK, no CA, no ambient trust. Everything else is unreachable by construction.
IN SHORT / BEFORE YOU READ EVERYTHING
What it does, who it is for, when you need it.
Everything below is implemented and tested in the current release. The manifesto explains why; the docs explain how.
Features
- Nothing by default
A managed container starts with no LAN, no host, no internet, no other containers. Every path exists because the manifest declared it.
- Names without a proxy
Allow
github.com, not an IP list. The container's own resolver answers only declared names and the answer becomes the authorization. No CA, no environment variable, TLS end to end. - Any image, unchanged
No SDK, no sidecar, no entrypoint.
--network noneand one label. The application sees an ordinaryeth0. - Ports and protocols
Relations are
to,via,protocol,ports. A database profile that only accepts 5432 from the app, and nothing from anyone else. - Out through the host or a tunnel
Egress via the host's own route, with private and link-local space guarded, or via WireGuard to a remote gateway.
- Nothing in the packet path
Policy is compiled into namespaces, routes and nftables. The controller only wakes up when a container starts or stops. Inspect the state with
nftandip. - One docker run
Linux or Mac with Docker Desktop. The controller is a container; the host installs nothing.
- Readable
A Go core and a one-file bash reference pass the same acceptance suite. Read the file, or the kernel state, instead of trusting a daemon.
Who it is for
- You run coding agents
An agent with a shell and your tokens should reach GitHub and the model API, and nothing else on your machine or network.
- You run code you did not write
Third-party tools, build steps, evaluation jobs. Give them the registry they need and no route to your LAN.
- You give workloads credentials
When a token leaks, the damage is bounded by where the workload could go. Let the network decide, not the application.
- You have docker run, not a cluster
Network policy for a laptop, a home server or a single VM, without Kubernetes, a service mesh or a proxy per app.
When you need it
An agent, a scraper or a notebook needs the internet, but your home or office network must stay invisible to it.
A CI or build job must reach the package registry and nothing else.
A service must only talk to its database, and the database must only hear from that service.
You want to prove, not promise, that a container cannot reach the LAN: the proof is kernel state you can list.
When you do not
- Kubernetes
use its network policies or a CNI that enforces them. ZTS is for hosts that run Docker.
- HTTP-level rules
methods, paths, request bodies are a proxy's job. ZTS can route a workload to one, it does not replace it.
- IPv6-only, Windows containers, Podman
not yet.
filter through an HTTP proxy: the app must honor HTTP_PROXY, and only TLS on 443 is covered. SSH, databases, UDP and QUIC fall outside.
authorize hosts, tags or subnets. Every container on the host inherits the same reach.
authorizes per container, at the network layer, by name, port and transport, for any protocol, with no proxy and nothing in the packet path. Meshes and proxies still have their place: ZTS routes to them.
TWO MINUTES / ONLY DOCKER
Four commands. One question. Nothing installed on the host.
Linux or Mac. The controller runs as a container, reaches the kernel through Docker's own VM or your host, and leaves only inspectable native state behind.
docker run --rm -it -v zts:/etc/zts ghcr.io/spektral-tech/zts init
docker run -d --name zts --restart unless-stopped --privileged --pid=host --network host \
-v /var/run/docker.sock:/var/run/docker.sock -v zts:/etc/zts ghcr.io/spektral-tech/zts up
docker run -d --name agent --network none --label zts.profile=dev alpine sleep infinity
docker exec agent wget -qO- https://api.github.com/zenThe first command asks what your containers will do and writes an editable zts.yaml. The second starts the controller. The third is any image you like, with --network none and one label; the network it is allowed to perceive appears as it starts. The fourth proves it: GitHub answers, and example.com does not even resolve. Swap dev for isolated and nothing answers. Everything else is in the docs.
WHAT HAPPENS / INSPECTABLE
Reference implementation, not trust anchor.
The reference shell shows one way to apply the model with ordinary Linux primitives: discover labeled --network none workloads, attach only their authorized paths, resolve only declared names, then get out of the packet path.
Use it, replace it, instrument containerd directly, or reproduce the same invariants by hand. The CLI demonstrates one implementation; ZTS does not depend on it.
Docker remains inventory-only. After convergence it can still report NetworkMode=none.
docker run -d --name agent-dev --network none \
--label zts.profile=dev alpine sleep infinity
7b0d5f6d9e4a
docker logs zts | tail -4
[zts] event: start agent-dev
[zts] reconcile workload agent-dev
[zts] reconcile nftables
[zts] converged (1 workload(s))
docker exec agent-dev wget -qO- https://api.github.com/zen
Keep it logically awesome.
docker exec agent-dev wget -T3 -qO- https://example.com
wget: bad address 'example.com'
docker inspect -f '{{.HostConfig.NetworkMode}}' agent-dev
noneONE REQUEST / THREE REALITIES
The workload asks. The substrate decides whether a path exists.
The application is unchanged. Select a profile to see the same logical request materialize differently.
- identity
- agent-17
- profile
- development
- decision
- sandbox egress
Development can reach GitHub only through the sandbox egress. The workload never chooses that realm.
ZERO PHYSICAL INGRESS / SAME RULES IN REVERSE
An authenticated tunnel is still just an untrusted workload.
A tunnel daemon may authenticate strongly to an external provider and keep every physical inbound port closed. ZTS still gives that daemon no ambient LAN, host or container access. It receives only the internal path explicitly materialized for its certified context.
physical ingress = 0identity = ingress/cloudflareallow → app:web:443reachable only through ZTSZTS does not ask you to replace Cloudflare's trust with ours. The reconciler compiles declared intent into inspectable native Linux state and exits. The security boundary is the kernel state you can audit: namespaces, routes, nftables and authenticated transports — not a proprietary packet daemon that must remain trusted forever.
IMPLEMENTATION INDEPENDENCE
You do not need to trust us either.
ZTS is a model, a manifest semantics and a set of observable invariants. You do not need our shell, our binary, our daemon, our control plane, or even our implementation choices.
Read how it works. Inspect the native state. Reproduce it yourself with Linux primitives, instrument your own runtime, or build another provider. If the resulting workload starts isolated, receives only declared paths, preserves transport authentication boundaries and fails closed, the substrate property exists without us in the trust chain.
DECLARATIVE, NOT IMPERATIVE
Describe the world. Do not script the network.
The manifest says which relationships should exist. The Linux provider chooses veth names, routes, tables, nftables state and WireGuard materialization.
Declare relationships. Let infrastructure enforce them.
transports:
host: { type: host }
resources:
accounting-db: { workload: accounting-db }
github: { names: [github.com, "*.github.com"] }
profiles:
isolated:
allow: []
dev:
allow:
- { to: accounting-db, protocol: tcp, ports: [5432] }
- { to: github, via: host, protocol: tcp, ports: [443] }There is no ip link add, interface name, routing table number or firewall command in the policy declaration. A name is resolved by the workload's own gateway and the answer becomes the authorization: no proxy, no CA, TLS end to end.
RECONCILIATION
Docker is inventory. Linux is the provider.
- 01Discover
Read labeled
--network nonecontainers and their netns. No Docker network writes. - 02Compare
Resolve manifest intent and inspect current kernel state.
- 03Plan
Compute missing, changed and orphaned materialization.
- 04Apply
Converge netns, veth, routes, nftables and WireGuard.
- 05Leave the packet path
Native dataplane state does the work. The controller only wakes up when a container starts or stops.
INVARIANTS
Small surface. Hard boundaries.
An arbitrary unmodified container must be unable to bypass the substrate.
The native state of a managed workload is no connectivity.
Identity gives policy trustworthy provenance. It does not grant trust.
Policy compiles into native dataplane state; ZTS is not a packet-by-packet policy daemon.
Proxies are optional next hops for L7 policy, not mandatory chokepoints.
Carriers transport the fabric. They do not own workload authorization.
Paths that cross a trust boundary are authenticated by the transport before they become reachable.
External credentials may remain opaque to the workload; infrastructure can exercise them only for authorized effects.
Ingress adapters are workloads too: authentication to an external tunnel provider never implies ambient internal reachability.
ZTS must be auditable as native state. Security must not depend on trusting a proprietary resident ZTS dataplane daemon.
COMPOSABLE TRUST
Use each layer for the smallest job it is good at.
Does not know ZTS exists.
Opaque credentials, method policy, audit, translation.
Workload reachability and route materialization.
Proves the transport peer before a remote path can exist.
Moves opaque transport traffic between places without owning workload policy.
Infrastructure the workload never receives directly.
AUTHORITY WITHOUT AMBIENT TRUST
Authenticate the path. Keep the credential opaque.
Two capabilities are part of ZTS itself, not roadmap garnish: remote transport can require cryptographic peer authentication, and external authentication can be exercised by infrastructure without handing the underlying credential to the workload.
WireGuard public keys bind a remote substrate peer to the encrypted path. Tailscale, LAN, relays or other carriers may carry that path, but the carrier does not become the workload authorization authority.
workload → policy → authenticated transport → remote substrateA policy proxy can authenticate outward from certified workload context while the bearer token, API key, password or signing key remains outside the workload. The workload receives the permitted effect, not the reusable secret.
identity + request + policy → brokered authn → external effectTrust is never ambient.
Being on the same host, LAN, tailnet, bridge, cluster, or cloud account must not grant authority. A workload begins with no assumed path to anything.
Every connection exists because the workload identity, current context, destination and policy make that specific path legitimate.
Identity is not trust.
Knowing that container A is container A solves provenance. It does not make every action by A trustworthy. Identity is evidence supplied to policy. Nothing more.
A workload may be known perfectly and still have no path to another workload, no route to the internet, and no right to reach a proxy.
No network by default.
The substrate does not join an existing LAN and then filter it. It materializes only the paths that should exist. If no grant produces a path, there is no path.
Prefer absence over rejection. Preferunreachableover403.
The application does not participate.
ZTS must work with an arbitrary unmodified container image. No SDK. No sidecar. No injected agent. No custom entrypoint. No application-aware firewall library. No secret mounted into the workload.
The application sees an ordinary network interface and ordinary destinations. Everything below that interface is infrastructure.
Policy declares relationships, not commands.
The manifest says which identities may reach which resources, through which trust boundary, and under what context. It does not prescribe interface names, routing table numbers or firewall commands.
How that policy becomes Linux state is implementation detail, not the security model.
Docker is inventory, not authority.
A workload can be created normally with --network none and labels describing its binding. ZTS reads metadata, finds the network namespace, observes drift, and materializes Linux state directly.
If every ZTS object disappears, the workload falls back to no network. Fail closed is the baseline.
Use the kernel before inventing a platform.
Network namespaces, veth pairs, routes, policy routing, nftables, WireGuard, and ordinary proxies are enough to express the core model. ZTS does not need a privileged always-on daemon in every packet path.
Boring is a security feature.
Reachability first. Mediation only when necessary.
The answer to a request may be nowhere, another local workload, a remote ZTS fabric, direct internet egress, or a proxy that applies additional policy. A proxy is an optional next hop, not the primary firewall.
Do not counterfeit cryptographic identity.
ZTS is not a TLS interception system. When software requires the real api.github.com, give it the real peer over end-to-end TLS or deny the path. Substitution is only valid when the application contract permits substitution.
Locality is an implementation detail.
A permitted peer may be in the same host, another rack, a home server, a cloud region, behind Tailscale, or behind another encrypted carrier. The workload asks for a resource. The substrate materializes a viable permitted path.
Carriers are not authorities.
WireGuard, Tailscale, direct LAN links and relays can carry ZTS traffic. They do not define workload policy. A carrier may know that host A can reach host B without learning which workload may reach which database.
Authenticate the transport.
Carrier reachability is not enough across a trust boundary. The remote path must be cryptographically authenticated before ZTS treats it as a valid substrate link.
The carrier answers can these hosts exchange packets? Transport authn answers did this path terminate at the expected substrate peer? ZTS policy answers may this workload use it?
Opaque credentials make authentication infrastructure.
A workload should receive the ability to cause permitted effects, not necessarily the credential used to cause them. A policy proxy can authenticate outward from certified workload context while bearer tokens, API keys, passwords, client certificates, or signing keys remain outside the workload.
Do not trust us either.
ZTS is not an authority you must install. You do not need our shell, binary, daemon or service. Understand the invariants, implement them yourself, and inspect the resulting native state.
The reference implementation is evidence that the model can be materialized. It is not part of the trust requirement.
Trust must be inspectable.
ZTS should reduce to ordinary, inspectable security boundaries: network namespaces, routes, nftables, authenticated transports and explicit proxy edges. The mechanism can vary; the trust relationship must remain visible.
If the only reason a path is safe is “trust our daemon,” it is not the model.
One sentence.
ZTS gives any unmodified workload only the network paths its identity and context are explicitly authorized to use, with no ambient trust and no required daemon in the packet path.
REFERENCE IMPLEMENTATION / BASH ESSENTIALS
You can read all of zts.sh. You do not need to trust it.
v0.1.2 ships two implementations that pass the same acceptance suite: a Go core, one static binary, which is the image above; and the bash essentials, one readable file that does the most with the least. They agree on every derived name and address. Neither is an authority: the mechanism is inspectable native state. Run either directly on a Linux host:
./zts.sh check zts.yaml
sudo ./zts.sh plan zts.yaml
sudo ./zts.sh apply zts.yaml
sudo ./zts.sh status zts.yamlGo core and bash essentials, in parity
read-only Docker discovery
IPv4 · netns + veth
deny-first nftables
same-host workload resources
names via gateway DNS, no proxy
host transport with private-space guard
WireGuard transports
controller on container events
presets on first run
Tailscale and Cloudflare transports
policy proxy with opaque credentials
IPv6
Podman, containerd, Kubernetes
multi-host fabric
