diff options
| -rw-r--r-- | README.md | 102 | ||||
| -rw-r--r-- | flake.lock | 69 | ||||
| -rw-r--r-- | flake.nix | 26 | ||||
| -rw-r--r-- | keys.nix | 39 | ||||
| -rw-r--r-- | keys/vg | 1 | ||||
| -rw-r--r-- | machines.nix | 225 | ||||
| -rw-r--r-- | tharos.nix | 133 |
7 files changed, 595 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..d60f3dc --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# IT-Infrastruktur für den Stadtteilbeirat Heimfeld + +Dieses Git-Repository enthält den Quellcode für Konfiguration und Verwaltung von Rechnern, welche Dienste unter der DNS-Domain `heimfeld.hamburg` bereitstellen. + +## Vorbereitung + +Um mit dem Code zu arbeiten, zunächst Nix installieren: + +- Debian/Ubuntu + + ```bash + apt install --yes curl git jq nix + ``` + +- Arch + + ```bash + pacman --sync --refresh --noconfirm curl git jq nix + ``` + +Anschließend Flakes aktivieren: + +```bash +echo extra-experimental-features = nix-command flakes >> /etc/nix/nix.conf +``` + +Beim ersten Aufruf von `nix run` oder `nix flake check` werden Abhängigkeiten geladen oder gebaut, was eine Weile dauern kann. +Spätere Aufrufe sind viel schneller, da nur Änderungen verarbeitet werden müssen. + +## Abläufe + +### Tests durchführen + +```bash +nix flake check +``` + +### Konfiguration lokal ausprobieren + +```bash +nix run .#vm-tharos +``` + +Dieser Befehl startet eine virtuelle Maschine mit der exakten Konfiguration des Produktionssystems. + +Anschließend kann man sich über SSH mit dem auf der Maschine eingerichteten Nutzer und dem entsprechenden SSH-Key verbinden: + +```bash +ssh localhost -p 10022 -o ForwardAgent=yes -i ~/.ssh/tharos +``` + +Der SSH-Port ist entsprechend der VM-Konfiguration versetzt um Konflikte mit möglicherweise bestehenden Diensten auf dem Host zu vermeiten. +`ForwardAgent=yes` ist erforderlich, um Befehle mit `sudo` auszuführen, wobei die Authentifizierung über direkt SSH statt einem Passwort erfolgt. + +### Eine neue Maschine aufsetzen + +**ACHTUNG: Alle Daten auf dem Zielsystem werden dabei gelöscht!** + +```bash +nix run .#infect-tharos +``` + +Die NixOS-Installation erfolgt mit [`nixos-anywhere`](https://nix-community.github.io/nixos-anywhere/). + +Angenommen, in `~/.ssh/config` ist Folgendes eingetragen: + +``` +Host tharos + ForwardAgent yes + HostName 81.169.239.254 + IdentityFile /home/user/.ssh/tharos +``` + +Dann kann man sich mit dem Produktionssystem über SSH verbinden: + +```bash +ssh tharos +``` + +### Geänderte Konfiguration anwenden + +```bash +nix run .#deploy-tharos -- switch +``` + +## Architektur + +Die Systeme laufen auf NixOS und werden mit Nix verwaltet. + +Dokumentation zu beiden ist zu finden unter <https://nix.dev>. +Die wesentlichen Aspekte sind dort die [Nix-Sprache](https://nix.dev/tutorials/nix-language) und das [Modulsystem](https://nix.dev/tutorials/module-system/). + +Der Code hier ist modular organisiert mit Flake Parts, was es einfacher macht isolierte Änderungen vorzunehmen und die Lesbarkeit verbessern hilft. +Flake Parts sind dokumentiert unter <https://flake.parts>. + +Zusätzlich wird mit `flakes.machines` eine eigene Abstraktion verwendet um Systeme und entsprechende Hilfswerkzeuge zu konfigurieren. +Dies ist momentan nur im Quellcode dokumentiert in [`machines.nix`](./machines.nix). + +Nutzernamen von Administratoren und ihre SSH-Keys sind unter [`keys`](./keys) organisiert. + +Zur Zeit ist genau eine Maschine in Betrieb, die in [`tharos.nix`](./tharos.nix) spezifiziert ist. +Der Server läuft bei [STRATO](https://www.strato.de) unter Kundennummer 73292174, Auftragsnummer 7709638. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3ee4e26 --- /dev/null +++ b/flake.lock @@ -0,0 +1,69 @@ +{ + "nodes": { + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1758287904, + "narHash": "sha256-IGmaEf3Do8o5Cwp1kXBN1wQmZwQN3NLfq5t4nHtVtcU=", + "owner": "nix-community", + "repo": "disko", + "rev": "67ff9807dd148e704baadbd4fd783b54282ca627", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1759362264, + "narHash": "sha256-wfG0S7pltlYyZTM+qqlhJ7GMw2fTF4mLKCIVhLii/4M=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "758cf7296bee11f1706a574c77d072b8a7baa881", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1759580034, + "narHash": "sha256-YWo57PL7mGZU7D4WeKFMiW4ex/O6ZolUS6UNBHTZfkI=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "3bcc93c5f7a4b30335d31f21e2f1281cba68c318", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "disko": "disko", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5c9b8d0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,26 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; + + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + outputs = + inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } ( + { self, lib, ... }: + { + imports = lib.fileset.toList ( + # Alle Nix-Dateien in diesem Projekt sind Flake-Parts-Module + lib.fileset.fileFilter (file: file.hasExt "nix" && file.name != "flake.nix") ./. + ); + systems = [ "x86_64-linux" ]; + } + ); +} diff --git a/keys.nix b/keys.nix new file mode 100644 index 0000000..0076788 --- /dev/null +++ b/keys.nix @@ -0,0 +1,39 @@ +{ lib, ... }: +let + inherit (lib) mkOption types; +in +{ + options.flake.keys = mkOption { + description = '' + Dateisystempfade zu öffentlichen SSH-Schlüsseln für alle Administratoren + + Kann benutzt werden um entsprechende Systemnutzer in `users.users` automatisch zu erstellen. + ''; + type = with types; attrsOf (listOf path); + default = + let + /* + Dateinamen mit öffentlichen SSH-Schlüsseln aus einem Verzeichnis mit Nutzernamen auslesen. + Format der Verzeichniseinträrge muss eines der Folgenden sein: + - Datei mit genau einem Eintrag für den jeweiligen Nutzer + - Verzeichnis mit Dateien die jeweils einen Eintrag enthalten + */ + get-key-files = + dir: + let + key-files = + username: type: + with builtins; + if type == "regular" then + [ "${dir}/${username}" ] + else + map (keyfile: "${dir}/${username}/${keyfile}") ( + attrValues (lib.filterAttrs (_: keytype: keytype == "regular") subdir) + ); + in + with builtins; + lib.mapAttrs key-files (builtins.readDir dir); + in + get-key-files ./keys; + }; +} @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINXpKx7ckbOaYr/3F51AKJJVJ7u/8A/f6sFbc9W8GUUr diff --git a/machines.nix b/machines.nix new file mode 100644 index 0000000..8f29ef0 --- /dev/null +++ b/machines.nix @@ -0,0 +1,225 @@ +{ + self, + config, + lib, + ... +}: +let + inherit (lib) mkOption types; +in +{ + options.flake.machines = mkOption { + description = '' + Hilfswerkzeuge um Rechner mit NixOS neu aufzusetzen und zu verwalten + + `networking.hostName` wird standardmäßig auf den Attributnamen der Maschine gesetzt. + ''; + type = + with types; + attrsOf ( + submodule ( + machine@{ name, options, ... }: + { + options = { + bootstrap-target = mkOption { + description = '' + SSH-Verbindung fürs Aufsetzen der Maschine + + Die jeweilige Maschine wird einen vorhandenen Authentifikationsmechanismus haben, z.b. SSH-Key oder Passwort, der hierfür beim Aufruf einmalig manuell genutzt wird. + ''; + type = types.str; + example = "root@example.org"; + }; + + deploy-target = mkOption { + description = '' + SSH-Verbindung für reguläres Verwaltung der Maschine + + Die Authentifizierung erfolg entsprechend der Maschinenkonfiguration, und Admins sollten die Verbindungsdaten bei sich entsprechend einrichten, z.B. über `~/.ssh/config`. + ''; + type = types.str; + example = "user@example.org"; + default = machine.config.bootstrap-target; + }; + + nixos = mkOption { + description = '' + NixOS Konfigurationsmodul für die Maschine + + Dokumentation: <https://search.nixos.org/options> + ''; + type = + with types; + deferredModuleWith { + staticModules = [ + { + _class = "nixos"; + networking.hostName = lib.mkDefault machine.name; + } + ]; + }; + }; + + vm = mkOption { + description = '' + Konfigurationsmodul zur virtuellen Maschine (VM) für lokale Tests + + Abgesehen von den hier festgelegten Einstellungen und Datenbankinhalten enspricht die virtuelle Maschine genau dem Produktionssystem. + Dies erlaubt recht zuverlässige manuelle Prüfung der Konfiguration im Vorfeld von Änderungen am Produktionssystem. + + Maschinenspezifische VM-Einstellungen werden mit den hier vorgegebenen Standardwerten zusammengefügt. + Bei Bedarf müssen die Standards explizit mit `lib.mkForce` überschrieben werden. + ''; + type = types.deferredModuleWith { + staticModules = [ options.vm.default ]; + }; + default = + { + config, + options, + modulesPath, + lib, + ... + }: + { + _class = "nixos"; + + imports = [ + "${modulesPath}/virtualisation/qemu-vm.nix" + ]; + + options = { + virtualisation.portOffset = mkOption { + description = '' + An den VM-Host exponierte Portnummern von Diensten innerhalb der VM um diesen Wert versetzen + + Dies dient dazu, Konflikte mit möglicherweise bestehenden Diensten auf dem Host zu vermeiden. + + Beispiel: SSH auf Port 22 in der VM wird beim Host exponiert auf Port ${ + toString (22 + options.virtualisation.portOffset.default) + }. + ''; + type = types.ints.positive; + default = 10000; + }; + }; + + config = { + virtualisation.forwardPorts = map (port: { + from = "host"; + guest.port = port; + host.port = port + config.virtualisation.portOffset; + proto = "tcp"; + }) config.networking.firewall.allowedTCPPorts; + + services.getty.autologinUser = lib.mkDefault "root"; + }; + }; + }; + + eval = mkOption { + readOnly = true; + internal = true; + default = lib.nixosSystem { + modules = [ machine.config.nixos ]; + }; + }; + + vm-eval = mkOption { + readOnly = true; + internal = true; + default = lib.nixosSystem { + modules = [ + machine.config.nixos + machine.config.vm + ]; + }; + }; + }; + } + ) + ); + }; + + config.flake.nixosConfigurations = lib.mapAttrs (name: machine: machine.eval) self.machines; + + config.perSystem = + { pkgs, ... }: + { + /* + Projektweite Tests, ausführen mit: + + nix flake check + */ + checks = lib.concatMapAttrs (name: machine: { + /* + Konsistenzprüfung der Partitionierung. + + Erfolgreicher Test bedeutet nicht unbedingt, dass die Konfiguration auch in der Produktionsumgebung funktioniert. + Beispielsweise muss die Konfiguration dazu passen, ob das Produktionssystem mit BIOS oder UEFI bootet. + */ + "${name}-installTest" = machine.eval.config.system.build.installTest; + }) self.machines; + + # Werkzeuge zur Verwaltung von Maschinen + packages = lib.concatMapAttrs (name: machine: { + "infect-${name}" = pkgs.writeShellApplication { + name = "infect"; + runtimeInputs = with pkgs; [ nixos-anywhere ]; + /* + ACHTUNG: Vor dem Aufsetzen erst Informationen zur Hardware abfragen und in die Konfiguration einbetten! + + nix run .#machines.infect-<machine> -- --no-reboot --generate-hardware-config nixos-hardware-config <datei> + */ + text = '' + nixos-anywhere --store-paths \ + ${machine.eval.config.system.build.diskoScript} \ + ${machine.eval.config.system.build.toplevel} \ + ${machine.bootstrap-target} "$@" + ''; + }; + + "deploy-${name}" = pkgs.writeShellApplication { + name = "deploy"; + text = + let + system = machine.eval.config.system.build.toplevel; + in + '' + ${ + "" # XXX: Keine Signaturen. Das richtig einzurichten ist für den Moment zu umständlich. + }nix copy --to ssh-ng://${machine.deploy-target} --no-check-sigs ${system} + + # shellcheck disable=SC2087 + ${ + "" + /* + ACHTUNG: Admins sollten sicherstellen, dass für diesen Host in `~/.ssh/config` folgendes enthalten ist: + + ForwardAgent: yes + + Außerdem muss der dort konfigurierte Nutzer dem Nutzer auf der jeweiligen Maschine enstprechen, und der SSH-Key zum SSH-Agent hinzugefügt sein. + Es gibt keine Passwörter und auch keine Möglichkeit eins einzugeben. + */ + }ssh ${machine.deploy-target} << EOF + sudo nix-env -p /nix/var/nix/profiles/system --set ${system} + sudo ${system}/bin/switch-to-configuration "$@" + EOF + ''; + }; + + "vm-${name}" = pkgs.writeShellApplication { + name = "vm"; + text = '' + ${ + "" # Festplattenabbild im flüchtigen Speicher erstellen, das ist deutlich schneller + }cd "$(mktemp -d)" + ${ + "" # Immer am Ende aufräumen + }trap 'rm -f nixos.qcow2' EXIT + ${lib.getExe machine.vm-eval.config.system.build.vm} "$@" + ''; + }; + }) self.machines; + }; +} diff --git a/tharos.nix b/tharos.nix new file mode 100644 index 0000000..6d96754 --- /dev/null +++ b/tharos.nix @@ -0,0 +1,133 @@ +{ + self, + inputs, + lib, + ... +}: +{ + flake.machines.tharos = { + bootstrap-target = "root@${self.machines.tharos.deploy-target}"; + # Administratoren verbinden sich mit ihrem selbst festgelegten Nutzernamen + deploy-target = "81.169.239.254"; + nixos = + { + config, + pkgs, + modulesPath, + ... + }: + + { + imports = [ + inputs.disko.nixosModules.default + "${modulesPath}/profiles/qemu-guest.nix" + ]; + + nixpkgs.hostPlatform = "x86_64-linux"; + system.stateVersion = "25.05"; + + services.cloud-init = { + enable = true; + network.enable = true; + }; + # `cloud-init` übernimmt Netzwerkeinstellungen + networking.useDHCP = false; + + # Kein Login für Nutzer die nicht explizit deklariert sind + users.mutableUsers = false; + users.users = lib.mapAttrs (username: keyFiles: { + isNormalUser = true; + openssh.authorizedKeys.keyFiles = keyFiles; + # ANMERKUNG: Der Einfachheit halber sind bis auf Weiteres alle Nutzer mit SSH-Zugang auch Administratoren + extraGroups = [ "wheel" ]; + }) self.keys; + + /* + `sudo` über SSH ohne Passworteingabe + ANMERKUNG: Nutzer sollten in ihrem ` ~/.ssh/config` für die Maschine einstellen: + + ForwardAgent: yes + */ + security.pam.sshAgentAuth.enable = true; + security.pam.services.sudo.sshAgentAuth = true; + + # Nur Administratoren können den angemeldeten Benutzer wechseln + security.pam.services.su.requireWheel = true; + + networking.firewall.allowPing = true; + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "prohibit-password"; + }; + }; + + nix = { + settings.trusted-users = [ + "root" + "@wheel" + ]; + settings.experimental-features = [ + "nix-command" + "flakes" + ]; + }; + + disko.devices.disk.main = { + device = "/dev/vda"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + # Die KVM läuft auf SeaBIOS, daher muss es hier eine MBR-Partition sein + boot = { + size = "1M"; + type = "EF02"; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + + /* + ANMERKUNG: Erhalten durch: + + nix run .#machines.infect-tharos -- --no-reboot --generate-hardware-config nixos-hardware-config <datei> + */ + boot.initrd.availableKernelModules = [ + "ata_piix" + "uhci_hcd" + "virtio_pci" + "virtio_blk" + ]; + boot.kernelModules = [ "kvm-amd" ]; + }; + + vm = + { + config, + lib, + pkgs, + ... + }: + { + virtualisation = { + memorySize = 4096; + diskSize = 4096; + cores = 2; + graphics = false; + }; + + services.cloud-init.enable = lib.mkForce false; + networking.useDHCP = lib.mkForce true; + }; + }; +} |
