nixos modules for convenient deployment of cloud resources

feat: initial implementation with frewall resource for hetzner

ptr.pet b7f69b5a

Changed files
+162
firewall
provider
+30
firewall/default.nix
··· 1 + {lib, config, options, ...}: let 2 + l = lib; 3 + t = l.types; 4 + cfg = config.networking.firewall.public; 5 + 6 + portOptions = { 7 + inherit (options.networking.firewall) 8 + allowedTCPPorts 9 + allowedUDPPorts 10 + allowedTCPPortRanges 11 + allowedUDPPortRanges; 12 + }; 13 + in { 14 + options = { 15 + networking.firewall.public = l.mkOption { 16 + default = { }; 17 + type = t.attrsOf (t.submodule [{ options = portOptions; }]); 18 + description = "Tagged open port sets."; 19 + }; 20 + }; 21 + 22 + config = let 23 + concatAll = name: l.concatLists (l.mapAttrsToList (_: opts: opts.${name}) cfg); 24 + in { 25 + networking.firewall.allowedTCPPorts = concatAll "allowedTCPPorts"; 26 + networking.firewall.allowedTCPPortRanges = concatAll "allowedTCPPortRanges"; 27 + networking.firewall.allowedUDPPorts = concatAll "allowedUDPPorts"; 28 + networking.firewall.allowedUDPPortRanges = concatAll "allowedUDPPortRanges"; 29 + }; 30 + }
+37
firewall/provider/hetzner/app.nu
··· 1 + use std/log 2 + 3 + let authHeader = ["authorization" $"Bearer ($env.HETZNER_API_TOKEN)"] 4 + 5 + def makeApiUrl [path: string] { 6 + return $"https://api.hetzner.cloud/v1($path)" 7 + } 8 + def post [path: string] { 9 + let resp = $in | http post -e --full -H authHeader --content-type application/json (makeApiUrl path) 10 + $resp.body = $resp.body | from json 11 + $resp 12 + } 13 + def get [path: string] { 14 + let resp = http get -e --full -H authHeader (makeApiUrl path) 15 + $resp.body = $resp.body | from json 16 + $resp 17 + } 18 + 19 + # first fetch firewall to see if it even exists 20 + let resp = get $"/firewalls/($firewallId)" 21 + if $resp.status == 404 { 22 + log error $"provided firewall \(id ($firewallId)\) does not exist" 23 + exit 1 24 + } 25 + let firewall = $resp.body | get firewall 26 + 27 + # backup firewall 28 + let backupPath = $".hetzner/($firewallId).json" 29 + mkdir .hetzner; $firewall | to json | save $backupPath 30 + log info $"backing up firewall ($firewallId) to ($backupPath)" 31 + 32 + # apply rules 33 + let resp = open $rulesFile | from json | post $"/firewalls/($firewallId)/actions/set_rules" 34 + if $resp.status != 201 { 35 + log error $"could not apply firewall \(id ($firewallId)\)" 36 + } 37 + log info $"applied firewall ($firewallId)"
+56
firewall/provider/hetzner/default.nix
··· 1 + {pkgs, lib, config, options, ...}: let 2 + l = lib; 3 + t = l.types; 4 + taggedPorts = config.networking.firewall.public; 5 + cfg = config.providers.hetzner; 6 + in { 7 + options = { 8 + providers.hetzner.firewall = { 9 + id = l.mkOption { 10 + type = t.ints.unsigned; 11 + description = "The ID of the firewall to update."; 12 + }; 13 + app = l.mkOption { 14 + type = t.package; 15 + readOnly = true; 16 + description = '' 17 + The generated app for this provider, run it to apply the configuration. 18 + 19 + For this to work, you need to set the `HETZNER_API_TOKEN` environment variable to a valid API token from Hetzner. 20 + ''; 21 + }; 22 + }; 23 + }; 24 + 25 + config = let 26 + mkRule = proto: tag: port: { 27 + description = tag; 28 + direction = "in"; 29 + protocol = proto; 30 + port = 31 + if l.isAttrs port 32 + then l.concatMapStringsSep "-" toString [port.from port.to] 33 + else toString port; 34 + }; 35 + mkTcpRule = mkRule "tcp"; 36 + mkUdpRule = mkRule "udp"; 37 + firewallRules = pkgs.writers.writeJSON "hetzner-firewall-${toString cfg.id}-rules.json" { 38 + rules = l.flatten ( 39 + l.mapAttrsToList 40 + (tag: ports: [ 41 + (l.map (mkTcpRule tag) ports.allowedTCPPorts) 42 + (l.map (mkTcpRule tag) ports.allowedTCPPortRanges) 43 + (l.map (mkUdpRule tag) ports.allowedUDPPorts) 44 + (l.map (mkUdpRule tag) ports.allowedUDPPortRanges) 45 + ]) 46 + taggedPorts 47 + ); 48 + }; 49 + in { 50 + providers.hetzner.firewall.app = pkgs.writers.writeNu "apply-hetzner" '' 51 + let firewallId = ${toString cfg.id} 52 + let rulesFile = ${firewallRules} 53 + ${l.fileContents ./app.nu} 54 + ''; 55 + }; 56 + }
+27
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1752480373, 6 + "narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=", 7 + "owner": "nixos", 8 + "repo": "nixpkgs", 9 + "rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "nixos", 14 + "ref": "nixos-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "root": { 20 + "inputs": { 21 + "nixpkgs": "nixpkgs" 22 + } 23 + } 24 + }, 25 + "root": "root", 26 + "version": 7 27 + }
+12
flake.nix
··· 1 + { 2 + description = "nixos modules for convenient deployment of cloud resources"; 3 + 4 + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 5 + 6 + outputs = inp: { 7 + nixosModules = { 8 + firewall = ./firewall/default.nix; 9 + firewall-hetzner = ./firewall/provider/hetzner/default.nix; 10 + }; 11 + }; 12 + }