From a28c2b5563d6b99233f8393542881085e6028466 Mon Sep 17 00:00:00 2001 From: Katharina Heidenreich Date: Wed, 8 Apr 2026 20:53:49 +0200 Subject: [PATCH] feat: initial commit --- .sops.yaml | 6 +++ config/openssh.nix | 7 +++ config/secrets.nix | 15 ++++++ config/services.nix | 6 +++ configuration.nix | 18 +++++++ hardware-configuration.nix | 25 ++++++++++ intermediate/secrets.nix | 79 ++++++++++++++++++++++++++++++ programs/default.nix | 6 +++ programs/git.nix | 16 ++++++ secrets/openssh/nudelerde/pub_keys | 18 +++++++ services/default.nix | 7 +++ services/forgejo.nix | 22 +++++++++ services/openssh.nix | 17 +++++++ system/boot.nix | 5 ++ system/default.nix | 9 ++++ system/network.nix | 5 ++ system/sops.nix | 14 ++++++ system/users.nix | 10 ++++ validation/secrets.nix | 29 +++++++++++ 19 files changed, 314 insertions(+) create mode 100644 .sops.yaml create mode 100644 config/openssh.nix create mode 100644 config/secrets.nix create mode 100644 config/services.nix create mode 100644 configuration.nix create mode 100644 hardware-configuration.nix create mode 100644 intermediate/secrets.nix create mode 100644 programs/default.nix create mode 100644 programs/git.nix create mode 100644 secrets/openssh/nudelerde/pub_keys create mode 100644 services/default.nix create mode 100644 services/forgejo.nix create mode 100644 services/openssh.nix create mode 100644 system/boot.nix create mode 100644 system/default.nix create mode 100644 system/network.nix create mode 100644 system/sops.nix create mode 100644 system/users.nix create mode 100644 validation/secrets.nix diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..cb2ee6a --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,6 @@ +creation_rules: + - path_regex: ^secrets/.*(?:$|\.(ya?ml|json|env|txt|key|pub))$ + key_groups: + - age: + - age1g5q3hwnpgsas682jkq0zmee3zqggucfe0v5ec0a6pv7wzexadehqne66cj + - age1z7ee2jgwvnrx6dg96rqzhc7gncuancrhm8zaehkelwqkeh4dze7qlsajes diff --git a/config/openssh.nix b/config/openssh.nix new file mode 100644 index 0000000..36288f7 --- /dev/null +++ b/config/openssh.nix @@ -0,0 +1,7 @@ +let + secrets = import ../intermediate/secrets.nix; + users = builtins.attrNames secrets.source.openssh.users; +in +rec { + ssh_users = users; +} diff --git a/config/secrets.nix b/config/secrets.nix new file mode 100644 index 0000000..6c78a85 --- /dev/null +++ b/config/secrets.nix @@ -0,0 +1,15 @@ +{ + openssh = { + users = { + nudelerde = { + pub_keys = { + file = ../secrets/openssh/nudelerde/pub_keys; + path = "/home/nudelerde/.ssh/authorized_keys"; + owner = "nudelerde"; + group = "users"; + mode = "0600"; + }; + }; + }; + }; +} diff --git a/config/services.nix b/config/services.nix new file mode 100644 index 0000000..2d42a3b --- /dev/null +++ b/config/services.nix @@ -0,0 +1,6 @@ +rec { + git = { + port = 3000; + domain = "git.nudelerde.de"; + }; +} diff --git a/configuration.nix b/configuration.nix new file mode 100644 index 0000000..a33b44f --- /dev/null +++ b/configuration.nix @@ -0,0 +1,18 @@ +{ pkgs, ... }: +let + sopsNixVersion = "8f093d0d2f08f37317778bd94db5951d6cce6c46"; +in +{ + imports = [ + "${builtins.fetchTarball "https://github.com/Mic92/sops-nix/archive/${sopsNixVersion}.tar.gz"}/modules/sops" + ./hardware-configuration.nix + ./system + ./services + ./programs + ]; + + environment.systemPackages = with pkgs; [ wget tree ]; + + system.stateVersion = "25.11"; +} + diff --git a/hardware-configuration.nix b/hardware-configuration.nix new file mode 100644 index 0000000..e495ba5 --- /dev/null +++ b/hardware-configuration.nix @@ -0,0 +1,25 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "usb_storage" "sd_mod" "rtsx_usb_sdmmc" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/be213f84-29a2-400e-b239-c339eddb6107"; + fsType = "ext4"; + }; + + swapDevices = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/intermediate/secrets.nix b/intermediate/secrets.nix new file mode 100644 index 0000000..f8f05f4 --- /dev/null +++ b/intermediate/secrets.nix @@ -0,0 +1,79 @@ +let + lib = import ; + secretsConfig = (import ../validation/secrets.nix).getSecretsConfig (import ../config/secrets.nix); + + getRuntimePath = path: + "/run/secrets/${builtins.concatStringsSep "_" path}"; + + defaultMetadata = { + path = null; + owner = null; + group = null; + mode = null; + }; + + normalizeLeaf = path: node: + if builtins.isString node || builtins.isPath node then + { + file = node; + metadata = defaultMetadata; + } + else if builtins.isAttrs node && node ? file then + { + file = node.file; + metadata = { + path = node.path or null; + owner = node.owner or null; + group = node.group or null; + mode = node.mode or null; + }; + } + else + throw "Invalid secret leaf at ${builtins.concatStringsSep "." path}: must be string, path, or attrset with 'file'"; + + flattenTree = path: node: + if builtins.isAttrs node && !(node ? file) then + lib.concatMap (name: + flattenTree (path ++ [ name ]) node.${name} + ) (builtins.attrNames node) + else + let + normalized = normalizeLeaf path node; + in + [ { + inherit path; + file = normalized.file; + metadata = normalized.metadata; + } ]; + + entries = flattenTree [] secretsConfig; + + isReady = entry: + builtins.pathExists entry.file; + + readyEntries = builtins.filter isReady entries; + missingEntries = builtins.filter (entry: !(isReady entry)) entries; + + mkSopsSecrets = sourceEntries: + builtins.listToAttrs (map (entry: + let + secretName = builtins.concatStringsSep "_" entry.path; + in + { + name = secretName; + value = { + sopsFile = entry.file; + format = "binary"; + path = if entry.metadata.path != null then entry.metadata.path else getRuntimePath entry.path; + owner = if entry.metadata.owner != null then entry.metadata.owner else "root"; + group = if entry.metadata.group != null then entry.metadata.group else "root"; + mode = if entry.metadata.mode != null then entry.metadata.mode else "0400"; + }; + } + ) sourceEntries); +in +{ + source = secretsConfig; + byName = mkSopsSecrets readyEntries; + missing = missingEntries; +} diff --git a/programs/default.nix b/programs/default.nix new file mode 100644 index 0000000..de19454 --- /dev/null +++ b/programs/default.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./git.nix + ]; +} diff --git a/programs/git.nix b/programs/git.nix new file mode 100644 index 0000000..64e0afd --- /dev/null +++ b/programs/git.nix @@ -0,0 +1,16 @@ +{ config, pkgs, lib, ... }: +{ + programs.git = { + enable = true; + config = { + user = { + name = "Katharina Heidenreich"; + email = "katharina.heidenreich02@gmail.com"; + }; + init.defaultBranch = "main"; + pull.rebase = false; + core.editor = "nano"; + safe.directory = ["/etc/nixos"]; + }; + }; +} diff --git a/secrets/openssh/nudelerde/pub_keys b/secrets/openssh/nudelerde/pub_keys new file mode 100644 index 0000000..d2bd4c4 --- /dev/null +++ b/secrets/openssh/nudelerde/pub_keys @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:D+pLLrpjFljWBUJ4qZ8qo62Ff+nRPw8IVohCMqY7m6BcBlK90AH2AN0I34ci6rPh3F2XenmL51kMReAHlV8BuuXCDxDB37lIvzVagvfla9sBMx1i8wINFV+DnpL0w78=,iv:0+11qTje7wLSDlCOsTyjKQ0d6Drt1zYkDxL8T2bxvg4=,tag:QD/LxHmPjnZo1XS6/0zLmA==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g5q3hwnpgsas682jkq0zmee3zqggucfe0v5ec0a6pv7wzexadehqne66cj", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3WER1bnJiMDNpK2hTOXhV\nV2g2TEFWTzBtMDFLOGtMb2doV3Q2eENlaHpVCktOa25QU1pNMStGVGtoZ04wZXB6\nVnBncTNURktYTFZUczBoRlhacFRYcUkKLS0tIG5FR3gvWmg4ZWtuUDRYZDBRU3lL\nU3NLeEVwQi9xdWF3MFYwTkZvcURMWmcKqtxEB+rHGQ7/RKd2CVTuJl/eDrGc9xrG\nou/4lCYVQL62EE/iOEsyTrzQhu3kKIhRdqICRz0R2b5fr0RLVRymWw==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1z7ee2jgwvnrx6dg96rqzhc7gncuancrhm8zaehkelwqkeh4dze7qlsajes", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGMk1DUFpjdzF0RFhXR0NU\nV293cUY5RlhFYzZhWjJNaFZvbXExRUVtRWkwCjVBMWlnVzdyTTVBZDE0U0g2SXhV\nK2pCK0dJVWZmN0QwUkpqUE44QjdOTGcKLS0tIHR5bUxlS0R3VUVTZzN4bSs0NVp3\nQkVxMjdQNnBOaUtKUTAzWElvb3V3RjAKQYSjLzdGqHoCjyQfgHdR+knIAFS/LY7a\nZLOMKJXjPcdEDALnRQJS0ys7dkDebCABizWeJX1fCERkzVANv1UhyQ==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-04-08T18:01:44Z", + "mac": "ENC[AES256_GCM,data:NlhzAXFHEVcXukSNRLAIJxWQ8GtYf0w/U3zkiDpH64fQs1yHjeRvF4BvQp83938jR5jkjVVdHwwXjPacztv+6zMzaMchUGglbc5dEx7OeLFmbM3gL8Q69pDrFjKo9g379pHH76JM3RWJLg+EaN2TDEU72fD9VtR9ak9V2/X4xfM=,iv:lwClzYes6Zu0aK8qVLgZ9qHgX+k/j3sNPqMamOnd7dg=,tag:BwIE4TBJEQpyBHN+Ivg0Dw==,type:str]", + "version": "3.12.1" + } +} diff --git a/services/default.nix b/services/default.nix new file mode 100644 index 0000000..8fc6c4b --- /dev/null +++ b/services/default.nix @@ -0,0 +1,7 @@ +{ ... }: +{ + imports = [ + ./openssh.nix + ./forgejo.nix + ]; +} diff --git a/services/forgejo.nix b/services/forgejo.nix new file mode 100644 index 0000000..94c2a30 --- /dev/null +++ b/services/forgejo.nix @@ -0,0 +1,22 @@ +{...}: +let + serv = import ../config/services.nix; + git = serv.git; +in +{ + services.forgejo = { + enable = true; + database.type = "postgres"; + lfs.enable = true; + settings = { + server = { + DOMAIN = git.domain; + ROOT_URL = "https://${git.domain}/"; + HTTP_PORT = git.port; + }; + service.DISABLE_REGISTRATION = true; + }; + }; + + networking.firewall.allowedTCPPorts = [ git.port ]; +} \ No newline at end of file diff --git a/services/openssh.nix b/services/openssh.nix new file mode 100644 index 0000000..63145bc --- /dev/null +++ b/services/openssh.nix @@ -0,0 +1,17 @@ +{ ... }: +let + opensshConfig = import ../config/openssh.nix; + usersWithKeys = opensshConfig.ssh_users; +in +{ + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = true; + PermitRootLogin = "no"; + AllowUsers = usersWithKeys; + }; + }; + + networking.firewall.allowedTCPPorts = [ 22 ]; +} diff --git a/system/boot.nix b/system/boot.nix new file mode 100644 index 0000000..81edcc7 --- /dev/null +++ b/system/boot.nix @@ -0,0 +1,5 @@ +{ ... }: +{ + boot.loader.grub.enable = true; + boot.loader.grub.device = "/dev/sda"; +} diff --git a/system/default.nix b/system/default.nix new file mode 100644 index 0000000..c61ecf9 --- /dev/null +++ b/system/default.nix @@ -0,0 +1,9 @@ +{ ... }: +{ + imports = [ + ./sops.nix + ./users.nix + ./network.nix + ./boot.nix + ]; +} diff --git a/system/network.nix b/system/network.nix new file mode 100644 index 0000000..0f1224d --- /dev/null +++ b/system/network.nix @@ -0,0 +1,5 @@ +{ ... }: +{ + networking.hostName = "tuserver"; + networking.networkmanager.enable = true; +} diff --git a/system/sops.nix b/system/sops.nix new file mode 100644 index 0000000..c37711e --- /dev/null +++ b/system/sops.nix @@ -0,0 +1,14 @@ +{ lib, ... }: + +let + secretData = import ../intermediate/secrets.nix; +in +{ + sops = { + age.keyFile = "/var/lib/sops-nix/key.txt"; + secrets = secretData.byName; + }; + + warnings = lib.optional (secretData.missing != []) + "Some SOPS source files are missing or not yet encrypted; no runtime secrets will be provisioned for: ${builtins.concatStringsSep ", " (map (item: builtins.concatStringsSep "_" item.path) secretData.missing)}"; +} diff --git a/system/users.nix b/system/users.nix new file mode 100644 index 0000000..1eb81fd --- /dev/null +++ b/system/users.nix @@ -0,0 +1,10 @@ +{ ... }: +{ + users.mutableUsers = false; + + users.users.nudelerde = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + hashedPassword = "$y$j9T$nWURcrCMWKPzj1xAydtNU/$qbOuwWcLSWQBiDTw8WJ2sRZYtP7qnGShQDA2USRC/C0"; + }; +} diff --git a/validation/secrets.nix b/validation/secrets.nix new file mode 100644 index 0000000..d99a7de --- /dev/null +++ b/validation/secrets.nix @@ -0,0 +1,29 @@ +let + lib = import ; + helperNames = [ "getSecretsConfig" ]; + + validateTree = context: node: + if builtins.isAttrs node then + lib.mapAttrs (name: value: + if lib.elem name helperNames then + value + else + validateTree "${context}.${name}" value + ) node + else + throw "${context} must be an attrset at non-leaf level."; + + getSecretsConfig = secretsConfig: + let + _ = + if builtins.isAttrs secretsConfig then + null + else + throw "config/secrets.nix must evaluate to an attrset."; + __ = validateTree "config/secrets.nix" secretsConfig; + in + secretsConfig; +in +rec { + inherit getSecretsConfig validateTree; +}