Table of Contents
- Tailscale Global Deployment
- Why this works
- Topology in one picture
- Bind-address cheat sheet
- Setup walkthrough
- 1. Provision the boxes and install Tailscale
- 2. Lock down the tailnet ACL
- 3. Firewall the public NIC
- 4. Master — start the control plane bound to Tailscale
- 5. Master — enroll workers with their MagicDNS names
- 6. Ship the bundle over Tailscale itself
- 7. Worker — start agent + forwarder bound to Tailscale
- 8. Deploy deckies as usual
- Single-host (UNIHOST) variant
- Live-QA hardening checklist
- Operational notes
- Funnel and Serve are not for this
- Cross-region latency
- What to watch in the dashboard
- When to break glass
- Troubleshooting
- Known limitations
Tailscale Global Deployment
This page is the playbook for taking DECNET into the wild — cheap VPSes scattered across regions, real attackers hitting them, telemetry flowing home — without leaving your management plane on the public internet.
The rule is simple:
Decoy services bind to the public NIC. Everything else binds to Tailscale.
Attackers reach sshd:22, smbd:445, httpd:80, etc. on the box's
public address. The dashboard, REST API, SWARM control plane, log
listener, agent, updater, and any management SSH port (2222, 22222,
whatever you use) are not reachable except via your tailnet. Port scans
of the host find decoys and nothing else.
This is not a replacement for PKI and mTLS — control-plane mTLS is still on, always. Tailscale is the network gate that drops the packet before TLS even sees it. Defense in depth: an attacker would have to first land on your tailnet (WireGuard + your IdP) and then present a CA-signed cert. Either layer alone is sufficient; both together are the policy.
See also: SWARM Mode, Deployment Modes, Networking: MACVLAN and IPVLAN, Security and Stealth.
Why this works
DECNET already separates decoy traffic from management traffic at
the worker design level — decoys live on a MACVLAN/IPVLAN bridge with
their own MAC and (often) public IP, while the management daemons
(decnet api, decnet agent, decnet listener, decnet updater,
decnet swarmctl) listen on the host's primary NIC.
The default bind for management is 0.0.0.0, which is fine on a LAN and
catastrophic on a $4/mo VPS. Tailscale gives you a second interface,
tailscale0, with a stable 100.x.y.z address and MagicDNS name
(<host>.<tailnet>.ts.net). Bind management to that interface and the
public side of the box advertises only the decoys.
What this does not do:
- It does not anonymize the deckies. Attackers see your VPS's real public IP. That's the whole point.
- It does not protect against a compromised decky pivoting back to the host's loopback. That's still on you (container isolation, no shared volumes, no host-net mode — see Networking-MACVLAN-IPVLAN).
- It does not stop a misconfigured firewall from leaking the management ports. Tailscale is a network path, not a firewall — you still need the firewall (below).
Topology in one picture
┌─── tailnet (100.64.0.0/10) ─── WireGuard mesh ───┐
│ │
┌── you ──┐ ┌─ master VPS ─┐ ┌─ worker VPS (DE) ─┐ ┌─ worker VPS (SG) ─┐
│ laptop │ │ tailscale0 │ │ tailscale0 │ │ tailscale0 │
│ tailnet │◀──▶│ 100.64.0.1 │◀──▶│ 100.64.0.2 │ │ 100.64.0.3 │
└─────────┘ │ │ │ │ │ │
│ decnet api │ │ decnet agent │ │ decnet agent │
│ swarmctl │ │ forwarder ───────┼─┐ │ forwarder ─────┐ │
│ listener │ │ updater │ │ │ updater │ │
│ updater │ │ │ │ │ │ │
│ (all bound │ │ (all bound to │ │ │ (all bound to │ │
│ to ts iface)│ │ ts iface) │ │ │ ts iface) │ │
└──────┬───────┘ └─────────┬─────────┘ │ └────────┬────────┘ │
│ eth0 public │ eth0 public│ │ eth0 │
│ (nothing! optional │ DECKIES │ │ DECKIES │
│ static brochure) │ on macvlan │ │ macvlan │
▼ ▼ │ ▼ │
│ │
Internet ◀─── attackers ───▶ Decoys ◀─┴─────────────────────┘
(public IP, no Tailscale)
Two planes, two NICs. Management is invisible to anyone not on the tailnet. Decoys are as exposed as they need to be to attract traffic.
Bind-address cheat sheet
| Daemon | Default bind | What you want | Flag / env |
|---|---|---|---|
decnet api |
0.0.0.0 |
tailscale0 | --host 100.x.y.z or DECNET_API_HOST |
decnet web |
0.0.0.0 |
tailscale0 | --host 100.x.y.z |
decnet swarmctl |
127.0.0.1 |
tailscale0 | --host 100.x.y.z |
decnet listener |
0.0.0.0 |
tailscale0 | --host 100.x.y.z |
decnet agent |
0.0.0.0 |
tailscale0 | --host 100.x.y.z |
decnet updater |
0.0.0.0 |
tailscale0 | --host 100.x.y.z |
| Management SSH | n/a | tailscale0 | ListenAddress 100.x.y.z in sshd_config |
| Decoy services | host bridge | public NIC | unchanged — DECNET handles this |
100.x.y.z is the box's own Tailscale-assigned address (tailscale ip -4). You can also bind by interface name on Linux with ip addr show tailscale0 if you want it scripted.
The MagicDNS name (master.<tailnet>.ts.net) is what you'll point your
browser and CLI at. Don't bake 100.x.y.z literals into configs —
they're stable but the DNS name is more portable.
Setup walkthrough
A live-QA-ready single-master, two-worker SWARM deployment on three VPSes. UNIHOST-on-Tailscale is the same minus steps 5–7.
1. Provision the boxes and install Tailscale
Pick a provider that explicitly permits honeypots in their AUP. Hetzner, OVH, Vultr are typically OK if you respond to abuse reports within 24h; some are not. Read the ToS. Getting your account nuked mid-experiment is a waste of a tailnet.
Per host:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --advertise-tags=tag:decnet-master # on the master
sudo tailscale up --ssh --advertise-tags=tag:decnet-worker # on each worker
tailscale ip -4 # write this down, you'll bind DECNET to it
--ssh lets you reach the box's management SSH via Tailscale SSH
without needing public 22 open. That alone gets you halfway.
--advertise-tags lets the ACL (next step) discriminate master from
worker. Tags require pre-authorization in the admin console — set them
up under Access controls → Tags before bringing hosts up.
2. Lock down the tailnet ACL
In the Tailscale admin console, replace the default "everyone can talk to everyone" ACL with role-based rules:
{
"tagOwners": {
"tag:decnet-master": ["autogroup:admin"],
"tag:decnet-worker": ["autogroup:admin"],
"tag:decnet-ops": ["autogroup:admin"] // your laptop / phone
},
"acls": [
// Ops machines can reach everything DECNET on its management ports.
{ "action": "accept",
"src": ["tag:decnet-ops"],
"dst": ["tag:decnet-master:8000,8770,6514",
"tag:decnet-worker:8765,8766"] },
// Master ↔ worker control plane.
{ "action": "accept",
"src": ["tag:decnet-master"],
"dst": ["tag:decnet-worker:8765,8766"] },
// Worker → master log forwarding (RFC 5425).
{ "action": "accept",
"src": ["tag:decnet-worker"],
"dst": ["tag:decnet-master:6514"] },
// SSH for ops.
{ "action": "accept",
"src": ["tag:decnet-ops"],
"dst": ["tag:decnet-master:22", "tag:decnet-worker:22"] }
],
"ssh": [
{ "action": "check",
"src": ["autogroup:admin"],
"dst": ["tag:decnet-master", "tag:decnet-worker"],
"users": ["root", "decnet"] }
]
}
Workers cannot reach each other's management ports. The master cannot be reached by random tagged nodes. This is a deny-by-default posture.
3. Firewall the public NIC
Tailscale is a path; the firewall is the wall. On every VPS, drop inbound to management ports on the public interface — even though nothing is bound there, a future misconfiguration shouldn't be an accident waiting to happen.
# Replace eth0 with your actual public NIC (`ip route show default`).
sudo nft add table inet decnet
sudo nft add chain inet decnet input '{ type filter hook input priority 0; policy accept; }'
# Drop management ports on the public NIC, regardless of bind.
sudo nft add rule inet decnet input iif eth0 tcp dport \
{ 8000, 8765, 8766, 8770, 6514, 2222, 22222 } drop
# Optionally: drop public 22 entirely now that Tailscale SSH carries you.
sudo nft add rule inet decnet input iif eth0 tcp dport 22 drop
Decoy ports (whatever the deployer assigns — typically 22, 80, 443, 445, 3389, 21, etc. on the MACVLAN sub-interface) are unaffected because they're on a separate logical interface.
If you're more comfortable in ufw:
sudo ufw deny in on eth0 to any port 8000,8765,8766,8770,6514,2222,22222 proto tcp
4. Master — start the control plane bound to Tailscale
TS_IP=$(tailscale ip -4 | head -n1)
# API + dashboard.
decnet api --daemon --host "$TS_IP" --port 8000
# SWARM control plane. The default is 127.0.0.1; we move it to ts0 so
# you can drive `decnet swarm ...` from your laptop without SSHing in.
decnet swarmctl --daemon --host "$TS_IP" --port 8770
# RFC 5425 syslog-over-TLS sink.
decnet listener --daemon \
--host "$TS_IP" --port 6514 \
--log-path ~/.decnet/master-logs/master.log \
--json-path ~/.decnet/master-logs/master.json
Verify from your laptop:
curl -sf http://master.<tailnet>.ts.net:8000/api/v1/health
# Expect 401 (the health endpoint is auth-gated). 401 means "reachable
# and rejecting unauthenticated calls" — that's success.
If you get a connection timeout, the bind is wrong or the firewall is
blocking the tailscale0 chain. ss -tlnp | grep 8000 on the master
should show 100.x.y.z:8000, not 0.0.0.0:8000.
5. Master — enroll workers with their MagicDNS names
When you enroll a worker, the --address is what the master will dial.
Use the MagicDNS name so you don't have to re-enroll if Tailscale
re-assigns the 100.x address.
decnet swarm enroll \
--name worker-de \
--address worker-de.<tailnet>.ts.net \
--sans worker-de.<tailnet>.ts.net,100.64.0.2 \
--out-dir /tmp/worker-de-bundle
Include both the DNS name and the current 100.x address in --sans
so cert validation succeeds whichever the master happens to dial.
6. Ship the bundle over Tailscale itself
scp -r /tmp/worker-de-bundle/* anti@worker-de.<tailnet>.ts.net:~/.decnet/agent/
No public SSH involved. The bundle never crosses the open internet.
7. Worker — start agent + forwarder bound to Tailscale
TS_IP=$(tailscale ip -4 | head -n1)
sudo decnet agent --daemon \
--host "$TS_IP" --port 8765 \
--agent-dir /home/anti/.decnet/agent
decnet forwarder --daemon \
--master-host master.<tailnet>.ts.net \
--master-port 6514 \
--log-file /var/log/decnet/decnet.log \
--state-db ~/.decnet/agent/forwarder.db \
--agent-dir /home/anti/.decnet/agent
master.<tailnet>.ts.net resolves over MagicDNS — no /etc/hosts
hack, no static IPs.
8. Deploy deckies as usual
decnet deploy --mode swarm --deckies 8 --randomize-services
The deployer doesn't know or care that the control plane is on Tailscale. Deckies bind to the worker's MACVLAN bridge on the public NIC; that path is untouched.
Single-host (UNIHOST) variant
If you're running the whole thing on one VPS and just want your laptop to reach the dashboard privately:
TS_IP=$(tailscale ip -4 | head -n1)
decnet api --daemon --host "$TS_IP" --port 8000
decnet web --daemon --host "$TS_IP" --port 5173 # if you serve the SPA separately
sudo decnet deploy --mode unihost --deckies 5 --interface eth0 --randomize-services
Browse to http://<host>.<tailnet>.ts.net:8000/. Done. The deckies are
on eth0's MACVLAN bridge and reachable to attackers; the dashboard is
not.
For HTTPS on the dashboard you can use Tailscale's own cert provisioning
(tailscale cert <host>.<tailnet>.ts.net) and feed the result to
decnet api --ssl-keyfile ... --ssl-certfile .... MagicDNS issues real
LetsEncrypt certs for tailnet hostnames at no cost.
Live-QA hardening checklist
You're throwing this at the wild. Before you flip the switch:
- Provider AUP read. Honeypots explicitly permitted, abuse contact monitored.
- No real secrets on the boxes. No personal SSH keys, no AWS credentials, no password reuse. Treat each VPS as compromised from day zero.
- Decky containers are not on host network. MACVLAN/IPVLAN
only.
--network hostanywhere is a bug, not a shortcut. - Management SSH on Tailscale only. Public 22 dropped at the
firewall (and ideally
ListenAddress 100.x.y.zinsshd_config). - All DECNET daemons bound to tailscale0.
ss -tlnpconfirms no0.0.0.0for management ports. - Firewall drops management ports on public NIC as belt-and- suspenders.
- mTLS bundles intact on every worker.
openssl s_client -connect worker:8765from a non-tailnet host fails at TCP (firewalled), and from a tailnet host without a client cert fails at handshake. - Time sync running. mTLS will fail with skew >5min.
chronyc trackinghealthy on every host. - Forwarder buffering tested.
systemctl stop decnet-listeneron the master for a minute; logs accumulate locally; on restart they replay without gaps. (See SWARM-Mode § Master crash.) - Kill switch documented. One command per box that stops the
decoys and seals the management plane.
decnet teardown --all+tailscale downon the master is the minimum. - Abuse playbook written. When the provider emails you about an SSH brute-force complaint, you need a one-paragraph reply ready that explains "this is an authorized honeypot, here's the research contact" — not a panicked decommission.
Operational notes
Funnel and Serve are not for this
Tailscale Funnel exposes a tailnet service to the public internet. Do not use it for any DECNET management endpoint, ever. The whole point is to keep them off the public internet. Funnel has its place (publicly reachable canary pages, decoy brochure sites) but not for the dashboard.
tailscale serve (intra-tailnet TLS termination) is fine and can save
you the cert dance for the dashboard:
tailscale serve --bg --https=443 http://localhost:8000
Now https://<master>.<tailnet>.ts.net/ works in a browser with a real
cert, and the underlying bind can stay on 100.x.y.z:8000.
Cross-region latency
The forwarder is async and tolerates 200–400ms RTT just fine. The mTLS handshake adds one round trip on reconnect. Sub-second is not a realistic target for SG↔EU log delivery and isn't required — RFC 5425 is offset-tracked and gap-free, not real-time.
decnet swarm check on a high-latency worker can take a couple of
seconds per host. That's the master polling /health synchronously over
mTLS over 200ms RTT — expected.
What to watch in the dashboard
For a wild deployment, the high-signal pages:
- Attackers — who's hitting you, source ASN, reputation.
- Sessions — full transcripts. Real attackers diverge from script kiddies fast.
- Credentials — what they're trying. (See the credentials view — this is where DEBT-040 phase 3 RDP captures end up.)
- Live logs (SSE) — useful in the first hour of a deploy to confirm the pipeline is wet.
When to break glass
If a worker's host itself looks compromised (not just a decky — the host), pull it from the tailnet first and the swarm second:
# From your laptop:
tailscale set --auto-update=false # (on the worker, but you may need ssh)
# Faster: yank it from the admin console — "Disable" the device.
# Then on the master:
decnet swarm decommission --name worker-xx --yes
Disabling at the Tailscale console severs the management plane instantly without needing the worker to cooperate.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
curl http://master.<tailnet>.ts.net:8000 from laptop times out |
API still bound to 0.0.0.0 but firewall drops it; or bound to 127.0.0.1; or laptop not on tailnet |
ss -tlnp | grep 8000 on master; tailscale status on laptop |
Worker forwarder logs ConnectionRefusedError to master:6514 |
Listener bound to 0.0.0.0 and firewall drops it from tailscale0; or bound to 127.0.0.1; or ACL blocks worker→master:6514 |
Re-bind listener with --host $TS_IP; check ACL dst includes tag:decnet-master:6514 |
decnet swarm check says reachable: false for one worker |
ACL doesn't allow tag:decnet-master → tag:decnet-worker:8765; or worker agent bound to 0.0.0.0 while firewall drops public side and Tailscale rule isn't matched |
Check ACL; ss -tlnp | grep 8765 on worker |
ssl.SSLCertVerificationError: Hostname mismatch from master to worker |
--address at enrollment didn't include the MagicDNS name in SANs |
Re-enroll with --sans worker.<tailnet>.ts.net,100.x.y.z |
Tailscale SSH works but scp doesn't |
scp doesn't use the Tailscale SSH server; it falls back to OpenSSH on port 22 — which you firewalled off |
Either keep public 22 open only from Tailscale CGNAT range (100.64.0.0/10), or use tailscale file cp |
| Public 22 still being brute-forced even though "firewalled" | The firewall rule is on the wrong interface, or ufw ordering put a permissive rule above the deny |
nft list ruleset and read it top to bottom — don't trust your own config without verifying |
| Decoy traffic also disappears after firewall changes | You dropped on the wrong interface or used INPUT policy DROP without explicit accepts for the MACVLAN bridge |
MACVLAN sub-interfaces have their own naming (e.g. decnet_macvlan0) — iif eth0 should not match them, but verify with tcpdump |
Browser says "your connection is not private" on <host>.ts.net |
tailscale serve not configured, or tailscale cert not run for this hostname |
tailscale cert <host>.<tailnet>.ts.net then point decnet api --ssl-* at the resulting files, or use tailscale serve --https=443 to terminate |
Master is on Tailscale but workers can't resolve master.<tailnet>.ts.net |
MagicDNS not enabled on the tailnet, or --accept-dns=false was used at tailscale up |
Enable MagicDNS in admin console; restart tailscaled on workers |
Known limitations
- No automated bind discovery. DECNET doesn't auto-detect "use
tailscale0 if present" — you pass
--hostor set the env var yourself. This is intentional: silently picking an interface based on what's up at startup is exactly the kind of magic that gets your management plane on the public internet by accident after a reboot. - Tailnet outage = control-plane outage. If Tailscale's coordination server is unreachable from a worker, new control-plane connections fail. Existing WireGuard tunnels stay up (DERP relays handle most cases), but a cold worker after a tailnet outage won't reach the master until tailnet recovers. The decoys keep serving attackers and the forwarder keeps buffering — same story as a master outage.
- Tailscale free plan caps. Free tier is generous (100 devices, 3 users at the time of writing) but has limits. A 50-VPS DECNET deployment fits comfortably; a 500-VPS one does not.
DECNET
User docs
- Quick-Start
- Installation
- Requirements-and-Python-Versions
- CLI-Reference
- INI-Config-Format
- Custom-Services
- Services-Catalog
- Service-Personas
- Archetypes
- Distro-Profiles
- OS-Fingerprint-Spoofing
- Networking-MACVLAN-IPVLAN
- Deployment-Modes
- SWARM-Mode
- Tailscale-Global-Deployment
- Resource-Footprint
- MazeNET
- Remote-Updates
- Environment-Variables
- Teardown-and-State
- Database-Drivers
- Systemd-Setup
- Logging-and-Syslog
- Fingerprinting
- Service-Bus
- Realism
- Web-Dashboard
- REST-API-Reference
- Mutation-and-Randomization
- Troubleshooting
Developer docs
DECNET — honeypot deception-network framework. Pre-1.0, active development — use with caution. See Sponsors to support the project. Contact: samuel@securejump.cl