𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Theme

Homelab

Tailscale Hardening

Hardening Tailscale: ACLs, Pinned Versions, and NetworkPolicies

Overview

This article hardens Tailscale by making admin-only access explicit. Currently any device on your tailnet can reach the Lab VLAN subnet. After this article, only admin-tagged devices can access the subnet route - if a device gets compromised or removed from your control, removing its tag revokes access. We also pin the Helm chart version and add NetworkPolicies to limit the operator's blast radius.

Tip:Having trouble? See v1.5.0 for what your setup should look like after completing this article.

Before You Begin

Prerequisites

What We're Setting Up

ComponentBeforeAfter
Helm chart1.x (floating)Pinned version
ACLsDefault allow-allAdmin-only access
NetworkPolicyNoneEgress limited to control plane

Why These Controls

Helm Pinning: Floating versions (1.x) can introduce breaking changes or vulnerabilities without notice. Pinning ensures reproducible deployments.

Tailscale ACLs: Control what devices can reach over the Tailscale tunnel. Without ACLs, any device can reach any destination. ACLs make admin-only access explicit - only tag:admin devices can reach the Lab VLAN through the subnet router.

NetworkPolicy: Control what pods can reach at the Kubernetes network layer. If the Tailscale operator pod is compromised (e.g., via a vulnerability or stolen service account token), NetworkPolicies prevent it from reaching other VLANs like Network (192.168.1.x) or Drive (192.168.5.x). This is separate from Tailscale ACLs - both layers work together for defense-in-depth.

How ACLs Restrict Access

After applying these ACLs, access is restricted as follows:

SourceDestinationAllowed?Why
Admin devicesOther admin devices (SSH to bastion)tag:admintag:admin
Admin devicesLab VLAN (Plex, game servers, APIs)tag:admin192.168.10.0/24:*
Untagged devicesLab VLANNo rule allows it

Key points:

  • Game servers via Tailscale: Admin devices can still access Minecraft, Factorio, and Plex via their LoadBalancer IPs - playit.gg is only needed for friends without Tailscale
  • Revocable access: Removing tag:admin from a device immediately revokes its Lab VLAN access

Two layers of defense:

LayerProtects AgainstScope
Tailscale ACLsCompromised tailnet device using tunnel to reach other devicesAll tailnet devices
NetworkPolicyCompromised pod reaching other VLANs via cluster networkPods in tailscale namespace

Both are needed - ACLs protect at the tunnel level, NetworkPolicies protect at the pod level.

Pin Helm Chart Version

Check Current Version

First, find what version is currently deployed:

helm list -n tailscale

Expected output:

NAME                NAMESPACE   REVISION  ...  CHART                      APP VERSION
tailscale-operator  tailscale   5         ...  tailscale-operator-1.94.2  v1.94.2

Note the chart version (e.g., 1.94.2) - this is what you'll pin.

Update HelmRelease

Pin to the specific version instead of floating 1.x:

k8s/core/tailscale/helmrelease.yaml:

# ... existing header ...
spec:
    interval: 1h
    chart:
        spec:
            chart: tailscale-operator
            version: "1.94.2" # CHANGE from "1.x"
            sourceRef:
                kind: HelmRepository
                name: tailscale
                namespace: flux-system
    # ... existing install, upgrade, values, valuesFrom ...

Replace 1.94.2 with your current version from the helm list output.

Add NetworkPolicy

Limit the Tailscale operator's egress to only what it needs.

NetworkPolicy

k8s/core/tailscale/networkpolicy.yaml:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
    name: tailscale-operator
    namespace: tailscale
spec:
    podSelector: {}
    policyTypes:
        - Egress
    egress:
        # Tailscale control plane (DERP relays, coordination servers)
        # Allow public internet, block all private ranges
        - to:
              - ipBlock:
                    cidr: 0.0.0.0/0
                    except:
                        - 10.0.0.0/8 # Class A private
                        - 172.16.0.0/12 # Class B private
                        - 192.168.0.0/16 # Class C private (our VLANs)
          ports:
              - protocol: UDP
                port: 41641 # Tailscale direct connections
              - protocol: TCP
                port: 443 # Tailscale coordination
        # DNS resolution
        - to:
              - namespaceSelector:
                    matchLabels:
                        kubernetes.io/metadata.name: kube-system
          ports:
              - protocol: UDP
                port: 53
        # Kubernetes API (operator needs this to manage resources)
        - to:
              - ipBlock:
                    cidr: 192.168.10.30/32 # control-plane-0
              - ipBlock:
                    cidr: 192.168.10.31/32 # control-plane-1
              - ipBlock:
                    cidr: 192.168.10.32/32 # control-plane-2
          ports:
              - protocol: TCP
                port: 6443
        # Lab VLAN only (for subnet routing)
        - to:
              - ipBlock:
                    cidr: 192.168.10.0/24

