This commit is contained in:
Graham McIntire 2026-06-21 13:39:36 -05:00
parent 763d7a938c
commit 216bd8e668
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
7 changed files with 445 additions and 1 deletions

View file

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Build a NixOS qcow2 image for Proxmox template and upload to Proxmox host.
# Run on a Linux host with nix installed.
set -euo pipefail
PROXMOX_HOST="10.0.0.2"
TEMPLATE_VMID="9010"
TEMPLATE_NAME="nixos-cloud-init"
STORAGE="local-lvm"
NIXOS_CONFIG="./nixos-template-config.nix"
echo "==> Building NixOS qcow2 image..."
NIX_PATH="nixpkgs=https://flakehub.com/f/DeterminateSystems/nixpkgs-weekly/*.tar.gz"
IMAGE=$(nix-build '<nixpkgs/nixos>' \
--argstr system x86_64-linux \
-A config.system.build.qcow2 \
--arg configuration "$(readlink -f "$NIXOS_CONFIG")" \
2>&1 | tail -1)
echo "==> Image built: $IMAGE"
QCOW2_PATH="$IMAGE/nixos.qcow2"
if [ ! -f "$QCOW2_PATH" ]; then
echo "ERROR: qcow2 not found at $QCOW2_PATH"
ls -la "$IMAGE/" 2>/dev/null || echo "No files in image dir"
exit 1
fi
echo "==> Uploading to Proxmox..."
scp "$QCOW2_PATH" "root@${PROXMOX_HOST}:/tmp/nixos-template.qcow2"
echo "==> Importing as VM disk..."
ssh "root@${PROXMOX_HOST}" bash -s << 'EOF'
set -e
qm destroy 9010 --purge 2>/dev/null || true
qm create 9010 --name nixos-cloud-init --memory 2048 --cores 2 \
--net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci \
--ide2 local-lvm:cloudinit --boot c --bootdisk scsi0 \
--serial0 socket --vga serial0
qm importdisk 9010 /tmp/nixos-template.qcow2 local-lvm
qm set 9010 --scsi0 "local-lvm:vm-9010-disk-0"
qm template 9010
rm /tmp/nixos-template.qcow2
echo "Template VM 9010 created successfully."
qm config 9010
EOF
echo "==> Done. Template VMID 9010 is ready."

View file

@ -0,0 +1,32 @@
# NixOS template configuration for Proxmox cloud-init VMs.
# Built into a qcow2 image and imported as a Proxmox template.
{ config, pkgs, lib, ... }:
{
# Boot
boot.loader.grub.device = "/dev/sda";
boot.loader.timeout = 0;
# Network — cloud-init will configure (override proxmox-image default)
networking.useDHCP = lib.mkForce true;
networking.hostName = "nixos";
# SSH for Ansible
services.openssh.enable = true;
services.openssh.settings.PermitRootLogin = lib.mkForce "prohibit-password";
# QEMU guest agent for Proxmox integration
services.qemuGuest.enable = true;
# cloud-init — consumes Proxmox config drive (hostname, SSH keys, network)
services.cloud-init.enable = true;
services.cloud-init.network.enable = true;
# Python for Ansible management
environment.systemPackages = [ pkgs.python3 ];
# Minimal system
documentation.enable = false;
system.stateVersion = "26.05";
}

View file

@ -0,0 +1,4 @@
---
ansible_user: root
ansible_python_interpreter: /run/current-system/sw/bin/python3
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'

View file

@ -63,3 +63,7 @@ db.w5isp.com
[irc_servers]
us.manero.org ansible_host=manero-us
ca.manero.org ansible_host=manero-ca
[nixos_resolvers]
nixos-resolver ansible_host=nixos-resolver

View file

@ -197,6 +197,248 @@
- role: ns
tags: ns
- name: Build and upload NixOS template to Proxmox
hosts: localhost
gather_facts: false
tags:
- template
- never
vars:
proxmox_node: "{{ lookup('env', 'PROXMOX_NODE') | default('vm2-380', true) }}"
proxmox_api_host: 10.0.0.2
proxmox_api_user: "{{ lookup('env', 'PROXMOX_API_USER') | default('root@pam', true) }}"
proxmox_api_password: "{{ lookup('env', 'PROXMOX_API_PASSWORD') }}"
nixos_template_vmid: 9010
build_host: ci
tasks:
- name: Upload NixOS config to build host
ansible.builtin.copy:
src: files/nixos-template-config.nix
dest: /tmp/nixos-template-config.nix
delegate_to: "{{ build_host }}"
- name: Build NixOS proxmox image
ansible.builtin.shell: >
nixos-generate -f proxmox
-o /tmp/nixos-template.vma.zst
-c /tmp/nixos-template-config.nix
args:
chdir: /tmp
environment:
PATH: "/run/current-system/sw/bin:{{ ansible_env.PATH }}"
delegate_to: "{{ build_host }}"
register: _nixos_build
- name: Find built VMA file in nix store
ansible.builtin.shell: >
nix-shell -p nixos-generators --run
"nixos-generate -f proxmox -o /tmp/nixos-template.vma.zst -c /tmp/nixos-template-config.nix" 2>&1
| grep '^/' | tail -1
args:
chdir: /tmp
delegate_to: "{{ build_host }}"
register: _vma_path
changed_when: false
- name: Copy VMA from build host to Proxmox
ansible.builtin.shell: >
scp {{ build_host }}:'{{ _vma_path.stdout }}' /tmp/nixos-template.vma.zst &&
scp /tmp/nixos-template.vma.zst root@{{ proxmox_api_host }}:/tmp/nixos-template.vma.zst
- name: Destroy existing template VM
community.proxmox.proxmox_kvm:
node: "{{ proxmox_node }}"
vmid: "{{ nixos_template_vmid }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
api_host: "{{ proxmox_api_host }}"
validate_certs: false
state: absent
force: true
- name: Create VM and import disk on Proxmox
ansible.builtin.shell: >
set -e;
vma extract /tmp/nixos-template.vma.zst /tmp/nixos-vma;
qm create {{ nixos_template_vmid }} --name nixos-cloud-init
--memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
--scsihw virtio-scsi-pci --ide2 local-lvm:cloudinit
--boot c --bootdisk scsi0 --serial0 socket --vga serial0;
qm importdisk {{ nixos_template_vmid }}
/tmp/nixos-vma/disk-drive-virtio0.raw local-lvm;
qm set {{ nixos_template_vmid }}
--scsi0 "local-lvm:vm-{{ nixos_template_vmid }}-disk-0";
qm template {{ nixos_template_vmid }};
rm -rf /tmp/nixos-vma /tmp/nixos-template.vma.zst
delegate_to: "{{ proxmox_node }}"
- name: Provision NixOS VM on Proxmox
hosts: localhost
gather_facts: false
tags:
- nixos
- provision
- never
vars:
proxmox_node: "{{ lookup('env', 'PROXMOX_NODE') | default('vm2-380', true) }}"
proxmox_api_host: 10.0.0.2
proxmox_api_user: "{{ lookup('env', 'PROXMOX_API_USER') | default('root@pam', true) }}"
proxmox_api_password: "{{ lookup('env', 'PROXMOX_API_PASSWORD') }}"
proxmox_storage: local-lvm
proxmox_template_vmid: 9010
proxmox_template_name: nixos-cloud-init
vm_name: nixos-resolver
vm_vmid: 110
vm_cores: 2
vm_memory: 4096
vm_disk_size: 64
vm_ssh_keys: |
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLyVSEEAEbGZZqwPwEup0XrlTpu3x3iF9ExvvQPA4xN
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOzrDMNn3nINGdIogzRxY05fkn05LSYi98BGW1Bz5yXD
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIER/8suIKcrT3a/NZHjvOpRosPC5uX4kcznIJHt/98qj
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEEKtgXwoW8HZGqC+KJok9iNpI/lK4glvoryL4Ng/AL+
tasks:
- name: Clone NixOS template
community.proxmox.proxmox_kvm:
node: "{{ proxmox_node }}"
vmid: "{{ proxmox_template_vmid }}"
clone: "{{ proxmox_template_name }}"
newid: "{{ vm_vmid }}"
name: "{{ vm_name }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
api_host: "{{ proxmox_api_host }}"
validate_certs: false
storage: "{{ proxmox_storage }}"
timeout: 90
full: true
agent: true
- name: Configure VM resources
community.proxmox.proxmox_kvm:
node: "{{ proxmox_node }}"
vmid: "{{ vm_vmid }}"
name: "{{ vm_name }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
api_host: "{{ proxmox_api_host }}"
validate_certs: false
cores: "{{ vm_cores }}"
memory: "{{ vm_memory }}"
update: true
- name: Resize disk
community.proxmox.proxmox_disk:
vmid: "{{ vm_vmid }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
api_host: "{{ proxmox_api_host }}"
validate_certs: false
disk: scsi0
size: "{{ vm_disk_size }}G"
state: resized
register: _disk_resize
failed_when:
- _disk_resize.failed
- '"shrinking disks is not supported" not in (_disk_resize.msg | default(""))'
- name: Configure cloud-init
community.proxmox.proxmox_kvm:
node: "{{ proxmox_node }}"
vmid: "{{ vm_vmid }}"
name: "{{ vm_name }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
api_host: "{{ proxmox_api_host }}"
validate_certs: false
ciuser: root
sshkeys: "{{ vm_ssh_keys }}"
ipconfig:
ipconfig0: "ip=dhcp"
nameservers:
- 1.1.1.1
onboot: true
update: true
- name: Start VM
community.proxmox.proxmox_kvm:
node: "{{ proxmox_node }}"
vmid: "{{ vm_vmid }}"
name: "{{ vm_name }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
api_host: "{{ proxmox_api_host }}"
validate_certs: false
state: started
- name: Wait for VM IP via QEMU agent
ansible.builtin.shell: >
python3 -c "
import urllib3, json, time
urllib3.disable_warnings()
from proxmoxer import ProxmoxAPI
proxmox = ProxmoxAPI('{{ proxmox_api_host }}',
user='{{ proxmox_api_user }}', password='{{ proxmox_api_password }}',
verify_ssl=False)
for _ in range(48):
try:
nets = proxmox.nodes('{{ proxmox_node }}').qemu('{{ vm_vmid }}').agent('network-get-interfaces').get()
for iface in nets.get('result', []):
for addr in iface.get('ip-addresses', []):
if addr.get('ip-address-type') == 'ipv4' and addr['ip-address'] != '127.0.0.1':
print(addr['ip-address'])
exit(0)
time.sleep(5)
except Exception:
time.sleep(5)
exit(1)
"
register: _agent_ip
changed_when: false
retries: 1
delay: 0
- name: Add VM to in-memory inventory
ansible.builtin.add_host:
name: "{{ vm_name }}"
ansible_host: "{{ _agent_ip.stdout | trim }}"
groups: nixos_resolvers
- name: Configure NixOS Unbound resolver
hosts: nixos_resolvers
become: true
gather_facts: true
tags:
- nixos
- unbound
- never
tasks:
- name: Load resolver group variables
ansible.builtin.include_vars:
file: group_vars/resolvers/main.yml
- name: Deploy NixOS configuration with Unbound
ansible.builtin.template:
src: roles/resolvers/templates/nixos-unbound.nix.j2
dest: /etc/nixos/unbound.nix
owner: root
group: root
mode: '0644'
register: _nixos_config
- name: Import unbound module in configuration.nix
ansible.builtin.lineinfile:
path: /etc/nixos/configuration.nix
line: "./unbound.nix"
regexp: '^./unbound\.nix'
insertafter: '^\{ config,'
state: present
register: _nixos_import
- name: Rebuild NixOS
ansible.builtin.command: nixos-rebuild switch
when: _nixos_config.changed or _nixos_import.changed
- name: Apply general configuration to all nodes
hosts: all
become: true

