feat: opt-in Tailscale support for sandbox containers - #89
Open
wbtracey wants to merge 5 commits into
Open
Conversation
wbtracey
force-pushed
the
feat/tailscale-support
branch
from
July 28, 2026 06:51
87a80e0 to
0d5cb89
Compare
Sandboxes can join the user's tailnet so agents can reach private services (internal APIs, databases, staging boxes), while the Squid allowlist stays the default and only egress for everything else. Enable per-command with `--tailscale` (run/shell/add/retry) or `tailscale = true` in tsk.toml. Off by default. Security model: with Tailscale on, the enforced egress boundary for tailnet traffic is your Tailscale ACLs + the auth key's tags, not Squid — the sandboxed agent holds NET_ADMIN and the tailscaled socket, so tsk's config flags are defense-in-depth, not a hard boundary. Recommended: a reusable, ephemeral, tagged auth key (minted once, scoped by tag + ACL; reused across tasks). Non-tailnet egress stays locked to Squid in every path (no direct egress; fails closed). - Auth key is read on the host at container start ($TS_AUTHKEY / env var / key file), passed in to join, then `unset` and the agent `exec`'d so the in-container agent can't recover it from /proc/<pid>/environ; it is never written to the task DB or image (it does live in Config.Env, readable via `docker inspect` on the host, for the container lifetime). - accept-routes is opt-in (`tailscale_accept_routes`, default false); isolation-weakening `tailscale_up_args` (--exit-node, --advertise-*, --accept-*, --netfilter-mode, incl. single-dash forms) are rejected at task creation; `--tailscale` requires network isolation. - Kernel mode (Docker + host /dev/net/tun) routes the tailnet transparently (tailnet in NO_PROXY, incl. IPv6 fd7a:115c:a1e0::/48); rootless Podman runs userspace mode via tailscaled's SOCKS5 proxy (ALL_PROXY). Docker Desktop on macOS (no host TUN) isn't supported. - Tailnet device names resolve via host-snapshotted /etc/hosts aliases (--add-host); opt out with `tailscale_host_aliases = false`. Device names only, not full MagicDNS. Docs: README.md "Tailscale (optional)" and docs/network-isolation.md. Co-Authored-By: Claude <noreply@anthropic.com>
wbtracey
force-pushed
the
feat/tailscale-support
branch
from
July 28, 2026 06:59
0d5cb89 to
22887dd
Compare
Sandbox nodes joined the tailnet but were never cleaned up, so the Tailscale admin console accumulated leftover tsk-* devices — the node's lifetime depended entirely on whether the auth key happened to be ephemeral, which a hand-made key easily gets wrong (non-ephemeral and/or untagged, attributing the node to the operator's personal identity). Add opt-in per-task key minting: when a Tailscale API credential (a PAT or an OAuth client) is configured, tsk mints a fresh single-use, ephemeral, tagged auth key per task via the Tailscale API at container start. The node is then always ephemeral (Tailscale auto-removes it a few minutes after the sandbox stops — robust even if tsk crashes) and always tagged (so its access is scoped by the tag's ACL, not by anyone's personal identity). The existing bring-your-own-key path is retained as a fallback, now with an in-container warning when the joined node is untagged. Config (all opt-in; minting only runs when one is set): - tailscale_api_key_env / tailscale_api_key_file (PAT) - tailscale_oauth_client_id / tailscale_oauth_secret_env / _file (OAuth) - tailscale_tailnet (default "-") and tailscale_tags (default ["tag:tsk-sandbox"]) Security: the API credential is read host-side only and is never written to the container environment, the task config snapshot, or logs; the minted per-task key flows through the existing unset+exec scrub, and MintCredential redacts its Debug output. OAuth takes precedence over a PAT; a configured OAuth secret without a client id now surfaces a clear error instead of silently falling back. README documents the minting setup (PAT vs OAuth) and node cleanup. Includes the design spec and implementation plan under docs/superpowers/.
wbtracey
force-pushed
the
feat/tailscale-support
branch
from
July 31, 2026 17:31
b538b49 to
fbf8e44
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Opt-in
--tailscalelets a sandbox join the operator's tailnet to reach private services (internal APIs, databases, staging boxes), while the Squid allowlist stays the default and only egress for everything else. Off by default; enabled per-command (run/shell/add/retry) ortailscale = trueintsk.toml.Why
Some tasks need the agent to reach a private service that isn't on — and shouldn't be added to — the public allowlist. Tailscale gives a scoped, ACL-controlled path to those services without opening general internet egress.
Security model
With Tailscale enabled, the enforced egress boundary for tailnet traffic is your Tailscale ACLs + the auth key's tags, not Squid. The sandboxed agent runs non-root but holds
NET_ADMINand owns thetailscaledcontrol socket, so tsk's config flags (accept_routes, theup_argsdenylist) are defense-in-depth, not a hard boundary — a determined agent can reconfigure its own tailnet egress at runtime. This is called out prominently in the docs; the recommended setup is a reusable, ephemeral, tagged auth key — minted once and reused across tasks, scoped by tag + ACL (ephemeral so sandbox nodes auto-clean). A per-task key would be impractical for a task runner; a future enhancement could mint short-lived keys automatically from a Tailscale OAuth client.Non-tailnet egress stays locked to Squid in every path (the internal no-gateway network means it fails closed even with
NET_ADMIN): no direct egress, allowlist enforced.How it works
$TS_AUTHKEY/ configurable env var / key file), passed in to join, thenunsetand the agentexec'd so the in-container agent can't recover it from/proc/<pid>/environ. Never written to the task DB or image. (It does live inConfig.Env, readable viadocker inspecton the host, for the container's lifetime — treat host access as trusted.)/dev/net/tun→ transparent kernel mode (tailnet inNO_PROXY, incl. IPv6fd7a:115c:a1e0::/48). Rootless Podman → userspace mode via tailscaled's SOCKS5 proxy (ALL_PROXY=socks5h://localhost:1055). Docker Desktop on macOS (no host TUN) is not supported.tailscale statusand injected into the container's/etc/hostsvia--add-host— no in-container privilege needed. Device names only, not full MagicDNS. Opt out withtailscale_host_aliases = false.accept_routesis opt-in (default false); isolation-weakeningup_args(--exit-node,--advertise-*,--accept-*,--netfilter-mode, incl. single-dash spellings) are rejected at task creation;--tailscalerequires network isolation..tailscale.com/.tailscale.io:443); Tailscale tasks get their own proxy fingerprint.Config
tailscale,tailscale_auth_key_env,tailscale_auth_key_file,tailscale_hostname,tailscale_accept_routes(default false),tailscale_host_aliases(default true),tailscale_up_args.Testing
~expansion), shell-quote injection safety, config merge/precedence, theup_argsdenylist (incl. single-dash), the network-isolation requirement, kernel vs userspace proxy env,/etc/hostsalias building, and hostname sanitization.NET_ADMIN+ Tailscale active.Docs
README.md"Tailscale (optional)" anddocs/network-isolation.md.Limitations
Full MagicDNS / split-DNS / subnet-router hostnames aren't covered (device names only); self-hosted control planes (Headscale /
--login-server) aren't supported (the allowlist only opens Tailscale SaaS domains); exit-node egress is intentionally blocked; userspace-mode HTTP-to-tailnet needs an explicit--socks5-hostname(non-HTTP is transparent); Docker Desktop on macOS is unsupported.🤖 Generated with Claude Code