commit 65de284a122fe80bd5ed504b139de4374ba6ce8d Author: Melchior Reimers Date: Wed Feb 4 12:16:10 2026 +0100 first commit diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..db29240 --- /dev/null +++ b/data.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "~> 1.45" + } + } +} + +provider "hcloud" { + token = var.hcloud_token +} + +data "hcloud_server_type" "selected" { + name = var.server_type +} + +data "hcloud_location" "selected" { + name = var.location +} \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..c2ee56d --- /dev/null +++ b/main.tf @@ -0,0 +1,81 @@ +# SSH Key +resource "hcloud_ssh_key" "default" { + name = "${var.server_name}-ssh-key" + public_key = var.ssh_public_key +} + +# Cloud-init script +locals { + user_data = templatefile("${path.module}/scripts/cloud-init.sh", { + tailscale_auth_key = var.tailscale_auth_key + username = var.username + ssh_public_key = var.ssh_public_key + }) +} + +# Server +resource "hcloud_server" "vps" { + name = var.server_name + image = var.image + server_type = data.hcloud_server_type.selected.name + location = data.hcloud_location.selected.name + ssh_keys = concat([hcloud_ssh_key.default.id], var.ssh_keys) + user_data = local.user_data + + labels = { + managed-by = "terraform" + environment = var.environment + purpose = "openclaw" + } +} + +# Firewall – locked down by default +resource "hcloud_firewall" "vps" { + name = "${var.server_name}-firewall" + + # SSH: Tailscale CGNAT range + explicit allowed IPs + rule { + direction = "in" + protocol = "tcp" + port = "22" + source_ips = var.tailscale_auth_key != "" ? concat(["100.64.0.0/10"], var.allowed_ssh_ips) : var.allowed_ssh_ips + description = "SSH access" + } + + # ICMP for diagnostics + rule { + direction = "in" + protocol = "icmp" + source_ips = ["0.0.0.0/0", "::/0"] + description = "ICMP (ping)" + } + + # Egress – allow all (Hetzner default, but explicit is better) + rule { + direction = "out" + protocol = "tcp" + port = "1-65535" + destination_ips = ["0.0.0.0/0", "::/0"] + description = "All TCP outbound" + } + + rule { + direction = "out" + protocol = "udp" + port = "1-65535" + destination_ips = ["0.0.0.0/0", "::/0"] + description = "All UDP outbound" + } + + rule { + direction = "out" + protocol = "icmp" + destination_ips = ["0.0.0.0/0", "::/0"] + description = "ICMP outbound" + } +} + +resource "hcloud_firewall_attachment" "vps" { + firewall_id = hcloud_firewall.vps.id + server_ids = [hcloud_server.vps.id] +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..12ab0e0 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,14 @@ +output "server_ip" { + description = "Public IPv4 address" + value = hcloud_server.vps.ipv4_address +} + +output "server_ipv6" { + description = "Public IPv6 address" + value = hcloud_server.vps.ipv6_address +} + +output "ssh_command" { + description = "SSH connection string" + value = "ssh ${var.username}@${hcloud_server.vps.ipv4_address}" +} \ No newline at end of file diff --git a/scripts/cloud-init.sh b/scripts/cloud-init.sh new file mode 100644 index 0000000..563be9e --- /dev/null +++ b/scripts/cloud-init.sh @@ -0,0 +1,154 @@ +#!/bin/bash +set -euo pipefail + +# Variables from Terraform +TAILSCALE_AUTH_KEY="${tailscale_auth_key}" +USERNAME="${username}" +SSH_PUBLIC_KEY="${ssh_public_key}" + +# Logging +exec > >(tee /var/log/cloud-init-custom.log) 2>&1 +echo "=== Cloud-init started at $(date) ===" + +# System updates +apt update +DEBIAN_FRONTEND=noninteractive apt upgrade -y + +# Install essentials +DEBIAN_FRONTEND=noninteractive apt install -y \ + curl \ + git \ + htop \ + ufw \ + unattended-upgrades \ + apt-listchanges + +# Create non-root user +#if ! id "$USERNAME" &>/dev/null; then +# useradd -m -s /bin/bash -G sudo "$USERNAME" +# echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME +# chmod 0440 /etc/sudoers.d/$USERNAME +#fi + +## SSH key for user +#USER_HOME="/home/$USERNAME" +#mkdir -p "$USER_HOME/.ssh" +#echo "$SSH_PUBLIC_KEY" > "$USER_HOME/.ssh/authorized_keys" +#chmod 700 "$USER_HOME/.ssh" +#chmod 600 "$USER_HOME/.ssh/authorized_keys" +#chown -R "$USERNAME:$USERNAME" "$USER_HOME/.ssh" + +# SSH hardening +cat > /etc/ssh/sshd_config.d/hardening.conf << 'EOF' +# Disable password authentication +PasswordAuthentication no +ChallengeResponseAuthentication no +UsePAM yes + +## Disable root login +#PermitRootLogin no + +# Key-based auth only +PubkeyAuthentication yes +AuthorizedKeysFile .ssh/authorized_keys + +# Timeouts and limits +ClientAliveInterval 300 +ClientAliveCountMax 2 +MaxAuthTries 3 +MaxSessions 3 +LoginGraceTime 30 + +# Disable unused auth methods +HostbasedAuthentication no +PermitEmptyPasswords no +KerberosAuthentication no +GSSAPIAuthentication no + +# Logging +LogLevel VERBOSE +EOF + +# Restart SSH +systemctl restart ssh + +## fail2ban configuration +#cat > /etc/fail2ban/jail.local << 'EOF' +#[DEFAULT] +#bantime = 1h +#findtime = 10m +#maxretry = 5 +#banaction = ufw + +[sshd] +enabled = true +port = ssh +logpath = /var/log/auth.log +maxretry = 3 +bantime = 24h +EOF + +#systemctl enable fail2ban +#systemctl restart fail2ban +# +# UFW firewall +ufw default deny incoming +ufw default allow outgoing +ufw allow ssh +ufw --force enable + +# Unattended upgrades – security patches only +cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF' +Unattended-Upgrade::Allowed-Origins { + "${distro_id}:${distro_codename}-security"; +}; +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; +Unattended-Upgrade::MinimalSteps "true"; +Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +Unattended-Upgrade::Automatic-Reboot "false"; +EOF + +cat > /etc/apt/apt.conf.d/20auto-upgrades << 'EOF' +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; +APT::Periodic::AutocleanInterval "7"; +EOF + +systemctl enable unattended-upgrades + +# Kernel hardening via sysctl +cat > /etc/sysctl.d/99-security.conf << 'EOF' +# IP Spoofing protection +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 + +# Ignore ICMP redirects +net.ipv4.conf.all.accept_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 + +# Ignore source routed packets +net.ipv4.conf.all.accept_source_route = 0 +net.ipv6.conf.all.accept_source_route = 0 + +# Log Martian packets +net.ipv4.conf.all.log_martians = 1 + +# Ignore broadcast pings +net.ipv4.icmp_echo_ignore_broadcasts = 1 + +# Disable IPv6 if not needed (optional) +# net.ipv6.conf.all.disable_ipv6 = 1 +EOF + +sysctl -p /etc/sysctl.d/99-security.conf + +## Tailscale (optional) +#if [ -n "$TAILSCALE_AUTH_KEY" ]; then +# curl -fsSL https://tailscale.com/install.sh | sh +# tailscale up --authkey="$TAILSCALE_AUTH_KEY" --ssh +# echo "Tailscale installed and connected" +#fi + +echo "=== Cloud-init completed at $(date) ===" \ No newline at end of file diff --git a/terraform.tfvars b/terraform.tfvars new file mode 100644 index 0000000..bd221b1 --- /dev/null +++ b/terraform.tfvars @@ -0,0 +1,12 @@ +hcloud_token = "your-hetzner-api-token" +server_name = "clawdbot-prod" +server_type = "cax21" +image = "ubuntu-24.04" +location = "nbg1-dc3" +ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCkKMAoaJiEWf5ruXnI1KOcwq3IRA2nShwPGaubklTVrCkTaXE8gSwpUtqnCJZvGxufUNVBLO/n/3KQNm8aFsEHX+c90F5FknrDro1S5szGRGlJz39YaMJeI0RSyHXNlOxIMl3zV1TpkvJGkdVCzTgafwSLfXws3/o/9fskNs5+jnTP03P8J4u1yEDbjmDNDRQRfBMHBbWOQ/+4Ci9japLSJrbUbmIpP78I8w2qCGPLBd/ksK3VTmn88sQ274m7nfjFMCIVondPz0u+oHg/5q0eiK1mbquTe9rdpx07BzNSLJbIHoEw+nqqf4L3mfoTNW7D67AgLZAztdrEj/GOzSL+tmgtO/lRQumZVCjWtyMtlL0D8F0O8lODngJhDbQHOOaoJz1kCBlJZOg2QqDsHQEO4FrNZ6gzlfj+q4LpYgEYMstJ2u/XFf5dEBWIA8JukYQeT/RLrMQJom0SmrEZ4kyzWWc7Rp9wF1QXk0Apgl2aKpqO6UTUP92P+hAPKmAArDgsSNZgiC/6ARYGi6igGObuGHW2pfB/5tK8mw55yiyH25XjP91BAaUDQfCab1yRU09nQenPzWQQ2bg87ZA6GQYi3xfzZoERCxmgfvhbvd7kEqBMHyOzD8WyFuNeBP8T06yKg0Qeo5jjQkrJySp8smnDqIkYcmhVAOYeZV/WfXtEzQ==" + +# Security: restrict SSH to your IP or VPN +allowed_ssh_ips = ["YOUR_IP/32"] + +# Optional: Tailscale for zero-trust access +# tailscale_auth_key = "tskey-auth-xxxxx" \ No newline at end of file diff --git a/terraform.tfvars.example b/terraform.tfvars.example new file mode 100644 index 0000000..bd221b1 --- /dev/null +++ b/terraform.tfvars.example @@ -0,0 +1,12 @@ +hcloud_token = "your-hetzner-api-token" +server_name = "clawdbot-prod" +server_type = "cax21" +image = "ubuntu-24.04" +location = "nbg1-dc3" +ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCkKMAoaJiEWf5ruXnI1KOcwq3IRA2nShwPGaubklTVrCkTaXE8gSwpUtqnCJZvGxufUNVBLO/n/3KQNm8aFsEHX+c90F5FknrDro1S5szGRGlJz39YaMJeI0RSyHXNlOxIMl3zV1TpkvJGkdVCzTgafwSLfXws3/o/9fskNs5+jnTP03P8J4u1yEDbjmDNDRQRfBMHBbWOQ/+4Ci9japLSJrbUbmIpP78I8w2qCGPLBd/ksK3VTmn88sQ274m7nfjFMCIVondPz0u+oHg/5q0eiK1mbquTe9rdpx07BzNSLJbIHoEw+nqqf4L3mfoTNW7D67AgLZAztdrEj/GOzSL+tmgtO/lRQumZVCjWtyMtlL0D8F0O8lODngJhDbQHOOaoJz1kCBlJZOg2QqDsHQEO4FrNZ6gzlfj+q4LpYgEYMstJ2u/XFf5dEBWIA8JukYQeT/RLrMQJom0SmrEZ4kyzWWc7Rp9wF1QXk0Apgl2aKpqO6UTUP92P+hAPKmAArDgsSNZgiC/6ARYGi6igGObuGHW2pfB/5tK8mw55yiyH25XjP91BAaUDQfCab1yRU09nQenPzWQQ2bg87ZA6GQYi3xfzZoERCxmgfvhbvd7kEqBMHyOzD8WyFuNeBP8T06yKg0Qeo5jjQkrJySp8smnDqIkYcmhVAOYeZV/WfXtEzQ==" + +# Security: restrict SSH to your IP or VPN +allowed_ssh_ips = ["YOUR_IP/32"] + +# Optional: Tailscale for zero-trust access +# tailscale_auth_key = "tskey-auth-xxxxx" \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..fc646b4 --- /dev/null +++ b/variables.tf @@ -0,0 +1,65 @@ +variable "hcloud_token" { + description = "Hetzner Cloud API token" + type = string + sensitive = true +} + +variable "server_name" { + description = "Server hostname" + type = string + default = "clawdbot" +} + +variable "server_type" { + description = "Hetzner server type (cx22 = 2 vCPU, 4GB RAM)" + type = string + default = "cx22" +} + +variable "image" { + description = "OS image" + type = string + default = "ubuntu-24.04" +} + +variable "location" { + description = "Hetzner datacenter" + type = string + default = "nbg1" # Nuremberg, DE +} + +variable "ssh_public_key" { + description = "SSH public key for access" + type = string +} + +variable "ssh_keys" { + description = "Additional SSH key IDs" + type = list(string) + default = [] +} + +variable "username" { + description = "Non-root user to create" + type = string + default = "clawdbot" +} + +variable "tailscale_auth_key" { + description = "Tailscale auth key (optional)" + type = string + default = "" + sensitive = true +} + +variable "allowed_ssh_ips" { + description = "IPs allowed to SSH (use your static IP or VPN range)" + type = list(string) + default = [] # Empty = SSH only via Tailscale if enable +} + +variable "environment" { + description = "Environment label" + type = string + default = "production" +} \ No newline at end of file