Skip to content

Instantly share code, notes, and snippets.

@sorend
Last active March 7, 2025 03:26
Show Gist options
  • Save sorend/7332c33ca9599d1cc330b68fd2f7b61a to your computer and use it in GitHub Desktop.
Save sorend/7332c33ca9599d1cc330b68fd2f7b61a to your computer and use it in GitHub Desktop.
Tailscale exit-node through gluetun (here AirVPN) on Kubernetes

Using tailscale with gluetun (and here AirVPN)

Network overview: Your device <-- internet --> VPN-provider <-- tailnet --> Kubernetes-Pod <-- vpn --> Internet

  1. Go into the AIRVPN client area and create a new device and a generate a configuration for the device.
  2. Go into the tailscale dashboard add a Linux server and note down the auth key
  3. Fill out the details in the secrets
  4. Apply the resources

TODO

DNS is using kubernetes internal DNS, so it is leaking. This is because tailscale needs access to the kubernetes API, and I have not figured out how to split DNS lookup.

Direct connection to tailscale on Kubernetes without going through the VPN provider would be nice. However, I have not figured out if tailscale can be forced to advertise a specific public ip:port.

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tailvpn
spec:
selector:
matchLabels:
app: tailvpn
template:
metadata:
labels:
app: tailvpn
spec:
volumes:
- name: tun
hostPath:
path: /dev/net/tun
type: CharDevice
serviceAccountName: tailscale
containers:
- image: ghcr.io/qdm12/gluetun:latest
name: gluetun
imagePullPolicy: Always
securityContext:
privileged: true
env:
- name: TZ
value: Asia/Kolkata
- name: VPN_SERVICE_PROVIDER
value: airvpn
- name: VPN_TYPE
value: wireguard
- name: FIREWALL_INPUT_PORTS
value: "41461"
- name: FIREWALL_OUTBOUND_SUBNETS
value: "10.0.0.0/8"
- name: DNS_KEEP_NAMESERVER
value: "on"
envFrom:
- secretRef:
name: gluetun-auth
livenessProbe:
exec:
command:
- /gluetun-entrypoint
- healthcheck
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5
volumeMounts:
- name: tun
mountPath: /dev/net/tun
- image: ghcr.io/tailscale/tailscale:latest
name: tailscale
securityContext:
runAsUser: 1000
runAsGroup: 1000
imagePullPolicy: Always
env:
- name: TS_NAME
value: tailvpn
- name: TS_KUBE_SECRET
value: tailscale
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_UID
valueFrom:
fieldRef:
fieldPath: metadata.uid
- name: TS_USERSPACE
value: "true"
- name: TS_ACCEPT_DNS
value: "false"
- name: TS_AUTHKEY
valueFrom:
secretKeyRef:
name: tailscale-auth
key: TS_AUTHKEY
- name: TS_EXTRA_ARGS
value: "--advertise-exit-node"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tailscale
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["secrets"]
# Create can not be restricted to a resource name.
verbs: ["create"]
- apiGroups: [""] # "" indicates the core API group
resourceNames: ["tailscale"]
resources: ["secrets"]
verbs: ["get", "update", "patch"]
- apiGroups: [""] # "" indicates the core API group
resources: ["events"]
verbs: ["get", "create", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tailscale
subjects:
- kind: ServiceAccount
name: "tailscale"
roleRef:
kind: Role
name: tailscale
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: tailscale
---
apiVersion: v1
kind: Secret
metadata:
name: tailscale-auth
stringData:
TS_AUTHKEY: <your auth-key>
---
apiVersion: v1
kind: Secret
metadata:
name: gluetun-auth
stringData:
WIREGUARD_PRIVATE_KEY: <your wireguard private key>
WIREGUARD_PUBLIC_KEY: <your wireguard public key>
WIREGUARD_PRESHARED_KEY: <your wireguard preshared key>
WIREGUARD_ADDRESSES: <your wireguard addresses>
SERVER_COUNTRIES: <countries for vpn>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment