feat: Limited rustdesk to vpn IP only
This commit is contained in:
+1
-1
@@ -59,7 +59,7 @@ public IP, so you must bring `vpn` up and connect to it before `dev`/`prod`
|
|||||||
are reachable at all.
|
are reachable at all.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# 1. vpn first - its firewall accepts SSH from var.allowed_ssh_source_ips directly
|
# 1. vpn first - its firewall accepts SSH from var.allowed_source_ips directly
|
||||||
ansible-playbook playbooks/bootstrap.yml -l role_vpn
|
ansible-playbook playbooks/bootstrap.yml -l role_vpn
|
||||||
ansible-playbook playbooks/deploy.yml -l role_vpn
|
ansible-playbook playbooks/deploy.yml -l role_vpn
|
||||||
ansible-playbook playbooks/spinup.yml -l role_vpn
|
ansible-playbook playbooks/spinup.yml -l role_vpn
|
||||||
|
|||||||
+22
-22
@@ -8,7 +8,7 @@ locals {
|
|||||||
# SSH to dev/prod is only permitted from the vpn server's public IP: admins must
|
# SSH to dev/prod is only permitted from the vpn server's public IP: admins must
|
||||||
# tunnel through the VPN first, whose egress traffic is then sourced from this IP.
|
# tunnel through the VPN first, whose egress traffic is then sourced from this IP.
|
||||||
# vpn itself can't require tunneling through itself, so it keeps
|
# vpn itself can't require tunneling through itself, so it keeps
|
||||||
# var.allowed_ssh_source_ips for direct SSH access.
|
# var.allowed_source_ips for direct SSH access.
|
||||||
vpn_ssh_source_ips = ["${module.vpn.ipv4}/32"]
|
vpn_ssh_source_ips = ["${module.vpn.ipv4}/32"]
|
||||||
|
|
||||||
# IDs for every named key in var.ssh_key_names - installed on every server.
|
# IDs for every named key in var.ssh_key_names - installed on every server.
|
||||||
@@ -56,14 +56,14 @@ module "network" {
|
|||||||
module "dev" {
|
module "dev" {
|
||||||
source = "./modules/dev"
|
source = "./modules/dev"
|
||||||
|
|
||||||
server_type = var.dev_server_type
|
server_type = var.dev_server_type
|
||||||
image = var.server_image
|
image = var.server_image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_key_ids = local.ssh_key_ids
|
ssh_key_ids = local.ssh_key_ids
|
||||||
network_id = module.network.id
|
network_id = module.network.id
|
||||||
private_ip = var.dev_private_ip
|
private_ip = var.dev_private_ip
|
||||||
allowed_ssh_source_ips = local.vpn_ssh_source_ips
|
allowed_source_ips = local.vpn_ssh_source_ips
|
||||||
network_ip_range = var.network_ip_range
|
network_ip_range = var.network_ip_range
|
||||||
|
|
||||||
# module.network.id alone doesn't guarantee the subnet exists yet, and a server
|
# module.network.id alone doesn't guarantee the subnet exists yet, and a server
|
||||||
# can't join a network before it has a subnet.
|
# can't join a network before it has a subnet.
|
||||||
@@ -73,14 +73,14 @@ module "dev" {
|
|||||||
module "prod" {
|
module "prod" {
|
||||||
source = "./modules/prod"
|
source = "./modules/prod"
|
||||||
|
|
||||||
server_type = var.prod_server_type
|
server_type = var.prod_server_type
|
||||||
image = var.server_image
|
image = var.server_image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_key_ids = local.ssh_key_ids
|
ssh_key_ids = local.ssh_key_ids
|
||||||
network_id = module.network.id
|
network_id = module.network.id
|
||||||
private_ip = var.prod_private_ip
|
private_ip = var.prod_private_ip
|
||||||
allowed_ssh_source_ips = local.vpn_ssh_source_ips
|
allowed_source_ips = local.vpn_ssh_source_ips
|
||||||
network_ip_range = var.network_ip_range
|
network_ip_range = var.network_ip_range
|
||||||
|
|
||||||
depends_on = [module.network]
|
depends_on = [module.network]
|
||||||
}
|
}
|
||||||
@@ -88,11 +88,11 @@ module "prod" {
|
|||||||
module "vpn" {
|
module "vpn" {
|
||||||
source = "./modules/vpn"
|
source = "./modules/vpn"
|
||||||
|
|
||||||
server_type = var.vpn_server_type
|
server_type = var.vpn_server_type
|
||||||
image = var.server_image
|
image = var.server_image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_key_ids = local.ssh_key_ids
|
ssh_key_ids = local.ssh_key_ids
|
||||||
allowed_ssh_source_ips = var.allowed_ssh_source_ips
|
allowed_source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
module "dns" {
|
module "dns" {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ resource "hcloud_firewall" "this" {
|
|||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "22"
|
port = "22"
|
||||||
source_ips = var.allowed_ssh_source_ips
|
source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule { # gitea ssh
|
rule { # gitea ssh
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ variable "private_ip" {
|
|||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_source_ips" {
|
||||||
description = "CIDRs allowed to reach port 22 on dev. Set by the root module to the vpn server's public IP."
|
description = "CIDRs allowed to reach port 22 on dev. Set by the root module to the vpn server's public IP."
|
||||||
type = list(string)
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ resource "hcloud_firewall" "this" {
|
|||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "22"
|
port = "22"
|
||||||
source_ips = var.allowed_ssh_source_ips
|
source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule { # Traefik http/https
|
rule { # Traefik http/https
|
||||||
@@ -27,35 +27,35 @@ resource "hcloud_firewall" "this" {
|
|||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "21115"
|
port = "21115"
|
||||||
source_ips = ["0.0.0.0/0", "::/0"]
|
source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule {
|
rule {
|
||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "21116"
|
port = "21116"
|
||||||
source_ips = ["0.0.0.0/0", "::/0"]
|
source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule {
|
rule {
|
||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "udp"
|
protocol = "udp"
|
||||||
port = "21116"
|
port = "21116"
|
||||||
source_ips = ["0.0.0.0/0", "::/0"]
|
source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule { # rustdesk hbbr
|
rule { # rustdesk hbbr
|
||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "21117"
|
port = "21117"
|
||||||
source_ips = ["0.0.0.0/0", "::/0"]
|
source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule {
|
rule {
|
||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "21119"
|
port = "21119"
|
||||||
source_ips = ["0.0.0.0/0", "::/0"]
|
source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule { # traffic from over the private network
|
rule { # traffic from over the private network
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ variable "private_ip" {
|
|||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_source_ips" {
|
||||||
description = "CIDRs allowed to reach port 22 on prod. Set by the root module to the vpn server's public IP."
|
description = "CIDRs allowed to reach port 22 on prod. Set by the root module to the vpn server's public IP."
|
||||||
type = list(string)
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ resource "hcloud_firewall" "this" {
|
|||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "22"
|
port = "22"
|
||||||
source_ips = var.allowed_ssh_source_ips
|
source_ips = var.allowed_source_ips
|
||||||
}
|
}
|
||||||
|
|
||||||
rule { # Traefik http/https
|
rule { # Traefik http/https
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ variable "ssh_key_ids" {
|
|||||||
type = list(string)
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_source_ips" {
|
||||||
description = "CIDRs allowed to reach port 22 on vpn."
|
description = "CIDRs allowed to reach port 22 on vpn."
|
||||||
type = list(string)
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ ssh_key_names = ["luke-else@PC", "luke-else@laptop"]
|
|||||||
# dev_server_type = "cx23"
|
# dev_server_type = "cx23"
|
||||||
# prod_server_type = "cx23"
|
# prod_server_type = "cx23"
|
||||||
# vpn_server_type = "cx23"
|
# vpn_server_type = "cx23"
|
||||||
# allowed_ssh_source_ips = ["0.0.0.0/0"]
|
# allowed_source_ips = ["0.0.0.0/0"]
|
||||||
# dns_zones = ["luke-else.co.uk"]
|
# dns_zones = ["luke-else.co.uk"]
|
||||||
+1
-1
@@ -63,7 +63,7 @@ variable "ssh_key_names" {
|
|||||||
type = list(string)
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_source_ips" {
|
||||||
description = "CIDRs allowed to reach port 22 on the vpn server. Narrow this to your own IP(s) once known. dev and prod don't use this - their SSH is restricted to the vpn server's own public IP instead (see infra/main.tf)."
|
description = "CIDRs allowed to reach port 22 on the vpn server. Narrow this to your own IP(s) once known. dev and prod don't use this - their SSH is restricted to the vpn server's own public IP instead (see infra/main.tf)."
|
||||||
type = list(string)
|
type = list(string)
|
||||||
default = ["0.0.0.0/0", "::/0"]
|
default = ["0.0.0.0/0", "::/0"]
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ architecture-beta
|
|||||||
| `prod` | Public websites, Bitwarden, RustDesk, status page, prod Traefik | Private network + public firewall |
|
| `prod` | Public websites, Bitwarden, RustDesk, status page, prod Traefik | Private network + public firewall |
|
||||||
| `vpn` | WireGuard (wg-easy) + its own Traefik | **Not** on the private network — isolated so a compromised VPN endpoint can't pivot to `dev`/`prod` |
|
| `vpn` | WireGuard (wg-easy) + its own Traefik | **Not** on the private network — isolated so a compromised VPN endpoint can't pivot to `dev`/`prod` |
|
||||||
|
|
||||||
`dev` and `prod` share a private Hetzner network (`10.0.1.0/24` by default) so CI/CD on `dev` can reach `prod` without exposing that traffic publicly. Each server has its own Hetzner Cloud Firewall (`infra/modules/<host>/main.tf`) opening only the ports its own compose stacks use, plus SSH restricted to `var.allowed_ssh_source_ips`.
|
`dev` and `prod` share a private Hetzner network (`10.0.1.0/24` by default) so CI/CD on `dev` can reach `prod` without exposing that traffic publicly. Each server has its own Hetzner Cloud Firewall (`infra/modules/<host>/main.tf`) opening only the ports its own compose stacks use, plus SSH restricted to `var.allowed_source_ips`.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ cd ansible
|
|||||||
ansible-galaxy collection install -r requirements.yml
|
ansible-galaxy collection install -r requirements.yml
|
||||||
export HCLOUD_TOKEN=your-hetzner-api-token # never commit this
|
export HCLOUD_TOKEN=your-hetzner-api-token # never commit this
|
||||||
|
|
||||||
# vpn first - its firewall accepts SSH from var.allowed_ssh_source_ips directly
|
# vpn first - its firewall accepts SSH from var.allowed_source_ips directly
|
||||||
ansible-playbook playbooks/site.yml -l role_vpn
|
ansible-playbook playbooks/site.yml -l role_vpn
|
||||||
|
|
||||||
# SSH to vpn as deploy and connect to the WireGuard tunnel it just started, then:
|
# SSH to vpn as deploy and connect to the WireGuard tunnel it just started, then:
|
||||||
@@ -272,7 +272,7 @@ Then reopen the repo in VS Code with the Dev Containers extension.
|
|||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
- Real secrets (`HCLOUD_TOKEN`, `BACKUP_S3_*`, `*.tfvars`, your SSH private key) must never be committed — see `.gitignore`. S3 credentials land in each host's `services/<host>/.env` (mode `0600`, gitignored, rendered by Ansible).
|
- Real secrets (`HCLOUD_TOKEN`, `BACKUP_S3_*`, `*.tfvars`, your SSH private key) must never be committed — see `.gitignore`. S3 credentials land in each host's `services/<host>/.env` (mode `0600`, gitignored, rendered by Ansible).
|
||||||
- SSH to `dev`/`prod` is restricted to `vpn`'s own public IP — you must be tunneled into the VPN to reach them. SSH to `vpn` itself is gated by `var.allowed_ssh_source_ips`; narrow this from the default `0.0.0.0/0` once you know your egress IP(s).
|
- SSH to `dev`/`prod` is restricted to `vpn`'s own public IP — you must be tunneled into the VPN to reach them. SSH to `vpn` itself is gated by `var.allowed_source_ips`; narrow this from the default `0.0.0.0/0` once you know your egress IP(s).
|
||||||
- Firewalls are per-host allowlists (`infra/modules/<host>/main.tf`) — only ports actually used by that host's compose stacks are open.
|
- Firewalls are per-host allowlists (`infra/modules/<host>/main.tf`) — only ports actually used by that host's compose stacks are open.
|
||||||
- Ansible's bootstrap role disables SSH password auth, restricts root login to key-only, and creates a separate sudo user (`deploy_user`) for day-to-day access.
|
- Ansible's bootstrap role disables SSH password auth, restricts root login to key-only, and creates a separate sudo user (`deploy_user`) for day-to-day access.
|
||||||
- DNS zones carry both Hetzner's `delete_protection` and Terraform's `prevent_destroy` — losing a zone takes every record in it with it, across all three managed domains.
|
- DNS zones carry both Hetzner's `delete_protection` and Terraform's `prevent_destroy` — losing a zone takes every record in it with it, across all three managed domains.
|
||||||
|
|||||||
Reference in New Issue
Block a user