feat: integrate tailscale access and lock SSH/API to tailnet
This commit is contained in:
@@ -16,6 +16,8 @@ env:
|
|||||||
TF_VAR_s3_secret_key: ${{ secrets.S3_SECRET_KEY }}
|
TF_VAR_s3_secret_key: ${{ secrets.S3_SECRET_KEY }}
|
||||||
TF_VAR_s3_endpoint: ${{ secrets.S3_ENDPOINT }}
|
TF_VAR_s3_endpoint: ${{ secrets.S3_ENDPOINT }}
|
||||||
TF_VAR_s3_bucket: ${{ secrets.S3_BUCKET }}
|
TF_VAR_s3_bucket: ${{ secrets.S3_BUCKET }}
|
||||||
|
TF_VAR_tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }}
|
||||||
|
TF_VAR_tailscale_tailnet: ${{ secrets.TAILSCALE_TAILNET }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
terraform:
|
terraform:
|
||||||
@@ -155,6 +157,12 @@ jobs:
|
|||||||
apt-get update && apt-get install -y python3-pip
|
apt-get update && apt-get install -y python3-pip
|
||||||
pip3 install --break-system-packages ansible kubernetes jinja2 pyyaml
|
pip3 install --break-system-packages ansible kubernetes jinja2 pyyaml
|
||||||
|
|
||||||
|
- name: Install Tailscale on runner
|
||||||
|
run: curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
|
|
||||||
|
- name: Connect runner to tailnet
|
||||||
|
run: tailscale up --authkey "${{ secrets.TAILSCALE_CI_AUTH_KEY }}" --hostname "gitea-runner-${{ github.run_number }}" --ssh=false --accept-routes=false
|
||||||
|
|
||||||
- name: Install Ansible Collections
|
- name: Install Ansible Collections
|
||||||
run: ansible-galaxy collection install -r ansible/requirements.yml
|
run: ansible-galaxy collection install -r ansible/requirements.yml
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ env:
|
|||||||
TF_VAR_s3_secret_key: ${{ secrets.S3_SECRET_KEY }}
|
TF_VAR_s3_secret_key: ${{ secrets.S3_SECRET_KEY }}
|
||||||
TF_VAR_s3_endpoint: ${{ secrets.S3_ENDPOINT }}
|
TF_VAR_s3_endpoint: ${{ secrets.S3_ENDPOINT }}
|
||||||
TF_VAR_s3_bucket: ${{ secrets.S3_BUCKET }}
|
TF_VAR_s3_bucket: ${{ secrets.S3_BUCKET }}
|
||||||
|
TF_VAR_tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }}
|
||||||
|
TF_VAR_tailscale_tailnet: ${{ secrets.TAILSCALE_TAILNET }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
destroy:
|
destroy:
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -11,6 +11,7 @@ Production-ready Kubernetes cluster on Hetzner Cloud using Terraform and Ansible
|
|||||||
| **Total Cost** | €28.93/mo |
|
| **Total Cost** | €28.93/mo |
|
||||||
| **K8s** | k3s (latest, HA) |
|
| **K8s** | k3s (latest, HA) |
|
||||||
| **Addons** | Hetzner CCM (load balancers) |
|
| **Addons** | Hetzner CCM (load balancers) |
|
||||||
|
| **Access** | SSH/API restricted to Tailnet |
|
||||||
| **Bootstrap** | Terraform + Ansible |
|
| **Bootstrap** | Terraform + Ansible |
|
||||||
|
|
||||||
### Cluster Resources
|
### Cluster Resources
|
||||||
@@ -87,7 +88,15 @@ s3_secret_key = "your-backblaze-application-key"
|
|||||||
s3_endpoint = "https://s3.eu-central-003.backblazeb2.com"
|
s3_endpoint = "https://s3.eu-central-003.backblazeb2.com"
|
||||||
s3_bucket = "k8s-terraform-state"
|
s3_bucket = "k8s-terraform-state"
|
||||||
|
|
||||||
allowed_ssh_ips = ["your.ip.address/32"]
|
tailscale_auth_key = "tskey-auth-..."
|
||||||
|
tailscale_tailnet = "yourtailnet.ts.net"
|
||||||
|
|
||||||
|
restrict_api_ssh_to_tailnet = true
|
||||||
|
tailnet_cidr = "100.64.0.0/10"
|
||||||
|
enable_nodeport_public = false
|
||||||
|
|
||||||
|
allowed_ssh_ips = []
|
||||||
|
allowed_api_ips = []
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Initialize Terraform
|
### 3. Initialize Terraform
|
||||||
@@ -153,6 +162,9 @@ Set these in your Gitea repository settings (**Settings** → **Secrets** → **
|
|||||||
| `S3_SECRET_KEY` | Backblaze B2 applicationKey |
|
| `S3_SECRET_KEY` | Backblaze B2 applicationKey |
|
||||||
| `S3_ENDPOINT` | Backblaze S3 endpoint (e.g., `https://s3.eu-central-003.backblazeb2.com`) |
|
| `S3_ENDPOINT` | Backblaze S3 endpoint (e.g., `https://s3.eu-central-003.backblazeb2.com`) |
|
||||||
| `S3_BUCKET` | S3 bucket name (e.g., `k8s-terraform-state`) |
|
| `S3_BUCKET` | S3 bucket name (e.g., `k8s-terraform-state`) |
|
||||||
|
| `TAILSCALE_AUTH_KEY` | Tailscale auth key for node bootstrap |
|
||||||
|
| `TAILSCALE_TAILNET` | Tailnet domain (e.g., `yourtailnet.ts.net`) |
|
||||||
|
| `TAILSCALE_CI_AUTH_KEY` | Tailscale auth key for CI runner |
|
||||||
| `SSH_PUBLIC_KEY` | SSH public key content |
|
| `SSH_PUBLIC_KEY` | SSH public key content |
|
||||||
| `SSH_PRIVATE_KEY` | SSH private key content |
|
| `SSH_PRIVATE_KEY` | SSH private key content |
|
||||||
|
|
||||||
@@ -192,14 +204,15 @@ Set these in your Gitea repository settings (**Settings** → **Secrets** → **
|
|||||||
|
|
||||||
| Port | Source | Purpose |
|
| Port | Source | Purpose |
|
||||||
|------|--------|---------|
|
|------|--------|---------|
|
||||||
| 22 | Any | SSH |
|
| 22 | Tailnet CIDR | SSH |
|
||||||
| 6443 | Configured IPs + internal | Kubernetes API |
|
| 6443 | Tailnet CIDR + internal | Kubernetes API |
|
||||||
|
| 41641/udp | Any | Tailscale WireGuard |
|
||||||
| 9345 | 10.0.0.0/16 | k3s Supervisor (HA join) |
|
| 9345 | 10.0.0.0/16 | k3s Supervisor (HA join) |
|
||||||
| 2379 | 10.0.0.0/16 | etcd Client |
|
| 2379 | 10.0.0.0/16 | etcd Client |
|
||||||
| 2380 | 10.0.0.0/16 | etcd Peer |
|
| 2380 | 10.0.0.0/16 | etcd Peer |
|
||||||
| 8472 | 10.0.0.0/16 | Flannel VXLAN |
|
| 8472 | 10.0.0.0/16 | Flannel VXLAN |
|
||||||
| 10250 | 10.0.0.0/16 | Kubelet |
|
| 10250 | 10.0.0.0/16 | Kubelet |
|
||||||
| 30000-32767 | Any | NodePorts |
|
| 30000-32767 | Optional | NodePorts (disabled by default) |
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
|
|||||||
@@ -26,29 +26,34 @@ def get_terraform_outputs():
|
|||||||
def main():
|
def main():
|
||||||
outputs = get_terraform_outputs()
|
outputs = get_terraform_outputs()
|
||||||
|
|
||||||
|
control_plane_names = outputs["control_plane_names"]["value"]
|
||||||
control_plane_ips = outputs["control_plane_ips"]["value"]
|
control_plane_ips = outputs["control_plane_ips"]["value"]
|
||||||
control_plane_private_ips = outputs["control_plane_private_ips"]["value"]
|
control_plane_private_ips = outputs["control_plane_private_ips"]["value"]
|
||||||
|
worker_names = outputs["worker_names"]["value"]
|
||||||
worker_ips = outputs["worker_ips"]["value"]
|
worker_ips = outputs["worker_ips"]["value"]
|
||||||
worker_private_ips = outputs["worker_private_ips"]["value"]
|
worker_private_ips = outputs["worker_private_ips"]["value"]
|
||||||
|
tailnet = outputs["tailscale_tailnet"]["value"]
|
||||||
|
|
||||||
control_planes = [
|
control_planes = [
|
||||||
{
|
{
|
||||||
"name": f"cp-{i + 1}",
|
"name": name,
|
||||||
"public_ip": public_ip,
|
"public_ip": f"{name}.{tailnet}" if tailnet else public_ip,
|
||||||
"private_ip": private_ip,
|
"private_ip": private_ip,
|
||||||
}
|
}
|
||||||
for i, (public_ip, private_ip) in enumerate(
|
for name, public_ip, private_ip in zip(
|
||||||
zip(control_plane_ips, control_plane_private_ips)
|
control_plane_names, control_plane_ips, control_plane_private_ips
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
workers = [
|
workers = [
|
||||||
{
|
{
|
||||||
"name": f"worker-{i + 1}",
|
"name": name,
|
||||||
"public_ip": public_ip,
|
"public_ip": f"{name}.{tailnet}" if tailnet else public_ip,
|
||||||
"private_ip": private_ip,
|
"private_ip": private_ip,
|
||||||
}
|
}
|
||||||
for i, (public_ip, private_ip) in enumerate(zip(worker_ips, worker_private_ips))
|
for name, public_ip, private_ip in zip(
|
||||||
|
worker_names, worker_ips, worker_private_ips
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ s3_bucket = "k8s-terraform-state"
|
|||||||
|
|
||||||
cluster_name = "k8s-prod"
|
cluster_name = "k8s-prod"
|
||||||
|
|
||||||
|
tailscale_auth_key = "tskey-auth-..."
|
||||||
|
tailscale_tailnet = "yourtailnet.ts.net"
|
||||||
|
|
||||||
|
restrict_api_ssh_to_tailnet = true
|
||||||
|
tailnet_cidr = "100.64.0.0/10"
|
||||||
|
enable_nodeport_public = false
|
||||||
|
|
||||||
control_plane_count = 3
|
control_plane_count = 3
|
||||||
control_plane_type = "cx23"
|
control_plane_type = "cx23"
|
||||||
|
|
||||||
@@ -18,6 +25,6 @@ worker_type = "cx33"
|
|||||||
|
|
||||||
location = "nbg1"
|
location = "nbg1"
|
||||||
|
|
||||||
allowed_ssh_ips = ["0.0.0.0/0"]
|
allowed_ssh_ips = []
|
||||||
|
|
||||||
allowed_api_ips = ["0.0.0.0/0"]
|
allowed_api_ips = []
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
locals {
|
||||||
|
ssh_source_ips = var.restrict_api_ssh_to_tailnet ? [var.tailnet_cidr] : var.allowed_ssh_ips
|
||||||
|
api_source_ips = var.restrict_api_ssh_to_tailnet ? [var.tailnet_cidr] : var.allowed_api_ips
|
||||||
|
}
|
||||||
|
|
||||||
resource "hcloud_firewall" "cluster" {
|
resource "hcloud_firewall" "cluster" {
|
||||||
name = "${var.cluster_name}-firewall"
|
name = "${var.cluster_name}-firewall"
|
||||||
|
|
||||||
@@ -6,7 +11,7 @@ resource "hcloud_firewall" "cluster" {
|
|||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "22"
|
port = "22"
|
||||||
source_ips = var.allowed_ssh_ips
|
source_ips = local.ssh_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule {
|
rule {
|
||||||
@@ -14,7 +19,15 @@ resource "hcloud_firewall" "cluster" {
|
|||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "6443"
|
port = "6443"
|
||||||
source_ips = var.allowed_api_ips
|
source_ips = local.api_source_ips
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
description = "Tailscale WireGuard"
|
||||||
|
direction = "in"
|
||||||
|
protocol = "udp"
|
||||||
|
port = "41641"
|
||||||
|
source_ips = ["0.0.0.0/0"]
|
||||||
}
|
}
|
||||||
|
|
||||||
rule {
|
rule {
|
||||||
@@ -65,12 +78,15 @@ resource "hcloud_firewall" "cluster" {
|
|||||||
source_ips = [var.subnet_cidr]
|
source_ips = [var.subnet_cidr]
|
||||||
}
|
}
|
||||||
|
|
||||||
rule {
|
dynamic "rule" {
|
||||||
description = "NodePorts"
|
for_each = var.enable_nodeport_public ? [1] : []
|
||||||
direction = "in"
|
content {
|
||||||
protocol = "tcp"
|
description = "NodePorts"
|
||||||
port = "30000-32767"
|
direction = "in"
|
||||||
source_ips = ["0.0.0.0/0"]
|
protocol = "tcp"
|
||||||
|
port = "30000-32767"
|
||||||
|
source_ips = ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rule {
|
rule {
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ output "control_plane_ips" {
|
|||||||
value = [for cp in hcloud_server.control_plane : cp.ipv4_address]
|
value = [for cp in hcloud_server.control_plane : cp.ipv4_address]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "control_plane_names" {
|
||||||
|
description = "Control plane hostnames"
|
||||||
|
value = [for cp in hcloud_server.control_plane : cp.name]
|
||||||
|
}
|
||||||
|
|
||||||
output "control_plane_private_ips" {
|
output "control_plane_private_ips" {
|
||||||
description = "Private IPs of control plane nodes"
|
description = "Private IPs of control plane nodes"
|
||||||
value = [for cp in hcloud_server.control_plane : one(cp.network).ip]
|
value = [for cp in hcloud_server.control_plane : one(cp.network).ip]
|
||||||
@@ -18,6 +23,11 @@ output "worker_ips" {
|
|||||||
value = [for worker in hcloud_server.workers : worker.ipv4_address]
|
value = [for worker in hcloud_server.workers : worker.ipv4_address]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "worker_names" {
|
||||||
|
description = "Worker hostnames"
|
||||||
|
value = [for worker in hcloud_server.workers : worker.name]
|
||||||
|
}
|
||||||
|
|
||||||
output "worker_private_ips" {
|
output "worker_private_ips" {
|
||||||
description = "Private IPs of worker nodes"
|
description = "Private IPs of worker nodes"
|
||||||
value = [for worker in hcloud_server.workers : one(worker.network).ip]
|
value = [for worker in hcloud_server.workers : one(worker.network).ip]
|
||||||
@@ -33,6 +43,11 @@ output "cluster_name" {
|
|||||||
value = var.cluster_name
|
value = var.cluster_name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "tailscale_tailnet" {
|
||||||
|
description = "Tailnet domain suffix"
|
||||||
|
value = var.tailscale_tailnet
|
||||||
|
}
|
||||||
|
|
||||||
output "network_cidr" {
|
output "network_cidr" {
|
||||||
description = "Private network CIDR"
|
description = "Private network CIDR"
|
||||||
value = var.subnet_cidr
|
value = var.subnet_cidr
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ resource "hcloud_server" "control_plane" {
|
|||||||
role = "control-plane"
|
role = "control-plane"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user_data = <<-EOF
|
||||||
|
#cloud-config
|
||||||
|
package_update: true
|
||||||
|
runcmd:
|
||||||
|
- curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
|
- tailscale up --authkey '${var.tailscale_auth_key}' --hostname '${var.cluster_name}-cp-${count.index + 1}' --ssh=false --accept-routes=false
|
||||||
|
EOF
|
||||||
|
|
||||||
network {
|
network {
|
||||||
network_id = hcloud_network.cluster.id
|
network_id = hcloud_network.cluster.id
|
||||||
ip = cidrhost(var.subnet_cidr, 10 + count.index)
|
ip = cidrhost(var.subnet_cidr, 10 + count.index)
|
||||||
@@ -44,6 +52,14 @@ resource "hcloud_server" "workers" {
|
|||||||
role = "worker"
|
role = "worker"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user_data = <<-EOF
|
||||||
|
#cloud-config
|
||||||
|
package_update: true
|
||||||
|
runcmd:
|
||||||
|
- curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
|
- tailscale up --authkey '${var.tailscale_auth_key}' --hostname '${var.cluster_name}-worker-${count.index + 1}' --ssh=false --accept-routes=false
|
||||||
|
EOF
|
||||||
|
|
||||||
network {
|
network {
|
||||||
network_id = hcloud_network.cluster.id
|
network_id = hcloud_network.cluster.id
|
||||||
ip = cidrhost(var.subnet_cidr, 20 + count.index)
|
ip = cidrhost(var.subnet_cidr, 20 + count.index)
|
||||||
|
|||||||
@@ -55,13 +55,42 @@ variable "location" {
|
|||||||
variable "allowed_ssh_ips" {
|
variable "allowed_ssh_ips" {
|
||||||
description = "IP ranges allowed for SSH access"
|
description = "IP ranges allowed for SSH access"
|
||||||
type = list(string)
|
type = list(string)
|
||||||
default = ["0.0.0.0/0"]
|
default = []
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_api_ips" {
|
variable "allowed_api_ips" {
|
||||||
description = "IP ranges allowed for Kubernetes API access"
|
description = "IP ranges allowed for Kubernetes API access"
|
||||||
type = list(string)
|
type = list(string)
|
||||||
default = ["0.0.0.0/0"]
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "restrict_api_ssh_to_tailnet" {
|
||||||
|
description = "Restrict SSH and Kubernetes API to tailnet CIDR"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "tailnet_cidr" {
|
||||||
|
description = "Tailnet CIDR used for SSH/API access"
|
||||||
|
type = string
|
||||||
|
default = "100.64.0.0/10"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "tailscale_auth_key" {
|
||||||
|
description = "Tailscale auth key for node bootstrap"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "tailscale_tailnet" {
|
||||||
|
description = "Tailnet domain suffix, e.g. mytailnet.ts.net"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "enable_nodeport_public" {
|
||||||
|
description = "Allow public NodePort traffic"
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "network_cidr" {
|
variable "network_cidr" {
|
||||||
|
|||||||
Reference in New Issue
Block a user