View file

@ -0,0 +1,114 @@
# Managed by Ansible (roles/resolvers/templates/nixos-unbound.nix.j2)
# Settings derived from unbound.conf.j2 — same configuration as resolver1.
{ config, pkgs, lib, ... }:
let
accessControl = [
"0.0.0.0/0 refuse"
"::0/0 refuse"
{%- for netblock in resolver_allowed_netblocks %}
"{{ netblock }} allow"
{%- endfor %}
];
blocklistZone = pkgs.writeText "blocklist.rpz" ''
$TTL 3600
$ORIGIN blocklist.rpz.
@ IN SOA localhost. root.localhost. 1 3600 600 86400 3600
@ IN NS localhost.
{%- for domain in resolver_blocked_domains | default([]) %}
{{ domain }} CNAME .
*.{{ domain }} CNAME .
{%- endfor %}
'';
in
{
services.unbound = {
enable = true;
settings = {
server = {
interface = [ "0.0.0.0" "::0" ];
port = 53;
# DoH endpoint via loopback (Caddy terminates TLS upstream)
interface-auto = false;
https-port = 8080;
http-endpoint = "/dns-query";
http-notls-downstream = true;
access-control = accessControl;
# Performance tuning
num-threads = 4;
msg-cache-size = "512m";
rrset-cache-size = "512m";
key-cache-size = "512m";
msg-cache-slabs = 8;
rrset-cache-slabs = 8;
infra-cache-slabs = 8;
key-cache-slabs = 8;
num-queries-per-thread = 1024;
outgoing-port-permit = "32768-60999";
outgoing-num-tcp = 100;
incoming-num-tcp = 600;
# Prefetch and serve-expired
prefetch = true;
prefetch-key = true;
serve-expired = true;
serve-expired-ttl = 86400;
serve-expired-client-timeout = 1800;
# Socket tuning
so-reuseport = true;
so-rcvbuf = "4m";
ip-transparent = true;
edns-buffer-size = 1232;
edns-tcp-keepalive = true;
# Hardening
harden-short-bufsize = true;
harden-large-queries = true;
harden-glue = true;
harden-dnssec-stripped = true;
harden-below-nxdomain = true;
aggressive-nsec = true;
qname-minimisation = true;
# Logging
log-time-ascii = true;
# Statistics
extended-statistics = true;
shm-enable = false;
};
remote-control = {
control-enable = true;
control-interface = "127.0.0.1";
control-port = 8953;
control-use-cert = false;
};
rpz = {
name = "blocklist.rpz";
zonefile = "${blocklistZone}";
rpz-action-override = "nxdomain";
rpz-log = true;
rpz-log-name = "blocklist";
};
};
# DNSSEC root trust anchor
enableRootTrustAnchor = true;
};
# Python for Ansible management (appended, doesn't clobber other packages)
environment.systemPackages = lib.mkAfter [ pkgs.python3 ];
# Open DNS ports
networking.firewall.allowedTCPPorts = [ 53 ];
networking.firewall.allowedUDPPorts = [ 53 ];
}

View file

@ -1363,7 +1363,7 @@ remote-control:
# from group_vars/resolvers/main.yml. Requires respip in module-config (above).
rpz:
name: "blocklist.rpz"
zonefile: "/etc/unbound/blocklist.rpz"
zonefile: "blocklist.rpz"
rpz-action-override: nxdomain
rpz-log: yes
rpz-log-name: "blocklist"