https://github.com/anqorithm/headscale-k0s-vpn
A local demo that runs a Kubernetes cluster behind a self-hosted VPN. Your apps are only accessible through the encrypted VPN mesh invisible to the outside world.
https://github.com/anqorithm/headscale-k0s-vpn
headscale k0s k8s security vpn wireguard
Last synced: about 1 month ago
JSON representation
A local demo that runs a Kubernetes cluster behind a self-hosted VPN. Your apps are only accessible through the encrypted VPN mesh invisible to the outside world.
- Host: GitHub
- URL: https://github.com/anqorithm/headscale-k0s-vpn
- Owner: anqorithm
- Created: 2026-03-06T21:02:52.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-06T21:18:40.000Z (4 months ago)
- Last Synced: 2026-04-04T17:29:42.994Z (3 months ago)
- Topics: headscale, k0s, k8s, security, vpn, wireguard
- Language: Shell
- Homepage:
- Size: 1.21 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Headscale + k0s VPN Cluster
A local demo that runs a Kubernetes cluster behind a self-hosted VPN.
Your apps are only accessible through the encrypted VPN mesh invisible to the outside world.
## Table of Contents
- [TLDR](#tldr)
- [Requirements](#requirements)
- [Versions](#versions)
- [What's inside](#whats-inside)
- [Architecture](#architecture)
- [How it works](#how-it-works)
- [Commands](#commands)
- [Demo](#demo)
## TLDR
1. **Start the cluster**
- `make up`
- Wait about a minute for everything to boot
2. **Deploy the app**
- `make deploy`
- Deploys nginx, only accessible through VPN
3. **Connect your device**
- `make connect`
- Run the printed `tailscale up` command on your machine
4. **Access the app**
- `curl http://100.64.0.1:8443`
- Or open it in your browser
5. **Done?**
- `make disconnect` to leave the VPN
- `make down` to tear everything down
## Requirements
- Docker + Docker Compose
- [Tailscale client](https://tailscale.com/download) (for connecting your device)
## Versions
| Tool | Version |
|------------|---------|
| Headscale | 0.28.0 |
| k0s | 1.35.1 |
| Tailscale | 1.94.2 |
## What's inside
- **Headscale** -self-hosted Tailscale control server (your own VPN)
- **k0s** -lightweight Kubernetes cluster (controller + worker)
- **Tailscale** -each node joins the VPN mesh automatically
- **nginx** -demo app that's only reachable through the VPN
## Architecture
```mermaid
graph TD
USER_OUT(Outside World) -. blocked .-> SOCAT
subgraph VPN [Headscale VPN Mesh]
HS(Headscale Control Server)
LAPTOP(Your Device -100.64.0.x)
subgraph K8S [k0s Cluster]
CTRL(k0s-controller -100.64.0.1)
WORKER(k0s-worker -100.64.0.2)
SOCAT(socat -100.64.0.1:8443)
NGINX(nginx -ClusterIP:80)
end
end
HS --- LAPTOP
HS --- CTRL
HS --- WORKER
CTRL -- WireGuard --- WORKER
LAPTOP -- curl :8443 --> SOCAT --> NGINX
```
## How it works
1. Headscale starts first and acts as the VPN control plane
2. k0s nodes boot up, join the VPN, and get Tailscale IPs (100.64.x.x)
3. The k0s cluster runs on the Docker network, but the app is exposed only on the Tailscale IP
4. `socat` binds to the Tailscale IP and forwards to the Kubernetes service
5. No Tailscale IP = no access
## Commands
| Command | What it does |
|-------------------|-------------------------------------|
| `make up` | Build and start the whole cluster |
| `make deploy` | Deploy nginx behind the VPN |
| `make status` | Show containers, nodes, and pods |
| `make users` | List all users and nodes on the VPN |
| `make connect` | Get the command to join the VPN |
| `make disconnect` | Disconnect your device from the VPN |
| `make logs` | Tail all container logs |
| `make down` | Stop and remove everything |
| `make clean` | Full cleanup including images |
## Demo

*`make up` — Building and starting headscale, k0s controller, and worker*

*`make status` — Containers, headscale nodes, tailscale peers, k8s nodes, and pods*

*`make deploy` — Deploying nginx behind the VPN with socat forwarding*

*Accessing the app via `curl` from inside the VPN mesh*

*`make connect` — Getting the tailscale command to join the VPN from your device*

*Browsing the app at `100.64.0.1:8443` — only reachable through the VPN*

*`make users` — All VPN users and connected nodes including the local device*