├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── packer ├── .gitignore ├── Makefile ├── build.pkr.hcl ├── config.pkr.hcl ├── digitalocean │ └── seed │ │ └── user-data ├── hetzner │ └── seed │ │ └── user-data ├── packer.auto.pkrvars.hcl.sample ├── qemu │ ├── http │ │ ├── seed-autoinstall │ │ │ ├── meta-data │ │ │ └── user-data │ │ └── seed │ │ │ ├── meta-data │ │ │ └── user-data │ └── start-vm.sh ├── rootfs │ ├── etc │ │ ├── apt │ │ │ ├── apt.conf.d │ │ │ │ ├── 20auto-upgrades │ │ │ │ └── 50unattended-upgrades │ │ │ └── preferences.d │ │ │ │ └── nosnap.pref │ │ ├── cloud │ │ │ └── cloud.cfg.d │ │ │ │ └── 99-custom.cfg │ │ ├── nftables.conf │ │ ├── ssh │ │ │ └── sshd_config │ │ ├── sysctl.d │ │ │ └── 60-forwarding.conf │ │ ├── unbound │ │ │ └── unbound.conf │ │ └── wireguard │ │ │ ├── wg0.conf │ │ │ └── wg0.conf.d │ │ │ ├── peer-local.tpl │ │ │ └── peer-remote.tpl │ └── usr │ │ └── local │ │ └── bin │ │ └── wg-create-peer ├── sources.pkr.hcl └── variables.pkr.hcl └── terraform ├── .gitignore ├── .terraform.lock.hcl ├── main.tf ├── outputs.tf ├── templates └── user-data.tpl ├── terraform.tfvars.sample └── variables.tf /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: "terraform" 6 | directory: "/terraform" 7 | schedule: 8 | interval: "weekly" 9 | groups: 10 | terraform-all: 11 | patterns: ["*"] 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | groups: 18 | github-actions-all: 19 | patterns: ["*"] 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Main" 3 | 4 | on: 5 | push: 6 | tags: ["*"] 7 | branches: ["*"] 8 | pull_request: 9 | branches: ["*"] 10 | schedule: 11 | - cron: "20 04 1,15 * *" 12 | workflow_dispatch: 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | validate-packer: 18 | name: "Validate Packer configuration" 19 | runs-on: "ubuntu-24.04" 20 | permissions: 21 | contents: "read" 22 | defaults: 23 | run: 24 | working-directory: "./packer/" 25 | steps: 26 | - name: "Checkout project" 27 | uses: "actions/checkout@v4" 28 | - name: "Install dependencies" 29 | run: | 30 | curl --proto '=https' --tlsv1.3 -sSf 'https://apt.releases.hashicorp.com/gpg' | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/hashicorp.gpg 31 | printf '%s\n' "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list >/dev/null 32 | sudo apt-get update && sudo apt-get install -y --no-install-recommends packer 33 | - name: "Init Packer" 34 | run: | 35 | packer init ./ 36 | - name: "Validate configuration" 37 | run: | 38 | packer validate -syntax-only ./ 39 | - name: "Check configuration format" 40 | run: | 41 | packer fmt -check -diff ./ 42 | 43 | validate-terraform: 44 | name: "Validate Terraform configuration" 45 | runs-on: "ubuntu-24.04" 46 | permissions: 47 | contents: "read" 48 | defaults: 49 | run: 50 | working-directory: "./terraform/" 51 | steps: 52 | - name: "Checkout project" 53 | uses: "actions/checkout@v4" 54 | - name: "Install dependencies" 55 | run: | 56 | curl --proto '=https' --tlsv1.3 -sSf 'https://apt.releases.hashicorp.com/gpg' | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/hashicorp.gpg 57 | printf '%s\n' "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list >/dev/null 58 | sudo apt-get update && sudo apt-get install -y --no-install-recommends terraform 59 | - name: "Init Terraform" 60 | run: | 61 | terraform init 62 | - name: "Validate configuration" 63 | run: | 64 | terraform validate ./ 65 | - name: "Check configuration format" 66 | run: | 67 | terraform fmt -check -diff ./ 68 | 69 | build-packer: 70 | name: "Build Packer image" 71 | needs: ["validate-packer"] 72 | runs-on: "ubuntu-24.04" 73 | permissions: 74 | contents: "read" 75 | defaults: 76 | run: 77 | working-directory: "./packer/" 78 | steps: 79 | - name: "Checkout project" 80 | uses: "actions/checkout@v4" 81 | - name: "Install dependencies" 82 | run: | 83 | curl --proto '=https' --tlsv1.3 -sSf 'https://apt.releases.hashicorp.com/gpg' | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/hashicorp.gpg 84 | printf '%s\n' "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list >/dev/null 85 | sudo apt-get update && sudo apt-get install -y --no-install-recommends packer qemu-utils qemu-system-x86 ovmf cloud-image-utils openssh-client sshpass 86 | - name: "Init Packer" 87 | run: | 88 | packer init ./ 89 | - name: "Build image" 90 | run: | 91 | make build-qemu PACKER_LOG=1 \ 92 | PKR_VAR_qemu_efi_firmware_code=/usr/share/OVMF/OVMF_CODE_4M.fd \ 93 | PKR_VAR_qemu_efi_firmware_vars=/usr/share/OVMF/OVMF_VARS_4M.fd 94 | - name: "Test image" 95 | run: | 96 | EFI_FIRMWARE_CODE=/usr/share/OVMF/OVMF_CODE_4M.fd \ 97 | EFI_FIRMWARE_VARS=/usr/share/OVMF/OVMF_VARS_4M.fd \ 98 | ./qemu/start-vm.sh & 99 | set -- sshpass -p toor ssh root@127.0.0.1 -p 1122 -o StrictHostKeyChecking=no -o ConnectTimeout=1 100 | attempt=0; until [ "${attempt:?}" -gt 60 ] || "$@" exit; do attempt=$((attempt+1)); sleep 5; done 101 | "$@" 'set -x; systemctl is-system-running --wait; ret="$?"; systemctl --failed; exit "${ret:?}"' 102 | "$@" 'set -x; wg show wg0' 103 | "$@" 'set -x; poweroff' 104 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.hcl": "hcl", 4 | "*.hcl.sample": "hcl", 5 | "*.tf": "terraform", 6 | "*.tf.sample": "terraform", 7 | "*.tfvars": "terraform", 8 | "*.tfvars.sample": "terraform", 9 | "meta-data": "yaml", 10 | "user-data": "yaml", 11 | "**/cloud/cloud.cfg": "yaml", 12 | "**/cloud/cloud.cfg.d/*.cfg": "yaml", 13 | "**/cloud/templates/*.tmpl": "jinja" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © Héctor Molinero Fernández 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WireGuard + Unbound 2 | 3 | [WireGuard](https://wireguard.com) and [Unbound](https://unbound.net) setup with 4 | [Packer](https://packer.io) and [Terraform](https://terraform.io) / [OpenTofu](https://opentofu.org) ready for deployment in 5 | [Hetzner Cloud](https://hetzner.com). 6 | 7 | ## Deployment instructions 8 | 9 | 1. Copy `./packer/packer.auto.pkrvars.hcl.sample` file to `./packer/packer.auto.pkrvars.hcl` and 10 | fill it with the appropriate values. 11 | 12 | 2. Build the server image with Packer. 13 | ```sh 14 | cd ./packer/ 15 | packer init ./ 16 | packer build -only=hcloud.main ./ 17 | ``` 18 | 19 | 3. Copy `./terraform/terraform.tfvars.sample` file to `./terraform/terraform.tfvars` and fill it 20 | with the appropriate values. 21 | 22 | 4. Deploy the server image with Terraform. 23 | ```sh 24 | cd ./terraform/ 25 | terraform init 26 | terraform apply 27 | ``` 28 | -------------------------------------------------------------------------------- /packer/.gitignore: -------------------------------------------------------------------------------- 1 | packer_cache/ 2 | dist/ 3 | 4 | packer.auto.pkrvars.hcl 5 | 6 | *.box 7 | 8 | crash.log 9 | -------------------------------------------------------------------------------- /packer/Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | SHELL := /bin/sh 4 | .SHELLFLAGS := -euc 5 | 6 | PACKER := $(shell command -v packer 2>/dev/null) 7 | 8 | PACKER_WORK_DIR := ./ 9 | PACKER_CACHE_DIR := ./packer_cache/ 10 | PACKER_HCLOUD_OUT := ./dist/hcloud/wireguard.log 11 | PACKER_DIGITALOCEAN_OUT := ./dist/digitalocean/wireguard.log 12 | PACKER_QEMU_OUT := ./dist/qemu/wireguard.qcow2 13 | PACKER_QEMU_BAREMETAL_OUT := ./dist/qemu-baremetal/wireguard.qcow2 14 | 15 | ################################################## 16 | ## "all" target 17 | ################################################## 18 | 19 | .PHONY: all 20 | all: build 21 | 22 | ################################################## 23 | ## "build" target 24 | ################################################## 25 | 26 | .PHONY: build 27 | build: build-qemu 28 | 29 | .PHONY: build-hcloud 30 | build-hcloud: $(PACKER_HCLOUD_OUT) 31 | 32 | $(PACKER_HCLOUD_OUT): 33 | mkdir -p '$(dir $(PACKER_HCLOUD_OUT))' 34 | '$(PACKER)' build -force -only=hcloud.main '$(PACKER_WORK_DIR)' 2>&1 | tee '$(PACKER_HCLOUD_OUT)' 35 | 36 | .PHONY: build-digitalocean 37 | build-digitalocean: $(PACKER_DIGITALOCEAN_OUT) 38 | 39 | $(PACKER_DIGITALOCEAN_OUT): 40 | mkdir -p '$(dir $(PACKER_DIGITALOCEAN_OUT))' 41 | '$(PACKER)' build -force -only=digitalocean.main '$(PACKER_WORK_DIR)' 2>&1 | tee '$(PACKER_DIGITALOCEAN_OUT)' 42 | 43 | .PHONY: build-qemu 44 | build-qemu: $(PACKER_QEMU_OUT) 45 | 46 | $(PACKER_QEMU_OUT): 47 | mkdir -p '$(dir $(PACKER_QEMU_OUT))' 48 | '$(PACKER)' build -force -only=qemu.main '$(PACKER_WORK_DIR)' 49 | 50 | .PHONY: build-qemu-baremetal 51 | build-qemu-baremetal: $(PACKER_QEMU_BAREMETAL_OUT) 52 | 53 | $(PACKER_QEMU_BAREMETAL_OUT): 54 | mkdir -p '$(dir $(PACKER_QEMU_BAREMETAL_OUT))' 55 | '$(PACKER)' build -force -only=qemu.baremetal '$(PACKER_WORK_DIR)' 56 | 57 | ################################################## 58 | ## "clean" target 59 | ################################################## 60 | 61 | .PHONY: clean 62 | clean: 63 | rm -rf '$(PACKER_HCLOUD_OUT)' '$(PACKER_DIGITALOCEAN_OUT)' '$(PACKER_QEMU_OUT)' '$(PACKER_QEMU_BAREMETAL_OUT)' '$(PACKER_CACHE_DIR)' 64 | -------------------------------------------------------------------------------- /packer/build.pkr.hcl: -------------------------------------------------------------------------------- 1 | build { 2 | sources = [ 3 | "source.hcloud.main", 4 | "source.digitalocean.main", 5 | "source.qemu.main", 6 | "source.qemu.baremetal" 7 | ] 8 | 9 | provisioner "file" { 10 | direction = "upload" 11 | source = "./rootfs" 12 | destination = "/tmp" 13 | } 14 | 15 | provisioner "shell" { 16 | environment_vars = [ 17 | "DPKG_FORCE=confold", 18 | "DEBIAN_FRONTEND=noninteractive" 19 | ] 20 | inline_shebang = "/bin/sh -eux" 21 | inline = [ 22 | # Set permissions and move files to "/" 23 | <<-EOT 24 | find /tmp/rootfs/ -type f -name .gitkeep -delete 25 | find /tmp/rootfs/ -type d -exec chmod 755 '{}' ';' -exec chown root:root '{}' ';' 26 | find /tmp/rootfs/ -type f -exec chmod 644 '{}' ';' -exec chown root:root '{}' ';' 27 | find /tmp/rootfs/ -type f -regex '.+/bin/.+' -exec chmod 755 '{}' ';' 28 | find /tmp/rootfs/ -type f -regex '.+/etc/wireguard/.+' -exec chmod 600 '{}' ';' 29 | find /tmp/rootfs/ -mindepth 1 -maxdepth 1 -exec cp -fa '{}' / ';' 30 | rm -rf /tmp/rootfs/ 31 | EOT 32 | , 33 | # Reload systemd manager configuration 34 | <<-EOT 35 | systemctl daemon-reload 36 | EOT 37 | , 38 | # Upgrade system 39 | <<-EOT 40 | apt-get update 41 | apt-get dist-upgrade -o DPkg::Lock::Timeout=300 -o APT::Get::Always-Include-Phased-Updates=true -y 42 | EOT 43 | , 44 | # Install packages 45 | <<-EOT 46 | apt-get install -y --no-install-recommends \ 47 | apparmor \ 48 | apparmor-profiles \ 49 | apparmor-utils \ 50 | apt-utils \ 51 | ca-certificates \ 52 | dns-root-data \ 53 | gettext-base \ 54 | htop \ 55 | knot-dnsutils \ 56 | locales \ 57 | nano \ 58 | nftables \ 59 | qrencode \ 60 | unattended-upgrades \ 61 | unbound \ 62 | wireguard 63 | EOT 64 | , 65 | # Remove packages 66 | <<-EOT 67 | apt-get purge -y \ 68 | lxd-agent-loader \ 69 | snapd \ 70 | ufw 71 | apt-get autoremove -y 72 | EOT 73 | , 74 | # Set timezone and locale 75 | <<-EOT 76 | timedatectl set-timezone UTC 77 | localectl set-locale LANG=en_US.UTF-8 78 | EOT 79 | , 80 | # Replace systemd-resolved with Unbound 81 | <<-EOT 82 | systemctl mask --now systemd-resolved.service unbound-resolvconf.service 83 | systemctl enable --now unbound.service 84 | unlink /etc/resolv.conf 85 | { 86 | printf 'nameserver %s\n' '127.0.0.1' '::1'; 87 | printf 'options %s\n' 'trust-ad'; 88 | } > /etc/resolv.conf 89 | chattr +i /etc/resolv.conf 90 | EOT 91 | , 92 | # Setup services and timers 93 | <<-EOT 94 | systemctl enable \ 95 | apparmor.service \ 96 | apt-daily-upgrade.timer \ 97 | apt-daily.timer \ 98 | nftables.service \ 99 | ssh.service \ 100 | unattended-upgrades.service \ 101 | wg-quick@wg0.service 102 | systemctl mask \ 103 | snapd.service \ 104 | ufw.service 105 | EOT 106 | , 107 | # Create "ssh-user" group 108 | <<-EOT 109 | groupadd -r ssh-user 110 | usermod -aG ssh-user root 111 | EOT 112 | , 113 | # Delete "root" user password 114 | <<-EOT 115 | usermod -p '*' root 116 | EOT 117 | , 118 | # Cleanup 119 | <<-EOT 120 | # Remove SSH keys 121 | rm -rf /etc/ssh/ssh_host_*key* /root/.ssh/ 122 | # Remove WireGuard keys 123 | rm -rf /etc/wireguard/*-*key 124 | # Remove APT cache 125 | apt-get clean; find /var/lib/apt/lists/ -mindepth 1 -delete 126 | # Remove APT backup files 127 | find / -type f -regex '.+\.\(dpkg\|ucf\)-\(old\|new\|dist\)' -delete ||: 128 | # Remove snap directories 129 | for d in /root/snap/ /home/*/snap/; do rm -rf "$d"; done 130 | # Remove cloud-init artifacts 131 | cloud-init clean --logs 132 | # Remove systemd journal logs 133 | journalctl --rotate && journalctl --vacuum-time=1s 134 | # Empty log files 135 | find /var/log/ -type f -not -path '/var/log/journal/*' -exec sh -euc '> "$1"' _ '{}' ';' 136 | # Remove temporary files 137 | find /tmp/ /var/tmp/ -ignore_readdir_race -mindepth 1 -delete ||: 138 | # Reset machine ID 139 | > /etc/machine-id 140 | # Clear unused disk space 141 | dd if=/dev/zero of=/zero bs=1M 2>/dev/null ||:; rm -f /zero 142 | EOT 143 | ] 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /packer/config.pkr.hcl: -------------------------------------------------------------------------------- 1 | packer { 2 | required_plugins { 3 | hcloud = { 4 | source = "github.com/hashicorp/hcloud" 5 | version = "~> 1" 6 | } 7 | digitalocean = { 8 | source = "github.com/digitalocean/digitalocean" 9 | version = "~> 1" 10 | } 11 | qemu = { 12 | source = "github.com/hashicorp/qemu" 13 | version = "~> 1" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packer/digitalocean/seed/user-data: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | # yaml-language-server: $schema=https://raw.githubusercontent.com/canonical/cloud-init/main/cloudinit/config/schemas/versions.schema.cloud-config.json 3 | 4 | users: [] 5 | disable_root: false 6 | ssh_pwauth: false 7 | -------------------------------------------------------------------------------- /packer/hetzner/seed/user-data: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | # yaml-language-server: $schema=https://raw.githubusercontent.com/canonical/cloud-init/main/cloudinit/config/schemas/versions.schema.cloud-config.json 3 | 4 | users: [] 5 | disable_root: false 6 | ssh_pwauth: false 7 | -------------------------------------------------------------------------------- /packer/packer.auto.pkrvars.hcl.sample: -------------------------------------------------------------------------------- 1 | hcloud_api_token = "" 2 | -------------------------------------------------------------------------------- /packer/qemu/http/seed-autoinstall/meta-data: -------------------------------------------------------------------------------- 1 | instance-id: "instance0" 2 | local-hostname: "localhost" 3 | -------------------------------------------------------------------------------- /packer/qemu/http/seed-autoinstall/user-data: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | # yaml-language-server: $schema=https://raw.githubusercontent.com/canonical/cloud-init/main/cloudinit/config/schemas/versions.schema.cloud-config.json 3 | 4 | autoinstall: 5 | version: 1 6 | locale: "en_US.UTF-8" 7 | keyboard: 8 | layout: "us" 9 | timezone: "UTC" 10 | storage: 11 | grub: 12 | reorder_uefi: false 13 | swap: 14 | size: 0 15 | config: 16 | - id: "disk-vda" 17 | type: "disk" 18 | ptable: "gpt" 19 | path: "/dev/vda" 20 | - id: "partition-0" 21 | type: "partition" 22 | device: "disk-vda" 23 | number: 1 24 | size: 564133888 25 | grub_device: true 26 | flag: "boot" 27 | - id: "format-0" 28 | type: "format" 29 | volume: "partition-0" 30 | fstype: "fat32" 31 | - id: "mount-0" 32 | type: "mount" 33 | device: "format-0" 34 | path: "/boot/efi" 35 | - id: "partition-1" 36 | type: "partition" 37 | device: "disk-vda" 38 | number: 2 39 | size: -1 40 | - id: "format-1" 41 | type: "format" 42 | volume: "partition-1" 43 | fstype: "xfs" 44 | - id: "mount-1" 45 | type: "mount" 46 | device: "format-1" 47 | path: "/" 48 | apt: 49 | geoip: false 50 | kernel: 51 | flavor: "generic" 52 | drivers: 53 | install: false 54 | source: 55 | id: "ubuntu-server-minimal" 56 | search_drivers: false 57 | ssh: 58 | install-server: true 59 | user-data: 60 | users: [] 61 | disable_root: false 62 | chpasswd: 63 | users: [{ name: "root", password: "toor", type: "text" }] 64 | expire: false 65 | runcmd: 66 | - "printf 'PermitRootLogin yes\nPasswordAuthentication yes\n' > /etc/ssh/sshd_config.d/50-cloud-init.conf" 67 | - "systemctl try-reload-or-restart ssh.service; sleep 5; rm -f /etc/ssh/sshd_config.d/50-cloud-init.conf" 68 | -------------------------------------------------------------------------------- /packer/qemu/http/seed/meta-data: -------------------------------------------------------------------------------- 1 | instance-id: "instance0" 2 | local-hostname: "localhost" 3 | -------------------------------------------------------------------------------- /packer/qemu/http/seed/user-data: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | # yaml-language-server: $schema=https://raw.githubusercontent.com/canonical/cloud-init/main/cloudinit/config/schemas/versions.schema.cloud-config.json 3 | 4 | users: [] 5 | disable_root: false 6 | chpasswd: 7 | users: [{ name: "root", password: "toor", type: "text" }] 8 | expire: false 9 | runcmd: 10 | - "printf 'PermitRootLogin yes\nPasswordAuthentication yes\n' > /etc/ssh/sshd_config.d/50-cloud-init.conf" 11 | - "systemctl try-reload-or-restart ssh.service; sleep 5; rm -f /etc/ssh/sshd_config.d/50-cloud-init.conf" 12 | -------------------------------------------------------------------------------- /packer/qemu/start-vm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | export LC_ALL=C 5 | 6 | SRC_DIR=$(CDPATH='' cd -- "$(dirname -- "$(dirname -- "${0:?}")")" && pwd -P) 7 | TMP_DIR=$(mktemp -d) 8 | 9 | : "${ORIGINAL_DISK:=${SRC_DIR:?}/dist/qemu/wireguard.qcow2}" 10 | : "${SNAPSHOT_DISK:=${TMP_DIR:?}/snapshot.qcow2}" 11 | 12 | : "${USERDATA_YAML:=${SRC_DIR:?}/qemu/http/seed/user-data}" 13 | : "${USERDATA_DISK:=${TMP_DIR:?}/seed.img}" 14 | 15 | : "${QEMU_SYSTEM_BINARY:=qemu-system-x86_64}" 16 | # : "${QEMU_SYSTEM_BINARY:=qemu-system-aarch64}" 17 | 18 | # Remove temporary files on exit 19 | # shellcheck disable=SC2154 20 | trap 'ret="$?"; rm -rf -- "${TMP_DIR:?}"; trap - EXIT; exit "${ret:?}"' EXIT TERM INT HUP 21 | 22 | # Remove keys from the known_hosts file 23 | for host in '[localhost]:1122' '[127.0.0.1]:1122' '[::1]:1122'; do 24 | ssh-keygen -R "${host:?}" 2>/dev/null ||: 25 | done 26 | 27 | # Set main arguments for QEMU 28 | set -- 29 | case "${QEMU_SYSTEM_BINARY#*qemu-system-}" in 30 | x86_64) 31 | : "${EFI_FIRMWARE_CODE:=/usr/share/edk2/x64/OVMF_CODE.4m.fd}" 32 | : "${EFI_FIRMWARE_VARS:=/usr/share/edk2/x64/OVMF_VARS.4m.fd}" 33 | set -- "$@" -machine q35 -smp 2 -m 1024 -cpu max 34 | ;; 35 | aarch64) 36 | : "${EFI_FIRMWARE_CODE:=/usr/share/AAVMF/AAVMF_CODE.fd}" 37 | : "${EFI_FIRMWARE_VARS:=/usr/share/AAVMF/AAVMF_VARS.fd}" 38 | set -- "$@" -machine virt,gic-version=max -cpu cortex-a76 -smp 2 -m 1024 39 | ;; 40 | esac 41 | set -- "$@" -nographic -serial mon:stdio 42 | set -- "$@" -device virtio-net,netdev=n0 43 | set -- "$@" -netdev user,id=n0"$(printf ',hostfwd=%s:%s:%s-:%s' \ 44 | tcp 127.0.0.1 1122 122 \ 45 | udp 0.0.0.0 51820 51820 \ 46 | udp 0.0.0.0 1053 53 \ 47 | tcp 0.0.0.0 1443 443 \ 48 | )" 49 | 50 | # Set EFI firmware code and variables 51 | set -- "$@" -drive file="${EFI_FIRMWARE_CODE:?}",if=pflash,unit=0,format=raw,readonly=on 52 | set -- "$@" -drive file="${EFI_FIRMWARE_VARS:?}",if=pflash,unit=1,format=raw,snapshot=on 53 | 54 | # Use KVM if available 55 | if [ -w /dev/kvm ] && [ "${QEMU_SYSTEM_BINARY#*qemu-system-}" = "$(uname -m)" ]; then 56 | set -- "$@" -accel kvm 57 | fi 58 | 59 | # Create a snapshot image to preserve the original image 60 | qemu-img create -f qcow2 -b "${ORIGINAL_DISK:?}" -F qcow2 "${SNAPSHOT_DISK:?}" 61 | qemu-img resize "${SNAPSHOT_DISK:?}" +2G 62 | set -- "$@" -drive file="${SNAPSHOT_DISK:?}",if=virtio,format=qcow2 63 | 64 | # Create a seed image with metadata using cloud-localds 65 | cloud-localds "${USERDATA_DISK:?}" "${USERDATA_YAML:?}" 66 | set -- "$@" -drive file="${USERDATA_DISK:?}",if=virtio,format=raw 67 | 68 | # Launch VM 69 | "${QEMU_SYSTEM_BINARY:?}" "$@" 70 | -------------------------------------------------------------------------------- /packer/rootfs/etc/apt/apt.conf.d/20auto-upgrades: -------------------------------------------------------------------------------- 1 | APT::Periodic::Enable "1"; 2 | APT::Periodic::Update-Package-Lists "1"; 3 | APT::Periodic::Unattended-Upgrade "1"; 4 | APT::Periodic::AutocleanInterval "1"; 5 | -------------------------------------------------------------------------------- /packer/rootfs/etc/apt/apt.conf.d/50unattended-upgrades: -------------------------------------------------------------------------------- 1 | Unattended-Upgrade::Origins-Pattern { "origin=*"; }; 2 | Unattended-Upgrade::AutoFixInterruptedDpkg "true"; 3 | Unattended-Upgrade::MinimalSteps "true"; 4 | Unattended-Upgrade::InstallOnShutdown "false"; 5 | Unattended-Upgrade::MailReport "on-change"; 6 | Unattended-Upgrade::Mail "root"; 7 | Unattended-Upgrade::Remove-Unused-Dependencies "false"; 8 | Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; 9 | Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; 10 | Unattended-Upgrade::Automatic-Reboot "true"; 11 | Unattended-Upgrade::Automatic-Reboot-Time "05:30"; 12 | -------------------------------------------------------------------------------- /packer/rootfs/etc/apt/preferences.d/nosnap.pref: -------------------------------------------------------------------------------- 1 | Package: snapd 2 | Pin: release a=* 3 | Pin-Priority: -10 4 | -------------------------------------------------------------------------------- /packer/rootfs/etc/cloud/cloud.cfg.d/99-custom.cfg: -------------------------------------------------------------------------------- 1 | users: [] 2 | ssh_pwauth: 3 | -------------------------------------------------------------------------------- /packer/rootfs/etc/nftables.conf: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/nft -f 2 | 3 | flush ruleset; 4 | 5 | table inet filter { 6 | chain INPUT { 7 | type filter hook input priority 0; policy drop; 8 | 9 | # Accept loopback interface. 10 | iif lo accept; 11 | 12 | # Accept traffic originated from us. 13 | ct state { established, related } accept; 14 | 15 | # Accept ICMP and ICMPv6 traffic. 16 | meta l4proto { icmp, ipv6-icmp } accept; 17 | 18 | # Accept neighbour discovery, otherwise connectivity breaks. 19 | icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept; 20 | 21 | # Accept SSH traffic. 22 | tcp dport 122 accept; 23 | 24 | # Accept WireGuard traffic. 25 | udp dport 51820 accept; 26 | 27 | # Accept DNS traffic on the WireGuard interface. 28 | iifname wg0 meta l4proto { tcp, udp } @th,16,16 53 accept; 29 | } 30 | 31 | chain FORWARD { 32 | type filter hook forward priority 0; policy drop; 33 | 34 | # Accept packet forwarding on the WireGuard interface. 35 | iifname wg0 accept; 36 | oifname wg0 ct state { established, related } accept; 37 | } 38 | 39 | chain OUTPUT { 40 | type filter hook output priority 0; policy accept; 41 | } 42 | } 43 | 44 | table inet nat { 45 | chain PREROUTING { 46 | type nat hook prerouting priority -100; policy accept; 47 | 48 | # Early drop of invalid packets. 49 | ct state invalid drop; 50 | 51 | # Accept WireGuard traffic via port 53/UDP (to circumvent some firewalls). 52 | iifname != wg0 udp dport 53 redirect to 51820; 53 | } 54 | 55 | chain POSTROUTING { 56 | type nat hook postrouting priority 100; policy accept; 57 | 58 | # Masquerade WireGuard traffic. 59 | oif != lo ip saddr 10.100.0.1/16 masquerade; 60 | oif != lo ip6 saddr fd10:100::1/112 masquerade; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packer/rootfs/etc/ssh/sshd_config: -------------------------------------------------------------------------------- 1 | Include /etc/ssh/sshd_config.d/*.conf 2 | HostKey /etc/ssh/ssh_host_ed25519_key 3 | HostKey /etc/ssh/ssh_host_rsa_key 4 | ListenAddress 0.0.0.0 5 | ListenAddress ::0 6 | Port 122 7 | UseDNS no 8 | UsePAM yes 9 | X11Forwarding no 10 | AllowTcpForwarding yes 11 | AllowGroups ssh-user 12 | PermitRootLogin without-password 13 | PermitEmptyPasswords no 14 | PermitUserEnvironment no 15 | PubkeyAuthentication yes 16 | PasswordAuthentication no 17 | ChallengeResponseAuthentication no 18 | GSSAPIAuthentication no 19 | Subsystem sftp internal-sftp 20 | LoginGraceTime 30 21 | TCPKeepAlive yes 22 | ClientAliveInterval 60 23 | ClientAliveCountMax 5 24 | PrintMotd yes 25 | PrintLastLog yes 26 | SyslogFacility AUTH 27 | LogLevel INFO 28 | -------------------------------------------------------------------------------- /packer/rootfs/etc/sysctl.d/60-forwarding.conf: -------------------------------------------------------------------------------- 1 | net.ipv4.ip_forward=1 2 | net.ipv6.conf.all.forwarding=1 3 | net.ipv6.conf.default.forwarding=1 4 | -------------------------------------------------------------------------------- /packer/rootfs/etc/unbound/unbound.conf: -------------------------------------------------------------------------------- 1 | server: 2 | interface: 0.0.0.0 3 | interface: ::0 4 | port: 53 5 | root-hints: "/usr/share/dns/root.hints" 6 | auto-trust-anchor-file: "/var/lib/unbound/root.key" 7 | access-control: 0.0.0.0/0 refuse 8 | access-control: 127.0.0.0/8 allow 9 | access-control: 10.100.0.1/16 allow 10 | access-control: ::0/0 refuse 11 | access-control: ::1 allow 12 | access-control: ::ffff:127.0.0.0/104 allow 13 | access-control: fd10:100::1/112 allow 14 | private-address: 0.0.0.0/8 15 | private-address: ::ffff:0.0.0.0/104 16 | private-address: 10.0.0.0/8 17 | private-address: ::ffff:10.0.0.0/104 18 | private-address: 100.64.0.0/10 19 | private-address: ::ffff:100.64.0.0/106 20 | private-address: 127.0.0.0/8 21 | private-address: ::ffff:127.0.0.0/104 22 | private-address: 169.254.0.0/16 23 | private-address: ::ffff:169.254.0.0/112 24 | private-address: 172.16.0.0/12 25 | private-address: ::ffff:172.16.0.0/108 26 | private-address: 192.168.0.0/16 27 | private-address: ::ffff:192.168.0.0/112 28 | private-address: ::/128 29 | private-address: ::1/128 30 | private-address: fc00::/7 31 | private-address: fd00::/8 32 | private-address: fe80::/10 33 | hide-identity: yes 34 | hide-version: yes 35 | qname-minimisation: yes 36 | cache-min-ttl: 300 37 | cache-max-ttl: 14400 38 | prefetch: yes 39 | prefetch-key: yes 40 | verbosity: 1 41 | val-log-level: 1 42 | 43 | #include: "/etc/unbound/unbound.conf.d/*.conf" 44 | -------------------------------------------------------------------------------- /packer/rootfs/etc/wireguard/wg0.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.100.0.1/16, fd10:100::1/112 3 | ListenPort = 51820 4 | # Load private key 5 | PostUp = if [ ! -d /etc/wireguard/'%i'.conf.d/ ]; then mkdir -p /etc/wireguard/'%i'.conf.d/; fi 6 | PostUp = if [ ! -s /etc/wireguard/'%i'.conf.d/privatekey ]; then umask 077 && wg genkey > /etc/wireguard/'%i'.conf.d/privatekey; fi 7 | PostUp = wg set '%i' private-key /etc/wireguard/'%i'.conf.d/privatekey 8 | # Load peers 9 | PostUp = for f in /etc/wireguard/'%i'.conf.d/peer-*.conf; do if [ -f "$f" ]; then wg addconf '%i' "$f"; fi; done 10 | -------------------------------------------------------------------------------- /packer/rootfs/etc/wireguard/wg0.conf.d/peer-local.tpl: -------------------------------------------------------------------------------- 1 | # ${WG_PEER_COMMENT} 2 | [Peer] 3 | PublicKey = ${WG_PEER_PUBLIC_KEY} 4 | PresharedKey = ${WG_PEER_PRESHARED_KEY} 5 | AllowedIPs = 10.100.${WG_PEER_IPV4_SUFFIX}/32, fd10:100::${WG_PEER_IPV6_SUFFIX}/128 6 | -------------------------------------------------------------------------------- /packer/rootfs/etc/wireguard/wg0.conf.d/peer-remote.tpl: -------------------------------------------------------------------------------- 1 | # ${WG_PEER_COMMENT} 2 | [Interface] 3 | PrivateKey = ${WG_PEER_PRIVATE_KEY} 4 | Address = 10.100.${WG_PEER_IPV4_SUFFIX}/32, fd10:100::${WG_PEER_IPV6_SUFFIX}/128 5 | DNS = 10.100.0.1, fd10:100::1 6 | 7 | [Peer] 8 | PublicKey = ${WG_OWN_PUBLIC_KEY} 9 | PresharedKey = ${WG_PEER_PRESHARED_KEY} 10 | AllowedIPs = 0.0.0.0/0, ::0/0 11 | Endpoint = ${WG_PEER_ENDPOINT} 12 | -------------------------------------------------------------------------------- /packer/rootfs/usr/local/bin/wg-create-peer: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # Parse command line options. 6 | optParse() { 7 | SEP="$(printf '\037')" 8 | while [ "${#}" -gt '0' ]; do 9 | case "${1?}" in 10 | # Short options that accept a value need a "*" in their pattern because they can be found in the "-A" form. 11 | '-i'*|'--interface') optArgStr "${@-}"; interface="${optArg?}"; shift "${optShift:?}" ;; 12 | '-n'*|'--peer-number') optArgStr "${@-}"; peerNumber="${optArg?}"; shift "${optShift:?}" ;; 13 | '-c'*|'--peer-comment') optArgStr "${@-}"; peerComment="${optArg?}"; shift "${optShift:?}" ;; 14 | '-e'*|'--peer-endpoint') optArgStr "${@-}"; peerEndpoint="${optArg?}"; shift "${optShift:?}" ;; 15 | '-k'*|'--peer-private-key') optArgStr "${@-}"; peerPrivateKey="${optArg?}"; shift "${optShift:?}" ;; 16 | '-p'*|'--peer-public-key') optArgStr "${@-}"; peerPublicKey="${optArg?}"; shift "${optShift:?}" ;; 17 | '-s'*|'--peer-preshared-key') optArgStr "${@-}"; peerPresharedKey="${optArg?}"; shift "${optShift:?}" ;; 18 | '-r' |'--qr'|'--no-qr') optArgBool "${@-}"; qr="${optArg:?}" ;; 19 | '-q' |'--quiet'|'--no-quiet') optArgBool "${@-}"; quiet="${optArg:?}" ;; 20 | '-h' |'--help') showHelp ;; 21 | # If "--" is found, the remaining positional parameters are saved and the parsing ends. 22 | --) shift; _IFS="${IFS?}"; IFS="${SEP:?}"; POS="${POS-}${POS+${SEP:?}}${*-}"; IFS="${_IFS?}"; break ;; 23 | # If a long option in the form "--opt=value" is found, it is split into "--opt" and "value". 24 | --*=*) optSplitEquals "${@-}"; shift; set -- "${optName:?}" "${optArg?}" "${@-}"; continue ;; 25 | # If an option did not match any pattern, an error is thrown. 26 | -?|--*) optDie "Illegal option ${1:?}" ;; 27 | # If multiple short options in the form "-AB" are found, they are split into "-A" and "-B". 28 | -?*) optSplitShort "${@-}"; shift; set -- "${optAName:?}" "${optBName:?}" "${@-}"; continue ;; 29 | # If a positional parameter is found, it is saved. 30 | *) POS="${POS-}${POS+${SEP:?}}${1?}" ;; 31 | esac 32 | shift 33 | done 34 | } 35 | optSplitShort() { 36 | optAName="${1%"${1#??}"}"; optBName="-${1#??}" 37 | } 38 | optSplitEquals() { 39 | optName="${1%="${1#--*=}"}"; optArg="${1#--*=}" 40 | } 41 | optArgStr() { 42 | if [ -n "${1#??}" ] && [ "${1#--}" = "${1:?}" ]; then optArg="${1#??}"; optShift='0'; 43 | elif [ -n "${2+x}" ]; then optArg="${2-}"; optShift='1'; 44 | else optDie "No argument for ${1:?} option"; fi 45 | } 46 | optArgBool() { 47 | if [ "${1#--no-}" = "${1:?}" ]; then optArg='true'; 48 | else optArg='false'; fi 49 | } 50 | optDie() { 51 | printf '%s\n' "${@-}" "Try '${0} --help' for more information" >&2 52 | exit 2 53 | } 54 | 55 | # Show help and quit. 56 | showHelp() { 57 | printf '%s\n' "$(cat <<-EOF 58 | Usage: ${0} [OPTION]... 59 | 60 | Create a WireGuard peer configuration. 61 | 62 | Options: 63 | 64 | -i, --interface 65 | WireGuard interface name. 66 | 67 | -n, --peer-number 68 | Peer number. 69 | 70 | -c, --peer-comment 71 | Peer comment. 72 | 73 | -e, --peer-endpoint 74 | Peer endpoint (host and port). 75 | 76 | -k, --peer-private-key 77 | Peer private key. 78 | 79 | -p, --peer-public-key 80 | Peer public key. 81 | 82 | -s, --peer-preshared-key 83 | Peer preshared key. 84 | 85 | -r, --[no-]qr 86 | Print a QR code with the remote peer configuration. 87 | 88 | -q, --[no-]quiet 89 | Suppress non-error messages. 90 | 91 | -h, --help 92 | Show this help and quit. 93 | EOF 94 | )" 95 | exit 0 96 | } 97 | 98 | main() { 99 | # Parse command line options. 100 | # shellcheck disable=SC2086 101 | { optParse "${@-}"; _IFS="${IFS?}"; IFS="${SEP:?}"; set -- ${POS-} >/dev/null; IFS="${_IFS?}"; } 102 | 103 | # Set default values. 104 | interface="${interface:-wg0}" 105 | ownPublicKey="$(wg show "${interface:?}" public-key)" 106 | peerNumber="${peerNumber:-$(i='0'; while [ -e "/etc/wireguard/${interface:?}.conf.d/peer-${i:?}.conf" ]; do i="$((i+1))"; done; printf '%d' "${i:?}")}" 107 | peerComment="${peerComment:-${interface:?} - peer #${peerNumber:?}}" 108 | peerEndpoint="${peerEndpoint:-$(hostname -f):51820}" 109 | peerPrivateKey="${peerPrivateKey:-$(wg genkey)}" 110 | peerPublicKey="${peerPublicKey:-$(printf '%s' "${peerPrivateKey:?}" | wg pubkey)}" 111 | peerPresharedKey="${peerPresharedKey:-$(wg genpsk)}" 112 | peerIpv4Suffix="$(printf '%d.%d' $((((peerNumber + 2) >> 8) & 0xFF)) $(((peerNumber + 2) & 0xFF)))" 113 | peerIpv6Suffix="$(printf '%x' $(((peerNumber + 2) & 0xFFFF)))" 114 | qr="${qr:-true}" 115 | quiet="${quiet:-false}" 116 | 117 | # Create local peer configuration. 118 | export WG_OWN_PUBLIC_KEY="${ownPublicKey:?}" 119 | export WG_PEER_COMMENT="${peerComment:?}" 120 | export WG_PEER_ENDPOINT="${peerEndpoint:?}" 121 | export WG_PEER_PRIVATE_KEY="${peerPrivateKey:?}" 122 | export WG_PEER_PUBLIC_KEY="${peerPublicKey:?}" 123 | export WG_PEER_PRESHARED_KEY="${peerPresharedKey:?}" 124 | export WG_PEER_IPV4_SUFFIX="${peerIpv4Suffix:?}" 125 | export WG_PEER_IPV6_SUFFIX="${peerIpv6Suffix:?}" 126 | envsubst < "/etc/wireguard/${interface:?}.conf.d/peer-local.tpl" > "/etc/wireguard/${interface:?}.conf.d/peer-${peerNumber:?}.conf" 127 | 128 | # Print remote peer configuration. 129 | if [ "${quiet:?}" != 'true' ]; then 130 | peerRemoteConfig="$(envsubst < "/etc/wireguard/${interface:?}.conf.d/peer-remote.tpl")" 131 | printf '%s\n' "${peerRemoteConfig:?}" 132 | if [ "${qr:?}" = 'true' ] && command -v qrencode >/dev/null 2>&1; then 133 | printf '\n' >&2 134 | printf '%s\n' "${peerRemoteConfig:?}" | qrencode -t UTF8 >&2 135 | fi 136 | fi 137 | } 138 | 139 | main "${@-}" 140 | -------------------------------------------------------------------------------- /packer/sources.pkr.hcl: -------------------------------------------------------------------------------- 1 | source "hcloud" "main" { 2 | token = var.hcloud_api_token 3 | 4 | image = "ubuntu-24.04" 5 | server_name = "wireguard-{{timestamp}}" 6 | server_type = "cax11" 7 | location = "fsn1" 8 | 9 | snapshot_name = "wireguard-{{timestamp}}" 10 | snapshot_labels = { 11 | service = "wireguard" 12 | } 13 | 14 | user_data_file = "./hetzner/seed/user-data" 15 | 16 | ssh_port = "22" 17 | ssh_username = "root" 18 | ssh_timeout = "30m" 19 | ssh_clear_authorized_keys = true 20 | } 21 | 22 | source "digitalocean" "main" { 23 | api_token = var.digitalocean_api_token 24 | 25 | image = "ubuntu-24-04-x64" 26 | droplet_name = "wireguard-{{timestamp}}" 27 | size = "s-1vcpu-512mb-10gb" 28 | region = "ams3" 29 | 30 | snapshot_name = "wireguard-{{timestamp}}" 31 | tags = [ 32 | "wireguard" 33 | ] 34 | 35 | user_data_file = "./digitalocean/seed/user-data" 36 | 37 | ssh_port = "22" 38 | ssh_username = "root" 39 | ssh_timeout = "30m" 40 | ssh_clear_authorized_keys = true 41 | } 42 | 43 | source "qemu" "main" { 44 | iso_url = "https://cloud-images.ubuntu.com/minimal/daily/noble/current/noble-minimal-cloudimg-${ 45 | var.qemu_binary == "qemu-system-aarch64" ? "arm64" : "amd64" 46 | }.img" 47 | iso_checksum = "file:https://cloud-images.ubuntu.com/minimal/daily/noble/current/SHA256SUMS" 48 | disk_image = true 49 | 50 | vm_name = "wireguard.qcow2" 51 | http_directory = "./qemu/http/" 52 | output_directory = "./dist/qemu/" 53 | 54 | headless = true 55 | accelerator = var.qemu_binary == "qemu-system-aarch64" ? "none" : null 56 | machine_type = var.qemu_binary == "qemu-system-aarch64" ? "virt,gic-version=max" : "q35" 57 | cpu_model = var.qemu_binary == "qemu-system-aarch64" ? "cortex-a76" : "max" 58 | cpus = 2 59 | memory = 1024 60 | qemu_binary = var.qemu_binary 61 | qemuargs = [ 62 | ["-smbios", "type=1,serial=ds=nocloud;s=http://{{.HTTPIP}}:{{.HTTPPort}}/seed/"] 63 | ] 64 | efi_firmware_code = var.qemu_efi_firmware_code 65 | efi_firmware_vars = var.qemu_efi_firmware_vars 66 | 67 | net_device = "virtio-net" 68 | 69 | format = "qcow2" 70 | disk_size = "4G" 71 | disk_interface = "virtio" 72 | disk_compression = false 73 | 74 | ssh_port = "22" 75 | ssh_username = "root" 76 | ssh_password = "toor" 77 | ssh_timeout = "30m" 78 | ssh_clear_authorized_keys = true 79 | 80 | shutdown_command = "shutdown -P now" 81 | } 82 | 83 | source "qemu" "baremetal" { 84 | iso_url = "https://cdimage.ubuntu.com/ubuntu-server/noble/daily-live/current/noble-live-server-${ 85 | var.qemu_binary == "qemu-system-aarch64" ? "arm64" : "amd64" 86 | }.iso" 87 | iso_checksum = "file:https://cdimage.ubuntu.com/ubuntu-server/noble/daily-live/current/SHA256SUMS" 88 | disk_image = false 89 | 90 | vm_name = "wireguard.qcow2" 91 | http_directory = "./qemu/http/" 92 | output_directory = "./dist/qemu-baremetal/" 93 | 94 | headless = true 95 | accelerator = var.qemu_binary == "qemu-system-aarch64" ? "none" : null 96 | machine_type = var.qemu_binary == "qemu-system-aarch64" ? "virt,gic-version=max" : "q35" 97 | cpu_model = var.qemu_binary == "qemu-system-aarch64" ? "cortex-a76" : "max" 98 | cpus = 2 99 | memory = 1024 100 | qemu_binary = var.qemu_binary 101 | qemuargs = var.qemu_binary == "qemu-system-aarch64" ? [ 102 | ["-monitor", "none"], 103 | ["-boot", "strict=off"], 104 | ["-device", "virtio-gpu-pci"], 105 | ["-device", "qemu-xhci,id=usb"], 106 | ["-device", "usb-kbd,id=input0,bus=usb.0,port=1"] 107 | ] : [] 108 | efi_firmware_code = var.qemu_efi_firmware_code 109 | efi_firmware_vars = var.qemu_efi_firmware_vars 110 | 111 | net_device = "virtio-net" 112 | 113 | format = "qcow2" 114 | disk_size = "4G" 115 | disk_interface = "virtio" 116 | disk_compression = false 117 | 118 | boot_wait = "25s" 119 | boot_command = [ 120 | "c", 121 | "set gfxpayload=keep", 122 | "linux /casper/vmlinuz --- autoinstall ds='nocloud;s=http://{{.HTTPIP}}:{{.HTTPPort}}/seed-autoinstall/'", 123 | "initrd /casper/initrd", 124 | "boot" 125 | ] 126 | 127 | ssh_port = "22" 128 | ssh_username = "root" 129 | ssh_password = "toor" 130 | ssh_timeout = "60m" 131 | ssh_clear_authorized_keys = true 132 | 133 | shutdown_command = "shutdown -P now" 134 | } 135 | -------------------------------------------------------------------------------- /packer/variables.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "hcloud_api_token" { 2 | type = string 3 | description = "Hetzner Cloud API token" 4 | default = "xxxx" 5 | } 6 | 7 | variable "digitalocean_api_token" { 8 | type = string 9 | description = "DigitalOcean API token" 10 | default = "xxxx" 11 | } 12 | 13 | variable "qemu_binary" { 14 | type = string 15 | description = "QEMU binary path" 16 | default = "qemu-system-x86_64" 17 | # default = "qemu-system-aarch64" 18 | } 19 | 20 | variable "qemu_efi_firmware_code" { 21 | type = string 22 | description = "EFI firmware file" 23 | default = "/usr/share/edk2/x64/OVMF_CODE.4m.fd" 24 | # default = "/usr/share/AAVMF/AAVMF_CODE.fd" 25 | } 26 | 27 | variable "qemu_efi_firmware_vars" { 28 | type = string 29 | description = "EFI NVRAM variables file" 30 | default = "/usr/share/edk2/x64/OVMF_VARS.4m.fd" 31 | # default = "/usr/share/AAVMF/AAVMF_VARS.fd" 32 | } 33 | -------------------------------------------------------------------------------- /terraform/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform/ 2 | 3 | *.tfstate 4 | *.tfstate.* 5 | 6 | terraform.tfvars 7 | 8 | override.tf 9 | override.tf.json 10 | *_override.tf 11 | *_override.tf.json 12 | 13 | .terraformrc 14 | terraform.rc 15 | 16 | crash.log 17 | -------------------------------------------------------------------------------- /terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hetznercloud/hcloud" { 5 | version = "1.50.1" 6 | constraints = "~> 1.0" 7 | hashes = [ 8 | "h1:vtP9LdKKbpmIZWOK1TxYQ9Jt9raKeMKpbsezV6XhfUo=", 9 | "zh:14417f21a91bf12f2f152cfa42271d833be0cce5a341016bcd2308a078e8b015", 10 | "zh:18de1bc63135bf72046d26ff53b685a0058919dd520122e6ae7d2c0fd6a579d1", 11 | "zh:2aa7edea02e380f3572d70705d4ba54b6d88645610db49eebeb5196cd7c72e30", 12 | "zh:3ee777744ed8b471e30f5d26da20246fe0d63c0c36fcd1b39c839582006ec30d", 13 | "zh:4fd3ad397a43e024ada6aa8dd78ca0513aff333e07bac99018e0d50469260cb3", 14 | "zh:5544e6576a01a2e6a571a0bffba9af2583a69e360425333ea20af029e2b56af0", 15 | "zh:5df521b743c68f9987b3f293db34255583fb6753a84dca158251b38bdf69c381", 16 | "zh:5e31f111171a6d7413db8a63ef46ef3ec372dafcb688ccf5c79279938218a249", 17 | "zh:5f9529c8d7ae375f0bb7e9de2b8586054df930a875831dd5f23588c6201415cf", 18 | "zh:81278226f47ce6e386f7479dceed20fd8db5fbf17ac10e0e56df5946e79f8f4a", 19 | "zh:86e61e515eb7f87d8944de1daef2538976ce81c293d38d7da47d22c41b5f9cf9", 20 | "zh:af6d38f0af128fd40bdbb0a75aed1793915fb353d4b8d1c6c0bda6e350a025ae", 21 | "zh:dbc50dd46157cc7a8e048517fbc0bfd664de1584a51f7e218b01daad51116d99", 22 | "zh:e2ecd963992ac5dfddfa4c8bf6860235bef07dd4cfe53e2a80c688e0822160f5", 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | hcloud = { 4 | source = "hetznercloud/hcloud" 5 | version = "~> 1" 6 | } 7 | } 8 | } 9 | 10 | provider "hcloud" { 11 | token = var.hcloud_api_token 12 | } 13 | 14 | data "hcloud_image" "wg_image" { 15 | with_selector = "service=wireguard" 16 | with_architecture = "arm" 17 | most_recent = true 18 | } 19 | 20 | resource "hcloud_firewall" "wg_firewall" { 21 | name = var.wg_firewall_name 22 | labels = { service = "wireguard" } 23 | rule { 24 | description = "ICMP" 25 | direction = "in" 26 | protocol = "icmp" 27 | source_ips = ["0.0.0.0/0", "::0/0"] 28 | } 29 | rule { 30 | description = "SSH" 31 | direction = "in" 32 | protocol = "tcp" 33 | port = "122" 34 | source_ips = ["0.0.0.0/0", "::0/0"] 35 | } 36 | rule { 37 | description = "WireGuard" 38 | direction = "in" 39 | protocol = "udp" 40 | port = "51820" 41 | source_ips = ["0.0.0.0/0", "::0/0"] 42 | } 43 | rule { 44 | description = "WireGuard (alt)" 45 | direction = "in" 46 | protocol = "udp" 47 | port = "53" 48 | source_ips = ["0.0.0.0/0", "::0/0"] 49 | } 50 | } 51 | 52 | resource "hcloud_ssh_key" "wg_ssh_key" { 53 | public_key = var.wg_ssh_publickey 54 | name = var.wg_ssh_publickey_name 55 | } 56 | 57 | resource "hcloud_server" "wg_server" { 58 | image = data.hcloud_image.wg_image.id 59 | name = var.wg_server_name 60 | server_type = var.wg_server_type 61 | location = var.wg_server_location 62 | labels = { service = "wireguard" } 63 | firewall_ids = [hcloud_firewall.wg_firewall.id] 64 | ssh_keys = [hcloud_ssh_key.wg_ssh_key.id] 65 | user_data = templatefile("${path.module}/templates/user-data.tpl", { 66 | wg_server_wg_privatekey = var.wg_server_wg_privatekey 67 | wg_server_wg_peers = var.wg_server_wg_peers 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "wg_server_ipv4_address" { 2 | value = hcloud_server.wg_server.ipv4_address 3 | description = "IPv4 address" 4 | } 5 | 6 | output "wg_server_ipv6_address" { 7 | value = hcloud_server.wg_server.ipv6_address 8 | description = "IPv6 address" 9 | } 10 | -------------------------------------------------------------------------------- /terraform/templates/user-data.tpl: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | write_files: 4 | - path: "/etc/wireguard/wg0.conf.d/privatekey" 5 | owner: "root:root" 6 | permissions: "0600" 7 | content: | 8 | ${wg_server_wg_privatekey} 9 | runcmd: 10 | %{~ for index, peer in wg_server_wg_peers ~} 11 | - | 12 | wg-create-peer \ 13 | --interface wg0 \ 14 | --peer-number "${index}" \ 15 | --peer-comment "${peer.comment}" \ 16 | --peer-private-key "none" \ 17 | --peer-public-key "${peer.publickey}" \ 18 | --peer-preshared-key "${peer.presharedkey}" \ 19 | --quiet 20 | %{~ endfor ~} 21 | - systemctl try-restart wg-quick@wg0.service 22 | -------------------------------------------------------------------------------- /terraform/terraform.tfvars.sample: -------------------------------------------------------------------------------- 1 | hcloud_api_token = "" 2 | 3 | wg_server_name = "" 4 | wg_server_type = "" 5 | wg_server_location = "" 6 | 7 | wg_server_wg_privatekey = "" 8 | wg_server_wg_peers = [ 9 | { 10 | publickey = "" 11 | presharedkey = "" 12 | } 13 | ] 14 | 15 | wg_firewall_name = "" 16 | 17 | wg_ssh_publickey = "" 18 | wg_ssh_publickey_name = "" 19 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "hcloud_api_token" { 2 | type = string 3 | description = "Hetzner Cloud API token" 4 | default = "xxxx" 5 | } 6 | 7 | variable "wg_server_name" { 8 | type = string 9 | description = "Server name" 10 | default = "wireguard" 11 | } 12 | 13 | variable "wg_server_type" { 14 | type = string 15 | description = "Server type" 16 | default = "cax11" 17 | } 18 | 19 | variable "wg_server_location" { 20 | type = string 21 | description = "Server location" 22 | default = "fsn1" 23 | } 24 | 25 | variable "wg_server_wg_privatekey" { 26 | type = string 27 | description = "WireGuard private key" 28 | default = "" 29 | } 30 | 31 | variable "wg_server_wg_peers" { 32 | type = list(object({ 33 | comment = optional(string, "") 34 | publickey = string 35 | presharedkey = string 36 | })) 37 | description = "WireGuard peers" 38 | default = [] 39 | } 40 | 41 | variable "wg_firewall_name" { 42 | type = string 43 | description = "Firewall name" 44 | default = "wireguard" 45 | } 46 | 47 | variable "wg_ssh_publickey" { 48 | type = string 49 | description = "SSH public key" 50 | } 51 | 52 | variable "wg_ssh_publickey_name" { 53 | type = string 54 | description = "SSH public key name" 55 | } 56 | --------------------------------------------------------------------------------