diff --git a/flake.lock b/flake.lock index 7b10a36..d0e7179 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,37 @@ { "nodes": { + "bats-assert": { + "flake": false, + "locked": { + "lastModified": 1636059754, + "narHash": "sha256-ewME0l27ZqfmAwJO4h5biTALc9bDLv7Bl3ftBzBuZwk=", + "owner": "bats-core", + "repo": "bats-assert", + "rev": "34551b1d7f8c7b677c1a66fc0ac140d6223409e5", + "type": "github" + }, + "original": { + "owner": "bats-core", + "repo": "bats-assert", + "type": "github" + } + }, + "bats-support": { + "flake": false, + "locked": { + "lastModified": 1548869839, + "narHash": "sha256-Gr4ntadr42F2Ks8Pte2D4wNDbijhujuoJi4OPZnTAZU=", + "owner": "bats-core", + "repo": "bats-support", + "rev": "d140a65044b2d6810381935ae7f0c94c7023c8c3", + "type": "github" + }, + "original": { + "owner": "bats-core", + "repo": "bats-support", + "type": "github" + } + }, "disko": { "inputs": { "nixpkgs": [ @@ -58,6 +90,27 @@ "type": "github" } }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "terranix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1736143030, + "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -95,6 +148,36 @@ "type": "github" } }, + "flake-utils_2": { + "locked": { + "lastModified": 1634851050, + "narHash": "sha256-N83GlSGPJJdcqhUxSCS/WwW5pksYf3VP1M13cDRTSVA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c91f3de5adaf1de973b797ef7485e441a65b8935", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "locked": { + "lastModified": 1634851050, + "narHash": "sha256-N83GlSGPJJdcqhUxSCS/WwW5pksYf3VP1M13cDRTSVA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c91f3de5adaf1de973b797ef7485e441a65b8935", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1751582995, @@ -111,11 +194,28 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1636273007, + "narHash": "sha256-eb6HcZNacO9vIP/KcJ5CoCRYSGfD+VxzYs2cCafEo4Y=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "c69c6533c820c55c3f1d924b399d8f6925a1e41a", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "disko": "disko", "flake-base": "flake-base", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "terranix": "terranix", + "terranix-hcloud": "terranix-hcloud" } }, "snowfall-lib": { @@ -156,6 +256,103 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "terranix": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems_2" + }, + "locked": { + "lastModified": 1749381683, + "narHash": "sha256-16z7tXZch12SAd3d8tbAiEOamyq3zFbw1oUq/ipmTkM=", + "owner": "terranix", + "repo": "terranix", + "rev": "9d2370279d595be9e728b68d29ff0b546d88e619", + "type": "github" + }, + "original": { + "owner": "terranix", + "repo": "terranix", + "type": "github" + } + }, + "terranix-examples": { + "locked": { + "lastModified": 1633465925, + "narHash": "sha256-BfXRW1ZHpK5jh5CVcw7eFpGsWE1CyVxL8R+V7uXemaU=", + "owner": "terranix", + "repo": "terranix-examples", + "rev": "70bf5d5a1ad4eabef1e4e71c1eb101021decd5a4", + "type": "github" + }, + "original": { + "owner": "terranix", + "repo": "terranix-examples", + "type": "github" + } + }, + "terranix-hcloud": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ], + "terranix": "terranix_2" + }, + "locked": { + "lastModified": 1745572802, + "narHash": "sha256-Zk2vdO5JSsOJ+WVpBauGvKHF+i8+YZGsn9EHNyLLUhw=", + "owner": "terranix", + "repo": "terranix-hcloud", + "rev": "5e380bf5f8cbc43de134a24d97097a972986bd6e", + "type": "github" + }, + "original": { + "owner": "terranix", + "repo": "terranix-hcloud", + "type": "github" + } + }, + "terranix_2": { + "inputs": { + "bats-assert": "bats-assert", + "bats-support": "bats-support", + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs_2", + "terranix-examples": "terranix-examples" + }, + "locked": { + "lastModified": 1636274023, + "narHash": "sha256-HDiyJGgyDUoLnpL8N+wDm3cM/vEfYYc/p4N1kKH/kLk=", + "owner": "terranix", + "repo": "terranix", + "rev": "342ec8490bc948c8589414eb89f26b265cbfd62a", + "type": "github" + }, + "original": { + "owner": "terranix", + "ref": "develop", + "repo": "terranix", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index ec96a99..a033781 100644 --- a/flake.nix +++ b/flake.nix @@ -11,18 +11,32 @@ url = "github:nix-community/disko"; inputs.nixpkgs.follows = "nixpkgs"; }; + terranix = { + url = "github:terranix/terranix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + terranix-hcloud = { + url = "github:terranix/terranix-hcloud"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = - inputs@{ self, nixpkgs, ... }: + inputs@{ self, ... }: let + dirsInPath = + path: + let + files = builtins.readDir path; + dirs = builtins.filterAttrs (name: kind: kind == "directory") files; + in + builtins.attrNames dirs; profileArgs = { inherit self; }; - profiles = builtins.readDir ./nix/profiles; - profilesDirs = nixpkgs.lib.filterAttrs (name: kind: kind == "directory") profiles; - profileNames = builtins.attrNames profilesDirs; + profileNames = dirsInPath ./nix/profiles; + nixosModules = dirsInPath ./nix/modules/nixos; inputModules = [ inputs.disko.nixosModules.disko ]; in - inputs.flake-base.lib.mkFlake { + (inputs.flake-base.lib.mkFlake { inherit inputs; src = ./.; systems.modules.nixos = inputModules; @@ -37,14 +51,7 @@ modules.nixos = { default = { - imports = [ - self.nixosModules.hetzner - self.nixosModules.sshd - self.nixosModules.sshd - self.nixosModules.systemd-boot - self.nixosModules.qemu-guest - inputs.disko.nixosModules.disko - ]; + imports = builtins.map (m: self.nixosModules.${m}) nixosModules ++ inputModules; } // (builtins.listToAttrs ( builtins.map (n: { @@ -53,5 +60,11 @@ }) profileNames )); }; + }) + // { + terranixModules.cloudflare = import ./nix/modules/terranix/cloudflare { + inherit inputs; + khscodesLib = inputs.self.lib; + }; }; } diff --git a/nix/lib/mkBwEnv/default.nix b/nix/lib/mkBwEnv/default.nix new file mode 100644 index 0000000..8a842f4 --- /dev/null +++ b/nix/lib/mkBwEnv/default.nix @@ -0,0 +1,71 @@ +{ ... }: +{ + mkBwEnv = + { + items, + pkgs, + name ? "bw-env", + exe, + }: + let + assertMsg = pred: msg: pred || builtins.throw msg; + concatLines = builtins.concatStringsSep "\n"; + itemNames = builtins.attrNames items; + exports = builtins.map ( + itemName: + let + item = items.${itemName}; + varNames = builtins.attrNames item; + vars = builtins.map ( + varName: + let + var = item.${varName}; + script = + if var == "login.password" || var == "login.username" then + "jq -r '.${var}'" + else + "jq -r --arg name '${var}' \"$JQ_FIELD_SCRIPT\""; + in + assert assertMsg (builtins.match "^[a-zA-Z0-9_]+$" != null) "Invalid variable name: ${varName}"; + '' + ${varName}="$(${script} <<< "$item")" + if [[ "''$${varName}" == "null" ]]; then + echo "Could not read ${varName} from ${itemName}" 1>&2 + exit 1 + fi + export ${varName} + '' + ) varNames; + in + '' + item="$(bw get item '${itemName}')" + ${concatLines vars} + '' + ) itemNames; + in + pkgs.writeShellApplication { + inherit name; + runtimeInputs = [ + pkgs.jq + pkgs.bitwarden-cli + ]; + meta = { + mainProgram = name; + }; + text = '' + if [ "''${DEBUG:-0}" == "1" ]; then + set -x + fi + if [ -z "''${BW_SESSION+x}" ]; then + BW_SESSION="$(bw unlock --raw)" + echo "Bitwarden session (export as BW_SESSION to avoid repeating entries): $BW_SESSION" 1>&2 + export BW_SESSION + fi + + JQ_FIELD_SCRIPT='.fields | map(select(.name == $'"name))[0].value" + ${concatLines exports} + + exec "${exe}" "$@" + ''; + }; +} diff --git a/nix/lib/mkSubmodule/default.nix b/nix/lib/mkSubmodule/default.nix new file mode 100644 index 0000000..ef43f4c --- /dev/null +++ b/nix/lib/mkSubmodule/default.nix @@ -0,0 +1,13 @@ +{ lib, ... }: +{ + mkSubmodule = + { + options, + description, + }: + lib.types.submoduleWith { + description = description; + shorthandOnlyDefinesConfig = true; + modules = lib.toList { inherit options; }; + }; +} diff --git a/nix/modules/nixos/fqdn/default.nix b/nix/modules/nixos/fqdn/default.nix new file mode 100644 index 0000000..504167c --- /dev/null +++ b/nix/modules/nixos/fqdn/default.nix @@ -0,0 +1,29 @@ +{ + config, + lib, + ... +}: +let + cfg = config.khscodes.fqdn; +in +{ + options.khscodes.fqdn = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Sets the FQDN of the machine. This is a prerequisite for many modules to be used"; + }; + + config = lib.mkIf (cfg != null) ( + let + hostname = builtins.head (lib.strings.splitString "." cfg); + domain = if hostname == cfg then null else (lib.strings.removePrefix "${hostname}." cfg); + in + { + networking.hostName = hostname; + networking.domain = domain; + boot.kernel.sysctl = { + "kernel.hostname" = cfg; + }; + } + ); +} diff --git a/nix/modules/nixos/sshd/default.nix b/nix/modules/nixos/sshd/default.nix index 18d0b32..9dab23e 100644 --- a/nix/modules/nixos/sshd/default.nix +++ b/nix/modules/nixos/sshd/default.nix @@ -1,4 +1,11 @@ { config, lib, ... }: +let + cfg = config.khscodes.sshd; +in { + options.khscodes.sshd.enable = lib.mkEnableOption "Enables sshd for the instance"; + config = lib.mkIf cfg.enable { + services.sshd.enable = true; + }; } diff --git a/nix/modules/nixos/terraform-hetzner/default.nix b/nix/modules/nixos/terraform-hetzner/default.nix new file mode 100644 index 0000000..059af8d --- /dev/null +++ b/nix/modules/nixos/terraform-hetzner/default.nix @@ -0,0 +1,283 @@ +{ + config, + lib, + inputs, + pkgs, + ... +}: +let + cfg = config.khscodes.terraform-hetzner; + fqdn = config.khscodes.fqdn; + hostPkgs = import inputs.nixpkgs { + system = pkgs.buildPlatform.system; + overlays = [ inputs.self.overlays.bitwarden-cli ]; + }; + firewallTcpRules = lib.lists.map (p: { + direction = "in"; + protocol = "tcp"; + port = p; + source_ips = [ + "0.0.0.0/0" + "::/0" + ]; + }) config.networking.firewall.allowedTCPPorts; + firewallUdpRules = lib.lists.map (p: { + direction = "in"; + protocol = "udp"; + port = p; + source_ips = [ + "0.0.0.0/0" + "::/0" + ]; + }) config.networking.firewall.allowedUDPPorts; + firewallIcmpRules = lib.lists.optional config.networking.firewall.allowPing { + direction = "in"; + protocol = "icmp"; + source_ips = [ + "0.0.0.0/0" + "::/0" + ]; + description = "ping"; + }; + firewallRules = firewallTcpRules ++ firewallUdpRules ++ firewallIcmpRules ++ cfg.extraFirewallRules; + firewallEnable = config.networking.firewall.enable; + mapRdns = cfg.mapRdns; + tldFromFqdn = + fqdn: + let + split = lib.strings.splitString "." fqdn; + in + if lib.lists.length split < 3 then + fqdn + else + lib.strings.removePrefix "${builtins.head split}." fqdn; +in +{ + options.khscodes.terraform-hetzner = { + enable = lib.mkEnableOption "enables generating a terraform config"; + dnsNames = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "DNS names for the server"; + default = [ fqdn ]; + }; + bucket = { + key = lib.mkOption { + type = lib.types.str; + description = "Key for use in the bucket"; + default = "${fqdn}.tfstate"; + }; + }; + secretsSource = lib.mkOption { + type = lib.types.enum [ + "bitwarden" + "vault" + ]; + description = "Whether to load terraform secrets from Bitwarden or Vault"; + default = "vault"; + }; + datacenter = lib.mkOption { + type = lib.types.str; + description = "The Hetzner datacenter to create a server in"; + default = "hel1-dc2"; + }; + output = lib.mkOption { + type = lib.types.nullOr lib.types.package; + description = "The terranix package built from the configuration"; + default = null; + }; + mapRdns = lib.mkOption { + type = lib.types.bool; + description = "Sets up RDNS for the server"; + default = false; + }; + server_type = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "The server type to create"; + default = null; + }; + extraFirewallRules = lib.mkOption { + type = lib.types.listOf lib.types.attrs; + description = "Extra firewall rules added to the instance"; + default = [ + { + direction = "out"; + protocol = "tcp"; + port = 80; + destination_ips = [ + "0.0.0.0/0" + "::/0" + ]; + description = "http"; + } + { + direction = "out"; + protocol = "tcp"; + port = 443; + destination_ips = [ + "0.0.0.0/0" + "::/0" + ]; + description = "https"; + } + { + direction = "out"; + protocol = "udp"; + port = 443; + destination_ips = [ + "0.0.0.0/0" + "::/0" + ]; + description = "quic"; + } + { + direction = "out"; + protocol = "icmp"; + destination_ips = [ + "0.0.0.0/0" + "::/0" + ]; + description = "Ping"; + } + ]; + }; + }; + config = lib.mkIf cfg.enable ( + let + labels = { + app = fqdn; + }; + config = inputs.terranix.lib.terranixConfiguration { + system = pkgs.hostPlatform.system; + modules = [ + { + imports = [ + inputs.self.terranixModules.cloudflare + inputs.terranix-hcloud.terranixModules.hcloud + ]; + + hcloud.enable = true; + terraform.required_providers.hcloud.version = "~> 1.45.0"; + terraform.backend.s3 = { + bucket = "bw-terraform"; + key = cfg.bucket.key; + region = "auto"; + endpoints = { + s3 = "https://477b394a6a545699445c40953e40f00b.r2.cloudflarestorage.com"; + }; + use_path_style = true; + skip_credentials_validation = true; + skip_region_validation = true; + skip_metadata_api_check = true; + skip_requesting_account_id = true; + skip_s3_checksum = true; + }; + + data.hcloud_ssh_key.khs = { + name = "ca.kaareskovgaard.net"; + }; + + resource.hcloud_primary_ip.ipv4 = { + inherit labels; + name = "${fqdn} ipv4"; + datacenter = cfg.datacenter; + type = "ipv4"; + assignee_type = "server"; + auto_delete = false; + }; + resource.hcloud_primary_ip.ipv6 = { + inherit labels; + name = "${fqdn} ipv6"; + datacenter = cfg.datacenter; + type = "ipv6"; + assignee_type = "server"; + auto_delete = false; + }; + khscodes.cloudflare = { + enable = true; + dns = { + enable = true; + zone_name = tldFromFqdn fqdn; + aRecords = [ + { + inherit fqdn; + content = "\${ hcloud_server.compute.ipv4_address }"; + } + ]; + aaaaRecords = [ + { + inherit fqdn; + content = "\${ hcloud_server.compute.ipv6_address }"; + } + ]; + }; + }; + resource.hcloud_firewall.fw = lib.mkIf firewallEnable { + inherit labels; + name = fqdn; + apply_to = { + server = "\${ hcloud_server.compute.id }"; + }; + rule = firewallRules; + }; + resource.hcloud_server.compute = { + inherit (cfg) server_type datacenter; + inherit labels; + name = fqdn; + image = "debian-12"; + public_net = { + ipv4_enabled = true; + ipv4 = "\${ hcloud_primary_ip.ipv4.id }"; + ipv6_enabled = true; + ipv6 = "\${ hcloud_primary_ip.ipv6.id }"; + }; + ssh_keys = [ "\${ data.hcloud_ssh_key.khs.id }" ]; + lifecycle = { + ignore_changes = [ + "ssh_keys" + "public_net" + "image" + ]; + }; + }; + output.ipv4_address = { + value = "\${ hcloud_server.compute.ipv4_address }"; + sensitive = false; + }; + + output.ipv6_address = { + value = "\${ hcloud_server.compute.ipv6_address }"; + sensitive = false; + }; + } + ( + { lib, ... }: + { + config = lib.mkIf mapRdns { + resource.hcloud_rdns.ipv4 = { + primary_ip_id = "\${ hcloud_primary_ip.ipv4.id }"; + ip_address = "\${ hcloud_server.compute.ipv4_address }"; + dns_ptr = fqdn; + }; + resource.hcloud_rdns.ipv6 = { + primary_ip_id = "\${ hcloud_primary_ip.ipv6.id }"; + ip_address = "\${ hcloud_server.compute.ipv6_address }"; + dns_ptr = fqdn; + }; + }; + } + ) + ]; + }; + in + { + assertions = [ + { + assertion = config.khscodes.fqdn != null; + message = "Must set config.khscodes.fqdn when using terraform"; + } + ]; + + khscodes.terraform-hetzner.output = config; + } + ); +} diff --git a/nix/modules/terranix/cloudflare/default.nix b/nix/modules/terranix/cloudflare/default.nix new file mode 100644 index 0000000..b598f30 --- /dev/null +++ b/nix/modules/terranix/cloudflare/default.nix @@ -0,0 +1,172 @@ +{ inputs, khscodesLib }: +{ config, lib, ... }: +let + cfg = config.khscodes.cloudflare; + nameFromFQDNAndZone = + fqdn: zone: + let + stripped = lib.strings.removeSuffix ".${zone}" fqdn; + in + if stripped != fqdn then + stripped + else if fqdn == zone then + "@" + else + fqdn; + fqdnToTFname = fqdn: builtins.replaceStrings [ "." ] [ "_" ] fqdn; + dnsARecordModule = khscodesLib.mkSubmodule { + description = "Module for defining dns A/AAAA record"; + options = { + fqdn = lib.mkOption { + type = lib.types.str; + description = "The FQDN of the A/AAAA record to create"; + }; + content = lib.mkOption { + type = lib.types.str; + description = "The content of the A/AAAA record (IPv4/IPv6 address)"; + }; + proxied = lib.mkOption { + type = lib.types.bool; + description = "Creates a proxied record in cloudflare"; + default = false; + }; + ttl = lib.mkOption { + type = lib.types.int; + description = "Time to Live for the A/AAAA record"; + default = 600; + }; + }; + }; + dnsTxtRecordModule = khscodesLib.mkSubmodule { + description = "Module for defining dns TXT record"; + options = { + fqdn = lib.mkOption { + type = lib.types.str; + description = "The FQDN of the TXT record to create"; + }; + content = lib.mkOption { + type = lib.types.str; + description = "The content of the TXT record"; + }; + ttl = lib.mkOption { + type = lib.types.int; + description = "Time to Live for the TXT record"; + default = 600; + }; + }; + }; + dnsMxRecordModule = khscodesLib.mkSubmodule { + description = "Module for defining dns MX record"; + options = { + fqdn = lib.mkOption { + type = lib.types.str; + description = "The FQDN of the MX record to create"; + }; + content = lib.mkOption { + type = lib.types.str; + description = "The content of the MX record"; + }; + priority = lib.mkOption { + type = lib.types.int; + description = "Priority for the MX record"; + }; + ttl = lib.mkOption { + type = lib.types.int; + description = "Time to Live for the MX record"; + default = 600; + }; + }; + }; +in +{ + options.khscodes.cloudflare = { + enable = lib.mkEnableOption "Enables khscodes cloudflare terranix integration"; + dns = { + enable = lib.mkEnableOption "Enables setting up DNS records"; + zone_name = lib.mkOption { + type = lib.types.str; + description = "The dns zone name (TLD)"; + }; + aRecords = lib.mkOption { + type = lib.types.listOf dnsARecordModule; + default = [ ]; + description = "A records to create in the zone"; + }; + aaaaRecords = lib.mkOption { + type = lib.types.listOf dnsARecordModule; + default = [ ]; + description = "AAAA records to create in the zone"; + }; + txtRecords = lib.mkOption { + type = lib.types.listOf dnsTxtRecordModule; + default = [ ]; + description = "TXT Records to create"; + }; + mxRecords = lib.mkOption { + type = lib.types.listOf dnsMxRecordModule; + default = [ ]; + description = "MX records to create"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + provider.cloudflare.api_token = "\${ var.cloudflare_token }"; + variable.cloudflare_token = { + type = "string"; + sensitive = true; + }; + terraform.required_providers.cloudflare = { + source = "cloudflare/cloudflare"; + version = "~> 4.0"; + }; + + data.cloudflare_zone.dns_zone = lib.attrsets.optionalAttrs cfg.dns.enable { + name = cfg.dns.zone_name; + }; + resource.cloudflare_record = lib.attrsets.optionalAttrs cfg.dns.enable ( + lib.listToAttrs ( + (lib.lists.map (record: { + name = "${fqdnToTFname record.fqdn}_a"; + value = { + inherit (record) content ttl proxied; + name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name; + type = "A"; + zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; + comment = "app=${cfg.dns.zone_name}"; + }; + }) cfg.dns.aRecords) + ++ (lib.lists.map (record: { + name = "${fqdnToTFname record.fqdn}_aaaa"; + value = { + inherit (record) content ttl proxied; + name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name; + type = "AAAA"; + zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; + comment = "app=${cfg.dns.zone_name}"; + }; + }) cfg.dns.aaaaRecords) + ++ (lib.lists.map (record: { + name = "${fqdnToTFname record.fqdn}_txt"; + value = { + inherit (record) content ttl; + name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name; + type = "TXT"; + zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; + comment = "app=${cfg.dns.zone_name}"; + }; + }) cfg.dns.txtRecords) + ++ (lib.lists.map (record: { + name = "${fqdnToTFname record.fqdn}_mx"; + value = { + inherit (record) content priority; + name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name; + type = "MX"; + zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; + comment = "app=${cfg.dns.zone_name}"; + }; + }) cfg.dns.mxRecords) + ) + ); + }; +} diff --git a/nix/overlays/bitwarden-cli/default.nix b/nix/overlays/bitwarden-cli/default.nix new file mode 100644 index 0000000..c8cf892 --- /dev/null +++ b/nix/overlays/bitwarden-cli/default.nix @@ -0,0 +1,12 @@ +{ ... }: +final: prev: { + bitwarden-cli = + if final.lib.strings.hasSuffix "-darwin" final.system then + # Bitwarden-cli is broken on darwin with the newer llvm + (prev.bitwarden-cli.overrideAttrs (oldAttrs: { + nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ final.llvmPackages_18.stdenv.cc ]; + stdenv = final.llvmPackages_18.stdenv; + })) + else + prev.bitwarden-cli; +} diff --git a/nix/packages/bw-opentofu/default.nix b/nix/packages/bw-opentofu/default.nix new file mode 100644 index 0000000..fe82516 --- /dev/null +++ b/nix/packages/bw-opentofu/default.nix @@ -0,0 +1,51 @@ +{ pkgs, lib, ... }: +let + opentofu = pkgs.opentofu; + bw-opentofu = lib.khscodes.mkBwEnv { + inherit pkgs; + name = "bw-opentofu"; + items = { + "KHS Openstack" = { + TF_VAR_openstack_username = "login.username"; + TF_VAR_openstack_password = "login.password"; + TF_VAR_openstack_tenant_name = "Project Name"; + TF_VAR_openstack_auth_url = "Auth URL"; + TF_VAR_openstack_endpoint_type = "Interface"; + TF_VAR_openstack_region = "Region Name"; + }; + "Cloudflare" = { + TF_VAR_cloudflare_token = "DNS API Token"; + TF_VAR_cloudflare_email = "login.username"; + AWS_ACCESS_KEY_ID = "BW Terraform access key id"; + AWS_SECRET_ACCESS_KEY = "BW Terraform secret access key"; + }; + "Hetzner Cloud" = { + TF_VAR_hcloud_api_token = "Terraform API Token"; + }; + }; + exe = lib.getExe opentofu; + }; +in +pkgs.writeShellApplication { + name = "bw-opentofu"; + runtimeInputs = [ + bw-opentofu + pkgs.uutils-coreutils-noprefix + pkgs.bitwarden-cli + ]; + text = '' + fqdn="$1" + config="$2" + lockHcl="$3" + dir="$(mktemp -d --tmpdir -t "terraform-hetzher-''${fqdn}.XXXXXXXXXX")" + cp "$lockHcl" "$dir/.terraform.lock.hcl" + cp "''${config}" "$dir/config.tf.json" + if [ "''${BW_SESSION:-}" == "" ]; then + BW_SESSION="$(bw unlock --raw)" + export BW_SESSION + trap "bw lock" EXIT + fi + bw-opentofu -chdir="$dir" init + bw-opentofu -chdir="$dir" apply + ''; +} diff --git a/nix/packages/opentofu-hetzner/default.nix b/nix/packages/opentofu-hetzner/default.nix new file mode 100644 index 0000000..a59f235 --- /dev/null +++ b/nix/packages/opentofu-hetzner/default.nix @@ -0,0 +1,16 @@ +{ + inputs, + pkgs, +}: +pkgs.writeShellApplication { + name = "opentofu-hetzner"; + runtimeInputs = [ + pkgs.nix + pkgs.khscodes.bw-opentofu + ]; + text = '' + hostname="$1" + config="$(nix build --no-link --print-out-paths '${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.terraform-hetzner.output')" + bw-opentofu "$hostname" "$config" "${./terraform.lock.hcl}" + ''; +} diff --git a/nix/packages/opentofu-hetzner/terraform.lock.hcl b/nix/packages/opentofu-hetzner/terraform.lock.hcl new file mode 100644 index 0000000..8dc594b --- /dev/null +++ b/nix/packages/opentofu-hetzner/terraform.lock.hcl @@ -0,0 +1,47 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/cloudflare/cloudflare" { + version = "4.52.0" + constraints = "~> 4.0" + hashes = [ + "h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=", + "zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0", + "zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8", + "zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238", + "zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f", + "zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f", + "zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8", + "zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9", + "zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6", + "zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb", + "zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b", + "zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380", + "zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7", + "zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672", + ] +} + +provider "registry.opentofu.org/hetznercloud/hcloud" { + version = "1.45.0" + constraints = "~> 1.45.0" + hashes = [ + "h1:dh2iL5GHfDui5DbZFD/kcWlwzmC6slgUirA0FbZBK7g=", + "zh:1c4b44a698cfaca215bdbadaf92669dd23533210c3cbf32895fbf4ff7acf6c24", + "zh:2915f8385559694e5097d8d0df16358200e9f0d9efb80559e9ea0bd072d792b9", + "zh:3a6b37b0bba50d263bd3dba26185bde13c825e59b6b301ab3f9f45686a21456b", + "zh:3e3910fa22a3a8d73d1aed38cc479c3e1958e9168b5f4a7d0da6cf03c2dfc155", + "zh:3f8d7d09e5c93162a1e9e6c89acac0799fb55765b44b7d1d020763c814263c57", + "zh:40bc5e94bff495440e1b4f797165d7f0dcee2282a86a61b158f47fe4bc57e9fb", + "zh:473f51d464b897d0e8e3d5ca2eb175b37e2f7ce03c8b26f47cc35885cf620946", + "zh:6fdd4bf71c19cfad78d7e1d2336be873eb8567a139d53e672e78ebcbc36a4d7d", + "zh:9e08638cbfc90d69f1c21ee34191db077d58d040cf7a9eed07a1dc335d463e97", + "zh:b1ed5ea81bc6d2c88efdefaeb244322874508d90d8217ac2e3541445254bdadc", + "zh:ced05776c27d550d15d4a71360243740ecb4ea1e65e67229fb2273a27353b00c", + "zh:da79b8a1a982a1d365ea206a2654e8b5003aeba9ccdc9c8751bb6ee3f40d8c49", + "zh:fabbad25bab09dd74f2b819992ab99b939c642374d6ca080b18d6e2a91d8d487", + "zh:fb0e083d2925f289999dc561ef1c2f84a9e0ab11388c40162ca8b470f50f71f5", + ] +} diff --git a/nix/profiles/hetzner-server.nix b/nix/profiles/hetzner-server.nix index 9f6ea5e..a0612e8 100644 --- a/nix/profiles/hetzner-server.nix +++ b/nix/profiles/hetzner-server.nix @@ -1,4 +1,7 @@ -{ self }: +{ ... }: { - + config.khscodes = { + hetzner.enable = true; + sshd.enable = true; + }; } diff --git a/nix/systems/x86_64-linux/khs.codes/default.nix b/nix/systems/x86_64-linux/khs.codes/default.nix new file mode 100644 index 0000000..b10a95d --- /dev/null +++ b/nix/systems/x86_64-linux/khs.codes/default.nix @@ -0,0 +1,15 @@ +{ + inputs, + ... +}: +{ + imports = [ "${inputs.self}/nix/profiles/hetzner-server.nix" ]; + khscodes.terraform-hetzner = { + enable = true; + mapRdns = true; + server_type = "cax11"; + secretsSource = "bitwarden"; + }; + khscodes.fqdn = "khs.codes"; + system.stateVersion = "25.05"; +} diff --git a/nix/systems/x86_64-linux/photos.kaareskovgaard.net/default.nix b/nix/systems/x86_64-linux/photos.kaareskovgaard.net/default.nix deleted file mode 100644 index 5fd381c..0000000 --- a/nix/systems/x86_64-linux/photos.kaareskovgaard.net/default.nix +++ /dev/null @@ -1,5 +0,0 @@ -{ config, lib, ... }: -{ - khscodes.hetzner.enable = true; - system.stateVersion = "25.05"; -}