ZTSSource →

ZTS / 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.

identity proves origin≠trust·no grant→no path

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 none and one label. The application sees an ordinary eth0.

  • 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 nft and ip.

  • 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.

agent sandboxes

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.

meshes and VPN ACLs

authorize hosts, tags or subnets. Every container on the host inherits the same reach.

ZTS

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.

terminalcopy, paste, done
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/zen

The 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.

asciinema / simulated session
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
none

ONE 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.

WORKLOADagent-17GET api.github.cometh0
NATIVE LINUXZTS
identity
agent-17
profile
development
decision
sandbox egress
OPTIONAL L7dev proxyisolated / brokered
DIRECT TLSGitHubreal WAN destination
NO PATH∅unreachable by construction

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.

PUBLIC REQUESTInternetNo service port is opened on the host.physical ingress = 0
AUTHENTICATED TUNNELcloudflaredStrong tunnel authentication does not grant lateral trust.identity = ingress/cloudflare
SUBSTRATEZTSEvaluates the adapter context and materializes only the permitted next hop.allow → app:web:443
INTERNAL WORKLOADwebNot attached to LAN. Not published on the host.reachable only through ZTS
Hostno route
LANno route
Other workloadsno route unless explicitly granted
Do not trust the tunnel. Do not trust us.

ZTS 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.

understand→instrument→inspect→trust the state

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.

zts.yamlpolicy intent
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.

  1. 01Discover

    Read labeled --network none containers and their netns. No Docker network writes.

  2. 02Compare

    Resolve manifest intent and inspect current kernel state.

  3. 03Plan

    Compute missing, changed and orphaned materialization.

  4. 04Apply

    Converge netns, veth, routes, nftables and WireGuard.

  5. 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.

01

An arbitrary unmodified container must be unable to bypass the substrate.

02

The native state of a managed workload is no connectivity.

03

Identity gives policy trustworthy provenance. It does not grant trust.

04

Policy compiles into native dataplane state; ZTS is not a packet-by-packet policy daemon.

05

Proxies are optional next hops for L7 policy, not mandatory chokepoints.

06

Carriers transport the fabric. They do not own workload authorization.

07

Paths that cross a trust boundary are authenticated by the transport before they become reachable.

08

External credentials may remain opaque to the workload; infrastructure can exercise them only for authorized effects.

09

Ingress adapters are workloads too: authentication to an external tunnel provider never implies ambient internal reachability.

10

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.

APPLICATIONordinary software

Does not know ZTS exists.

L7 / OPTIONALpolicy proxy / credential broker

Opaque credentials, method policy, audit, translation.

SUBSTRATEZTS

Workload reachability and route materialization.

TRANSPORT AUTHNWireGuard / authenticated carrier

Proves the transport peer before a remote path can exist.

CARRIERTailscale / LAN / relay

Moves opaque transport traffic between places without owning workload policy.

UNDERLAYhost / LAN / internet

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.

01 / TRANSPORT AUTHNRemote reachability begins with an authenticated transport.

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 substrate
02 / OPAQUE CREDENTIALSAuthentication can be infrastructure, not application state.

A 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 effect

Trust 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. Prefer unreachable over 403.

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.yaml
implemented in v0.1.2

Go 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

not yet

Tailscale and Cloudflare transports
policy proxy with opaque credentials
IPv6
Podman, containerd, Kubernetes
multi-host fabric