terraform { required_providers { digitalocean = { source = "digitalocean/digitalocean" version = "~> 2.0" } cloudflare = { source = "cloudflare/cloudflare" version = "~> 4.0" } } backend "s3" { endpoints = { s3 = "https://nyc3.digitaloceanspaces.com" } bucket = "lunarfront-infra" key = "terraform/gitea.tfstate" region = "us-east-1" # required by S3 backend, ignored by Spaces skip_credentials_validation = true skip_metadata_api_check = true skip_region_validation = true skip_requesting_account_id = true force_path_style = true } } provider "digitalocean" { token = var.do_token } provider "cloudflare" { api_token = var.cloudflare_api_token } # ─── Cloudflare zone lookup ─────────────────────────────────────────────────── data "cloudflare_zone" "main" { name = var.domain } # ─── Droplet ────────────────────────────────────────────────────────────────── data "digitalocean_ssh_key" "main" { name = var.ssh_key_name } resource "digitalocean_droplet" "gitea" { name = "gitea" region = var.region size = var.droplet_size image = "ubuntu-24-04-x64" ssh_keys = [data.digitalocean_ssh_key.main.id] tags = ["infra", "gitea"] } # ─── Firewall ───────────────────────────────────────────────────────────────── # Cloudflare IPv4 ranges — http://www.cloudflare.com/ips-v4 locals { cloudflare_ipv4 = [ "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22", "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13", "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22", ] cloudflare_ipv6 = [ "2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32", "2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32", ] } resource "digitalocean_firewall" "gitea" { name = "gitea-firewall" droplet_ids = [digitalocean_droplet.gitea.id] # SSH — your IP only inbound_rule { protocol = "tcp" port_range = "22" source_addresses = ["${var.admin_ip}/32"] } # HTTP — Cloudflare IPs only (web UI) inbound_rule { protocol = "tcp" port_range = "80" source_addresses = concat(local.cloudflare_ipv4, local.cloudflare_ipv6) } # HTTPS — Cloudflare IPs for proxied domains + all IPs for registry (DNS-only) inbound_rule { protocol = "tcp" port_range = "443" source_addresses = ["0.0.0.0/0", "::/0"] } # Gitea SSH for git push/pull — open until Gitea is migrated to DOKS inbound_rule { protocol = "tcp" port_range = "2222" source_addresses = ["0.0.0.0/0", "::/0"] } outbound_rule { protocol = "tcp" port_range = "1-65535" destination_addresses = ["0.0.0.0/0", "::/0"] } outbound_rule { protocol = "udp" port_range = "1-65535" destination_addresses = ["0.0.0.0/0", "::/0"] } } # ─── DNS records ────────────────────────────────────────────────────────────── resource "cloudflare_record" "gitea" { zone_id = data.cloudflare_zone.main.id name = "git" type = "A" content = digitalocean_droplet.gitea.ipv4_address proxied = true ttl = 1 } resource "cloudflare_record" "vaultwarden" { zone_id = data.cloudflare_zone.main.id name = "vault" type = "A" content = digitalocean_droplet.gitea.ipv4_address proxied = true ttl = 1 } # DNS only — no Cloudflare proxy, for direct SSH/git access resource "cloudflare_record" "git_ssh" { zone_id = data.cloudflare_zone.main.id name = "git-ssh" type = "A" content = digitalocean_droplet.gitea.ipv4_address proxied = false ttl = 3600 } # DNS only — no Cloudflare proxy, for container registry (no 100MB upload limit) resource "cloudflare_record" "registry" { zone_id = data.cloudflare_zone.main.id name = "registry" type = "A" content = digitalocean_droplet.gitea.ipv4_address proxied = false ttl = 3600 }