Kustomization

Add the NetworkPolicy to resources:

k8s/core/tailscale/kustomization.yaml:

---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
    - namespace.yaml
    - secret.sops.yaml
    - helmrepository.yaml
    - helmrelease.yaml
    - connector.flux.yaml
    - networkpolicy.yaml # ADD

Deploy Changes

Commit Changes

git add k8s/core/tailscale/
git commit -m "feat(tailscale): harden with pinned version and NetworkPolicy"
git push

Reconcile

flux reconcile source git flux-system
flux reconcile kustomization sync

Verify Helm Version

kubectl get helmrelease -n tailscale tailscale-operator -o jsonpath='{.spec.chart.spec.version}'

Expected: Your pinned version (e.g., 1.94.2).

Verify NetworkPolicy

kubectl get networkpolicy -n tailscale

Expected: tailscale-operator policy listed.

Configure Tailscale ACLs

Update ACLs in the Tailscale admin console. ACLs are managed outside the cluster - even if someone compromises the cluster, they can't modify ACLs.

Update ACL Policy

In Tailscale Admin ConsoleAccess Controls1, replace your existing policy with:

{
    "tagOwners": {
        "tag:admin": ["autogroup:admin"],
        "tag:k8s-operator": ["autogroup:admin"],
        "tag:k8s": ["tag:k8s-operator"]
    },

    "autoApprovers": {
        "routes": {
            "192.168.10.0/24": ["tag:k8s"]
        }
    },

    "acls": [
        {
            "action": "accept",
            "src": ["tag:admin"],
            "dst": ["tag:admin:*"]
        },
        {
            "action": "accept",
            "src": ["tag:admin"],
            "dst": ["192.168.10.0/24:*"]
        }
    ],

    "ssh": [
        {
            "action": "check",
            "src": ["autogroup:member"],
            "dst": ["autogroup:self"],
            "users": ["autogroup:nonroot", "root"]
        }
    ]
}

This removes the allow-all grants and adds restrictive acls. Only tag:admin devices can now reach the Lab VLAN.

What changed from v2:

SectionBefore (v2)After (v3)
tagOwnerstag:k8s-operator, tag:k8sAdded tag:admin
autoApproversIndividual 192.168.1.x routesSingle 192.168.10.0/24 (Lab VLAN)
grants{"src": ["*"], "dst": ["*"]} (allow-all)Removed
acls(none)Admin-only access rules
ssh(unchanged)(unchanged)

What the ACLs do:

  • Admin → Admin - SSH between admin devices (laptop to bastion)
  • Admin → Lab VLAN - Full access to Plex, game servers, cluster APIs (via subnet router when remote)
  • Untagged devices - No access to Lab VLAN

The subnet router doesn't need its own ACL rule - it just forwards traffic for authorized admin devices. The ACL check happens on the source device, not the forwarder.

Tag Your Devices

Important:Tag your devices immediately after saving. Without tag:admin, you'll lose remote access.

In Machines tab:

  1. Click the menu on your device
  2. Select Edit ACL tags...
  3. Select tag:admin from the dropdown
  4. Save

Repeat for each admin workstation (laptop, bastion, etc.).

Verify ACLs

ACLs only affect traffic through the Tailscale tunnel. To test, disconnect from your local network (use mobile hotspot) or test from a remote location.

Admin Device Access

From an admin-tagged device over Tailscale (not on local network):

kubectl get nodes

Expected: Nodes listed. This confirms the tunnel reaches the API server at 192.168.10.30:6443.

Without Tailscale

Disable Tailscale on the same device:

kubectl get nodes

Expected: Connection times out (no route to cluster).

Next Steps

With Tailscale hardened, continue with MetalLB and ingress-nginx hardening.

See: MetalLB & Ingress Hardening

Resources

Footnotes

  1. Tailscale, "Access Controls," tailscale.com. Accessed: Feb. 28, 2026. [Online]. Available: https://tailscale.com/kb/1018/acls

Previous
Plex LAN Configuration