first commit
This commit is contained in:
20
data.tf
Normal file
20
data.tf
Normal file
@@ -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
|
||||||
|
}
|
||||||
81
main.tf
Normal file
81
main.tf
Normal file
@@ -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]
|
||||||
|
}
|
||||||
14
outputs.tf
Normal file
14
outputs.tf
Normal file
@@ -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}"
|
||||||
|
}
|
||||||
154
scripts/cloud-init.sh
Normal file
154
scripts/cloud-init.sh
Normal file
@@ -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) ==="
|
||||||
12
terraform.tfvars
Normal file
12
terraform.tfvars
Normal file
@@ -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"
|
||||||
12
terraform.tfvars.example
Normal file
12
terraform.tfvars.example
Normal file
@@ -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"
|
||||||
65
variables.tf
Normal file
65
variables.tf
Normal file
@@ -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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user