commit 430194beda856ca03f2a650cbdfaf31a76d3268c Author: stherm Date: Fri Mar 6 08:31:13 2026 +0100 Initial commit diff --git a/.forgejo/workflows/build-hosts.yml b/.forgejo/workflows/build-hosts.yml new file mode 100644 index 0000000..7987dc3 --- /dev/null +++ b/.forgejo/workflows/build-hosts.yml @@ -0,0 +1,24 @@ +name: Build hosts + +on: + pull_request: + branches: + - main + +jobs: + build-hosts: + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Build cryodev-main + run: nix build .#nixosConfigurations.cryodev-main.config.system.build.toplevel --impure + + - name: Build cryodev-pi + run: nix build .#nixosConfigurations.cryodev-pi.config.system.build.toplevel --impure diff --git a/.forgejo/workflows/deploy-main.yml b/.forgejo/workflows/deploy-main.yml new file mode 100644 index 0000000..4140f01 --- /dev/null +++ b/.forgejo/workflows/deploy-main.yml @@ -0,0 +1,34 @@ +name: Deploy cryodev-main + +on: + push: + branches: + - main + +jobs: + deploy-cryodev-main: + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Set up SSH + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + mkdir -p ~/.ssh + echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + + # Add host key (replace with actual host key or use ssh-keyscan in unsafe environments) + ssh-keyscan -H cryodev.xyz >> ~/.ssh/known_hosts + + - name: Deploy with deploy-rs + run: | + # Deploy using deploy-rs + nix run github:serokell/deploy-rs -- -s .#cryodev-main diff --git a/.forgejo/workflows/flake-check.yml b/.forgejo/workflows/flake-check.yml new file mode 100644 index 0000000..aed0b59 --- /dev/null +++ b/.forgejo/workflows/flake-check.yml @@ -0,0 +1,18 @@ +name: Flake check + +on: [pull_request] + +jobs: + flake-check: + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Run flake check + run: nix flake check --impure diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..104f256 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,13 @@ +keys: + - &admin_key age1e8p35795htf7twrejyugpzw0qja2v33awcw76y4gp6acnxnkzq0s935t4t # Admin key (Steffen) +creation_rules: + - path_regex: hosts/cryodev-main/secrets.yaml$ + key_groups: + - age: + - *admin_key + # - *server_key # Add server key here once obtained + - path_regex: hosts/cryodev-pi/secrets.yaml$ + key_groups: + - age: + - *admin_key + # - *pi_key # Add pi key here once obtained diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..91c1fbf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,114 @@ +# Agent Guidelines for NixOS Configuration + +## Project Overview +This repository contains a NixOS configuration managed with Nix Flakes. It defines system configurations for one or more hosts (currently `cryodev-main` and `cryodev-pi`), custom packages, modules, overlays, and templates. + +## Environment & Build Commands + +### Prerequisites +- **Nix** with Flakes enabled. +- **Git** + +### Core Commands +- **Build Host Configuration**: + ```bash + nix build .#nixosConfigurations..config.system.build.toplevel + # Examples: + nix build .#nixosConfigurations.cryodev-main.config.system.build.toplevel + nix build .#nixosConfigurations.cryodev-pi.config.system.build.toplevel + ``` +- **Format Code**: + ```bash + nix fmt + ``` + This uses the formatter defined in `flake.nix` (typically `nixfmt` via `pre-commit-hooks`). +- **Run Checks (Lint/Test)**: + ```bash + nix flake check + ``` + This validates the flake outputs and runs configured checks (including formatting checks and deploy-rs checks). +- **Update Flake Inputs**: + ```bash + nix flake update + ``` + +### Development Shell +You can enter a development shell with necessary tools (if configured in `devShells`): +```bash +nix develop +``` + +## Code Style & Conventions + +### General Nix Style +- **Formatting**: Strictly adhere to `nixfmt` style. Run `nix fmt` before committing. +- **Indentation**: Use 2 spaces for indentation. +- **Lines**: Limit lines to 80-100 characters where possible, but follow the formatter's lead. +- **Comments**: Use `#` for single-line comments. Avoid block comments `/* ... */` unless necessary for large blocks of text. + +### Module Structure +- **Function Header**: Always use the standard module arguments pattern: + ```nix + { config, lib, pkgs, inputs, outputs, constants, ... }: + ``` + - Include `inputs`, `outputs`, and `constants` if you need access to flake inputs, the flake's own outputs, or the central constants. +- **Option Definitions**: + - Define options in `options = { ... };`. + - Implement configuration in `config = { ... };`. + - Use `mkEnableOption` for boolean flags. + - Use `mkOption` with types (e.g., `types.str`, `types.bool`) and descriptions. +- **Imports**: + - Use relative paths for local modules: `imports = [ ./module.nix ];`. + - Use `inputs` or `outputs` for external/shared modules: `imports = [ outputs.nixosModules.common ];`. + +### Naming Conventions +- **Files**: `kebab-case.nix` (e.g., `hardware-configuration.nix`). +- **Options**: `camelCase` (e.g., `services.myService.enable`). +- **Variables**: `camelCase` in `let ... in` blocks. + +### Flake Specifics +- **Inputs**: Defined in `flake.nix`. + - `nixpkgs`: Main package set (typically following a stable release). + - `nixpkgs-unstable`: Unstable channel for latest packages. +- **Outputs**: + - `nixosConfigurations`: Host definitions. + - `nixosModules`: Reusable modules exported by this flake. + - `packages`: Custom packages exported by this flake. + - `overlays`: Overlays to modify `nixpkgs`. + - `templates`: Project templates for creating new hosts. + +### Error Handling +- Use `assert` for critical configuration invariants. +- Use `warnings` or `trace` for deprecation or non-critical issues during evaluation. +- Example: + ```nix + config = lib.mkIf cfg.enable { + assertions = [ + { assertion = cfg.port > 1024; message = "Port must be non-privileged"; } + ]; + }; + ``` + +## Directory Structure +- `flake.nix`: Entry point, defines inputs and outputs. +- `hosts/`: Specific host configurations. + - `/default.nix`: Host entry point. +- `modules/`: Reusable NixOS modules. + - `nixos/`: Modules specific to NixOS (e.g. `nixvim`, `comin`, `forgejo`). +- `pkgs/`: Custom package definitions. +- `overlays/`: Nixpkgs overlays. +- `templates/`: Templates for bootstrapping new hosts (`raspberry-pi`, `generic-server`). +- `scripts/`: Helper scripts (e.g., `install.sh` for bootstrapping). +- `constants.nix`: Central configuration for domains, IPs, and ports. +- `INSTRUCTIONS.md`: Setup and deployment instructions (DNS, SOPS, bootstrap). +- `README.md`: General project documentation and architecture overview. + +## Deployment Workflows +- **cryodev-main**: Push-based deployment via Forgejo Actions using `deploy-rs`. +- **cryodev-pi**: Pull-based deployment using `comin` (polling the repository). + +## Working with Agents +- **Context**: When asking for changes, specify if it's for a specific host (`hosts/cryodev-main`) or a shared module (`modules/`). +- **Verification**: Always run `nix flake check` after changes to ensure validity. +- **Refactoring**: When moving code, update `imports` carefully. +- **Constants**: Use `constants.nix` for values like domains, IPs, and ports instead of hardcoding them in modules. diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md new file mode 100644 index 0000000..4bf38be --- /dev/null +++ b/INSTRUCTIONS.md @@ -0,0 +1,391 @@ +# Server Setup Instructions / Server-Einrichtungsanleitung + +--- + +# πŸ‡¬πŸ‡§ English Instructions + +## 1. Prerequisites + +Ensure you have the following tools installed on your local machine: +- `nix` (with flakes enabled) +- `sops` +- `age` +- `ssh` + +## 2. DNS Configuration + +Configure the following DNS records for your domain `cryodev.xyz`: + +| Hostname | Type | Value | Purpose | +|----------|------|-------|---------| +| `@` | A | `` | Main entry point | +| `@` | AAAA | `` | Main entry point (IPv6) | +| `git` | CNAME | `@` | Forgejo | +| `headscale` | CNAME | `@` | Headscale | +| `headplane` | CNAME | `@` | Headplane | +| `netdata` | CNAME | `@` | Netdata Monitoring | +| `mail` | A | `` | Mailserver | +| `mail` | AAAA | `` | Mailserver (IPv6) | +| `@` | MX | `10 mail.cryodev.xyz.` | Mail delivery | +| `@` | TXT | `"v=spf1 mx ~all"` | SPF Record | +| `_dmarc` | TXT | `"v=DMARC1; p=none"` | DMARC Record | + +## 3. Secret Management (SOPS) + +This repository uses `sops-nix` to manage secrets encrypted with `age`, utilizing the SSH host keys of the servers. + +### 3.1 Get Server Public Keys +You need to convert the servers' SSH host public keys to age public keys. + +**For `cryodev-main`:** +```bash +nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 | ssh-to-age' +``` + +**For `cryodev-pi`:** +```bash +nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 | ssh-to-age' +``` + +### 3.2 Configure `.sops.yaml` +Edit the `.sops.yaml` file in the root of this repository. Add the age public keys to the `keys` section and ensure creation rules exist for both hosts. + +```yaml +keys: + - &admin_key age1e8p35795htf7twrejyugpzw0qja2v33awcw76y4gp6acnxnkzq0s935t4t # Admin Key (Steffen) + - &main_key age1... # cryodev-main Key + - &pi_key age1... # cryodev-pi Key +creation_rules: + - path_regex: hosts/cryodev-main/secrets.yaml$ + key_groups: + - age: + - *admin_key + - *main_key + - path_regex: hosts/cryodev-pi/secrets.yaml$ + key_groups: + - age: + - *admin_key + - *pi_key +``` + +### 3.3 Generating Secret Values + +**Mailserver Passwords (for `cryodev-main`):** +```bash +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' +``` + +**Headplane Secrets (for `cryodev-main`):** +```bash +nix-shell -p openssl --run "openssl rand -hex 16" +# Agent Pre-Authkey requires Headscale running: +sudo headscale users create headplane-agent +sudo headscale preauthkeys create --expiration 99y --reusable --user headplane-agent +``` + +**Tailscale Auth Keys (for both hosts):** +*Requires Headscale running on `cryodev-main`.* +```bash +# For cryodev-main: +sudo headscale preauthkeys create --expiration 99y --reusable --user default +# For cryodev-pi: +sudo headscale preauthkeys create --expiration 99y --reusable --user default +``` + +**Netdata Child UUID (for `cryodev-pi`):** +```bash +uuidgen +``` + +**Forgejo Runner Token:** +Get from Forgejo Admin Panel. + +### 3.4 Creating Secrets Files + +**`hosts/cryodev-main/secrets.yaml`:** +```bash +sops hosts/cryodev-main/secrets.yaml +``` +```yaml +mailserver: + accounts: + forgejo: "$2y$05$..." + admin: "$2y$05$..." + +forgejo-runner: + token: "..." + +headplane: + cookie_secret: "..." + agent_pre_authkey: "..." + +tailscale: + auth-key: "..." +``` + +**`hosts/cryodev-pi/secrets.yaml`:** +```bash +sops hosts/cryodev-pi/secrets.yaml +``` +```yaml +tailscale: + auth-key: "..." + +netdata: + stream: + child-uuid: "..." # Output from uuidgen +``` + +## 4. Initial Deployment (Bootstrap) + +Before the continuous deployment can take over, you must perform an initial deployment manually using the provided install script. + +### 4.1 Prepare Target Machine +1. Boot into the NixOS Installation ISO. +2. Set a root password (for SSH): `passwd`. +3. Ensure internet connectivity. + +### 4.2 Run Install Script +From your local machine (where this repo is), copy the script to the target or run it directly if you can fetch it. + +**Method A: Copy Script via SSH** +```bash +scp scripts/install.sh nixos@:install.sh +ssh nixos@ +sudo -i +chmod +x /home/nixos/install.sh +./home/nixos/install.sh -r -n +``` + +**Method B: Run on Target (if repo is public or reachable)** +```bash +# On the target machine (as root) +nix-shell -p git +git clone /tmp/nixos +cd /tmp/nixos +bash scripts/install.sh -n +``` + +*Note: The script handles disk partitioning (via disko/script), hardware config generation, and installation.* + +## 5. Continuous Deployment (CD) + +### 5.1 cryodev-pi (Pull-based via Comin) +The `cryodev-pi` host is configured to pull updates automatically via `comin`. + +1. **Create Repository:** Create a new repository named `cryodev-server` on your Forgejo instance (`https://git.cryodev.xyz`). +2. **Push Configuration:** Push this entire NixOS configuration to the `main` branch of that repository. +3. **Comin URL:** The configuration expects the repository at: `https://git.cryodev.xyz/steffen/cryodev-server.git`. + +### 5.2 cryodev-main (Push-based via Forgejo Actions) +The main server is deployed via a Forgejo Action. + +1. **Generate SSH Key:** + ```bash + ssh-keygen -t ed25519 -f deploy_key -C "forgejo-actions" + ``` +2. **Add Public Key:** Add the content of `deploy_key.pub` to `/root/.ssh/authorized_keys` on `cryodev-main`. +3. **Add Secret:** Add the content of `deploy_key` (private key) as a secret named `DEPLOY_SSH_KEY` in your Forgejo repository settings. + +## 6. Creating New Hosts (Templates) + +To quickly bootstrap a new host configuration, you can use the provided templates. + +1. **Copy Template:** + ```bash + # For a Raspberry Pi: + cp -r templates/raspberry-pi hosts/new-pi-name + + # For a generic x86 server: + cp -r templates/generic-server hosts/new-server-name + ``` +2. **Adjust Configuration:** + * **Hostname:** Edit `hosts/new-name/networking.nix`. + * **Flake:** Register the new host in `flake.nix` under `nixosConfigurations`. + * **Constants:** Add IP and ports to `constants.nix`. + * **Secrets:** Add keys to `.sops.yaml` and create `hosts/new-name/secrets.yaml`. + +--- + +# πŸ‡©πŸ‡ͺ Deutsche Anleitung + +## 1. Voraussetzungen + +Stellen Sie sicher, dass folgende Tools lokal installiert sind: +- `nix` (mit Flakes) +- `sops` +- `age` +- `ssh` +- `ssh-to-age` +- `uuidgen` + +## 2. DNS-Konfiguration + +Richten Sie folgende DNS-EintrΓ€ge fΓΌr `cryodev.xyz` ein: + +| Hostname | Typ | Wert | Zweck | +|----------|-----|------|-------| +| `@` | A | `` | Hauptserver | +| `@` | AAAA | `` | Hauptserver (IPv6) | +| `git` | CNAME | `@` | Forgejo | +| `headscale` | CNAME | `@` | Headscale | +| `headplane` | CNAME | `@` | Headplane | +| `netdata` | CNAME | `@` | Netdata Monitoring | +| `mail` | A | `` | Mailserver | +| `mail` | AAAA | `` | Mailserver (IPv6) | +| `@` | MX | `10 mail.cryodev.xyz.` | Mail-Empfang | +| `@` | TXT | `"v=spf1 mx ~all"` | SPF-Record | +| `_dmarc` | TXT | `"v=DMARC1; p=none"` | DMARC-Record | + +## 3. Verwaltung von Geheimnissen (SOPS) + +Dieses Repository nutzt `sops-nix` mit den SSH-Host-Keys der Server. + +### 3.1 Public Keys abrufen + +**FΓΌr `cryodev-main`:** +```bash +nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 | ssh-to-age' +``` + +**FΓΌr `cryodev-pi`:** +```bash +nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 | ssh-to-age' +``` + +### 3.2 `.sops.yaml` konfigurieren +Bearbeiten Sie `.sops.yaml` und fΓΌgen Sie die Keys sowie Regeln fΓΌr beide Hosts hinzu: + +```yaml +keys: + - &admin_key age1e8p35795htf7twrejyugpzw0qja2v33awcw76y4gp6acnxnkzq0s935t4t # Admin Key (Steffen) + - &main_key age1... # cryodev-main Key + - &pi_key age1... # cryodev-pi Key +creation_rules: + - path_regex: hosts/cryodev-main/secrets.yaml$ + key_groups: + - age: + - *admin_key + - *main_key + - path_regex: hosts/cryodev-pi/secrets.yaml$ + key_groups: + - age: + - *admin_key + - *pi_key +``` + +### 3.3 Werte generieren + +**Mailserver:** `mkpasswd -sm bcrypt` +**Headplane:** `openssl rand -hex 16` +**Netdata UUID:** `uuidgen` + +**Tailscale Auth Keys (auf `cryodev-main`):** +```bash +sudo headscale preauthkeys create --expiration 99y --reusable --user default +``` + +### 3.4 Secrets-Dateien erstellen + +**`hosts/cryodev-main/secrets.yaml`:** +```bash +sops hosts/cryodev-main/secrets.yaml +``` +```yaml +mailserver: + accounts: + forgejo: "$2y$05$..." + admin: "$2y$05$..." + +forgejo-runner: + token: "..." + +headplane: + cookie_secret: "..." + agent_pre_authkey: "..." + +tailscale: + auth-key: "..." +``` + +**`hosts/cryodev-pi/secrets.yaml`:** +```bash +sops hosts/cryodev-pi/secrets.yaml +``` +```yaml +tailscale: + auth-key: "..." + +netdata: + stream: + child-uuid: "..." # Output von uuidgen +``` + +## 4. Erstes Deployment (Bootstrap) + +Bevor das automatische Deployment funktionieren kann, mΓΌssen Sie das System einmal manuell mit dem Installationsskript installieren. + +### 4.1 Zielmaschine vorbereiten +1. Booten Sie das NixOS Installations-ISO. +2. Setzen Sie ein Root-Passwort: `passwd`. +3. Stellen Sie eine Internetverbindung her. + +### 4.2 Install-Script ausfΓΌhren +Kopieren Sie das Skript von Ihrem lokalen Rechner auf das Zielsystem. + +**Methode A: Per SCP** +```bash +scp scripts/install.sh nixos@:install.sh +ssh nixos@ +sudo -i +chmod +x /home/nixos/install.sh +./home/nixos/install.sh -r -n +``` + +**Methode B: Direkt auf dem Ziel (bei ΓΆffentlichem/erreichbarem Repo)** +```bash +# Auf der Zielmaschine (als root) +nix-shell -p git +git clone /tmp/nixos +cd /tmp/nixos +bash scripts/install.sh -n +``` + +*Hinweis: Das Skript kΓΌmmert sich um Partitionierung, Hardware-Config und Installation.* + +## 5. Continuous Deployment (CD) + +### 5.1 cryodev-pi (Pull-basiert via Comin) +Der Host `cryodev-pi` zieht Updates automatisch via `comin`. + +1. **Repository erstellen:** Erstellen Sie ein Repository namens `cryodev-server` auf `https://git.cryodev.xyz`. +2. **Konfiguration pushen:** Pushen Sie diese Konfiguration in den `main`-Branch. +3. **Comin URL:** `https://git.cryodev.xyz/steffen/cryodev-server.git`. + +### 5.2 cryodev-main (Push-basiert via Forgejo Actions) +Der Hauptserver wird ΓΌber eine Forgejo Action deployt. + +1. **SSH Key generieren:** + ```bash + ssh-keygen -t ed25519 -f deploy_key -C "forgejo-actions" + ``` +2. **Public Key hinzufΓΌgen:** Inhalt von `deploy_key.pub` in `/root/.ssh/authorized_keys` auf `cryodev-main` eintragen. +3. **Secret hinzufΓΌgen:** Inhalt von `deploy_key` (Private Key) als Secret `DEPLOY_SSH_KEY` im Forgejo-Repository hinterlegen. + +## 6. Neue Hosts erstellen (Templates) + +Um schnell eine neue Host-Konfiguration zu erstellen, kΓΆnnen Sie die bereitgestellten Templates nutzen. + +1. **Template kopieren:** + ```bash + # FΓΌr einen Raspberry Pi: + cp -r templates/raspberry-pi hosts/neuer-pi-name + + # FΓΌr einen generischen x86 Server: + cp -r templates/generic-server hosts/neuer-server-name + ``` +2. **Konfiguration anpassen:** + * **Hostname:** Bearbeiten Sie `hosts/neuer-name/networking.nix`. + * **Flake:** Registrieren Sie den neuen Host in `flake.nix` unter `nixosConfigurations`. + * **Constants:** FΓΌgen Sie IP und Ports in `constants.nix` hinzu. + * **Secrets:** FΓΌgen Sie Keys zu `.sops.yaml` hinzu und erstellen Sie `hosts/neuer-name/secrets.yaml`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..51b9eab --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# cryodev-server NixOS Configuration + +This repository contains the declarative NixOS configuration for the **cryodev** infrastructure, managed using **Nix Flakes**. It defines a robust, secure, and self-hosted environment spanning a main server and client devices. + +--- + +# πŸ‡¬πŸ‡§ English Description + +## Overview + +The infrastructure is designed around a central server (`cryodev-main`) and satellite clients (e.g., `cryodev-pi`). It leverages modern DevOps practices including Infrastructure as Code (IaC), GitOps, and Mesh VPNs. + +## Key Features & Architecture + +### πŸ–₯️ Hosts +* **`cryodev-main` (x86_64 Server)**: The core infrastructure hub. +* **`cryodev-pi` (Raspberry Pi 4)**: A remote client/worker node. + +### πŸš€ Continuous Deployment (CD) +We utilize different deployment strategies optimized for each host type: +* **Push-based (Server):** The main server is deployed via **Forgejo Actions** using **[deploy-rs](https://github.com/serokell/deploy-rs)**. This ensures immediate updates and robust rollbacks in case of failure. +* **Pull-based (Client):** The Raspberry Pi uses **[Comin](https://github.com/nlewo/comin)** to periodically poll the Git repository and apply updates automatically. This is ideal for devices behind NAT or with unstable connections. + +### 🌐 Networking (Tailscale & Headscale) +* **Self-hosted VPN:** We run **Headscale**, an open-source implementation of the Tailscale control server. +* **Headplane:** A web frontend for managing Headscale users and routes. +* **Mesh Network:** All hosts are connected via a secure, private WireGuard mesh network. +* **MagicDNS:** Automatic DNS resolution for devices within the tailnet. + +### πŸ“Š Monitoring (Netdata) +* **Parent/Child Streaming:** The main server acts as a centralized Netdata parent node. +* **Distributed Monitoring:** `cryodev-pi` (and other clients) stream their metrics securely over the Tailscale VPN to the parent node. +* **Alerting:** Integrated with the mailserver to send health alerts. + +### πŸ“§ Mail Services +* **NixOS Mailserver:** A fully functional mail stack (Postfix/Dovecot). +* **Integration:** Used by internal services (Forgejo, Netdata) to send notifications. +* **Security:** SPF, DKIM, and DMARC configured for `cryodev.xyz`. + +### πŸ› οΈ Development & Productivity +* **Forgejo:** Self-hosted Git service (fork of Gitea) with built-in CI/CD Actions. +* **Forgejo Runner:** Self-hosted runners executing the CI/CD pipelines. +* **Neovim:** A fully pre-configured Neovim environment (aliased as `v`) available system-wide via a custom NixOS module. +* **Secret Management:** **[sops-nix](https://github.com/Mic92/sops-nix)** encrypts secrets using Age and SSH host keys, ensuring no sensitive data is committed in plain text. +* **Templates:** Ready-to-use NixOS configurations for quickly bootstrapping new clients. + * `#raspberry-pi`: Template for Raspberry Pi 4 clients. + * `#generic-server`: Template for generic x86_64 servers. +* **Bootstrap Script:** An `install.sh` script automates disk partitioning (via disko) and system installation for new hosts. + +## 🚧 Roadmap & Missing Features + +### BioSafe Gateway (Dual Ethernet) +The Raspberry Pi hosts utilize a custom board with two Ethernet ports: +* **WAN:** Standard Internet connection. +* **LAN (`eth1`):** A dedicated local connection managed specifically by the **BioSafe Gateway App**. +* *Status:* The network configuration logic and the integration of the controlling app are currently missing. + +### Closed Source Integration +The **BioSafe Gateway App** is closed source. +* It needs to be added as a **private Flake input**. +* Authentication mechanism (e.g., access tokens via Secrets) to fetch this private input during the build process is not yet implemented. + +### SD Card Image Pipeline +Currently, the Pi requires manual setup. +* *Goal:* A CI/CD pipeline that builds a fully configured, flashable SD card image. +* *Usage:* Download image -> Flash to SD -> Insert in Pi -> Boot & Auto-connect. + +## Directory Structure +* `flake.nix`: The entry point defining inputs (dependencies) and outputs (system configs). +* `hosts/`: Configuration specific to each machine. +* `modules/`: Reusable NixOS modules (custom or imported). +* `pkgs/`: Custom packages. +* `constants.nix`: Central source of truth for IPs, ports, and domains. +* `INSTRUCTIONS.md`: Detailed setup guide (DNS, SOPS, Initial Deployment). +* `AGENTS.md`: Guidelines for AI agents working on this repository. + +--- + +# πŸ‡©πŸ‡ͺ Deutsche Beschreibung + +## Überblick + +Die Infrastruktur ist um einen zentralen Server (`cryodev-main`) und Satelliten-Clients (z.B. `cryodev-pi`) herum aufgebaut. Sie nutzt moderne DevOps-Praktiken wie Infrastructure as Code (IaC), GitOps und Mesh-VPNs. + +## Hauptfunktionen & Architektur + +### πŸ–₯️ Hosts +* **`cryodev-main` (x86_64 Server)**: Der zentrale Infrastruktur-Knoten. +* **`cryodev-pi` (Raspberry Pi 4)**: Ein entfernter Client/Worker-Node. + +### πŸš€ Continuous Deployment (CD) +Wir verwenden unterschiedliche Deployment-Strategien, optimiert fΓΌr den jeweiligen Host-Typ: +* **Push-basiert (Server):** Der Hauptserver wird ΓΌber **Forgejo Actions** mittels **[deploy-rs](https://github.com/serokell/deploy-rs)** aktualisiert. Dies garantiert sofortige Updates und robuste Rollbacks bei Fehlern. +* **Pull-basiert (Client):** Der Raspberry Pi nutzt **[Comin](https://github.com/nlewo/comin)**, um das Git-Repository periodisch abzufragen und Updates automatisch anzuwenden. Ideal fΓΌr GerΓ€te hinter NAT oder mit instabilen Verbindungen. + +### 🌐 Netzwerk (Tailscale & Headscale) +* **Self-hosted VPN:** Wir betreiben **Headscale**, eine Open-Source-Implementierung des Tailscale-Kontrollservers. +* **Headplane:** Ein Web-Frontend zur Verwaltung von Headscale-Benutzern und Routen. +* **Mesh-Netzwerk:** Alle Hosts sind ΓΌber ein sicheres, privates WireGuard-Mesh-Netzwerk verbunden. +* **MagicDNS:** Automatische NamensauflΓΆsung fΓΌr GerΓ€te innerhalb des Tailnets. + +### πŸ“Š Monitoring (Netdata) +* **Parent/Child Streaming:** Der Hauptserver agiert als zentraler Netdata Parent-Node. +* **Verteiltes Monitoring:** `cryodev-pi` (und andere Clients) streamen ihre Metriken sicher ΓΌber das Tailscale-VPN an den Parent-Node. +* **Alarmierung:** Integriert mit dem Mailserver zum Versenden von Gesundheitswarnungen. + +### πŸ“§ Mail-Dienste +* **NixOS Mailserver:** Ein voll funktionsfΓ€higer Mail-Stack (Postfix/Dovecot). +* **Integration:** Wird von internen Diensten (Forgejo, Netdata) fΓΌr Benachrichtigungen genutzt. +* **Sicherheit:** SPF, DKIM und DMARC sind fΓΌr `cryodev.xyz` konfiguriert. + +### πŸ› οΈ Entwicklung & ProduktivitΓ€t +* **Forgejo:** Self-hosted Git-Dienst (Gitea-Fork) mit integrierten CI/CD Actions. +* **Forgejo Runner:** Eigene Runner, die die CI/CD-Pipelines ausfΓΌhren. +* **Neovim:** Eine vollstΓ€ndig vorkonfigurierte Neovim-Umgebung (Alias `v`), die systemweit ΓΌber ein eigenes NixOS-Modul bereitgestellt wird. +* **Secret Management:** **[sops-nix](https://github.com/Mic92/sops-nix)** verschlΓΌsselt Geheimnisse mittels Age und SSH-Host-Keys, sodass keine sensiblen Daten im Klartext gespeichert werden. +* **Templates:** Vorgefertigte NixOS-Konfigurationen zum schnellen Aufsetzen neuer Clients. + * `#raspberry-pi`: Template fΓΌr Raspberry Pi 4 Clients. + * `#generic-server`: Template fΓΌr generische x86_64 Server. +* **Bootstrap Skript:** Ein `install.sh` Skript automatisiert die Disk-Partitionierung (via disko) und Systeminstallation fΓΌr neue Hosts. + +## 🚧 Roadmap & Fehlende Funktionen + +### BioSafe Gateway (Dual-Ethernet) +Die Raspberry Pi Hosts nutzen ein spezielles Board mit zwei Ethernet-AnschlΓΌssen: +* **WAN:** Normale Internetverbindung. +* **LAN (`eth1`):** Eine dedizierte lokale Verbindung, die spezifisch durch die **BioSafe Gateway App** verwaltet wird. +* *Status:* Die Logik fΓΌr die Netzwerkkonfiguration und die Integration der steuernden App fehlen aktuell. + +### Integration von Closed Source +Die **BioSafe Gateway App** ist Closed Source. +* Sie muss als **privater Flake Input** hinzugefΓΌgt werden. +* Mechanismen zur Authentifizierung (z.B. Access Tokens via Secrets) fΓΌr das Abrufen dieses privaten Inputs wΓ€hrend des Build-Prozesses sind noch nicht implementiert. + +### Pipeline fΓΌr SD-Karten-Images +Momentan erfordert der Pi eine manuelle Einrichtung. +* *Ziel:* Eine CI/CD-Pipeline, die ein fertig konfiguriertes, flashbares SD-Karten-Image baut. +* *Nutzung:* Image herunterladen -> auf SD flashen -> in Pi stecken -> booten & automatisch verbinden. + +## Verzeichnisstruktur +* `flake.nix`: Der Einstiegspunkt, der Inputs (AbhΓ€ngigkeiten) und Outputs (Systemkonfigurationen) definiert. +* `hosts/`: Maschinenspezifische Konfigurationen. +* `modules/`: Wiederverwendbare NixOS-Module (eigene oder importierte). +* `pkgs/`: Benutzerdefinierte Pakete. +* `constants.nix`: Zentrale Quelle fΓΌr IPs, Ports und Domains. +* `INSTRUCTIONS.md`: Detaillierte Einrichtungsanleitung (DNS, SOPS, Initial Deployment). +* `AGENTS.md`: Richtlinien fΓΌr KI-Agenten, die an diesem Repository arbeiten. diff --git a/constants.nix b/constants.nix new file mode 100644 index 0000000..d5a1f9d --- /dev/null +++ b/constants.nix @@ -0,0 +1,38 @@ +{ + # Domain + domain = "cryodev.xyz"; + + # Hosts + hosts = { + cryodev-main = { + ip = "100.64.0.1"; # Tailscale IP example + }; + cryodev-pi = { + ip = "100.64.0.2"; # Tailscale IP example + }; + }; + + # Services + services = { + forgejo = { + fqdn = "git.cryodev.xyz"; + port = 3000; + }; + headscale = { + fqdn = "headscale.cryodev.xyz"; + port = 8080; + }; + headplane = { + fqdn = "headplane.cryodev.xyz"; + port = 3001; + }; + netdata = { + fqdn = "netdata.cryodev.xyz"; + port = 19999; + }; + mail = { + fqdn = "mail.cryodev.xyz"; + port = 587; + }; + }; +} diff --git a/digest.txt b/digest.txt new file mode 100644 index 0000000..a52f6e5 --- /dev/null +++ b/digest.txt @@ -0,0 +1,4930 @@ +Directory structure: +└── cryodev-server/ + β”œβ”€β”€ README.md + β”œβ”€β”€ AGENTS.md + β”œβ”€β”€ constants.nix + β”œβ”€β”€ flake.nix + β”œβ”€β”€ INSTRUCTIONS.md + β”œβ”€β”€ .sops.yaml + β”œβ”€β”€ hosts/ + β”‚ β”œβ”€β”€ cryodev-main/ + β”‚ β”‚ β”œβ”€β”€ boot.nix + β”‚ β”‚ β”œβ”€β”€ default.nix + β”‚ β”‚ β”œβ”€β”€ disks.sh + β”‚ β”‚ β”œβ”€β”€ hardware.nix + β”‚ β”‚ β”œβ”€β”€ networking.nix + β”‚ β”‚ β”œβ”€β”€ packages.nix + β”‚ β”‚ β”œβ”€β”€ users.nix + β”‚ β”‚ └── services/ + β”‚ β”‚ β”œβ”€β”€ default.nix + β”‚ β”‚ β”œβ”€β”€ forgejo.nix + β”‚ β”‚ β”œβ”€β”€ headplane.nix + β”‚ β”‚ β”œβ”€β”€ headscale.nix + β”‚ β”‚ β”œβ”€β”€ mailserver.nix + β”‚ β”‚ β”œβ”€β”€ netdata.nix + β”‚ β”‚ β”œβ”€β”€ nginx.nix + β”‚ β”‚ β”œβ”€β”€ openssh.nix + β”‚ β”‚ β”œβ”€β”€ sops.nix + β”‚ β”‚ └── tailscale.nix + β”‚ └── cryodev-pi/ + β”‚ β”œβ”€β”€ boot.nix + β”‚ β”œβ”€β”€ default.nix + β”‚ β”œβ”€β”€ disks.sh + β”‚ β”œβ”€β”€ hardware.nix + β”‚ β”œβ”€β”€ networking.nix + β”‚ β”œβ”€β”€ packages.nix + β”‚ β”œβ”€β”€ users.nix + β”‚ └── services/ + β”‚ β”œβ”€β”€ comin.nix + β”‚ β”œβ”€β”€ default.nix + β”‚ β”œβ”€β”€ netdata.nix + β”‚ β”œβ”€β”€ nginx.nix + β”‚ β”œβ”€β”€ openssh.nix + β”‚ └── tailscale.nix + β”œβ”€β”€ modules/ + β”‚ └── nixos/ + β”‚ β”œβ”€β”€ default.nix + β”‚ β”œβ”€β”€ comin/ + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ common/ + β”‚ β”‚ β”œβ”€β”€ default.nix + β”‚ β”‚ β”œβ”€β”€ environment.nix + β”‚ β”‚ β”œβ”€β”€ htop.nix + β”‚ β”‚ β”œβ”€β”€ nationalization.nix + β”‚ β”‚ β”œβ”€β”€ networking.nix + β”‚ β”‚ β”œβ”€β”€ nix.nix + β”‚ β”‚ β”œβ”€β”€ overlays.nix + β”‚ β”‚ β”œβ”€β”€ sudo.nix + β”‚ β”‚ β”œβ”€β”€ well-known.nix + β”‚ β”‚ β”œβ”€β”€ zsh.nix + β”‚ β”‚ └── shared/ + β”‚ β”‚ β”œβ”€β”€ default.nix + β”‚ β”‚ └── nix.nix + β”‚ β”œβ”€β”€ forgejo/ + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ forgejo-runner/ + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ headplane/ + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ headscale/ + β”‚ β”‚ β”œβ”€β”€ acl.hujson + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ mailserver/ + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ nginx/ + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ nixvim/ + β”‚ β”‚ β”œβ”€β”€ default.nix + β”‚ β”‚ β”œβ”€β”€ keymaps.nix + β”‚ β”‚ β”œβ”€β”€ spellfiles.nix + β”‚ β”‚ └── plugins/ + β”‚ β”‚ β”œβ”€β”€ cmp.nix + β”‚ β”‚ β”œβ”€β”€ default.nix + β”‚ β”‚ β”œβ”€β”€ lsp.nix + β”‚ β”‚ β”œβ”€β”€ lualine.nix + β”‚ β”‚ β”œβ”€β”€ telescope.nix + β”‚ β”‚ β”œβ”€β”€ treesitter.nix + β”‚ β”‚ └── trouble.nix + β”‚ β”œβ”€β”€ normalUsers/ + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ openssh/ + β”‚ β”‚ └── default.nix + β”‚ β”œβ”€β”€ sops/ + β”‚ β”‚ └── default.nix + β”‚ └── tailscale/ + β”‚ └── default.nix + β”œβ”€β”€ overlays/ + β”‚ └── default.nix + β”œβ”€β”€ pkgs/ + β”‚ └── default.nix + β”œβ”€β”€ scripts/ + β”‚ └── install.sh + β”œβ”€β”€ templates/ + β”‚ β”œβ”€β”€ generic-server/ + β”‚ β”‚ β”œβ”€β”€ boot.nix + β”‚ β”‚ β”œβ”€β”€ default.nix + β”‚ β”‚ β”œβ”€β”€ disks.sh + β”‚ β”‚ β”œβ”€β”€ flake.nix + β”‚ β”‚ β”œβ”€β”€ hardware.nix + β”‚ β”‚ β”œβ”€β”€ networking.nix + β”‚ β”‚ β”œβ”€β”€ packages.nix + β”‚ β”‚ β”œβ”€β”€ users.nix + β”‚ β”‚ └── services/ + β”‚ β”‚ β”œβ”€β”€ comin.nix + β”‚ β”‚ β”œβ”€β”€ default.nix + β”‚ β”‚ β”œβ”€β”€ netdata.nix + β”‚ β”‚ β”œβ”€β”€ nginx.nix + β”‚ β”‚ β”œβ”€β”€ openssh.nix + β”‚ β”‚ └── tailscale.nix + β”‚ └── raspberry-pi/ + β”‚ β”œβ”€β”€ boot.nix + β”‚ β”œβ”€β”€ default.nix + β”‚ β”œβ”€β”€ disks.sh + β”‚ β”œβ”€β”€ flake.nix + β”‚ β”œβ”€β”€ hardware.nix + β”‚ β”œβ”€β”€ networking.nix + β”‚ β”œβ”€β”€ packages.nix + β”‚ β”œβ”€β”€ users.nix + β”‚ └── services/ + β”‚ β”œβ”€β”€ comin.nix + β”‚ β”œβ”€β”€ default.nix + β”‚ β”œβ”€β”€ netdata.nix + β”‚ β”œβ”€β”€ nginx.nix + β”‚ β”œβ”€β”€ openssh.nix + β”‚ └── tailscale.nix + β”œβ”€β”€ users/ + β”‚ β”œβ”€β”€ cryotherm/ + β”‚ β”‚ └── default.nix + β”‚ └── steffen/ + β”‚ β”œβ”€β”€ default.nix + β”‚ └── pubkeys/ + β”‚ └── X670E.pub + └── .forgejo/ + └── workflows/ + β”œβ”€β”€ build-hosts.yml + β”œβ”€β”€ deploy-main.yml + └── flake-check.yml + +================================================ +FILE: README.md +================================================ +# cryodev-server NixOS Configuration + +This repository contains the declarative NixOS configuration for the **cryodev** infrastructure, managed using **Nix Flakes**. It defines a robust, secure, and self-hosted environment spanning a main server and client devices. + +--- + +# πŸ‡¬πŸ‡§ English Description + +## Overview + +The infrastructure is designed around a central server (`cryodev-main`) and satellite clients (e.g., `cryodev-pi`). It leverages modern DevOps practices including Infrastructure as Code (IaC), GitOps, and Mesh VPNs. + +## Key Features & Architecture + +### πŸ–₯️ Hosts +* **`cryodev-main` (x86_64 Server)**: The core infrastructure hub. +* **`cryodev-pi` (Raspberry Pi 4)**: A remote client/worker node. + +### πŸš€ Continuous Deployment (CD) +We utilize different deployment strategies optimized for each host type: +* **Push-based (Server):** The main server is deployed via **Forgejo Actions** using **[deploy-rs](https://github.com/serokell/deploy-rs)**. This ensures immediate updates and robust rollbacks in case of failure. +* **Pull-based (Client):** The Raspberry Pi uses **[Comin](https://github.com/nlewo/comin)** to periodically poll the Git repository and apply updates automatically. This is ideal for devices behind NAT or with unstable connections. + +### 🌐 Networking (Tailscale & Headscale) +* **Self-hosted VPN:** We run **Headscale**, an open-source implementation of the Tailscale control server. +* **Headplane:** A web frontend for managing Headscale users and routes. +* **Mesh Network:** All hosts are connected via a secure, private WireGuard mesh network. +* **MagicDNS:** Automatic DNS resolution for devices within the tailnet. + +### πŸ“Š Monitoring (Netdata) +* **Parent/Child Streaming:** The main server acts as a centralized Netdata parent node. +* **Distributed Monitoring:** `cryodev-pi` (and other clients) stream their metrics securely over the Tailscale VPN to the parent node. +* **Alerting:** Integrated with the mailserver to send health alerts. + +### πŸ“§ Mail Services +* **NixOS Mailserver:** A fully functional mail stack (Postfix/Dovecot). +* **Integration:** Used by internal services (Forgejo, Netdata) to send notifications. +* **Security:** SPF, DKIM, and DMARC configured for `cryodev.xyz`. + +### πŸ› οΈ Development & Productivity +* **Forgejo:** Self-hosted Git service (fork of Gitea) with built-in CI/CD Actions. +* **Forgejo Runner:** Self-hosted runners executing the CI/CD pipelines. +* **Neovim:** A fully pre-configured Neovim environment (aliased as `v`) available system-wide via a custom NixOS module. +* **Secret Management:** **[sops-nix](https://github.com/Mic92/sops-nix)** encrypts secrets using Age and SSH host keys, ensuring no sensitive data is committed in plain text. +* **Templates:** Ready-to-use NixOS configurations for quickly bootstrapping new clients. + * `#raspberry-pi`: Template for Raspberry Pi 4 clients. + * `#generic-server`: Template for generic x86_64 servers. +* **Bootstrap Script:** An `install.sh` script automates disk partitioning (via disko) and system installation for new hosts. + +## 🚧 Roadmap & Missing Features + +### BioSafe Gateway (Dual Ethernet) +The Raspberry Pi hosts utilize a custom board with two Ethernet ports: +* **WAN:** Standard Internet connection. +* **LAN (`eth1`):** A dedicated local connection managed specifically by the **BioSafe Gateway App**. +* *Status:* The network configuration logic and the integration of the controlling app are currently missing. + +### Closed Source Integration +The **BioSafe Gateway App** is closed source. +* It needs to be added as a **private Flake input**. +* Authentication mechanism (e.g., access tokens via Secrets) to fetch this private input during the build process is not yet implemented. + +### SD Card Image Pipeline +Currently, the Pi requires manual setup. +* *Goal:* A CI/CD pipeline that builds a fully configured, flashable SD card image. +* *Usage:* Download image -> Flash to SD -> Insert in Pi -> Boot & Auto-connect. + +## Directory Structure +* `flake.nix`: The entry point defining inputs (dependencies) and outputs (system configs). +* `hosts/`: Configuration specific to each machine. +* `modules/`: Reusable NixOS modules (custom or imported). +* `pkgs/`: Custom packages. +* `constants.nix`: Central source of truth for IPs, ports, and domains. +* `INSTRUCTIONS.md`: Detailed setup guide (DNS, SOPS, Initial Deployment). +* `AGENTS.md`: Guidelines for AI agents working on this repository. + +--- + +# πŸ‡©πŸ‡ͺ Deutsche Beschreibung + +## Überblick + +Die Infrastruktur ist um einen zentralen Server (`cryodev-main`) und Satelliten-Clients (z.B. `cryodev-pi`) herum aufgebaut. Sie nutzt moderne DevOps-Praktiken wie Infrastructure as Code (IaC), GitOps und Mesh-VPNs. + +## Hauptfunktionen & Architektur + +### πŸ–₯️ Hosts +* **`cryodev-main` (x86_64 Server)**: Der zentrale Infrastruktur-Knoten. +* **`cryodev-pi` (Raspberry Pi 4)**: Ein entfernter Client/Worker-Node. + +### πŸš€ Continuous Deployment (CD) +Wir verwenden unterschiedliche Deployment-Strategien, optimiert fΓΌr den jeweiligen Host-Typ: +* **Push-basiert (Server):** Der Hauptserver wird ΓΌber **Forgejo Actions** mittels **[deploy-rs](https://github.com/serokell/deploy-rs)** aktualisiert. Dies garantiert sofortige Updates und robuste Rollbacks bei Fehlern. +* **Pull-basiert (Client):** Der Raspberry Pi nutzt **[Comin](https://github.com/nlewo/comin)**, um das Git-Repository periodisch abzufragen und Updates automatisch anzuwenden. Ideal fΓΌr GerΓ€te hinter NAT oder mit instabilen Verbindungen. + +### 🌐 Netzwerk (Tailscale & Headscale) +* **Self-hosted VPN:** Wir betreiben **Headscale**, eine Open-Source-Implementierung des Tailscale-Kontrollservers. +* **Headplane:** Ein Web-Frontend zur Verwaltung von Headscale-Benutzern und Routen. +* **Mesh-Netzwerk:** Alle Hosts sind ΓΌber ein sicheres, privates WireGuard-Mesh-Netzwerk verbunden. +* **MagicDNS:** Automatische NamensauflΓΆsung fΓΌr GerΓ€te innerhalb des Tailnets. + +### πŸ“Š Monitoring (Netdata) +* **Parent/Child Streaming:** Der Hauptserver agiert als zentraler Netdata Parent-Node. +* **Verteiltes Monitoring:** `cryodev-pi` (und andere Clients) streamen ihre Metriken sicher ΓΌber das Tailscale-VPN an den Parent-Node. +* **Alarmierung:** Integriert mit dem Mailserver zum Versenden von Gesundheitswarnungen. + +### πŸ“§ Mail-Dienste +* **NixOS Mailserver:** Ein voll funktionsfΓ€higer Mail-Stack (Postfix/Dovecot). +* **Integration:** Wird von internen Diensten (Forgejo, Netdata) fΓΌr Benachrichtigungen genutzt. +* **Sicherheit:** SPF, DKIM und DMARC sind fΓΌr `cryodev.xyz` konfiguriert. + +### πŸ› οΈ Entwicklung & ProduktivitΓ€t +* **Forgejo:** Self-hosted Git-Dienst (Gitea-Fork) mit integrierten CI/CD Actions. +* **Forgejo Runner:** Eigene Runner, die die CI/CD-Pipelines ausfΓΌhren. +* **Neovim:** Eine vollstΓ€ndig vorkonfigurierte Neovim-Umgebung (Alias `v`), die systemweit ΓΌber ein eigenes NixOS-Modul bereitgestellt wird. +* **Secret Management:** **[sops-nix](https://github.com/Mic92/sops-nix)** verschlΓΌsselt Geheimnisse mittels Age und SSH-Host-Keys, sodass keine sensiblen Daten im Klartext gespeichert werden. +* **Templates:** Vorgefertigte NixOS-Konfigurationen zum schnellen Aufsetzen neuer Clients. + * `#raspberry-pi`: Template fΓΌr Raspberry Pi 4 Clients. + * `#generic-server`: Template fΓΌr generische x86_64 Server. +* **Bootstrap Skript:** Ein `install.sh` Skript automatisiert die Disk-Partitionierung (via disko) und Systeminstallation fΓΌr neue Hosts. + +## 🚧 Roadmap & Fehlende Funktionen + +### BioSafe Gateway (Dual-Ethernet) +Die Raspberry Pi Hosts nutzen ein spezielles Board mit zwei Ethernet-AnschlΓΌssen: +* **WAN:** Normale Internetverbindung. +* **LAN (`eth1`):** Eine dedizierte lokale Verbindung, die spezifisch durch die **BioSafe Gateway App** verwaltet wird. +* *Status:* Die Logik fΓΌr die Netzwerkkonfiguration und die Integration der steuernden App fehlen aktuell. + +### Integration von Closed Source +Die **BioSafe Gateway App** ist Closed Source. +* Sie muss als **privater Flake Input** hinzugefΓΌgt werden. +* Mechanismen zur Authentifizierung (z.B. Access Tokens via Secrets) fΓΌr das Abrufen dieses privaten Inputs wΓ€hrend des Build-Prozesses sind noch nicht implementiert. + +### Pipeline fΓΌr SD-Karten-Images +Momentan erfordert der Pi eine manuelle Einrichtung. +* *Ziel:* Eine CI/CD-Pipeline, die ein fertig konfiguriertes, flashbares SD-Karten-Image baut. +* *Nutzung:* Image herunterladen -> auf SD flashen -> in Pi stecken -> booten & automatisch verbinden. + +## Verzeichnisstruktur +* `flake.nix`: Der Einstiegspunkt, der Inputs (AbhΓ€ngigkeiten) und Outputs (Systemkonfigurationen) definiert. +* `hosts/`: Maschinenspezifische Konfigurationen. +* `modules/`: Wiederverwendbare NixOS-Module (eigene oder importierte). +* `pkgs/`: Benutzerdefinierte Pakete. +* `constants.nix`: Zentrale Quelle fΓΌr IPs, Ports und Domains. +* `INSTRUCTIONS.md`: Detaillierte Einrichtungsanleitung (DNS, SOPS, Initial Deployment). +* `AGENTS.md`: Richtlinien fΓΌr KI-Agenten, die an diesem Repository arbeiten. + + + +================================================ +FILE: AGENTS.md +================================================ +# Agent Guidelines for NixOS Configuration + +## Project Overview +This repository contains a NixOS configuration managed with Nix Flakes. It defines system configurations for one or more hosts (currently `cryodev-main` and `cryodev-pi`), custom packages, modules, overlays, and templates. + +## Environment & Build Commands + +### Prerequisites +- **Nix** with Flakes enabled. +- **Git** + +### Core Commands +- **Build Host Configuration**: + ```bash + nix build .#nixosConfigurations..config.system.build.toplevel + # Examples: + nix build .#nixosConfigurations.cryodev-main.config.system.build.toplevel + nix build .#nixosConfigurations.cryodev-pi.config.system.build.toplevel + ``` +- **Format Code**: + ```bash + nix fmt + ``` + This uses the formatter defined in `flake.nix` (typically `nixfmt` via `pre-commit-hooks`). +- **Run Checks (Lint/Test)**: + ```bash + nix flake check + ``` + This validates the flake outputs and runs configured checks (including formatting checks and deploy-rs checks). +- **Update Flake Inputs**: + ```bash + nix flake update + ``` + +### Development Shell +You can enter a development shell with necessary tools (if configured in `devShells`): +```bash +nix develop +``` + +## Code Style & Conventions + +### General Nix Style +- **Formatting**: Strictly adhere to `nixfmt` style. Run `nix fmt` before committing. +- **Indentation**: Use 2 spaces for indentation. +- **Lines**: Limit lines to 80-100 characters where possible, but follow the formatter's lead. +- **Comments**: Use `#` for single-line comments. Avoid block comments `/* ... */` unless necessary for large blocks of text. + +### Module Structure +- **Function Header**: Always use the standard module arguments pattern: + ```nix + { config, lib, pkgs, inputs, outputs, constants, ... }: + ``` + - Include `inputs`, `outputs`, and `constants` if you need access to flake inputs, the flake's own outputs, or the central constants. +- **Option Definitions**: + - Define options in `options = { ... };`. + - Implement configuration in `config = { ... };`. + - Use `mkEnableOption` for boolean flags. + - Use `mkOption` with types (e.g., `types.str`, `types.bool`) and descriptions. +- **Imports**: + - Use relative paths for local modules: `imports = [ ./module.nix ];`. + - Use `inputs` or `outputs` for external/shared modules: `imports = [ outputs.nixosModules.common ];`. + +### Naming Conventions +- **Files**: `kebab-case.nix` (e.g., `hardware-configuration.nix`). +- **Options**: `camelCase` (e.g., `services.myService.enable`). +- **Variables**: `camelCase` in `let ... in` blocks. + +### Flake Specifics +- **Inputs**: Defined in `flake.nix`. + - `nixpkgs`: Main package set (typically following a stable release). + - `nixpkgs-unstable`: Unstable channel for latest packages. +- **Outputs**: + - `nixosConfigurations`: Host definitions. + - `nixosModules`: Reusable modules exported by this flake. + - `packages`: Custom packages exported by this flake. + - `overlays`: Overlays to modify `nixpkgs`. + - `templates`: Project templates for creating new hosts. + +### Error Handling +- Use `assert` for critical configuration invariants. +- Use `warnings` or `trace` for deprecation or non-critical issues during evaluation. +- Example: + ```nix + config = lib.mkIf cfg.enable { + assertions = [ + { assertion = cfg.port > 1024; message = "Port must be non-privileged"; } + ]; + }; + ``` + +## Directory Structure +- `flake.nix`: Entry point, defines inputs and outputs. +- `hosts/`: Specific host configurations. + - `/default.nix`: Host entry point. +- `modules/`: Reusable NixOS modules. + - `nixos/`: Modules specific to NixOS (e.g. `nixvim`, `comin`, `forgejo`). +- `pkgs/`: Custom package definitions. +- `overlays/`: Nixpkgs overlays. +- `templates/`: Templates for bootstrapping new hosts (`raspberry-pi`, `generic-server`). +- `scripts/`: Helper scripts (e.g., `install.sh` for bootstrapping). +- `constants.nix`: Central configuration for domains, IPs, and ports. +- `INSTRUCTIONS.md`: Setup and deployment instructions (DNS, SOPS, bootstrap). +- `README.md`: General project documentation and architecture overview. + +## Deployment Workflows +- **cryodev-main**: Push-based deployment via Forgejo Actions using `deploy-rs`. +- **cryodev-pi**: Pull-based deployment using `comin` (polling the repository). + +## Working with Agents +- **Context**: When asking for changes, specify if it's for a specific host (`hosts/cryodev-main`) or a shared module (`modules/`). +- **Verification**: Always run `nix flake check` after changes to ensure validity. +- **Refactoring**: When moving code, update `imports` carefully. +- **Constants**: Use `constants.nix` for values like domains, IPs, and ports instead of hardcoding them in modules. + + + +================================================ +FILE: constants.nix +================================================ +{ + # Domain + domain = "cryodev.xyz"; + + # Hosts + hosts = { + cryodev-main = { + ip = "100.64.0.1"; # Tailscale IP example + }; + cryodev-pi = { + ip = "100.64.0.2"; # Tailscale IP example + }; + }; + + # Services + services = { + forgejo = { + fqdn = "git.cryodev.xyz"; + port = 3000; + }; + headscale = { + fqdn = "headscale.cryodev.xyz"; + port = 8080; + }; + headplane = { + fqdn = "headplane.cryodev.xyz"; + port = 3001; + }; + netdata = { + fqdn = "netdata.cryodev.xyz"; + port = 19999; + }; + mail = { + fqdn = "mail.cryodev.xyz"; + port = 587; + }; + }; +} + + + +================================================ +FILE: flake.nix +================================================ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-old-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + sops-nix.url = "github:Mic92/sops-nix"; + sops-nix.inputs.nixpkgs.follows = "nixpkgs"; + + nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver"; + nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs"; + + headplane.url = "github:yrd/headplane-nix"; + + comin.url = "github:nlewo/comin"; + comin.inputs.nixpkgs.follows = "nixpkgs"; + + deploy-rs.url = "github:serokell/deploy-rs"; + deploy-rs.inputs.nixpkgs.follows = "nixpkgs"; + + nixvim.url = "github:nix-community/nixvim/nixos-25.11"; + nixvim.inputs.nixpkgs.follows = "nixpkgs"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + lib = nixpkgs.lib; + constants = import ./constants.nix; + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit + inputs + outputs + lib + constants + ; + }; + }; + in + { + packages = forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system}); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + nixosConfigurations = { + cryodev-main = mkNixosConfiguration "x86_64-linux" [ ./hosts/cryodev-main ]; + cryodev-pi = mkNixosConfiguration "aarch64-linux" [ ./hosts/cryodev-pi ]; + }; + + templates = { + raspberry-pi = { + path = ./templates/raspberry-pi; + description = "Raspberry Pi 4 Client"; + }; + generic-server = { + path = ./templates/generic-server; + description = "Generic x86_64 Customer Server"; + }; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + deploy = { + nodes = { + cryodev-main = { + hostname = constants.domain; + profiles.system = { + user = "root"; + path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.cryodev-main; + }; + }; + }; + }; + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + deployChecks = inputs.deploy-rs.lib.${system}.deployChecks self.deploy; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + # package = overlaidPkgs.package; + }; + } + // deployChecks + ); + }; +} + + + +================================================ +FILE: INSTRUCTIONS.md +================================================ +# Server Setup Instructions / Server-Einrichtungsanleitung + +--- + +# πŸ‡¬πŸ‡§ English Instructions + +## 1. Prerequisites + +Ensure you have the following tools installed on your local machine: +- `nix` (with flakes enabled) +- `sops` +- `age` +- `ssh` + +## 2. DNS Configuration + +Configure the following DNS records for your domain `cryodev.xyz`: + +| Hostname | Type | Value | Purpose | +|----------|------|-------|---------| +| `@` | A | `` | Main entry point | +| `@` | AAAA | `` | Main entry point (IPv6) | +| `git` | CNAME | `@` | Forgejo | +| `headscale` | CNAME | `@` | Headscale | +| `headplane` | CNAME | `@` | Headplane | +| `netdata` | CNAME | `@` | Netdata Monitoring | +| `mail` | A | `` | Mailserver | +| `mail` | AAAA | `` | Mailserver (IPv6) | +| `@` | MX | `10 mail.cryodev.xyz.` | Mail delivery | +| `@` | TXT | `"v=spf1 mx ~all"` | SPF Record | +| `_dmarc` | TXT | `"v=DMARC1; p=none"` | DMARC Record | + +## 3. Secret Management (SOPS) + +This repository uses `sops-nix` to manage secrets encrypted with `age`, utilizing the SSH host keys of the servers. + +### 3.1 Get Server Public Keys +You need to convert the servers' SSH host public keys to age public keys. + +**For `cryodev-main`:** +```bash +nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 | ssh-to-age' +``` + +**For `cryodev-pi`:** +```bash +nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 | ssh-to-age' +``` + +### 3.2 Configure `.sops.yaml` +Edit the `.sops.yaml` file in the root of this repository. Add the age public keys to the `keys` section and ensure creation rules exist for both hosts. + +```yaml +keys: + - &admin_key age1e8p35795htf7twrejyugpzw0qja2v33awcw76y4gp6acnxnkzq0s935t4t # Admin Key (Steffen) + - &main_key age1... # cryodev-main Key + - &pi_key age1... # cryodev-pi Key +creation_rules: + - path_regex: hosts/cryodev-main/secrets.yaml$ + key_groups: + - age: + - *admin_key + - *main_key + - path_regex: hosts/cryodev-pi/secrets.yaml$ + key_groups: + - age: + - *admin_key + - *pi_key +``` + +### 3.3 Generating Secret Values + +**Mailserver Passwords (for `cryodev-main`):** +```bash +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' +``` + +**Headplane Secrets (for `cryodev-main`):** +```bash +nix-shell -p openssl --run "openssl rand -hex 16" +# Agent Pre-Authkey requires Headscale running: +sudo headscale users create headplane-agent +sudo headscale preauthkeys create --expiration 99y --reusable --user headplane-agent +``` + +**Tailscale Auth Keys (for both hosts):** +*Requires Headscale running on `cryodev-main`.* +```bash +# For cryodev-main: +sudo headscale preauthkeys create --expiration 99y --reusable --user default +# For cryodev-pi: +sudo headscale preauthkeys create --expiration 99y --reusable --user default +``` + +**Netdata Child UUID (for `cryodev-pi`):** +```bash +uuidgen +``` + +**Forgejo Runner Token:** +Get from Forgejo Admin Panel. + +### 3.4 Creating Secrets Files + +**`hosts/cryodev-main/secrets.yaml`:** +```bash +sops hosts/cryodev-main/secrets.yaml +``` +```yaml +mailserver: + accounts: + forgejo: "$2y$05$..." + admin: "$2y$05$..." + +forgejo-runner: + token: "..." + +headplane: + cookie_secret: "..." + agent_pre_authkey: "..." + +tailscale: + auth-key: "..." +``` + +**`hosts/cryodev-pi/secrets.yaml`:** +```bash +sops hosts/cryodev-pi/secrets.yaml +``` +```yaml +tailscale: + auth-key: "..." + +netdata: + stream: + child-uuid: "..." # Output from uuidgen +``` + +## 4. Initial Deployment (Bootstrap) + +Before the continuous deployment can take over, you must perform an initial deployment manually using the provided install script. + +### 4.1 Prepare Target Machine +1. Boot into the NixOS Installation ISO. +2. Set a root password (for SSH): `passwd`. +3. Ensure internet connectivity. + +### 4.2 Run Install Script +From your local machine (where this repo is), copy the script to the target or run it directly if you can fetch it. + +**Method A: Copy Script via SSH** +```bash +scp scripts/install.sh nixos@:install.sh +ssh nixos@ +sudo -i +chmod +x /home/nixos/install.sh +./home/nixos/install.sh -r -n +``` + +**Method B: Run on Target (if repo is public or reachable)** +```bash +# On the target machine (as root) +nix-shell -p git +git clone /tmp/nixos +cd /tmp/nixos +bash scripts/install.sh -n +``` + +*Note: The script handles disk partitioning (via disko/script), hardware config generation, and installation.* + +## 5. Continuous Deployment (CD) + +### 5.1 cryodev-pi (Pull-based via Comin) +The `cryodev-pi` host is configured to pull updates automatically via `comin`. + +1. **Create Repository:** Create a new repository named `cryodev-server` on your Forgejo instance (`https://git.cryodev.xyz`). +2. **Push Configuration:** Push this entire NixOS configuration to the `main` branch of that repository. +3. **Comin URL:** The configuration expects the repository at: `https://git.cryodev.xyz/steffen/cryodev-server.git`. + +### 5.2 cryodev-main (Push-based via Forgejo Actions) +The main server is deployed via a Forgejo Action. + +1. **Generate SSH Key:** + ```bash + ssh-keygen -t ed25519 -f deploy_key -C "forgejo-actions" + ``` +2. **Add Public Key:** Add the content of `deploy_key.pub` to `/root/.ssh/authorized_keys` on `cryodev-main`. +3. **Add Secret:** Add the content of `deploy_key` (private key) as a secret named `DEPLOY_SSH_KEY` in your Forgejo repository settings. + +## 6. Creating New Hosts (Templates) + +To quickly bootstrap a new host configuration, you can use the provided templates. + +1. **Copy Template:** + ```bash + # For a Raspberry Pi: + cp -r templates/raspberry-pi hosts/new-pi-name + + # For a generic x86 server: + cp -r templates/generic-server hosts/new-server-name + ``` +2. **Adjust Configuration:** + * **Hostname:** Edit `hosts/new-name/networking.nix`. + * **Flake:** Register the new host in `flake.nix` under `nixosConfigurations`. + * **Constants:** Add IP and ports to `constants.nix`. + * **Secrets:** Add keys to `.sops.yaml` and create `hosts/new-name/secrets.yaml`. + +--- + +# πŸ‡©πŸ‡ͺ Deutsche Anleitung + +## 1. Voraussetzungen + +Stellen Sie sicher, dass folgende Tools lokal installiert sind: +- `nix` (mit Flakes) +- `sops` +- `age` +- `ssh` +- `ssh-to-age` +- `uuidgen` + +## 2. DNS-Konfiguration + +Richten Sie folgende DNS-EintrΓ€ge fΓΌr `cryodev.xyz` ein: + +| Hostname | Typ | Wert | Zweck | +|----------|-----|------|-------| +| `@` | A | `` | Hauptserver | +| `@` | AAAA | `` | Hauptserver (IPv6) | +| `git` | CNAME | `@` | Forgejo | +| `headscale` | CNAME | `@` | Headscale | +| `headplane` | CNAME | `@` | Headplane | +| `netdata` | CNAME | `@` | Netdata Monitoring | +| `mail` | A | `` | Mailserver | +| `mail` | AAAA | `` | Mailserver (IPv6) | +| `@` | MX | `10 mail.cryodev.xyz.` | Mail-Empfang | +| `@` | TXT | `"v=spf1 mx ~all"` | SPF-Record | +| `_dmarc` | TXT | `"v=DMARC1; p=none"` | DMARC-Record | + +## 3. Verwaltung von Geheimnissen (SOPS) + +Dieses Repository nutzt `sops-nix` mit den SSH-Host-Keys der Server. + +### 3.1 Public Keys abrufen + +**FΓΌr `cryodev-main`:** +```bash +nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 | ssh-to-age' +``` + +**FΓΌr `cryodev-pi`:** +```bash +nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 | ssh-to-age' +``` + +### 3.2 `.sops.yaml` konfigurieren +Bearbeiten Sie `.sops.yaml` und fΓΌgen Sie die Keys sowie Regeln fΓΌr beide Hosts hinzu: + +```yaml +keys: + - &admin_key age1e8p35795htf7twrejyugpzw0qja2v33awcw76y4gp6acnxnkzq0s935t4t # Admin Key (Steffen) + - &main_key age1... # cryodev-main Key + - &pi_key age1... # cryodev-pi Key +creation_rules: + - path_regex: hosts/cryodev-main/secrets.yaml$ + key_groups: + - age: + - *admin_key + - *main_key + - path_regex: hosts/cryodev-pi/secrets.yaml$ + key_groups: + - age: + - *admin_key + - *pi_key +``` + +### 3.3 Werte generieren + +**Mailserver:** `mkpasswd -sm bcrypt` +**Headplane:** `openssl rand -hex 16` +**Netdata UUID:** `uuidgen` + +**Tailscale Auth Keys (auf `cryodev-main`):** +```bash +sudo headscale preauthkeys create --expiration 99y --reusable --user default +``` + +### 3.4 Secrets-Dateien erstellen + +**`hosts/cryodev-main/secrets.yaml`:** +```bash +sops hosts/cryodev-main/secrets.yaml +``` +```yaml +mailserver: + accounts: + forgejo: "$2y$05$..." + admin: "$2y$05$..." + +forgejo-runner: + token: "..." + +headplane: + cookie_secret: "..." + agent_pre_authkey: "..." + +tailscale: + auth-key: "..." +``` + +**`hosts/cryodev-pi/secrets.yaml`:** +```bash +sops hosts/cryodev-pi/secrets.yaml +``` +```yaml +tailscale: + auth-key: "..." + +netdata: + stream: + child-uuid: "..." # Output von uuidgen +``` + +## 4. Erstes Deployment (Bootstrap) + +Bevor das automatische Deployment funktionieren kann, mΓΌssen Sie das System einmal manuell mit dem Installationsskript installieren. + +### 4.1 Zielmaschine vorbereiten +1. Booten Sie das NixOS Installations-ISO. +2. Setzen Sie ein Root-Passwort: `passwd`. +3. Stellen Sie eine Internetverbindung her. + +### 4.2 Install-Script ausfΓΌhren +Kopieren Sie das Skript von Ihrem lokalen Rechner auf das Zielsystem. + +**Methode A: Per SCP** +```bash +scp scripts/install.sh nixos@:install.sh +ssh nixos@ +sudo -i +chmod +x /home/nixos/install.sh +./home/nixos/install.sh -r -n +``` + +**Methode B: Direkt auf dem Ziel (bei ΓΆffentlichem/erreichbarem Repo)** +```bash +# Auf der Zielmaschine (als root) +nix-shell -p git +git clone /tmp/nixos +cd /tmp/nixos +bash scripts/install.sh -n +``` + +*Hinweis: Das Skript kΓΌmmert sich um Partitionierung, Hardware-Config und Installation.* + +## 5. Continuous Deployment (CD) + +### 5.1 cryodev-pi (Pull-basiert via Comin) +Der Host `cryodev-pi` zieht Updates automatisch via `comin`. + +1. **Repository erstellen:** Erstellen Sie ein Repository namens `cryodev-server` auf `https://git.cryodev.xyz`. +2. **Konfiguration pushen:** Pushen Sie diese Konfiguration in den `main`-Branch. +3. **Comin URL:** `https://git.cryodev.xyz/steffen/cryodev-server.git`. + +### 5.2 cryodev-main (Push-basiert via Forgejo Actions) +Der Hauptserver wird ΓΌber eine Forgejo Action deployt. + +1. **SSH Key generieren:** + ```bash + ssh-keygen -t ed25519 -f deploy_key -C "forgejo-actions" + ``` +2. **Public Key hinzufΓΌgen:** Inhalt von `deploy_key.pub` in `/root/.ssh/authorized_keys` auf `cryodev-main` eintragen. +3. **Secret hinzufΓΌgen:** Inhalt von `deploy_key` (Private Key) als Secret `DEPLOY_SSH_KEY` im Forgejo-Repository hinterlegen. + +## 6. Neue Hosts erstellen (Templates) + +Um schnell eine neue Host-Konfiguration zu erstellen, kΓΆnnen Sie die bereitgestellten Templates nutzen. + +1. **Template kopieren:** + ```bash + # FΓΌr einen Raspberry Pi: + cp -r templates/raspberry-pi hosts/neuer-pi-name + + # FΓΌr einen generischen x86 Server: + cp -r templates/generic-server hosts/neuer-server-name + ``` +2. **Konfiguration anpassen:** + * **Hostname:** Bearbeiten Sie `hosts/neuer-name/networking.nix`. + * **Flake:** Registrieren Sie den neuen Host in `flake.nix` unter `nixosConfigurations`. + * **Constants:** FΓΌgen Sie IP und Ports in `constants.nix` hinzu. + * **Secrets:** FΓΌgen Sie Keys zu `.sops.yaml` hinzu und erstellen Sie `hosts/neuer-name/secrets.yaml`. + + + +================================================ +FILE: .sops.yaml +================================================ +keys: + - &admin_key age1e8p35795htf7twrejyugpzw0qja2v33awcw76y4gp6acnxnkzq0s935t4t # Admin key (Steffen) +creation_rules: + - path_regex: hosts/cryodev-main/secrets.yaml$ + key_groups: + - age: + - *admin_key + # - *server_key # Add server key here once obtained + - path_regex: hosts/cryodev-pi/secrets.yaml$ + key_groups: + - age: + - *admin_key + # - *pi_key # Add pi key here once obtained + + + +================================================ +FILE: hosts/cryodev-main/boot.nix +================================================ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} + + + +================================================ +FILE: hosts/cryodev-main/default.nix +================================================ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + outputs.nixosModules.common + outputs.nixosModules.nixvim + ]; + + system.stateVersion = "25.11"; +} + + + +================================================ +FILE: hosts/cryodev-main/disks.sh +================================================ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL + + + +================================================ +FILE: hosts/cryodev-main/hardware.nix +================================================ +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "nvme" + "sd_mod" + "sdhci_pci" + "sr_mod" + "usb_storage" + "virtio_pci" + "virtio_scsi" + "xhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ { device = "/dev/disk/by-label/SWAP"; } ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} + + + +================================================ +FILE: hosts/cryodev-main/networking.nix +================================================ +{ + networking.hostName = "cryodev-main"; + networking.domain = "cryodev.xyz"; +} + + + +================================================ +FILE: hosts/cryodev-main/packages.nix +================================================ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} + + + +================================================ +FILE: hosts/cryodev-main/users.nix +================================================ +{ inputs, outputs, ... }: + +{ + imports = [ + outputs.nixosModules.normalUsers + ../../users/steffen + ]; +} + + + +================================================ +FILE: hosts/cryodev-main/services/default.nix +================================================ +{ + imports = [ + ./forgejo.nix + ./headplane.nix + ./headscale.nix + ./mailserver.nix + ./netdata.nix + ./nginx.nix + ./openssh.nix + ./sops.nix + ./tailscale.nix + ]; +} + + + +================================================ +FILE: hosts/cryodev-main/services/forgejo.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.forgejo + outputs.nixosModules.forgejo-runner + ]; + + services.forgejo = { + enable = true; + settings = { + server = { + DOMAIN = constants.services.forgejo.fqdn; + ROOT_URL = "https://${constants.services.forgejo.fqdn}/"; + HTTP_PORT = constants.services.forgejo.port; + }; + service = { + DISABLE_REGISTRATION = true; + }; + mailer = { + ENABLED = true; + FROM = "forgejo@${constants.domain}"; + SMTP_ADDR = constants.services.mail.fqdn; + SMTP_PORT = constants.services.mail.port; + USER = "forgejo@${constants.domain}"; + }; + }; + sops = true; # Enable sops integration for secrets + }; + + services.forgejo-runner = { + enable = true; + url = "https://${constants.services.forgejo.fqdn}"; + # Token needs to be set up via sops/secrets + sops = true; + }; + + services.nginx.virtualHosts."${constants.services.forgejo.fqdn}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString constants.services.forgejo.port}"; + }; + }; +} + + + +================================================ +FILE: hosts/cryodev-main/services/headplane.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.headplane + ]; + + services.headplane = { + enable = true; + port = constants.services.headplane.port; + headscale = { + url = "http://127.0.0.1:${toString constants.services.headscale.port}"; + public_url = "https://${constants.services.headscale.fqdn}"; + }; + # Secrets for headplane need to be configured via sops + sops.secrets = { + "headplane/cookie_secret" = { }; + "headplane/agent_pre_authkey" = { }; + }; + }; + + services.nginx.virtualHosts."${constants.services.headplane.fqdn}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString constants.services.headplane.port}"; + }; + }; +} + + + +================================================ +FILE: hosts/cryodev-main/services/headscale.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.headscale + ]; + + services.headscale = { + enable = true; + address = "127.0.0.1"; + port = constants.services.headscale.port; + settings = { + server_url = "https://${constants.services.headscale.fqdn}"; + dns_config.base_domain = constants.domain; + }; + }; + + services.nginx.virtualHosts."${constants.services.headscale.fqdn}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString constants.services.headscale.port}"; + proxyWebsockets = true; + }; + }; +} + + + +================================================ +FILE: hosts/cryodev-main/services/mailserver.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.mailserver + ]; + + mailserver = { + enable = true; + fqdn = constants.services.mail.fqdn; + domains = [ constants.domain ]; + accounts = { + forgejo = { }; + admin = { + aliases = [ "postmaster" ]; + }; + }; + certificateScheme = "acme-nginx"; + sops = true; + }; +} + + + +================================================ +FILE: hosts/cryodev-main/services/netdata.nix +================================================ +{ + config, + pkgs, + constants, + ... +}: + +{ + services.netdata = { + enable = true; + package = pkgs.netdata.override { + withCloudUi = true; + }; + config = { + global = { + "debug log" = "syslog"; + "access log" = "syslog"; + "error log" = "syslog"; + "bind to" = "127.0.0.1"; + }; + }; + }; + + services.nginx.virtualHosts."${constants.services.netdata.fqdn}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString constants.services.netdata.port}"; + proxyWebsockets = true; + # Basic Auth can be added here if desired, or restrict by IP + # extraConfig = "allow 100.64.0.0/10; deny all;"; # Example for Tailscale only + }; + }; +} + + + +================================================ +FILE: hosts/cryodev-main/services/nginx.nix +================================================ +{ + inputs, + outputs, + lib, + config, + pkgs, + ... +}: + +{ + imports = [ outputs.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; # Force SSL for all vhosts by default if configured to use this option + openFirewall = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + }; +} + + + +================================================ +FILE: hosts/cryodev-main/services/openssh.nix +================================================ +{ + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.openssh + ]; + + services.openssh.enable = true; +} + + + +================================================ +FILE: hosts/cryodev-main/services/sops.nix +================================================ +{ + config, + pkgs, + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.sops + ]; + + sops = { + defaultSopsFile = ../secrets.yaml; + # age.keyFile is not set, sops-nix defaults to using /etc/ssh/ssh_host_ed25519_key + secrets = { + "forgejo-runner/token" = { }; + "tailscale/auth-key" = { }; + }; + }; +} + + + +================================================ +FILE: hosts/cryodev-main/services/tailscale.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.tailscale + ]; + + services.tailscale = { + enable = true; + # Connect to our own headscale instance + loginServer = "https://${constants.services.headscale.fqdn}"; + # Allow SSH access over Tailscale + enableSSH = true; + # Use MagicDNS names + acceptDNS = true; + }; +} + + + +================================================ +FILE: hosts/cryodev-pi/boot.nix +================================================ +{ + boot = { + loader = { + grub.enable = false; + generic-extlinux-compatible.enable = true; + }; + }; +} + + + +================================================ +FILE: hosts/cryodev-pi/default.nix +================================================ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + outputs.nixosModules.common + outputs.nixosModules.nixvim + ]; + + system.stateVersion = "25.11"; +} + + + +================================================ +FILE: hosts/cryodev-pi/disks.sh +================================================ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL + + + +================================================ +FILE: hosts/cryodev-pi/hardware.nix +================================================ +{ pkgs, lib, ... }: + +{ + boot = { + kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; + initrd.availableKernelModules = [ + "xhci_pci" + "usbhid" + "usb_storage" + ]; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + }; + }; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + hardware.enableRedistributableFirmware = true; +} + + + +================================================ +FILE: hosts/cryodev-pi/networking.nix +================================================ +{ + networking.hostName = "cryodev-pi"; + networking.domain = "cryodev.xyz"; +} + + + +================================================ +FILE: hosts/cryodev-pi/packages.nix +================================================ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} + + + +================================================ +FILE: hosts/cryodev-pi/users.nix +================================================ +{ inputs, outputs, ... }: + +{ + imports = [ + outputs.nixosModules.normalUsers + ../../users/steffen + ../../users/cryotherm + ]; +} + + + +================================================ +FILE: hosts/cryodev-pi/services/comin.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.comin + ]; + + services.comin = { + enable = true; + remotes = [ + { + name = "origin"; + url = "https://${constants.services.forgejo.fqdn}/steffen/cryodev-server.git"; + branches.main.name = "main"; + } + ]; + }; +} + + + +================================================ +FILE: hosts/cryodev-pi/services/default.nix +================================================ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ./tailscale.nix + ./netdata.nix + ./comin.nix + ]; +} + + + +================================================ +FILE: hosts/cryodev-pi/services/netdata.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + services.netdata = { + enable = true; + config = { + stream = { + enabled = "yes"; + destination = "${constants.hosts.cryodev-main.ip}:${toString constants.services.netdata.port}"; + "api key" = config.sops.placeholder."netdata/stream/child-uuid"; + }; + }; + }; + + # Make sure sops is enabled/imported for this host to handle the secret + imports = [ outputs.nixosModules.sops ]; + + sops = { + defaultSopsFile = ../secrets.yaml; + secrets."netdata/stream/child-uuid" = { + owner = "netdata"; + group = "netdata"; + }; + }; +} + + + +================================================ +FILE: hosts/cryodev-pi/services/nginx.nix +================================================ +{ + outputs, + ... +}: + +{ + imports = [ outputs.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} + + + +================================================ +FILE: hosts/cryodev-pi/services/openssh.nix +================================================ +{ + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.openssh + ]; + + services.openssh.enable = true; +} + + + +================================================ +FILE: hosts/cryodev-pi/services/tailscale.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.tailscale + ]; + + services.tailscale = { + enable = true; + # Connect to our own headscale instance + loginServer = "https://${constants.services.headscale.fqdn}"; + # Allow SSH access over Tailscale + enableSSH = true; + # Use MagicDNS names + acceptDNS = true; + + # Auth key for automated enrollment + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + }; + + sops.secrets."tailscale/auth-key" = { }; +} + + + +================================================ +FILE: modules/nixos/default.nix +================================================ +{ + common = import ./common; + comin = import ./comin; + forgejo = import ./forgejo; + forgejo-runner = import ./forgejo-runner; + mailserver = import ./mailserver; + nixvim = import ./nixvim; + normalUsers = import ./normalUsers; + nginx = import ./nginx; + openssh = import ./openssh; + sops = import ./sops; + tailscale = import ./tailscale; +} + + + +================================================ +FILE: modules/nixos/comin/default.nix +================================================ +{ + inputs, + ... +}: + +{ + imports = [ inputs.comin.nixosModules.comin ]; +} + + + +================================================ +FILE: modules/nixos/common/default.nix +================================================ +{ + imports = [ + ./environment.nix + ./htop.nix + ./nationalization.nix + ./networking.nix + ./nix.nix + ./sudo.nix + ./well-known.nix + ./zsh.nix + + ./shared + ./overlays.nix + ]; +} + + + +================================================ +FILE: modules/nixos/common/environment.nix +================================================ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkDefault optionals; +in +{ + environment.systemPackages = + with pkgs; + [ + cryptsetup + curl + dig + dnsutils + fzf + gptfdisk + iproute2 + jq + lm_sensors + lsof + netcat-openbsd + nettools + nixos-container + nmap + nurl + p7zip + pciutils + psmisc + rclone + rsync + tcpdump + tmux + tree + unzip + usbutils + wget + xxd + zip + + (callPackage ../../../apps/rebuild { }) + ] + ++ optionals (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) [ + pkgs.kitty.terminfo + ]; + + environment.shellAliases = { + l = "ls -lh"; + ll = "ls -lAh"; + ports = "ss -tulpn"; + publicip = "curl ifconfig.me/all"; + sudo = "sudo "; # make aliases work with `sudo` + }; + + # saves one instance of nixpkgs. + environment.ldso32 = null; + + boot.tmp.cleanOnBoot = mkDefault true; + boot.initrd.systemd.enable = mkDefault (!config.boot.swraid.enable && !config.boot.isContainer); +} + + + +================================================ +FILE: modules/nixos/common/htop.nix +================================================ +{ + programs.htop = { + enable = true; + settings = { + highlight_base_name = 1; + }; + }; +} + + + +================================================ +FILE: modules/nixos/common/nationalization.nix +================================================ +{ lib, ... }: + +let + de = "de_DE.UTF-8"; + en = "en_US.UTF-8"; + + inherit (lib) mkDefault; +in +{ + i18n = { + defaultLocale = mkDefault en; + extraLocaleSettings = { + LC_ADDRESS = mkDefault de; + LC_IDENTIFICATION = mkDefault de; + LC_MEASUREMENT = mkDefault de; + LC_MONETARY = mkDefault de; + LC_NAME = mkDefault de; + LC_NUMERIC = mkDefault de; + LC_PAPER = mkDefault de; + LC_TELEPHONE = mkDefault de; + LC_TIME = mkDefault en; + }; + }; + + console = { + font = mkDefault "Lat2-Terminus16"; + keyMap = mkDefault "de"; + }; + + time.timeZone = mkDefault "Europe/Berlin"; +} + + + +================================================ +FILE: modules/nixos/common/networking.nix +================================================ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkDefault; + inherit (lib.utils) isNotEmptyStr; +in +{ + config = { + assertions = [ + { + assertion = isNotEmptyStr config.networking.domain; + message = "synix/nixos/common: config.networking.domain cannot be empty."; + } + { + assertion = isNotEmptyStr config.networking.hostName; + message = "synix/nixos/common: config.networking.hostName cannot be empty."; + } + ]; + + networking = { + domain = mkDefault "${config.networking.hostName}.local"; + hostId = mkDefault "8425e349"; # same as NixOS install ISO and nixos-anywhere + + # NetworkManager + useDHCP = false; + networkmanager = { + enable = true; + plugins = with pkgs; [ + networkmanager-openconnect + networkmanager-openvpn + ]; + }; + }; + }; +} + + + +================================================ +FILE: modules/nixos/common/nix.nix +================================================ +{ + config, + lib, + ... +}: + +let + inherit (lib) mkDefault; +in +{ + nix = { + # use flakes + channel.enable = mkDefault false; + + # De-duplicate store paths using hardlinks except in containers + # where the store is host-managed. + optimise.automatic = mkDefault (!config.boot.isContainer); + }; +} + + + +================================================ +FILE: modules/nixos/common/overlays.nix +================================================ +{ outputs, ... }: + +{ + nixpkgs.overlays = [ + outputs.overlays.local-packages + outputs.overlays.modifications + outputs.overlays.old-stable-packages + outputs.overlays.unstable-packages + ]; +} + + + +================================================ +FILE: modules/nixos/common/sudo.nix +================================================ +{ config, ... }: + +{ + security.sudo = { + enable = true; + execWheelOnly = true; + extraConfig = '' + Defaults lecture = never + ''; + }; + + assertions = + let + validUsers = users: users == [ ] || users == [ "root" ]; + validGroups = groups: groups == [ ] || groups == [ "wheel" ]; + validUserGroups = builtins.all ( + r: validUsers (r.users or [ ]) && validGroups (r.groups or [ ]) + ) config.security.sudo.extraRules; + in + [ + { + assertion = config.security.sudo.execWheelOnly -> validUserGroups; + message = "Some definitions in `security.sudo.extraRules` refer to users other than 'root' or groups other than 'wheel'. Disable `config.security.sudo.execWheelOnly`, or adjust the rules."; + } + ]; +} + + + +================================================ +FILE: modules/nixos/common/well-known.nix +================================================ +{ + # avoid TOFU MITM + programs.ssh.knownHosts = { + "github.com".hostNames = [ "github.com" ]; + "github.com".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + + "gitlab.com".hostNames = [ "gitlab.com" ]; + "gitlab.com".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf"; + + "git.sr.ht".hostNames = [ "git.sr.ht" ]; + "git.sr.ht".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZvRd4EtM7R+IHVMWmDkVU3VLQTSwQDSAvW0t2Tkj60"; + }; + # TODO: add synix +} + + + +================================================ +FILE: modules/nixos/common/zsh.nix +================================================ +{ + programs.zsh = { + enable = true; + syntaxHighlighting = { + enable = true; + highlighters = [ + "main" + "brackets" + "cursor" + "pattern" + ]; + patterns = { + "rm -rf" = "fg=white,bold,bg=red"; + "rm -fr" = "fg=white,bold,bg=red"; + }; + }; + autosuggestions = { + enable = true; + strategy = [ + "completion" + "history" + ]; + }; + enableLsColors = true; + }; +} + + + +================================================ +FILE: modules/nixos/common/shared/default.nix +================================================ +{ + imports = [ + ./nix.nix + ]; +} + + + +================================================ +FILE: modules/nixos/common/shared/nix.nix +================================================ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + mkDefault + optional + versionOlder + versions + ; +in +{ + nix.package = mkDefault pkgs.nix; + + # for `nix run synix#foo`, `nix build synix#bar`, etc + nix.registry = { + synix = { + from = { + id = "synix"; + type = "indirect"; + }; + to = { + owner = "sid"; + repo = "synix"; + host = "git.sid.ovh"; + type = "gitea"; + }; + }; + }; + + # fallback quickly if substituters are not available. + nix.settings.connect-timeout = mkDefault 5; + nix.settings.fallback = true; + + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ] + ++ optional ( + config.nix.package != null && versionOlder (versions.majorMinor config.nix.package.version) "2.22" + ) "repl-flake"; + + nix.settings.log-lines = mkDefault 25; + + # avoid disk full issues + nix.settings.max-free = mkDefault (3000 * 1024 * 1024); + nix.settings.min-free = mkDefault (512 * 1024 * 1024); + + # avoid copying unnecessary stuff over SSH + nix.settings.builders-use-substitutes = true; + + # workaround for https://github.com/NixOS/nix/issues/9574 + nix.settings.nix-path = config.nix.nixPath; + + nix.settings.download-buffer-size = 524288000; # 500 MiB + + # add all wheel users to the trusted-users group + nix.settings.trusted-users = [ + "@wheel" + ]; + + # binary caches + nix.settings.substituters = [ + "https://cache.nixos.org" + "https://nix-community.cachix.org" + "https://cache.garnix.io" + "https://numtide.cachix.org" + ]; + nix.settings.trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "numtide.cachix.org-1:2ps1kLBUWjxIneOy1Ik6cQjb41X0iXVXeHigGmycPPE=" + ]; + + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; +} + + + +================================================ +FILE: modules/nixos/forgejo/default.nix +================================================ +{ + config, + lib, + ... +}: + +let + cfg = config.services.forgejo; + + inherit (cfg) settings; + inherit (lib) + getExe + head + mkDefault + mkIf + ; +in +{ + config = mkIf cfg.enable { + services.forgejo = { + database.type = "postgres"; + lfs.enable = true; + settings = { + server = { + DOMAIN = "git.${config.networking.domain}"; + PROTOCOL = "http"; + ROOT_URL = "https://${settings.server.DOMAIN}/"; + HTTP_ADDR = "0.0.0.0"; + HTTP_PORT = 3456; + SSH_PORT = head config.services.openssh.ports; + }; + service = { + DISABLE_REGISTRATION = true; + }; + ui = { + DEFAULT_THEME = "forgejo-dark"; + }; + actions = { + ENABLED = true; + }; + mailer = { + ENABLED = mkDefault false; + SMTP_ADDR = "mail.${config.networking.domain}"; + FROM = "git@${settings.server.DOMAIN}"; + USER = "git@${settings.server.DOMAIN}"; + }; + }; + secrets = { + mailer.PASSWD = mkIf settings.mailer.ENABLED config.sops.secrets."forgejo/mail-pw".path; + }; + }; + + environment.shellAliases = { + forgejo = "sudo -u ${cfg.user} ${getExe cfg.package} --config ${cfg.stateDir}/custom/conf/app.ini"; + }; + + sops.secrets."forgejo/mail-pw" = mkIf settings.mailer.ENABLED { + owner = cfg.user; + group = cfg.group; + mode = "0400"; + }; + }; +} + + + +================================================ +FILE: modules/nixos/forgejo-runner/default.nix +================================================ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.forgejo-runner; + + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; +in +{ + options.services.forgejo-runner = { + enable = mkEnableOption "Nix-based Forgejo Runner service"; + url = mkOption { + type = types.str; + description = "Forgejo instance URL."; + }; + tokenFile = mkOption { + type = types.path; + description = "Path to EnvironmentFile containing TOKEN=..."; + }; + }; + + config = mkIf cfg.enable { + nix.settings.trusted-users = [ "gitea-runner" ]; + + services.gitea-actions-runner = { + package = pkgs.forgejo-runner; + instances.default = { + enable = true; + name = "${config.networking.hostName}-nix"; + inherit (cfg) url tokenFile; + + labels = [ "host:host" ]; + + hostPackages = with pkgs; [ + bash + coreutils + curl + gitMinimal + gnused + nix + nodejs + openssh + deploy-rs + ]; + + settings = { + log.level = "info"; + runner = { + capacity = 1; + envs = { + NIX_CONFIG = "extra-experimental-features = nix-command flakes"; + NIX_REMOTE = "daemon"; + }; + }; + }; + }; + }; + }; +} + + + +================================================ +FILE: modules/nixos/headplane/default.nix +================================================ +{ + inputs, + config, + lib, + ... +}: + +let + cfg = config.services.headplane; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + headscale = config.services.headscale; + + inherit (lib) + mkDefault + mkIf + ; + + inherit (lib.utils) + mkReverseProxyOption + mkVirtualHost + ; +in +{ + imports = [ inputs.headplane.nixosModules.headplane ]; + + options.services.headplane = { + reverseProxy = mkReverseProxyOption "Headplane" "hp"; + }; + + config = mkIf cfg.enable { + nixpkgs.overlays = [ + inputs.headplane.overlays.default + ]; + + services.headplane = { + settings = { + server = { + host = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"); + port = mkDefault 3000; + cookie_secret_path = config.sops.secrets."headplane/cookie_secret".path; + }; + headscale = { + url = "http://127.0.0.1:${toString headscale.port}"; + public_url = headscale.settings.server_url; + config_path = "/etc/headscale/config.yaml"; + }; + integration.agent = { + enabled = mkDefault true; + pre_authkey_path = config.sops.secrets."headplane/agent_pre_authkey".path; + }; + }; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + port = cfg.settings.server.port; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + + sops.secrets = + let + owner = headscale.user; + group = headscale.group; + mode = "0400"; + in + { + "headplane/cookie_secret" = { + inherit owner group mode; + }; + "headplane/agent_pre_authkey" = { + inherit owner group mode; + }; + }; + }; +} + + + +================================================ +FILE: modules/nixos/headscale/acl.hujson +================================================ +{ + "acls": [ + { + "action": "accept", + "src": ["*"], + "dst": ["*:*"] + } + ], + "ssh": [ + { + "action": "accept", + "src": ["autogroup:member"], + "dst": ["autogroup:member"], + "users": ["autogroup:nonroot", "root"] + } + ] +} + + + +================================================ +FILE: modules/nixos/headscale/default.nix +================================================ +{ + config, + lib, + ... +}: + +let + cfg = config.services.headscale; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + acl = "headscale/acl.hujson"; + + inherit (lib) + mkDefault + mkIf + mkOption + optional + optionals + types + ; + + inherit (lib.utils) + mkReverseProxyOption + mkUrl + mkVirtualHost + ; +in +{ + options.services.headscale = { + reverseProxy = mkReverseProxyOption "Headscale" "hs"; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to automatically open firewall ports. TCP: 80, 443; UDP: 3478."; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !cfg.settings.derp.server.enable || cfg.reverseProxy.forceSSL; + message = "cryodev/nixos/headscale: DERP requires TLS"; + } + { + assertion = fqdn != cfg.settings.dns.base_domain; + message = "cryodev/nixos/headscale: `settings.server_url` must be different from `settings.dns.base_domain`"; + } + { + assertion = !cfg.settings.dns.override_local_dns || cfg.settings.dns.nameservers.global != [ ]; + message = "cryodev/nixos/headscale: `settings.dns.nameservers.global` must be set when `settings.dns.override_local_dns` is true"; + } + ]; + + environment.etc.${acl} = { + inherit (config.services.headscale) user group; + source = ./acl.hujson; + }; + + environment.shellAliases = { + hs = "${cfg.package}/bin/headscale"; + }; + + services.headscale = { + address = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"); + port = mkDefault 8077; + settings = { + policy.path = "/etc/${acl}"; + database.type = "sqlite"; # postgres is highly discouraged as it is only supported for legacy reasons + server_url = mkUrl { + inherit fqdn; + ssl = with cfg.reverseProxy; enable && forceSSL; + }; + derp.server.enable = cfg.reverseProxy.forceSSL; + dns = { + magic_dns = mkDefault true; + base_domain = mkDefault "tail"; + search_domains = [ cfg.settings.dns.base_domain ]; + override_local_dns = mkDefault true; + nameservers.global = optionals cfg.settings.dns.override_local_dns [ + "1.1.1.1" + "1.0.0.1" + "2606:4700:4700::1111" + "2606:4700:4700::1001" + ]; + }; + }; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + inherit (cfg) address port; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ + 80 + 443 + ]; + allowedUDPPorts = optional cfg.settings.derp.server.enable 3478; + }; + }; +} + + + +================================================ +FILE: modules/nixos/mailserver/default.nix +================================================ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + cfg = config.mailserver; + domain = config.networking.domain; + fqdn = "${cfg.subdomain}.${domain}"; + + inherit (lib) + mapAttrs' + mkDefault + mkIf + mkOption + nameValuePair + types + ; +in +{ + imports = [ inputs.nixos-mailserver.nixosModules.mailserver ]; + + options.mailserver = { + subdomain = mkOption { + type = types.str; + default = "mail"; + description = "Subdomain for rDNS"; + }; + accounts = mkOption { + type = types.attrsOf ( + types.submodule { + options = { + aliases = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "A list of aliases of this account. `@domain` will be appended automatically."; + }; + sendOnly = mkOption { + type = types.bool; + default = false; + description = "Specifies if the account should be a send-only account."; + }; + }; + } + ); + default = { }; + description = '' + This options wraps `loginAccounts`. + `loginAccounts..name` will be automatically set to `@`. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.subdomain != ""; + message = "cryodev/nixos/mailserver: config.mailserver.subdomain cannot be empty."; + } + ]; + + mailserver = { + fqdn = mkDefault fqdn; + + domains = mkDefault [ domain ]; + certificateScheme = mkDefault "acme-nginx"; + stateVersion = mkDefault 1; + + loginAccounts = mapAttrs' ( + user: accConf: + nameValuePair "${user}@${domain}" { + name = "${user}@${domain}"; + aliases = map (alias: "${alias}@${domain}") (accConf.aliases or [ ]); + sendOnly = accConf.sendOnly; + quota = mkDefault "5G"; + hashedPasswordFile = config.sops.secrets."mailserver/accounts/${user}".path; + } + ) cfg.accounts; + }; + + security.acme = { + acceptTerms = true; + defaults.email = mkDefault "postmaster@cryodev.xyz"; + defaults.webroot = mkDefault "/var/lib/acme/acme-challenge"; + }; + + environment.systemPackages = [ pkgs.mailutils ]; + + sops = { + secrets = mapAttrs' ( + user: _config: + nameValuePair "mailserver/accounts/${user}" { + restartUnits = [ + "postfix.service" + "dovecot.service" + ]; + } + ) cfg.accounts; + }; + }; +} + + + +================================================ +FILE: modules/nixos/nginx/default.nix +================================================ +{ config, lib, ... }: + +let + cfg = config.services.nginx; + + inherit (lib) + mkDefault + mkIf + mkOption + optional + optionals + types + ; +in +{ + options.services.nginx = { + forceSSL = mkOption { + type = types.bool; + default = false; + description = "Force SSL for Nginx virtual host."; + }; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for HTTP (and HTTPS if forceSSL is enabled)."; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = optionals cfg.openFirewall ( + [ + 80 + ] + ++ optional cfg.forceSSL 443 + ); + + services.nginx = { + recommendedOptimisation = mkDefault true; + recommendedGzipSettings = mkDefault true; + recommendedProxySettings = mkDefault true; + recommendedTlsSettings = cfg.forceSSL; + + commonHttpConfig = "access_log syslog:server=unix:/dev/log;"; + + resolver.addresses = + let + isIPv6 = addr: builtins.match ".*:.*:.*" addr != null; + escapeIPv6 = addr: if isIPv6 addr then "[${addr}]" else addr; + cloudflare = [ + "1.1.1.1" + "2606:4700:4700::1111" + ]; + resolvers = + if config.networking.nameservers == [ ] then cloudflare else config.networking.nameservers; + in + map escapeIPv6 resolvers; + + sslDhparam = mkIf cfg.forceSSL config.security.dhparams.params.nginx.path; + }; + + security.acme = mkIf cfg.forceSSL { + acceptTerms = true; + defaults.email = mkDefault "postmaster@${config.networking.domain}"; + defaults.webroot = mkDefault "/var/lib/acme/acme-challenge"; + }; + + security.dhparams = mkIf cfg.forceSSL { + enable = true; + params.nginx = { }; + }; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/default.nix +================================================ +{ + inputs, + config, + lib, + ... +}: + +let + cfg = config.programs.nixvim; + + inherit (lib) mkDefault mkIf; +in +{ + imports = [ + inputs.nixvim.nixosModules.nixvim + ./plugins + + ./spellfiles.nix + ]; + + config = { + programs.nixvim = { + enable = true; # Enable globally on NixOS + defaultEditor = mkDefault true; + viAlias = mkDefault true; + vimAlias = mkDefault true; + + # Removed home-manager specific options like 'enableMan' which is handled differently or not needed in system module context + # Removed clipboard.providers.wl-copy as it's home-manager specific. + # System-wide clipboard integration for headless servers is less critical but can be added if needed. + + # vim.g.* + globals = { + mapleader = mkDefault " "; + }; + + # vim.opt.* + opts = { + # behavior + cursorline = mkDefault true; # highlights the line under the cursor + mouse = mkDefault "a"; # enable mouse support + nu = mkDefault true; # line numbers + relativenumber = mkDefault true; # relative line numbers + scrolloff = mkDefault 20; # keeps some context above/below cursor + signcolumn = mkDefault "yes"; # reserve space for signs (e.g., GitGutter) + undofile = mkDefault true; # persistent undo + updatetime = mkDefault 500; # ms to wait for trigger an event (default 4000ms) + wrap = mkDefault true; # wraps text if it exceeds the width of the window + + # search + ignorecase = mkDefault true; # ignore case in search patterns + smartcase = mkDefault true; # smart case + incsearch = mkDefault true; # incremental search + hlsearch = mkDefault true; # highlight search + + # windows + splitbelow = mkDefault true; # new windows are created below current + splitright = mkDefault true; # new windows are created to the right of current + equalalways = mkDefault true; # window sizes are automatically updated. + + # tabs + expandtab = mkDefault true; # convert tabs into spaces + shiftwidth = mkDefault 2; # number of spaces to use for each step of (auto)indent + smartindent = mkDefault true; # smart autoindenting on new lines + softtabstop = mkDefault 2; # number of spaces in tab when editing + tabstop = mkDefault 2; # number of visual spaces per tab + + # spell checking + spell = mkDefault true; + spelllang = mkDefault [ + "en_us" + "de_20" + ]; + + }; + + # vim.diagnostic.config.* + diagnostic.settings = { + virtual_text = { + spacing = 4; + prefix = "●"; + severity_sort = true; + }; + signs = true; + underline = true; + update_in_insert = false; + }; + + extraConfigLua = '' + vim.cmd "set noshowmode" -- Hides "--INSERT--" mode indicator + ''; + + keymaps = import ./keymaps.nix; + }; + + environment = { + variables = { + EDITOR = mkIf cfg.enable "nvim"; + VISUAL = mkIf cfg.enable "nvim"; + }; + shellAliases = { + v = mkIf cfg.enable "nvim"; + }; + }; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/keymaps.nix +================================================ +[ + # cursor navigation + { + # scroll down, recenter + key = ""; + action = "zz"; + mode = "n"; + } + { + # scroll up, recenter + key = ""; + action = "zz"; + mode = "n"; + } + + # searching + { + # center cursor after search next + key = "n"; + action = "nzzzv"; + mode = "n"; + } + { + # center cursor after search previous + key = "N"; + action = "Nzzzv"; + mode = "n"; + } + { + # ex command + key = "pv"; + action = "Ex"; + mode = "n"; + } + + # search and replace + { + # search and replace word under cursor + key = "s"; + action = ":%s///gI"; + mode = "n"; + } + # search and replace selected text + { + key = "s"; + action = "y:%s/0/0/gI"; + mode = "v"; + } + + # clipboard operations + { + # copy to system clipboard in visual mode + key = ""; + action = ''"+y ''; + mode = "v"; + } + { + # paste from system clipboard in visual mode + key = ""; + action = ''"+p ''; + mode = "v"; + } + { + # yank to system clipboard + key = "Y"; + action = "+Y"; + mode = "n"; + } + { + # replace selected text with clipboard content + key = "p"; + action = "_dP"; + mode = "x"; + } + { + # delete without copying to clipboard + key = "d"; + action = "_d"; + mode = [ + "n" + "v" + ]; + } + + # line operations + { + # move lines down in visual mode + key = "J"; + action = ":m '>+1gv=gv"; + mode = "v"; + } + { + # move lines up in visual mode + key = "K"; + action = ":m '<-2gv=gv"; + mode = "v"; + } + { + # join lines + key = "J"; + action = "mzJ`z"; + mode = "n"; + } + + # quickfix + { + # Run make command + key = "m"; + action = ":make"; + mode = "n"; + } + { + # previous quickfix item + key = ""; + action = "cprevzz"; + mode = "n"; + } + { + # next quickfix item + key = ""; + action = "cnextzz"; + mode = "n"; + } + + # location list navigation + { + # previous location list item + key = "j"; + action = "lprevzz"; + mode = "n"; + } + { + # next location list item + key = "k"; + action = "lnextzz"; + mode = "n"; + } + + # disabling keys + { + # disable the 'Q' key + key = "Q"; + action = ""; + mode = "n"; + } + + # text selection + { + # select whole buffer + key = ""; + action = "ggVG"; + mode = "n"; + } + + # window operations + { + # focus next window + key = ""; + action = ":wincmd W"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # focus previous window + key = ""; + action = ":wincmd w"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window size adjustments + { + # increase window width + key = ""; + action = ":vertical resize +5"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # decrease window width + key = ""; + action = ":vertical resize -5"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window closing and opening + { + # close current window + key = "c"; + action = ":q"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # new vertical split at $HOME + key = "n"; + action = ":vsp $HOME"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window split orientation toggling + { + # toggle split orientation + key = "t"; + action = ":wincmd T"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # spell checking + { + # toggle spell checking + key = "ss"; + action = ":setlocal spell!"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # switch to english spell checking + key = "se"; + action = ":setlocal spelllang=en_us"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # switch to german spell checking + key = "sg"; + action = ":setlocal spelllang=de_20"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # move to next misspelling + key = "]s"; + action = "]szz"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # move to previous misspelling + key = "[s"; + action = "[szz"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # correction suggestions for a misspelled word + key = "z="; + action = "z="; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # adding words to the dictionary + key = "zg"; + action = "zg"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # buffer navigation + { + # next buffer + key = ""; + action = ":bnext"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # previous buffer + key = ""; + action = ":bprevious"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # close current buffer + key = "bd"; + action = ":bdelete"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + { + # apply code action + key = "ca"; + action = ":lua vim.lsp.buf.code_action()"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } +] + + + +================================================ +FILE: modules/nixos/nixvim/spellfiles.nix +================================================ +{ config, pkgs, ... }: + +let + spellDir = config.xdg.dataHome + "/nvim/site/spell"; + baseUrl = "http://ftp.de.vim.org/runtime/spell"; +in +{ + home.file = { + de-spl = { + enable = true; + source = pkgs.fetchurl { + url = baseUrl + "/de.utf-8.spl"; + sha256 = "sha256-c8cQfqM5hWzb6SHeuSpFk5xN5uucByYdobndGfaDo9E="; + }; + target = spellDir + "/de.utf8.spl"; + }; + de-sug = { + enable = true; + source = pkgs.fetchurl { + url = baseUrl + "/de.utf-8.sug"; + sha256 = "sha256-E9Ds+Shj2J72DNSopesqWhOg6Pm6jRxqvkerqFcUqUg="; + }; + target = spellDir + "/de.utf8.sug"; + }; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/plugins/cmp.nix +================================================ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.cmp; + + inherit (lib) mkDefault mkIf; +in +{ + programs.nixvim = { + plugins = { + cmp = { + enable = mkDefault true; + settings = { + autoEnableSources = mkDefault true; + experimental.ghost_text = mkDefault true; + snippet.expand = mkDefault "luasnip"; + formatting.fields = mkDefault [ + "kind" + "abbr" + "menu" + ]; + sources = [ + { name = "git"; } + { name = "nvim_lsp"; } + { + name = "buffer"; + option.get_bufnrs.__raw = "vim.api.nvim_list_bufs"; + keywordLength = 3; + } + { + name = "path"; + keywordLength = 3; + } + { name = "luasnip"; } + ]; + mapping = { + "" = "cmp.mapping.complete()"; + "" = "cmp.mapping.scroll_docs(-4)"; + "" = "cmp.mapping.close()"; + "" = "cmp.mapping.scroll_docs(4)"; + "" = "cmp.mapping.confirm({ select = true })"; + "" = "cmp.mapping(cmp.mapping.select_prev_item(), {'i', 's'})"; + "" = "cmp.mapping(cmp.mapping.select_next_item(), {'i', 's'})"; + }; + }; + }; + cmp-cmdline = mkIf plugin.enable { enable = mkDefault false; }; # autocomplete for cmdline + cmp_luasnip = mkIf plugin.enable { enable = mkDefault true; }; + luasnip = mkIf plugin.enable { enable = mkDefault true; }; + cmp-treesitter = mkIf (plugin.enable && cfg.plugins.treesitter.enable) { enable = mkDefault true; }; + }; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/plugins/default.nix +================================================ +{ lib, ... }: + +{ + imports = [ + ./cmp.nix + ./lsp.nix + ./lualine.nix + ./telescope.nix + # ./treesitter.nix # HOTFIX: does not build + ./trouble.nix + ]; + + config.programs.nixvim.plugins = { + markdown-preview.enable = lib.mkDefault true; + # warning: Nixvim: `plugins.web-devicons` was enabled automatically because the following plugins are enabled. This behaviour is deprecated. Please explicitly define `plugins.web-devicons.enable` + web-devicons.enable = true; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/plugins/lsp.nix +================================================ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.lsp; + + inherit (lib) mkDefault mkIf optional; +in +{ + config = { + programs.nixvim = { + plugins = { + lsp-format = mkIf plugin.enable { enable = mkDefault true; }; + + lsp = { + enable = mkDefault true; + postConfig = ""; + keymaps = { + silent = mkDefault true; + diagnostic = mkDefault { + # Navigate in diagnostics + "k" = "goto_prev"; + "j" = "goto_next"; + }; + + lspBuf = mkDefault { + gd = "definition"; + gD = "references"; + gt = "type_definition"; + gi = "implementation"; + K = "hover"; + "" = "rename"; + }; + }; + + servers = { + bashls.enable = mkDefault true; + clangd.enable = mkDefault true; + cssls.enable = mkDefault true; + dockerls.enable = mkDefault true; + gopls.enable = mkDefault true; + html.enable = mkDefault true; + jsonls.enable = mkDefault true; + nixd.enable = mkDefault true; + pyright.enable = mkDefault true; + rust_analyzer = { + enable = mkDefault true; + installCargo = mkDefault true; + installRustc = mkDefault true; + settings.rustfmt.overrideCommand = mkDefault [ + "${pkgs.rustfmt}/bin/rustfmt --edition 2021" # --config tab_spaces=2" + ]; + }; + texlab.enable = mkDefault true; + vhdl_ls.enable = mkDefault true; + yamlls.enable = mkDefault true; + }; + }; + }; + }; + + home.packages = optional (cfg.enable && plugin.servers.nixd.enable) pkgs.nixfmt; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/plugins/lualine.nix +================================================ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.lualine; + + inherit (lib) mkDefault; +in +{ + config = { + programs.nixvim = { + plugins.lualine = { + enable = mkDefault true; + settings.options.icons_enabled = mkDefault false; + }; + }; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/plugins/telescope.nix +================================================ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.telescope; + + inherit (lib) mkDefault optionals; +in +{ + config = { + programs.nixvim = { + plugins.telescope = { + enable = mkDefault true; + extensions = { + file-browser.enable = mkDefault true; + fzf-native.enable = mkDefault true; + live-grep-args.enable = mkDefault true; + manix.enable = mkDefault true; + }; + keymaps = mkDefault { + "" = "file_browser"; + "" = "git_files"; + "bl" = "buffers"; + "fd" = "diagnostics"; + "ff" = "find_files"; + "fg" = "live_grep"; + "fh" = "help_tags"; + "fm" = "man_pages"; + "fn" = "manix"; + "fo" = "oldfiles"; + "fb" = "file_browser"; + }; + }; + keymaps = optionals plugin.enable [ + { + key = ""; + action = ":lua require('telescope').extensions.live_grep_args.live_grep_args()"; + mode = "n"; + } + ]; + }; + + home.packages = optionals plugin.enable [ + pkgs.ripgrep # for "live_grep" + ]; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/plugins/treesitter.nix +================================================ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.treesitter; + + cc = "${pkgs.gcc}/bin/gcc"; + + inherit (lib) mkDefault mkIf; +in +{ + config = { + programs.nixvim = { + plugins.treesitter = { + enable = mkDefault true; + nixvimInjections = mkDefault true; + settings = { + folding.enable = mkDefault true; + highlight.enable = mkDefault true; + indent.enable = mkDefault true; + }; + }; + plugins.treesitter-context = mkIf plugin.enable { enable = mkDefault true; }; + plugins.treesitter-textobjects = mkIf plugin.enable { enable = mkDefault true; }; + }; + + # Fix for: ERROR `cc` executable not found. + home.sessionVariables = mkIf plugin.enable { + CC = mkDefault cc; + }; + + # Fix for: WARNING `tree-sitter` executable not found + home.packages = mkIf plugin.enable [ + plugin.package + ]; + }; +} + + + +================================================ +FILE: modules/nixos/nixvim/plugins/trouble.nix +================================================ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.trouble; + + inherit (lib) mkDefault mkIf; +in +{ + config = { + programs.nixvim = { + plugins.trouble = { + enable = mkDefault true; + }; + keymaps = mkIf plugin.enable [ + { + mode = "n"; + key = "xq"; + action = "Trouble qflist toggle"; + options = { + desc = "Trouble quifick toggle"; + }; + } + { + mode = "n"; + key = "xl"; + action = "Trouble loclist toggle"; + options = { + desc = "Trouble loclist toggle"; + }; + } + { + mode = "n"; + key = "xx"; + action = "Trouble diagnostics toggle"; + options = { + desc = "Trouble diagnostics toggle"; + }; + } + ]; + }; + }; +} + + + +================================================ +FILE: modules/nixos/normalUsers/default.nix +================================================ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.normalUsers; + + inherit (lib) + attrNames + genAttrs + mkOption + types + ; +in +{ + options.normalUsers = mkOption { + type = types.attrsOf ( + types.submodule { + options = { + extraGroups = mkOption { + type = (types.listOf types.str); + default = [ ]; + description = "Extra groups for the user"; + example = [ "wheel" ]; + }; + shell = mkOption { + type = types.path; + default = pkgs.zsh; + description = "Shell for the user"; + }; + initialPassword = mkOption { + type = types.str; + default = "changeme"; + description = "Initial password for the user"; + }; + sshKeyFiles = mkOption { + type = (types.listOf types.path); + default = [ ]; + description = "SSH key files for the user"; + example = [ "/path/to/id_rsa.pub" ]; + }; + }; + } + ); + default = { }; + description = "Users to create. The usernames are the attribute names."; + }; + + config = { + # Create user groups + users.groups = genAttrs (attrNames cfg) (userName: { + name = userName; + }); + + # Create users + users.users = genAttrs (attrNames cfg) (userName: { + name = userName; + inherit (cfg.${userName}) extraGroups shell initialPassword; + + isNormalUser = true; + group = "${userName}"; + home = "/home/${userName}"; + openssh.authorizedKeys.keyFiles = cfg.${userName}.sshKeyFiles; + }); + }; +} + + + +================================================ +FILE: modules/nixos/openssh/default.nix +================================================ +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + services.openssh = { + enable = mkDefault true; + ports = mkDefault [ 2299 ]; + openFirewall = mkDefault true; + settings = { + PermitRootLogin = mkDefault "no"; + PasswordAuthentication = mkDefault false; + }; + }; +} + + + +================================================ +FILE: modules/nixos/sops/default.nix +================================================ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + secrets = "${toString inputs.self}/hosts/${config.networking.hostName}/secrets/secrets.yaml"; +in +{ + imports = [ inputs.sops-nix.nixosModules.sops ]; + + environment.systemPackages = with pkgs; [ + age + sops + ]; + + sops.defaultSopsFile = lib.mkIf (builtins.pathExists secrets) (lib.mkDefault secrets); +} + + + +================================================ +FILE: modules/nixos/tailscale/default.nix +================================================ +{ config, lib, ... }: + +let + cfg = config.services.tailscale; + + inherit (lib) + mkIf + mkOption + optional + types + ; +in +{ + options.services.tailscale = { + loginServer = mkOption { + type = types.str; + description = "The Tailscale login server to use."; + }; + enableSSH = mkOption { + type = types.bool; + default = false; + description = "Enable Tailscale SSH functionality."; + }; + acceptDNS = mkOption { + type = types.bool; + default = true; + description = "Enable Tailscale's MagicDNS and custom DNS configuration."; + }; + }; + + config = mkIf cfg.enable { + services.tailscale = { + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + extraSetFlags = optional cfg.enableSSH "--ssh" ++ optional cfg.acceptDNS "--accept-dns"; + extraUpFlags = [ + "--login-server=${cfg.loginServer}" + ] + ++ optional cfg.enableSSH "--ssh" + ++ optional cfg.acceptDNS "--accept-dns"; + }; + + environment.shellAliases = { + ts = "${cfg.package}/bin/tailscale"; + }; + + networking.firewall.trustedInterfaces = [ cfg.interfaceName ]; + + sops.secrets."tailscale/auth-key" = { }; + }; +} + + + +================================================ +FILE: overlays/default.nix +================================================ +{ inputs, ... }: + +{ + # packages in `pkgs/` accessible through 'pkgs.local' + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; + + # https://nixos.wiki/wiki/Overlays + modifications = + final: prev: + let + files = [ + ]; + imports = builtins.map (f: import f final prev) files; + in + builtins.foldl' (a: b: a // b) { } imports; + + # old-stable nixpkgs accessible through 'pkgs.old-stable' + old-stable-packages = final: prev: { + old-stable = import inputs.nixpkgs-old-stable { + inherit (final) system; + inherit (prev) config; + }; + }; + + # unstable nixpkgs accessible through 'pkgs.unstable' + unstable-packages = final: prev: { + unstable = import inputs.nixpkgs-unstable { + inherit (final) system; + inherit (prev) config; + }; + }; +} + + + +================================================ +FILE: pkgs/default.nix +================================================ +{ + pkgs ? import , + ... +}: + +{ + # example = pkgs.callPackage ./example { }; +} + + + +================================================ +FILE: scripts/install.sh +================================================ +#!/usr/bin/env bash + +# NixOS install script + + +### VARIABLES ### + +ASK_VERIFICATION=1 # Default to ask for verification +CONFIG_DIR="/tmp/nixos" # Directory to copy flake to / clone flake into +GIT_BRANCH="master" # Default Git branch +GIT_REPO="" # Git repository URL +HOSTNAME="" # Hostname +MNT="/mnt" # root mount point +SEPARATOR="________________________________________" # line separator + +### FUNCTIONS ### + +# Function to display help information +Show_help() { + echo "Usage: $0 [-r REPO] [-n HOSTNAME] [-b BRANCH] [-y] [-h]" + echo + echo "Options:" + echo " -r, --repo REPO Your NixOS configuration Git repository URL" + echo " -n, --hostname HOSTNAME Specify the hostname for the NixOS configuration" + echo " -b, --branch BRANCH Specify the Git branch to use (default: $GIT_BRANCH)" + echo " -y, --yes Do not ask for user verification before proceeding" + echo " -h, --help Show this help message and exit" +} + +# Function to format, partition, and mount disks for $HOSTNAME using disko +Run_disko() { + echo "$SEPARATOR" + echo "Running disko..." + nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode disko "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.nix +} + +# Function to format, partition, and mount disks for $HOSTNAME using a partitioning script +Run_script() { + echo "$SEPARATOR" + echo "Running partitioning script..." + bash "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.sh +} + +# Function to check mount points and partitioning +Check_partitioning() { + echo "$SEPARATOR" + echo "Printing mount points and partitioning..." + mount | grep "$MNT" + lsblk -f + [[ "$ASK_VERIFICATION" == 1 ]] && read -rp "Verify the mount points and partitioning. Press Ctrl+c to cancel or Enter to continue..." +} + +# Function to generate hardware configuration +Generate_hardware_config() { + [[ "$ASK_VERIFICATION" == 1 ]] && read -rp "No hardware configuration found. Press Ctrl+c to cancel or Enter to generate one..." + + echo "$SEPARATOR" + echo "Generating hardware configuration..." + nixos-generate-config --root "$MNT" --show-hardware-config > "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix + + # Check if hardware configuration has been generated + if [[ ! -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix ]]; then + echo "Error: Hardware configuration cannot be generated." + exit 1 + fi + + # Add configuration to git + # TODO: get rid of cd + cd "$CONFIG_DIR"/hosts/"$HOSTNAME" || exit 1 + git add "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix + cd || exit 1 + + echo "Hardware configuration generated successfully." +}; + +# Function to install configuration for $HOSTNAME +Install() { + # Check if hardware configuration exists + [[ ! -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix ]] && Generate_hardware_config + + echo "$SEPARATOR" + echo "Installing NixOS..." + nixos-install --root "$MNT" --no-root-password --flake "$CONFIG_DIR"#"$HOSTNAME" && echo "You can reboot the system now." +} + +### PARSE ARGUMENTS ### + +while [[ "$#" -gt 0 ]]; do + case $1 in + -r|--repo) GIT_REPO="$2"; shift ;; + -b|--branch) GIT_BRANCH="$2"; shift ;; + -y|--yes) ASK_VERIFICATION=0 ;; + -h|--help) Show_help; exit 0 ;; + -n|--hostname) HOSTNAME="$2"; shift ;; + *) echo "Unknown option: $1"; Show_help; exit 1 ;; + esac + shift +done + +### PREREQUISITES ### + +echo "$SEPARATOR" +mkdir -p "$CONFIG_DIR" + +# Clone NixOS configuration from $GIT_REPO if provided +if [[ ! -z "$GIT_REPO" ]]; then + # Install git if not already installed + if ! command -v git &> /dev/null; then + echo "Git is not installed. Installing..." + nix-env -iA nixos.git + fi + + # Clone Git repo if directory is empty + if [[ -z "$(ls -A "$CONFIG_DIR" 2>/dev/null)" ]]; then + echo "Cloning NixOS configuration repo..." + git clone --depth 1 -b "$GIT_BRANCH" "$GIT_REPO" "$CONFIG_DIR" + + # Check if git repository has been cloned + if [[ ! -d "$CONFIG_DIR"/.git ]]; then + echo "Error: Git repository could not be cloned." + exit 1 + fi + else + echo "$CONFIG_DIR is not empty. Skip cloning $GIT_REPO." + fi +fi + +if [[ ! -f "$CONFIG_DIR"/flake.nix ]]; then + echo "Error: $CONFIG_DIR does not contain 'flake.nix'." + exit 1 +fi + +### CHOOSE CONFIG ### + +# If hostname is not provided via options, prompt the user +if [[ -z "$HOSTNAME" ]]; then + # Get list of available hostnames + HOSTNAMES=$(ls "$CONFIG_DIR"/hosts) + + echo "$SEPARATOR" + echo "Please choose a hostname to install its NixOS configuration." + echo "$HOSTNAMES" + read -rp "Enter hostname: " HOSTNAME + + # Check if hostname is empty + if [[ -z "$HOSTNAME" ]]; then + echo "Error: Hostname cannot be empty." + exit 1 + fi +fi + +### INSTALLATION ### + +# Check if NixOS configuration exists +if [[ -d "$CONFIG_DIR"/hosts/"$HOSTNAME" ]]; then + + # Check for existing disko configuration + if [[ -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.nix ]]; then + Run_disko || ( echo "Error: disko failed." && exit 1 ) + # Check for partitioning script + elif [[ -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.sh ]]; then + Run_script || ( echo "Error: Partitioning script failed." && exit 1 ) + else + echo "Error: No disko configuration (disks.nix) or partitioning script (disks.sh) found for host '$HOSTNAME'." + exit 1 + fi + + Check_partitioning + Install || ( echo "Error: Installation failed." && exit 1 ) +else + echo "Error: Configuration for host '$HOSTNAME' does not exist." + exit 1 +fi + + + +================================================ +FILE: templates/generic-server/boot.nix +================================================ +{ + boot = { + loader = { + grub.enable = false; + generic-extlinux-compatible.enable = true; + }; + }; +} + + + +================================================ +FILE: templates/generic-server/default.nix +================================================ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + outputs.nixosModules.common + outputs.nixosModules.nixvim + ]; + + system.stateVersion = "25.11"; +} + + + +================================================ +FILE: templates/generic-server/disks.sh +================================================ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL + + + +================================================ +FILE: templates/generic-server/flake.nix +================================================ +{ + description = "A generic x86_64 server client template"; + path = ./.; +} + + + +================================================ +FILE: templates/generic-server/hardware.nix +================================================ +{ pkgs, lib, ... }: + +{ + boot = { + kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; + initrd.availableKernelModules = [ + "xhci_pci" + "usbhid" + "usb_storage" + ]; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + }; + }; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + hardware.enableRedistributableFirmware = true; +} + + + +================================================ +FILE: templates/generic-server/networking.nix +================================================ +{ + networking.hostName = "cryodev-pi"; + networking.domain = "cryodev.xyz"; +} + + + +================================================ +FILE: templates/generic-server/packages.nix +================================================ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} + + + +================================================ +FILE: templates/generic-server/users.nix +================================================ +{ inputs, outputs, ... }: + +{ + imports = [ + outputs.nixosModules.normalUsers + ../../users/steffen + ../../users/cryotherm + ]; +} + + + +================================================ +FILE: templates/generic-server/services/comin.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.comin + ]; + + services.comin = { + enable = true; + remotes = [ + { + name = "origin"; + url = "https://${constants.services.forgejo.fqdn}/steffen/cryodev-server.git"; + branches.main.name = "main"; + } + ]; + }; +} + + + +================================================ +FILE: templates/generic-server/services/default.nix +================================================ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ./tailscale.nix + ./netdata.nix + ./comin.nix + ]; +} + + + +================================================ +FILE: templates/generic-server/services/netdata.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + services.netdata = { + enable = true; + config = { + stream = { + enabled = "yes"; + destination = "${constants.hosts.cryodev-main.ip}:${toString constants.services.netdata.port}"; + "api key" = config.sops.placeholder."netdata/stream/child-uuid"; + }; + }; + }; + + # Make sure sops is enabled/imported for this host to handle the secret + imports = [ outputs.nixosModules.sops ]; + + sops = { + defaultSopsFile = ../secrets.yaml; + secrets."netdata/stream/child-uuid" = { + owner = "netdata"; + group = "netdata"; + }; + }; +} + + + +================================================ +FILE: templates/generic-server/services/nginx.nix +================================================ +{ + outputs, + ... +}: + +{ + imports = [ outputs.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} + + + +================================================ +FILE: templates/generic-server/services/openssh.nix +================================================ +{ + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.openssh + ]; + + services.openssh.enable = true; +} + + + +================================================ +FILE: templates/generic-server/services/tailscale.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.tailscale + ]; + + services.tailscale = { + enable = true; + # Connect to our own headscale instance + loginServer = "https://${constants.services.headscale.fqdn}"; + # Allow SSH access over Tailscale + enableSSH = true; + # Use MagicDNS names + acceptDNS = true; + + # Auth key for automated enrollment + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + }; + + sops.secrets."tailscale/auth-key" = { }; +} + + + +================================================ +FILE: templates/raspberry-pi/boot.nix +================================================ +{ + boot = { + loader = { + grub.enable = false; + generic-extlinux-compatible.enable = true; + }; + }; +} + + + +================================================ +FILE: templates/raspberry-pi/default.nix +================================================ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + outputs.nixosModules.common + outputs.nixosModules.nixvim + ]; + + system.stateVersion = "25.11"; +} + + + +================================================ +FILE: templates/raspberry-pi/disks.sh +================================================ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL + + + +================================================ +FILE: templates/raspberry-pi/flake.nix +================================================ +{ + description = "A Raspberry Pi 4 client template"; + path = ./.; +} + + + +================================================ +FILE: templates/raspberry-pi/hardware.nix +================================================ +{ pkgs, lib, ... }: + +{ + boot = { + kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; + initrd.availableKernelModules = [ + "xhci_pci" + "usbhid" + "usb_storage" + ]; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + }; + }; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + hardware.enableRedistributableFirmware = true; +} + + + +================================================ +FILE: templates/raspberry-pi/networking.nix +================================================ +{ + networking.hostName = "cryodev-pi"; + networking.domain = "cryodev.xyz"; +} + + + +================================================ +FILE: templates/raspberry-pi/packages.nix +================================================ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} + + + +================================================ +FILE: templates/raspberry-pi/users.nix +================================================ +{ inputs, outputs, ... }: + +{ + imports = [ + outputs.nixosModules.normalUsers + ../../users/steffen + ../../users/cryotherm + ]; +} + + + +================================================ +FILE: templates/raspberry-pi/services/comin.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.comin + ]; + + services.comin = { + enable = true; + remotes = [ + { + name = "origin"; + url = "https://${constants.services.forgejo.fqdn}/steffen/cryodev-server.git"; + branches.main.name = "main"; + } + ]; + }; +} + + + +================================================ +FILE: templates/raspberry-pi/services/default.nix +================================================ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ./tailscale.nix + ./netdata.nix + ./comin.nix + ]; +} + + + +================================================ +FILE: templates/raspberry-pi/services/netdata.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + services.netdata = { + enable = true; + config = { + stream = { + enabled = "yes"; + destination = "${constants.hosts.cryodev-main.ip}:${toString constants.services.netdata.port}"; + "api key" = config.sops.placeholder."netdata/stream/child-uuid"; + }; + }; + }; + + # Make sure sops is enabled/imported for this host to handle the secret + imports = [ outputs.nixosModules.sops ]; + + sops = { + defaultSopsFile = ../secrets.yaml; + secrets."netdata/stream/child-uuid" = { + owner = "netdata"; + group = "netdata"; + }; + }; +} + + + +================================================ +FILE: templates/raspberry-pi/services/nginx.nix +================================================ +{ + outputs, + ... +}: + +{ + imports = [ outputs.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} + + + +================================================ +FILE: templates/raspberry-pi/services/openssh.nix +================================================ +{ + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.openssh + ]; + + services.openssh.enable = true; +} + + + +================================================ +FILE: templates/raspberry-pi/services/tailscale.nix +================================================ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.tailscale + ]; + + services.tailscale = { + enable = true; + # Connect to our own headscale instance + loginServer = "https://${constants.services.headscale.fqdn}"; + # Allow SSH access over Tailscale + enableSSH = true; + # Use MagicDNS names + acceptDNS = true; + + # Auth key for automated enrollment + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + }; + + sops.secrets."tailscale/auth-key" = { }; +} + + + +================================================ +FILE: users/cryotherm/default.nix +================================================ +{ + normalUsers.cryotherm = { + extraGroups = [ ]; + # No sshKeyFiles, so password login only (if allowed) or local access + sshKeyFiles = [ ]; + }; +} + + + +================================================ +FILE: users/steffen/default.nix +================================================ +{ outputs, ... }: + +{ + normalUsers.steffen = { + extraGroups = [ + "wheel" + ]; + sshKeyFiles = [ ./pubkeys/X670E.pub ]; + }; +} + + + +================================================ +FILE: users/steffen/pubkeys/X670E.pub +================================================ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKNTpsF9Z313gWHiHi4SvjeXI4Mh80mtq0bR0AjsZr/SnPsXEiM8/ODbQNJ806qHLFSA4uA4vaevdZIJkpDqRIQviW7zHGp/weRh2+2ynH8RyFqJvsWIqWn8G5wXPYcRZ6eFjcqKraAQC46ITER4+NPgdC6Cr+dsHWyIroBep4m3EGhSLYNRaMYoKZ5aqD2jJLBolokVfseF06Y7tQ3QSwUioXgiodBdZ9hgXc/5AJdsXSxJMHmRArqbHwbWI0fhwkX+0jiUpOMXMGsJZx5G20X70mQpJu+UnQsGcw+ylQw6ZYtFmzNcYmOS//91DTzraHprnrENyb+pYV2UUZhKxjdkexpSBkkPoVEzMcw9+LCg4e/jsZ+urlRhdTPWW0/AaWJx3UJc1pHHu5UpIvQKfMdt9dZbgG7oYYE1JeCoTvtQKiBcdc54cmJuvwshaAkfN92tYGvj/L1Jeb06M34dycdCXGDGMIofMsZOsnDcHuY1CT82NlRjXmatAUOaO0rCbVNPluNmu4gmWhclQmhoUEmojBGaIXrcRuxrIJYZpWubQdBUCZiJFBJzEb2qnT0nFSe0Gu0tPOYdD/jcUVgYPRWggxQV6hssSlgERTJdzC5PhBnSe8Xi8W/rMgZA8+YBIKBJpJjF5HZTJ67EBZmNS3HWaZNIUmRXcgsONr41RCrw== steffen@X670E + + + +================================================ +FILE: .forgejo/workflows/build-hosts.yml +================================================ +name: Build hosts + +on: + pull_request: + branches: + - main + +jobs: + build-hosts: + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Build cryodev-main + run: nix build .#nixosConfigurations.cryodev-main.config.system.build.toplevel --impure + + - name: Build cryodev-pi + run: nix build .#nixosConfigurations.cryodev-pi.config.system.build.toplevel --impure + + + +================================================ +FILE: .forgejo/workflows/deploy-main.yml +================================================ +name: Deploy cryodev-main + +on: + push: + branches: + - main + +jobs: + deploy-cryodev-main: + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Set up SSH + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + mkdir -p ~/.ssh + echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + + # Add host key (replace with actual host key or use ssh-keyscan in unsafe environments) + ssh-keyscan -H cryodev.xyz >> ~/.ssh/known_hosts + + - name: Deploy with deploy-rs + run: | + # Deploy using deploy-rs + nix run github:serokell/deploy-rs -- -s .#cryodev-main + + + +================================================ +FILE: .forgejo/workflows/flake-check.yml +================================================ +name: Flake check + +on: [pull_request] + +jobs: + flake-check: + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Run flake check + run: nix flake check --impure + + diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9eca4c5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,135 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-old-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + sops-nix.url = "github:Mic92/sops-nix"; + sops-nix.inputs.nixpkgs.follows = "nixpkgs"; + + nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver"; + nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs"; + + headplane.url = "github:yrd/headplane-nix"; + + comin.url = "github:nlewo/comin"; + comin.inputs.nixpkgs.follows = "nixpkgs"; + + deploy-rs.url = "github:serokell/deploy-rs"; + deploy-rs.inputs.nixpkgs.follows = "nixpkgs"; + + nixvim.url = "github:nix-community/nixvim/nixos-25.11"; + nixvim.inputs.nixpkgs.follows = "nixpkgs"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + lib = nixpkgs.lib; + constants = import ./constants.nix; + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit + inputs + outputs + lib + constants + ; + }; + }; + in + { + packages = forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system}); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + nixosConfigurations = { + cryodev-main = mkNixosConfiguration "x86_64-linux" [ ./hosts/cryodev-main ]; + cryodev-pi = mkNixosConfiguration "aarch64-linux" [ ./hosts/cryodev-pi ]; + }; + + templates = { + raspberry-pi = { + path = ./templates/raspberry-pi; + description = "Raspberry Pi 4 Client"; + }; + generic-server = { + path = ./templates/generic-server; + description = "Generic x86_64 Customer Server"; + }; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + deploy = { + nodes = { + cryodev-main = { + hostname = constants.domain; + profiles.system = { + user = "root"; + path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.cryodev-main; + }; + }; + }; + }; + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + deployChecks = inputs.deploy-rs.lib.${system}.deployChecks self.deploy; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + # package = overlaidPkgs.package; + }; + } + // deployChecks + ); + }; +} diff --git a/hosts/cryodev-main/boot.nix b/hosts/cryodev-main/boot.nix new file mode 100644 index 0000000..53a9686 --- /dev/null +++ b/hosts/cryodev-main/boot.nix @@ -0,0 +1,7 @@ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} diff --git a/hosts/cryodev-main/default.nix b/hosts/cryodev-main/default.nix new file mode 100644 index 0000000..482c6a8 --- /dev/null +++ b/hosts/cryodev-main/default.nix @@ -0,0 +1,21 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + outputs.nixosModules.common + outputs.nixosModules.nixvim + ]; + + system.stateVersion = "25.11"; +} diff --git a/hosts/cryodev-main/disks.sh b/hosts/cryodev-main/disks.sh new file mode 100644 index 0000000..5e06bc8 --- /dev/null +++ b/hosts/cryodev-main/disks.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/hosts/cryodev-main/hardware.nix b/hosts/cryodev-main/hardware.nix new file mode 100644 index 0000000..2bfd7b4 --- /dev/null +++ b/hosts/cryodev-main/hardware.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "nvme" + "sd_mod" + "sdhci_pci" + "sr_mod" + "usb_storage" + "virtio_pci" + "virtio_scsi" + "xhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ { device = "/dev/disk/by-label/SWAP"; } ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/hosts/cryodev-main/networking.nix b/hosts/cryodev-main/networking.nix new file mode 100644 index 0000000..03d5c10 --- /dev/null +++ b/hosts/cryodev-main/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "cryodev-main"; + networking.domain = "cryodev.xyz"; +} diff --git a/hosts/cryodev-main/packages.nix b/hosts/cryodev-main/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/hosts/cryodev-main/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/hosts/cryodev-main/services/default.nix b/hosts/cryodev-main/services/default.nix new file mode 100644 index 0000000..0bd3c67 --- /dev/null +++ b/hosts/cryodev-main/services/default.nix @@ -0,0 +1,13 @@ +{ + imports = [ + ./forgejo.nix + ./headplane.nix + ./headscale.nix + ./mailserver.nix + ./netdata.nix + ./nginx.nix + ./openssh.nix + ./sops.nix + ./tailscale.nix + ]; +} diff --git a/hosts/cryodev-main/services/forgejo.nix b/hosts/cryodev-main/services/forgejo.nix new file mode 100644 index 0000000..ae676c6 --- /dev/null +++ b/hosts/cryodev-main/services/forgejo.nix @@ -0,0 +1,51 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.forgejo + outputs.nixosModules.forgejo-runner + ]; + + services.forgejo = { + enable = true; + settings = { + server = { + DOMAIN = constants.services.forgejo.fqdn; + ROOT_URL = "https://${constants.services.forgejo.fqdn}/"; + HTTP_PORT = constants.services.forgejo.port; + }; + service = { + DISABLE_REGISTRATION = true; + }; + mailer = { + ENABLED = true; + FROM = "forgejo@${constants.domain}"; + SMTP_ADDR = constants.services.mail.fqdn; + SMTP_PORT = constants.services.mail.port; + USER = "forgejo@${constants.domain}"; + }; + }; + sops = true; # Enable sops integration for secrets + }; + + services.forgejo-runner = { + enable = true; + url = "https://${constants.services.forgejo.fqdn}"; + # Token needs to be set up via sops/secrets + sops = true; + }; + + services.nginx.virtualHosts."${constants.services.forgejo.fqdn}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString constants.services.forgejo.port}"; + }; + }; +} diff --git a/hosts/cryodev-main/services/headplane.nix b/hosts/cryodev-main/services/headplane.nix new file mode 100644 index 0000000..1b28198 --- /dev/null +++ b/hosts/cryodev-main/services/headplane.nix @@ -0,0 +1,35 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.headplane + ]; + + services.headplane = { + enable = true; + port = constants.services.headplane.port; + headscale = { + url = "http://127.0.0.1:${toString constants.services.headscale.port}"; + public_url = "https://${constants.services.headscale.fqdn}"; + }; + # Secrets for headplane need to be configured via sops + sops.secrets = { + "headplane/cookie_secret" = { }; + "headplane/agent_pre_authkey" = { }; + }; + }; + + services.nginx.virtualHosts."${constants.services.headplane.fqdn}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString constants.services.headplane.port}"; + }; + }; +} diff --git a/hosts/cryodev-main/services/headscale.nix b/hosts/cryodev-main/services/headscale.nix new file mode 100644 index 0000000..e299295 --- /dev/null +++ b/hosts/cryodev-main/services/headscale.nix @@ -0,0 +1,32 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.headscale + ]; + + services.headscale = { + enable = true; + address = "127.0.0.1"; + port = constants.services.headscale.port; + settings = { + server_url = "https://${constants.services.headscale.fqdn}"; + dns_config.base_domain = constants.domain; + }; + }; + + services.nginx.virtualHosts."${constants.services.headscale.fqdn}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString constants.services.headscale.port}"; + proxyWebsockets = true; + }; + }; +} diff --git a/hosts/cryodev-main/services/mailserver.nix b/hosts/cryodev-main/services/mailserver.nix new file mode 100644 index 0000000..42bead3 --- /dev/null +++ b/hosts/cryodev-main/services/mailserver.nix @@ -0,0 +1,27 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.mailserver + ]; + + mailserver = { + enable = true; + fqdn = constants.services.mail.fqdn; + domains = [ constants.domain ]; + accounts = { + forgejo = { }; + admin = { + aliases = [ "postmaster" ]; + }; + }; + certificateScheme = "acme-nginx"; + sops = true; + }; +} diff --git a/hosts/cryodev-main/services/netdata.nix b/hosts/cryodev-main/services/netdata.nix new file mode 100644 index 0000000..6389465 --- /dev/null +++ b/hosts/cryodev-main/services/netdata.nix @@ -0,0 +1,34 @@ +{ + config, + pkgs, + constants, + ... +}: + +{ + services.netdata = { + enable = true; + package = pkgs.netdata.override { + withCloudUi = true; + }; + config = { + global = { + "debug log" = "syslog"; + "access log" = "syslog"; + "error log" = "syslog"; + "bind to" = "127.0.0.1"; + }; + }; + }; + + services.nginx.virtualHosts."${constants.services.netdata.fqdn}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString constants.services.netdata.port}"; + proxyWebsockets = true; + # Basic Auth can be added here if desired, or restrict by IP + # extraConfig = "allow 100.64.0.0/10; deny all;"; # Example for Tailscale only + }; + }; +} diff --git a/hosts/cryodev-main/services/nginx.nix b/hosts/cryodev-main/services/nginx.nix new file mode 100644 index 0000000..8616c61 --- /dev/null +++ b/hosts/cryodev-main/services/nginx.nix @@ -0,0 +1,22 @@ +{ + inputs, + outputs, + lib, + config, + pkgs, + ... +}: + +{ + imports = [ outputs.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; # Force SSL for all vhosts by default if configured to use this option + openFirewall = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + }; +} diff --git a/hosts/cryodev-main/services/openssh.nix b/hosts/cryodev-main/services/openssh.nix new file mode 100644 index 0000000..f71c084 --- /dev/null +++ b/hosts/cryodev-main/services/openssh.nix @@ -0,0 +1,12 @@ +{ + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/hosts/cryodev-main/services/sops.nix b/hosts/cryodev-main/services/sops.nix new file mode 100644 index 0000000..8df48e1 --- /dev/null +++ b/hosts/cryodev-main/services/sops.nix @@ -0,0 +1,21 @@ +{ + config, + pkgs, + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.sops + ]; + + sops = { + defaultSopsFile = ../secrets.yaml; + # age.keyFile is not set, sops-nix defaults to using /etc/ssh/ssh_host_ed25519_key + secrets = { + "forgejo-runner/token" = { }; + "tailscale/auth-key" = { }; + }; + }; +} diff --git a/hosts/cryodev-main/services/tailscale.nix b/hosts/cryodev-main/services/tailscale.nix new file mode 100644 index 0000000..c0c3793 --- /dev/null +++ b/hosts/cryodev-main/services/tailscale.nix @@ -0,0 +1,23 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.tailscale + ]; + + services.tailscale = { + enable = true; + # Connect to our own headscale instance + loginServer = "https://${constants.services.headscale.fqdn}"; + # Allow SSH access over Tailscale + enableSSH = true; + # Use MagicDNS names + acceptDNS = true; + }; +} diff --git a/hosts/cryodev-main/users.nix b/hosts/cryodev-main/users.nix new file mode 100644 index 0000000..a198c5a --- /dev/null +++ b/hosts/cryodev-main/users.nix @@ -0,0 +1,8 @@ +{ inputs, outputs, ... }: + +{ + imports = [ + outputs.nixosModules.normalUsers + ../../users/steffen + ]; +} diff --git a/hosts/cryodev-pi/boot.nix b/hosts/cryodev-pi/boot.nix new file mode 100644 index 0000000..a459d88 --- /dev/null +++ b/hosts/cryodev-pi/boot.nix @@ -0,0 +1,8 @@ +{ + boot = { + loader = { + grub.enable = false; + generic-extlinux-compatible.enable = true; + }; + }; +} diff --git a/hosts/cryodev-pi/default.nix b/hosts/cryodev-pi/default.nix new file mode 100644 index 0000000..482c6a8 --- /dev/null +++ b/hosts/cryodev-pi/default.nix @@ -0,0 +1,21 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + outputs.nixosModules.common + outputs.nixosModules.nixvim + ]; + + system.stateVersion = "25.11"; +} diff --git a/hosts/cryodev-pi/disks.sh b/hosts/cryodev-pi/disks.sh new file mode 100644 index 0000000..5e06bc8 --- /dev/null +++ b/hosts/cryodev-pi/disks.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/hosts/cryodev-pi/hardware.nix b/hosts/cryodev-pi/hardware.nix new file mode 100644 index 0000000..a0d751a --- /dev/null +++ b/hosts/cryodev-pi/hardware.nix @@ -0,0 +1,23 @@ +{ pkgs, lib, ... }: + +{ + boot = { + kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; + initrd.availableKernelModules = [ + "xhci_pci" + "usbhid" + "usb_storage" + ]; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + }; + }; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + hardware.enableRedistributableFirmware = true; +} diff --git a/hosts/cryodev-pi/networking.nix b/hosts/cryodev-pi/networking.nix new file mode 100644 index 0000000..c00bcf5 --- /dev/null +++ b/hosts/cryodev-pi/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "cryodev-pi"; + networking.domain = "cryodev.xyz"; +} diff --git a/hosts/cryodev-pi/packages.nix b/hosts/cryodev-pi/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/hosts/cryodev-pi/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/hosts/cryodev-pi/services/comin.nix b/hosts/cryodev-pi/services/comin.nix new file mode 100644 index 0000000..2317b06 --- /dev/null +++ b/hosts/cryodev-pi/services/comin.nix @@ -0,0 +1,24 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.comin + ]; + + services.comin = { + enable = true; + remotes = [ + { + name = "origin"; + url = "https://${constants.services.forgejo.fqdn}/steffen/cryodev-server.git"; + branches.main.name = "main"; + } + ]; + }; +} diff --git a/hosts/cryodev-pi/services/default.nix b/hosts/cryodev-pi/services/default.nix new file mode 100644 index 0000000..e4c52d5 --- /dev/null +++ b/hosts/cryodev-pi/services/default.nix @@ -0,0 +1,9 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ./tailscale.nix + ./netdata.nix + ./comin.nix + ]; +} diff --git a/hosts/cryodev-pi/services/netdata.nix b/hosts/cryodev-pi/services/netdata.nix new file mode 100644 index 0000000..9ff9a6c --- /dev/null +++ b/hosts/cryodev-pi/services/netdata.nix @@ -0,0 +1,31 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + services.netdata = { + enable = true; + config = { + stream = { + enabled = "yes"; + destination = "${constants.hosts.cryodev-main.ip}:${toString constants.services.netdata.port}"; + "api key" = config.sops.placeholder."netdata/stream/child-uuid"; + }; + }; + }; + + # Make sure sops is enabled/imported for this host to handle the secret + imports = [ outputs.nixosModules.sops ]; + + sops = { + defaultSopsFile = ../secrets.yaml; + secrets."netdata/stream/child-uuid" = { + owner = "netdata"; + group = "netdata"; + }; + }; +} diff --git a/hosts/cryodev-pi/services/nginx.nix b/hosts/cryodev-pi/services/nginx.nix new file mode 100644 index 0000000..00734e5 --- /dev/null +++ b/hosts/cryodev-pi/services/nginx.nix @@ -0,0 +1,14 @@ +{ + outputs, + ... +}: + +{ + imports = [ outputs.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/hosts/cryodev-pi/services/openssh.nix b/hosts/cryodev-pi/services/openssh.nix new file mode 100644 index 0000000..f71c084 --- /dev/null +++ b/hosts/cryodev-pi/services/openssh.nix @@ -0,0 +1,12 @@ +{ + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/hosts/cryodev-pi/services/tailscale.nix b/hosts/cryodev-pi/services/tailscale.nix new file mode 100644 index 0000000..b9ab502 --- /dev/null +++ b/hosts/cryodev-pi/services/tailscale.nix @@ -0,0 +1,28 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.tailscale + ]; + + services.tailscale = { + enable = true; + # Connect to our own headscale instance + loginServer = "https://${constants.services.headscale.fqdn}"; + # Allow SSH access over Tailscale + enableSSH = true; + # Use MagicDNS names + acceptDNS = true; + + # Auth key for automated enrollment + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + }; + + sops.secrets."tailscale/auth-key" = { }; +} diff --git a/hosts/cryodev-pi/users.nix b/hosts/cryodev-pi/users.nix new file mode 100644 index 0000000..3570761 --- /dev/null +++ b/hosts/cryodev-pi/users.nix @@ -0,0 +1,9 @@ +{ inputs, outputs, ... }: + +{ + imports = [ + outputs.nixosModules.normalUsers + ../../users/steffen + ../../users/cryotherm + ]; +} diff --git a/modules/nixos/comin/default.nix b/modules/nixos/comin/default.nix new file mode 100644 index 0000000..aac8c06 --- /dev/null +++ b/modules/nixos/comin/default.nix @@ -0,0 +1,8 @@ +{ + inputs, + ... +}: + +{ + imports = [ inputs.comin.nixosModules.comin ]; +} diff --git a/modules/nixos/common/default.nix b/modules/nixos/common/default.nix new file mode 100644 index 0000000..ab63e24 --- /dev/null +++ b/modules/nixos/common/default.nix @@ -0,0 +1,15 @@ +{ + imports = [ + ./environment.nix + ./htop.nix + ./nationalization.nix + ./networking.nix + ./nix.nix + ./sudo.nix + ./well-known.nix + ./zsh.nix + + ./shared + ./overlays.nix + ]; +} diff --git a/modules/nixos/common/environment.nix b/modules/nixos/common/environment.nix new file mode 100644 index 0000000..6921f2c --- /dev/null +++ b/modules/nixos/common/environment.nix @@ -0,0 +1,63 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkDefault optionals; +in +{ + environment.systemPackages = + with pkgs; + [ + cryptsetup + curl + dig + dnsutils + fzf + gptfdisk + iproute2 + jq + lm_sensors + lsof + netcat-openbsd + nettools + nixos-container + nmap + nurl + p7zip + pciutils + psmisc + rclone + rsync + tcpdump + tmux + tree + unzip + usbutils + wget + xxd + zip + + (callPackage ../../../apps/rebuild { }) + ] + ++ optionals (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) [ + pkgs.kitty.terminfo + ]; + + environment.shellAliases = { + l = "ls -lh"; + ll = "ls -lAh"; + ports = "ss -tulpn"; + publicip = "curl ifconfig.me/all"; + sudo = "sudo "; # make aliases work with `sudo` + }; + + # saves one instance of nixpkgs. + environment.ldso32 = null; + + boot.tmp.cleanOnBoot = mkDefault true; + boot.initrd.systemd.enable = mkDefault (!config.boot.swraid.enable && !config.boot.isContainer); +} diff --git a/modules/nixos/common/htop.nix b/modules/nixos/common/htop.nix new file mode 100644 index 0000000..c95c3fc --- /dev/null +++ b/modules/nixos/common/htop.nix @@ -0,0 +1,8 @@ +{ + programs.htop = { + enable = true; + settings = { + highlight_base_name = 1; + }; + }; +} diff --git a/modules/nixos/common/nationalization.nix b/modules/nixos/common/nationalization.nix new file mode 100644 index 0000000..1d35507 --- /dev/null +++ b/modules/nixos/common/nationalization.nix @@ -0,0 +1,31 @@ +{ lib, ... }: + +let + de = "de_DE.UTF-8"; + en = "en_US.UTF-8"; + + inherit (lib) mkDefault; +in +{ + i18n = { + defaultLocale = mkDefault en; + extraLocaleSettings = { + LC_ADDRESS = mkDefault de; + LC_IDENTIFICATION = mkDefault de; + LC_MEASUREMENT = mkDefault de; + LC_MONETARY = mkDefault de; + LC_NAME = mkDefault de; + LC_NUMERIC = mkDefault de; + LC_PAPER = mkDefault de; + LC_TELEPHONE = mkDefault de; + LC_TIME = mkDefault en; + }; + }; + + console = { + font = mkDefault "Lat2-Terminus16"; + keyMap = mkDefault "de"; + }; + + time.timeZone = mkDefault "Europe/Berlin"; +} diff --git a/modules/nixos/common/networking.nix b/modules/nixos/common/networking.nix new file mode 100644 index 0000000..15d1b1f --- /dev/null +++ b/modules/nixos/common/networking.nix @@ -0,0 +1,40 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkDefault; + inherit (lib.utils) isNotEmptyStr; +in +{ + config = { + assertions = [ + { + assertion = isNotEmptyStr config.networking.domain; + message = "synix/nixos/common: config.networking.domain cannot be empty."; + } + { + assertion = isNotEmptyStr config.networking.hostName; + message = "synix/nixos/common: config.networking.hostName cannot be empty."; + } + ]; + + networking = { + domain = mkDefault "${config.networking.hostName}.local"; + hostId = mkDefault "8425e349"; # same as NixOS install ISO and nixos-anywhere + + # NetworkManager + useDHCP = false; + networkmanager = { + enable = true; + plugins = with pkgs; [ + networkmanager-openconnect + networkmanager-openvpn + ]; + }; + }; + }; +} diff --git a/modules/nixos/common/nix.nix b/modules/nixos/common/nix.nix new file mode 100644 index 0000000..e793401 --- /dev/null +++ b/modules/nixos/common/nix.nix @@ -0,0 +1,19 @@ +{ + config, + lib, + ... +}: + +let + inherit (lib) mkDefault; +in +{ + nix = { + # use flakes + channel.enable = mkDefault false; + + # De-duplicate store paths using hardlinks except in containers + # where the store is host-managed. + optimise.automatic = mkDefault (!config.boot.isContainer); + }; +} diff --git a/modules/nixos/common/overlays.nix b/modules/nixos/common/overlays.nix new file mode 100644 index 0000000..6e4a070 --- /dev/null +++ b/modules/nixos/common/overlays.nix @@ -0,0 +1,10 @@ +{ outputs, ... }: + +{ + nixpkgs.overlays = [ + outputs.overlays.local-packages + outputs.overlays.modifications + outputs.overlays.old-stable-packages + outputs.overlays.unstable-packages + ]; +} diff --git a/modules/nixos/common/shared/default.nix b/modules/nixos/common/shared/default.nix new file mode 100644 index 0000000..3925d83 --- /dev/null +++ b/modules/nixos/common/shared/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./nix.nix + ]; +} diff --git a/modules/nixos/common/shared/nix.nix b/modules/nixos/common/shared/nix.nix new file mode 100644 index 0000000..2d0daa3 --- /dev/null +++ b/modules/nixos/common/shared/nix.nix @@ -0,0 +1,85 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + mkDefault + optional + versionOlder + versions + ; +in +{ + nix.package = mkDefault pkgs.nix; + + # for `nix run synix#foo`, `nix build synix#bar`, etc + nix.registry = { + synix = { + from = { + id = "synix"; + type = "indirect"; + }; + to = { + owner = "sid"; + repo = "synix"; + host = "git.sid.ovh"; + type = "gitea"; + }; + }; + }; + + # fallback quickly if substituters are not available. + nix.settings.connect-timeout = mkDefault 5; + nix.settings.fallback = true; + + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ] + ++ optional ( + config.nix.package != null && versionOlder (versions.majorMinor config.nix.package.version) "2.22" + ) "repl-flake"; + + nix.settings.log-lines = mkDefault 25; + + # avoid disk full issues + nix.settings.max-free = mkDefault (3000 * 1024 * 1024); + nix.settings.min-free = mkDefault (512 * 1024 * 1024); + + # avoid copying unnecessary stuff over SSH + nix.settings.builders-use-substitutes = true; + + # workaround for https://github.com/NixOS/nix/issues/9574 + nix.settings.nix-path = config.nix.nixPath; + + nix.settings.download-buffer-size = 524288000; # 500 MiB + + # add all wheel users to the trusted-users group + nix.settings.trusted-users = [ + "@wheel" + ]; + + # binary caches + nix.settings.substituters = [ + "https://cache.nixos.org" + "https://nix-community.cachix.org" + "https://cache.garnix.io" + "https://numtide.cachix.org" + ]; + nix.settings.trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "numtide.cachix.org-1:2ps1kLBUWjxIneOy1Ik6cQjb41X0iXVXeHigGmycPPE=" + ]; + + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; +} diff --git a/modules/nixos/common/sudo.nix b/modules/nixos/common/sudo.nix new file mode 100644 index 0000000..3459e93 --- /dev/null +++ b/modules/nixos/common/sudo.nix @@ -0,0 +1,26 @@ +{ config, ... }: + +{ + security.sudo = { + enable = true; + execWheelOnly = true; + extraConfig = '' + Defaults lecture = never + ''; + }; + + assertions = + let + validUsers = users: users == [ ] || users == [ "root" ]; + validGroups = groups: groups == [ ] || groups == [ "wheel" ]; + validUserGroups = builtins.all ( + r: validUsers (r.users or [ ]) && validGroups (r.groups or [ ]) + ) config.security.sudo.extraRules; + in + [ + { + assertion = config.security.sudo.execWheelOnly -> validUserGroups; + message = "Some definitions in `security.sudo.extraRules` refer to users other than 'root' or groups other than 'wheel'. Disable `config.security.sudo.execWheelOnly`, or adjust the rules."; + } + ]; +} diff --git a/modules/nixos/common/well-known.nix b/modules/nixos/common/well-known.nix new file mode 100644 index 0000000..07a938c --- /dev/null +++ b/modules/nixos/common/well-known.nix @@ -0,0 +1,17 @@ +{ + # avoid TOFU MITM + programs.ssh.knownHosts = { + "github.com".hostNames = [ "github.com" ]; + "github.com".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + + "gitlab.com".hostNames = [ "gitlab.com" ]; + "gitlab.com".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf"; + + "git.sr.ht".hostNames = [ "git.sr.ht" ]; + "git.sr.ht".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZvRd4EtM7R+IHVMWmDkVU3VLQTSwQDSAvW0t2Tkj60"; + }; + # TODO: add synix +} diff --git a/modules/nixos/common/zsh.nix b/modules/nixos/common/zsh.nix new file mode 100644 index 0000000..a0ff7fb --- /dev/null +++ b/modules/nixos/common/zsh.nix @@ -0,0 +1,26 @@ +{ + programs.zsh = { + enable = true; + syntaxHighlighting = { + enable = true; + highlighters = [ + "main" + "brackets" + "cursor" + "pattern" + ]; + patterns = { + "rm -rf" = "fg=white,bold,bg=red"; + "rm -fr" = "fg=white,bold,bg=red"; + }; + }; + autosuggestions = { + enable = true; + strategy = [ + "completion" + "history" + ]; + }; + enableLsColors = true; + }; +} diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix new file mode 100644 index 0000000..512cd66 --- /dev/null +++ b/modules/nixos/default.nix @@ -0,0 +1,13 @@ +{ + common = import ./common; + comin = import ./comin; + forgejo = import ./forgejo; + forgejo-runner = import ./forgejo-runner; + mailserver = import ./mailserver; + nixvim = import ./nixvim; + normalUsers = import ./normalUsers; + nginx = import ./nginx; + openssh = import ./openssh; + sops = import ./sops; + tailscale = import ./tailscale; +} diff --git a/modules/nixos/forgejo-runner/default.nix b/modules/nixos/forgejo-runner/default.nix new file mode 100644 index 0000000..2547f0e --- /dev/null +++ b/modules/nixos/forgejo-runner/default.nix @@ -0,0 +1,68 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.forgejo-runner; + + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; +in +{ + options.services.forgejo-runner = { + enable = mkEnableOption "Nix-based Forgejo Runner service"; + url = mkOption { + type = types.str; + description = "Forgejo instance URL."; + }; + tokenFile = mkOption { + type = types.path; + description = "Path to EnvironmentFile containing TOKEN=..."; + }; + }; + + config = mkIf cfg.enable { + nix.settings.trusted-users = [ "gitea-runner" ]; + + services.gitea-actions-runner = { + package = pkgs.forgejo-runner; + instances.default = { + enable = true; + name = "${config.networking.hostName}-nix"; + inherit (cfg) url tokenFile; + + labels = [ "host:host" ]; + + hostPackages = with pkgs; [ + bash + coreutils + curl + gitMinimal + gnused + nix + nodejs + openssh + deploy-rs + ]; + + settings = { + log.level = "info"; + runner = { + capacity = 1; + envs = { + NIX_CONFIG = "extra-experimental-features = nix-command flakes"; + NIX_REMOTE = "daemon"; + }; + }; + }; + }; + }; + }; +} diff --git a/modules/nixos/forgejo/default.nix b/modules/nixos/forgejo/default.nix new file mode 100644 index 0000000..6c7a65a --- /dev/null +++ b/modules/nixos/forgejo/default.nix @@ -0,0 +1,63 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.forgejo; + + inherit (cfg) settings; + inherit (lib) + getExe + head + mkDefault + mkIf + ; +in +{ + config = mkIf cfg.enable { + services.forgejo = { + database.type = "postgres"; + lfs.enable = true; + settings = { + server = { + DOMAIN = "git.${config.networking.domain}"; + PROTOCOL = "http"; + ROOT_URL = "https://${settings.server.DOMAIN}/"; + HTTP_ADDR = "0.0.0.0"; + HTTP_PORT = 3456; + SSH_PORT = head config.services.openssh.ports; + }; + service = { + DISABLE_REGISTRATION = true; + }; + ui = { + DEFAULT_THEME = "forgejo-dark"; + }; + actions = { + ENABLED = true; + }; + mailer = { + ENABLED = mkDefault false; + SMTP_ADDR = "mail.${config.networking.domain}"; + FROM = "git@${settings.server.DOMAIN}"; + USER = "git@${settings.server.DOMAIN}"; + }; + }; + secrets = { + mailer.PASSWD = mkIf settings.mailer.ENABLED config.sops.secrets."forgejo/mail-pw".path; + }; + }; + + environment.shellAliases = { + forgejo = "sudo -u ${cfg.user} ${getExe cfg.package} --config ${cfg.stateDir}/custom/conf/app.ini"; + }; + + sops.secrets."forgejo/mail-pw" = mkIf settings.mailer.ENABLED { + owner = cfg.user; + group = cfg.group; + mode = "0400"; + }; + }; +} diff --git a/modules/nixos/headplane/default.nix b/modules/nixos/headplane/default.nix new file mode 100644 index 0000000..017c4e3 --- /dev/null +++ b/modules/nixos/headplane/default.nix @@ -0,0 +1,78 @@ +{ + inputs, + config, + lib, + ... +}: + +let + cfg = config.services.headplane; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + headscale = config.services.headscale; + + inherit (lib) + mkDefault + mkIf + ; + + inherit (lib.utils) + mkReverseProxyOption + mkVirtualHost + ; +in +{ + imports = [ inputs.headplane.nixosModules.headplane ]; + + options.services.headplane = { + reverseProxy = mkReverseProxyOption "Headplane" "hp"; + }; + + config = mkIf cfg.enable { + nixpkgs.overlays = [ + inputs.headplane.overlays.default + ]; + + services.headplane = { + settings = { + server = { + host = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"); + port = mkDefault 3000; + cookie_secret_path = config.sops.secrets."headplane/cookie_secret".path; + }; + headscale = { + url = "http://127.0.0.1:${toString headscale.port}"; + public_url = headscale.settings.server_url; + config_path = "/etc/headscale/config.yaml"; + }; + integration.agent = { + enabled = mkDefault true; + pre_authkey_path = config.sops.secrets."headplane/agent_pre_authkey".path; + }; + }; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + port = cfg.settings.server.port; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + + sops.secrets = + let + owner = headscale.user; + group = headscale.group; + mode = "0400"; + in + { + "headplane/cookie_secret" = { + inherit owner group mode; + }; + "headplane/agent_pre_authkey" = { + inherit owner group mode; + }; + }; + }; +} diff --git a/modules/nixos/headscale/acl.hujson b/modules/nixos/headscale/acl.hujson new file mode 100644 index 0000000..e975c9e --- /dev/null +++ b/modules/nixos/headscale/acl.hujson @@ -0,0 +1,17 @@ +{ + "acls": [ + { + "action": "accept", + "src": ["*"], + "dst": ["*:*"] + } + ], + "ssh": [ + { + "action": "accept", + "src": ["autogroup:member"], + "dst": ["autogroup:member"], + "users": ["autogroup:nonroot", "root"] + } + ] +} diff --git a/modules/nixos/headscale/default.nix b/modules/nixos/headscale/default.nix new file mode 100644 index 0000000..f0cd0b0 --- /dev/null +++ b/modules/nixos/headscale/default.nix @@ -0,0 +1,105 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.headscale; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + acl = "headscale/acl.hujson"; + + inherit (lib) + mkDefault + mkIf + mkOption + optional + optionals + types + ; + + inherit (lib.utils) + mkReverseProxyOption + mkUrl + mkVirtualHost + ; +in +{ + options.services.headscale = { + reverseProxy = mkReverseProxyOption "Headscale" "hs"; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to automatically open firewall ports. TCP: 80, 443; UDP: 3478."; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !cfg.settings.derp.server.enable || cfg.reverseProxy.forceSSL; + message = "cryodev/nixos/headscale: DERP requires TLS"; + } + { + assertion = fqdn != cfg.settings.dns.base_domain; + message = "cryodev/nixos/headscale: `settings.server_url` must be different from `settings.dns.base_domain`"; + } + { + assertion = !cfg.settings.dns.override_local_dns || cfg.settings.dns.nameservers.global != [ ]; + message = "cryodev/nixos/headscale: `settings.dns.nameservers.global` must be set when `settings.dns.override_local_dns` is true"; + } + ]; + + environment.etc.${acl} = { + inherit (config.services.headscale) user group; + source = ./acl.hujson; + }; + + environment.shellAliases = { + hs = "${cfg.package}/bin/headscale"; + }; + + services.headscale = { + address = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"); + port = mkDefault 8077; + settings = { + policy.path = "/etc/${acl}"; + database.type = "sqlite"; # postgres is highly discouraged as it is only supported for legacy reasons + server_url = mkUrl { + inherit fqdn; + ssl = with cfg.reverseProxy; enable && forceSSL; + }; + derp.server.enable = cfg.reverseProxy.forceSSL; + dns = { + magic_dns = mkDefault true; + base_domain = mkDefault "tail"; + search_domains = [ cfg.settings.dns.base_domain ]; + override_local_dns = mkDefault true; + nameservers.global = optionals cfg.settings.dns.override_local_dns [ + "1.1.1.1" + "1.0.0.1" + "2606:4700:4700::1111" + "2606:4700:4700::1001" + ]; + }; + }; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + inherit (cfg) address port; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ + 80 + 443 + ]; + allowedUDPPorts = optional cfg.settings.derp.server.enable 3478; + }; + }; +} diff --git a/modules/nixos/mailserver/default.nix b/modules/nixos/mailserver/default.nix new file mode 100644 index 0000000..7c28972 --- /dev/null +++ b/modules/nixos/mailserver/default.nix @@ -0,0 +1,104 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + cfg = config.mailserver; + domain = config.networking.domain; + fqdn = "${cfg.subdomain}.${domain}"; + + inherit (lib) + mapAttrs' + mkDefault + mkIf + mkOption + nameValuePair + types + ; +in +{ + imports = [ inputs.nixos-mailserver.nixosModules.mailserver ]; + + options.mailserver = { + subdomain = mkOption { + type = types.str; + default = "mail"; + description = "Subdomain for rDNS"; + }; + accounts = mkOption { + type = types.attrsOf ( + types.submodule { + options = { + aliases = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "A list of aliases of this account. `@domain` will be appended automatically."; + }; + sendOnly = mkOption { + type = types.bool; + default = false; + description = "Specifies if the account should be a send-only account."; + }; + }; + } + ); + default = { }; + description = '' + This options wraps `loginAccounts`. + `loginAccounts..name` will be automatically set to `@`. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.subdomain != ""; + message = "cryodev/nixos/mailserver: config.mailserver.subdomain cannot be empty."; + } + ]; + + mailserver = { + fqdn = mkDefault fqdn; + + domains = mkDefault [ domain ]; + certificateScheme = mkDefault "acme-nginx"; + stateVersion = mkDefault 1; + + loginAccounts = mapAttrs' ( + user: accConf: + nameValuePair "${user}@${domain}" { + name = "${user}@${domain}"; + aliases = map (alias: "${alias}@${domain}") (accConf.aliases or [ ]); + sendOnly = accConf.sendOnly; + quota = mkDefault "5G"; + hashedPasswordFile = config.sops.secrets."mailserver/accounts/${user}".path; + } + ) cfg.accounts; + }; + + security.acme = { + acceptTerms = true; + defaults.email = mkDefault "postmaster@cryodev.xyz"; + defaults.webroot = mkDefault "/var/lib/acme/acme-challenge"; + }; + + environment.systemPackages = [ pkgs.mailutils ]; + + sops = { + secrets = mapAttrs' ( + user: _config: + nameValuePair "mailserver/accounts/${user}" { + restartUnits = [ + "postfix.service" + "dovecot.service" + ]; + } + ) cfg.accounts; + }; + }; +} diff --git a/modules/nixos/nginx/default.nix b/modules/nixos/nginx/default.nix new file mode 100644 index 0000000..d28d7a9 --- /dev/null +++ b/modules/nixos/nginx/default.nix @@ -0,0 +1,72 @@ +{ config, lib, ... }: + +let + cfg = config.services.nginx; + + inherit (lib) + mkDefault + mkIf + mkOption + optional + optionals + types + ; +in +{ + options.services.nginx = { + forceSSL = mkOption { + type = types.bool; + default = false; + description = "Force SSL for Nginx virtual host."; + }; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for HTTP (and HTTPS if forceSSL is enabled)."; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = optionals cfg.openFirewall ( + [ + 80 + ] + ++ optional cfg.forceSSL 443 + ); + + services.nginx = { + recommendedOptimisation = mkDefault true; + recommendedGzipSettings = mkDefault true; + recommendedProxySettings = mkDefault true; + recommendedTlsSettings = cfg.forceSSL; + + commonHttpConfig = "access_log syslog:server=unix:/dev/log;"; + + resolver.addresses = + let + isIPv6 = addr: builtins.match ".*:.*:.*" addr != null; + escapeIPv6 = addr: if isIPv6 addr then "[${addr}]" else addr; + cloudflare = [ + "1.1.1.1" + "2606:4700:4700::1111" + ]; + resolvers = + if config.networking.nameservers == [ ] then cloudflare else config.networking.nameservers; + in + map escapeIPv6 resolvers; + + sslDhparam = mkIf cfg.forceSSL config.security.dhparams.params.nginx.path; + }; + + security.acme = mkIf cfg.forceSSL { + acceptTerms = true; + defaults.email = mkDefault "postmaster@${config.networking.domain}"; + defaults.webroot = mkDefault "/var/lib/acme/acme-challenge"; + }; + + security.dhparams = mkIf cfg.forceSSL { + enable = true; + params.nginx = { }; + }; + }; +} diff --git a/modules/nixos/nixvim/default.nix b/modules/nixos/nixvim/default.nix new file mode 100644 index 0000000..2d6a105 --- /dev/null +++ b/modules/nixos/nixvim/default.nix @@ -0,0 +1,106 @@ +{ + inputs, + config, + lib, + ... +}: + +let + cfg = config.programs.nixvim; + + inherit (lib) mkDefault mkIf; +in +{ + imports = [ + inputs.nixvim.nixosModules.nixvim + ./plugins + + ./spellfiles.nix + ]; + + config = { + programs.nixvim = { + enable = true; # Enable globally on NixOS + defaultEditor = mkDefault true; + viAlias = mkDefault true; + vimAlias = mkDefault true; + + # Removed home-manager specific options like 'enableMan' which is handled differently or not needed in system module context + # Removed clipboard.providers.wl-copy as it's home-manager specific. + # System-wide clipboard integration for headless servers is less critical but can be added if needed. + + # vim.g.* + globals = { + mapleader = mkDefault " "; + }; + + # vim.opt.* + opts = { + # behavior + cursorline = mkDefault true; # highlights the line under the cursor + mouse = mkDefault "a"; # enable mouse support + nu = mkDefault true; # line numbers + relativenumber = mkDefault true; # relative line numbers + scrolloff = mkDefault 20; # keeps some context above/below cursor + signcolumn = mkDefault "yes"; # reserve space for signs (e.g., GitGutter) + undofile = mkDefault true; # persistent undo + updatetime = mkDefault 500; # ms to wait for trigger an event (default 4000ms) + wrap = mkDefault true; # wraps text if it exceeds the width of the window + + # search + ignorecase = mkDefault true; # ignore case in search patterns + smartcase = mkDefault true; # smart case + incsearch = mkDefault true; # incremental search + hlsearch = mkDefault true; # highlight search + + # windows + splitbelow = mkDefault true; # new windows are created below current + splitright = mkDefault true; # new windows are created to the right of current + equalalways = mkDefault true; # window sizes are automatically updated. + + # tabs + expandtab = mkDefault true; # convert tabs into spaces + shiftwidth = mkDefault 2; # number of spaces to use for each step of (auto)indent + smartindent = mkDefault true; # smart autoindenting on new lines + softtabstop = mkDefault 2; # number of spaces in tab when editing + tabstop = mkDefault 2; # number of visual spaces per tab + + # spell checking + spell = mkDefault true; + spelllang = mkDefault [ + "en_us" + "de_20" + ]; + + }; + + # vim.diagnostic.config.* + diagnostic.settings = { + virtual_text = { + spacing = 4; + prefix = "●"; + severity_sort = true; + }; + signs = true; + underline = true; + update_in_insert = false; + }; + + extraConfigLua = '' + vim.cmd "set noshowmode" -- Hides "--INSERT--" mode indicator + ''; + + keymaps = import ./keymaps.nix; + }; + + environment = { + variables = { + EDITOR = mkIf cfg.enable "nvim"; + VISUAL = mkIf cfg.enable "nvim"; + }; + shellAliases = { + v = mkIf cfg.enable "nvim"; + }; + }; + }; +} diff --git a/modules/nixos/nixvim/keymaps.nix b/modules/nixos/nixvim/keymaps.nix new file mode 100644 index 0000000..aaefaa0 --- /dev/null +++ b/modules/nixos/nixvim/keymaps.nix @@ -0,0 +1,347 @@ +[ + # cursor navigation + { + # scroll down, recenter + key = ""; + action = "zz"; + mode = "n"; + } + { + # scroll up, recenter + key = ""; + action = "zz"; + mode = "n"; + } + + # searching + { + # center cursor after search next + key = "n"; + action = "nzzzv"; + mode = "n"; + } + { + # center cursor after search previous + key = "N"; + action = "Nzzzv"; + mode = "n"; + } + { + # ex command + key = "pv"; + action = "Ex"; + mode = "n"; + } + + # search and replace + { + # search and replace word under cursor + key = "s"; + action = ":%s///gI"; + mode = "n"; + } + # search and replace selected text + { + key = "s"; + action = "y:%s/0/0/gI"; + mode = "v"; + } + + # clipboard operations + { + # copy to system clipboard in visual mode + key = ""; + action = ''"+y ''; + mode = "v"; + } + { + # paste from system clipboard in visual mode + key = ""; + action = ''"+p ''; + mode = "v"; + } + { + # yank to system clipboard + key = "Y"; + action = "+Y"; + mode = "n"; + } + { + # replace selected text with clipboard content + key = "p"; + action = "_dP"; + mode = "x"; + } + { + # delete without copying to clipboard + key = "d"; + action = "_d"; + mode = [ + "n" + "v" + ]; + } + + # line operations + { + # move lines down in visual mode + key = "J"; + action = ":m '>+1gv=gv"; + mode = "v"; + } + { + # move lines up in visual mode + key = "K"; + action = ":m '<-2gv=gv"; + mode = "v"; + } + { + # join lines + key = "J"; + action = "mzJ`z"; + mode = "n"; + } + + # quickfix + { + # Run make command + key = "m"; + action = ":make"; + mode = "n"; + } + { + # previous quickfix item + key = ""; + action = "cprevzz"; + mode = "n"; + } + { + # next quickfix item + key = ""; + action = "cnextzz"; + mode = "n"; + } + + # location list navigation + { + # previous location list item + key = "j"; + action = "lprevzz"; + mode = "n"; + } + { + # next location list item + key = "k"; + action = "lnextzz"; + mode = "n"; + } + + # disabling keys + { + # disable the 'Q' key + key = "Q"; + action = ""; + mode = "n"; + } + + # text selection + { + # select whole buffer + key = ""; + action = "ggVG"; + mode = "n"; + } + + # window operations + { + # focus next window + key = ""; + action = ":wincmd W"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # focus previous window + key = ""; + action = ":wincmd w"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window size adjustments + { + # increase window width + key = ""; + action = ":vertical resize +5"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # decrease window width + key = ""; + action = ":vertical resize -5"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window closing and opening + { + # close current window + key = "c"; + action = ":q"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # new vertical split at $HOME + key = "n"; + action = ":vsp $HOME"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window split orientation toggling + { + # toggle split orientation + key = "t"; + action = ":wincmd T"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # spell checking + { + # toggle spell checking + key = "ss"; + action = ":setlocal spell!"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # switch to english spell checking + key = "se"; + action = ":setlocal spelllang=en_us"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # switch to german spell checking + key = "sg"; + action = ":setlocal spelllang=de_20"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # move to next misspelling + key = "]s"; + action = "]szz"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # move to previous misspelling + key = "[s"; + action = "[szz"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # correction suggestions for a misspelled word + key = "z="; + action = "z="; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # adding words to the dictionary + key = "zg"; + action = "zg"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # buffer navigation + { + # next buffer + key = ""; + action = ":bnext"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # previous buffer + key = ""; + action = ":bprevious"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # close current buffer + key = "bd"; + action = ":bdelete"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + { + # apply code action + key = "ca"; + action = ":lua vim.lsp.buf.code_action()"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } +] diff --git a/modules/nixos/nixvim/plugins/cmp.nix b/modules/nixos/nixvim/plugins/cmp.nix new file mode 100644 index 0000000..b93fcb6 --- /dev/null +++ b/modules/nixos/nixvim/plugins/cmp.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.cmp; + + inherit (lib) mkDefault mkIf; +in +{ + programs.nixvim = { + plugins = { + cmp = { + enable = mkDefault true; + settings = { + autoEnableSources = mkDefault true; + experimental.ghost_text = mkDefault true; + snippet.expand = mkDefault "luasnip"; + formatting.fields = mkDefault [ + "kind" + "abbr" + "menu" + ]; + sources = [ + { name = "git"; } + { name = "nvim_lsp"; } + { + name = "buffer"; + option.get_bufnrs.__raw = "vim.api.nvim_list_bufs"; + keywordLength = 3; + } + { + name = "path"; + keywordLength = 3; + } + { name = "luasnip"; } + ]; + mapping = { + "" = "cmp.mapping.complete()"; + "" = "cmp.mapping.scroll_docs(-4)"; + "" = "cmp.mapping.close()"; + "" = "cmp.mapping.scroll_docs(4)"; + "" = "cmp.mapping.confirm({ select = true })"; + "" = "cmp.mapping(cmp.mapping.select_prev_item(), {'i', 's'})"; + "" = "cmp.mapping(cmp.mapping.select_next_item(), {'i', 's'})"; + }; + }; + }; + cmp-cmdline = mkIf plugin.enable { enable = mkDefault false; }; # autocomplete for cmdline + cmp_luasnip = mkIf plugin.enable { enable = mkDefault true; }; + luasnip = mkIf plugin.enable { enable = mkDefault true; }; + cmp-treesitter = mkIf (plugin.enable && cfg.plugins.treesitter.enable) { enable = mkDefault true; }; + }; + }; +} diff --git a/modules/nixos/nixvim/plugins/default.nix b/modules/nixos/nixvim/plugins/default.nix new file mode 100644 index 0000000..1941f43 --- /dev/null +++ b/modules/nixos/nixvim/plugins/default.nix @@ -0,0 +1,18 @@ +{ lib, ... }: + +{ + imports = [ + ./cmp.nix + ./lsp.nix + ./lualine.nix + ./telescope.nix + # ./treesitter.nix # HOTFIX: does not build + ./trouble.nix + ]; + + config.programs.nixvim.plugins = { + markdown-preview.enable = lib.mkDefault true; + # warning: Nixvim: `plugins.web-devicons` was enabled automatically because the following plugins are enabled. This behaviour is deprecated. Please explicitly define `plugins.web-devicons.enable` + web-devicons.enable = true; + }; +} diff --git a/modules/nixos/nixvim/plugins/lsp.nix b/modules/nixos/nixvim/plugins/lsp.nix new file mode 100644 index 0000000..b3c70a8 --- /dev/null +++ b/modules/nixos/nixvim/plugins/lsp.nix @@ -0,0 +1,69 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.lsp; + + inherit (lib) mkDefault mkIf optional; +in +{ + config = { + programs.nixvim = { + plugins = { + lsp-format = mkIf plugin.enable { enable = mkDefault true; }; + + lsp = { + enable = mkDefault true; + postConfig = ""; + keymaps = { + silent = mkDefault true; + diagnostic = mkDefault { + # Navigate in diagnostics + "k" = "goto_prev"; + "j" = "goto_next"; + }; + + lspBuf = mkDefault { + gd = "definition"; + gD = "references"; + gt = "type_definition"; + gi = "implementation"; + K = "hover"; + "" = "rename"; + }; + }; + + servers = { + bashls.enable = mkDefault true; + clangd.enable = mkDefault true; + cssls.enable = mkDefault true; + dockerls.enable = mkDefault true; + gopls.enable = mkDefault true; + html.enable = mkDefault true; + jsonls.enable = mkDefault true; + nixd.enable = mkDefault true; + pyright.enable = mkDefault true; + rust_analyzer = { + enable = mkDefault true; + installCargo = mkDefault true; + installRustc = mkDefault true; + settings.rustfmt.overrideCommand = mkDefault [ + "${pkgs.rustfmt}/bin/rustfmt --edition 2021" # --config tab_spaces=2" + ]; + }; + texlab.enable = mkDefault true; + vhdl_ls.enable = mkDefault true; + yamlls.enable = mkDefault true; + }; + }; + }; + }; + + home.packages = optional (cfg.enable && plugin.servers.nixd.enable) pkgs.nixfmt; + }; +} diff --git a/modules/nixos/nixvim/plugins/lualine.nix b/modules/nixos/nixvim/plugins/lualine.nix new file mode 100644 index 0000000..72d2238 --- /dev/null +++ b/modules/nixos/nixvim/plugins/lualine.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.lualine; + + inherit (lib) mkDefault; +in +{ + config = { + programs.nixvim = { + plugins.lualine = { + enable = mkDefault true; + settings.options.icons_enabled = mkDefault false; + }; + }; + }; +} diff --git a/modules/nixos/nixvim/plugins/telescope.nix b/modules/nixos/nixvim/plugins/telescope.nix new file mode 100644 index 0000000..6e1dabc --- /dev/null +++ b/modules/nixos/nixvim/plugins/telescope.nix @@ -0,0 +1,52 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.telescope; + + inherit (lib) mkDefault optionals; +in +{ + config = { + programs.nixvim = { + plugins.telescope = { + enable = mkDefault true; + extensions = { + file-browser.enable = mkDefault true; + fzf-native.enable = mkDefault true; + live-grep-args.enable = mkDefault true; + manix.enable = mkDefault true; + }; + keymaps = mkDefault { + "" = "file_browser"; + "" = "git_files"; + "bl" = "buffers"; + "fd" = "diagnostics"; + "ff" = "find_files"; + "fg" = "live_grep"; + "fh" = "help_tags"; + "fm" = "man_pages"; + "fn" = "manix"; + "fo" = "oldfiles"; + "fb" = "file_browser"; + }; + }; + keymaps = optionals plugin.enable [ + { + key = ""; + action = ":lua require('telescope').extensions.live_grep_args.live_grep_args()"; + mode = "n"; + } + ]; + }; + + home.packages = optionals plugin.enable [ + pkgs.ripgrep # for "live_grep" + ]; + }; +} diff --git a/modules/nixos/nixvim/plugins/treesitter.nix b/modules/nixos/nixvim/plugins/treesitter.nix new file mode 100644 index 0000000..22b7040 --- /dev/null +++ b/modules/nixos/nixvim/plugins/treesitter.nix @@ -0,0 +1,42 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.treesitter; + + cc = "${pkgs.gcc}/bin/gcc"; + + inherit (lib) mkDefault mkIf; +in +{ + config = { + programs.nixvim = { + plugins.treesitter = { + enable = mkDefault true; + nixvimInjections = mkDefault true; + settings = { + folding.enable = mkDefault true; + highlight.enable = mkDefault true; + indent.enable = mkDefault true; + }; + }; + plugins.treesitter-context = mkIf plugin.enable { enable = mkDefault true; }; + plugins.treesitter-textobjects = mkIf plugin.enable { enable = mkDefault true; }; + }; + + # Fix for: ERROR `cc` executable not found. + home.sessionVariables = mkIf plugin.enable { + CC = mkDefault cc; + }; + + # Fix for: WARNING `tree-sitter` executable not found + home.packages = mkIf plugin.enable [ + plugin.package + ]; + }; +} diff --git a/modules/nixos/nixvim/plugins/trouble.nix b/modules/nixos/nixvim/plugins/trouble.nix new file mode 100644 index 0000000..139576d --- /dev/null +++ b/modules/nixos/nixvim/plugins/trouble.nix @@ -0,0 +1,43 @@ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.trouble; + + inherit (lib) mkDefault mkIf; +in +{ + config = { + programs.nixvim = { + plugins.trouble = { + enable = mkDefault true; + }; + keymaps = mkIf plugin.enable [ + { + mode = "n"; + key = "xq"; + action = "Trouble qflist toggle"; + options = { + desc = "Trouble quifick toggle"; + }; + } + { + mode = "n"; + key = "xl"; + action = "Trouble loclist toggle"; + options = { + desc = "Trouble loclist toggle"; + }; + } + { + mode = "n"; + key = "xx"; + action = "Trouble diagnostics toggle"; + options = { + desc = "Trouble diagnostics toggle"; + }; + } + ]; + }; + }; +} diff --git a/modules/nixos/nixvim/spellfiles.nix b/modules/nixos/nixvim/spellfiles.nix new file mode 100644 index 0000000..35b4a6e --- /dev/null +++ b/modules/nixos/nixvim/spellfiles.nix @@ -0,0 +1,26 @@ +{ config, pkgs, ... }: + +let + spellDir = config.xdg.dataHome + "/nvim/site/spell"; + baseUrl = "http://ftp.de.vim.org/runtime/spell"; +in +{ + home.file = { + de-spl = { + enable = true; + source = pkgs.fetchurl { + url = baseUrl + "/de.utf-8.spl"; + sha256 = "sha256-c8cQfqM5hWzb6SHeuSpFk5xN5uucByYdobndGfaDo9E="; + }; + target = spellDir + "/de.utf8.spl"; + }; + de-sug = { + enable = true; + source = pkgs.fetchurl { + url = baseUrl + "/de.utf-8.sug"; + sha256 = "sha256-E9Ds+Shj2J72DNSopesqWhOg6Pm6jRxqvkerqFcUqUg="; + }; + target = spellDir + "/de.utf8.sug"; + }; + }; +} diff --git a/modules/nixos/normalUsers/default.nix b/modules/nixos/normalUsers/default.nix new file mode 100644 index 0000000..7f769bb --- /dev/null +++ b/modules/nixos/normalUsers/default.nix @@ -0,0 +1,69 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.normalUsers; + + inherit (lib) + attrNames + genAttrs + mkOption + types + ; +in +{ + options.normalUsers = mkOption { + type = types.attrsOf ( + types.submodule { + options = { + extraGroups = mkOption { + type = (types.listOf types.str); + default = [ ]; + description = "Extra groups for the user"; + example = [ "wheel" ]; + }; + shell = mkOption { + type = types.path; + default = pkgs.zsh; + description = "Shell for the user"; + }; + initialPassword = mkOption { + type = types.str; + default = "changeme"; + description = "Initial password for the user"; + }; + sshKeyFiles = mkOption { + type = (types.listOf types.path); + default = [ ]; + description = "SSH key files for the user"; + example = [ "/path/to/id_rsa.pub" ]; + }; + }; + } + ); + default = { }; + description = "Users to create. The usernames are the attribute names."; + }; + + config = { + # Create user groups + users.groups = genAttrs (attrNames cfg) (userName: { + name = userName; + }); + + # Create users + users.users = genAttrs (attrNames cfg) (userName: { + name = userName; + inherit (cfg.${userName}) extraGroups shell initialPassword; + + isNormalUser = true; + group = "${userName}"; + home = "/home/${userName}"; + openssh.authorizedKeys.keyFiles = cfg.${userName}.sshKeyFiles; + }); + }; +} diff --git a/modules/nixos/openssh/default.nix b/modules/nixos/openssh/default.nix new file mode 100644 index 0000000..0958445 --- /dev/null +++ b/modules/nixos/openssh/default.nix @@ -0,0 +1,16 @@ +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + services.openssh = { + enable = mkDefault true; + ports = mkDefault [ 2299 ]; + openFirewall = mkDefault true; + settings = { + PermitRootLogin = mkDefault "no"; + PasswordAuthentication = mkDefault false; + }; + }; +} diff --git a/modules/nixos/sops/default.nix b/modules/nixos/sops/default.nix new file mode 100644 index 0000000..627014b --- /dev/null +++ b/modules/nixos/sops/default.nix @@ -0,0 +1,21 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + secrets = "${toString inputs.self}/hosts/${config.networking.hostName}/secrets/secrets.yaml"; +in +{ + imports = [ inputs.sops-nix.nixosModules.sops ]; + + environment.systemPackages = with pkgs; [ + age + sops + ]; + + sops.defaultSopsFile = lib.mkIf (builtins.pathExists secrets) (lib.mkDefault secrets); +} diff --git a/modules/nixos/tailscale/default.nix b/modules/nixos/tailscale/default.nix new file mode 100644 index 0000000..3e43963 --- /dev/null +++ b/modules/nixos/tailscale/default.nix @@ -0,0 +1,50 @@ +{ config, lib, ... }: + +let + cfg = config.services.tailscale; + + inherit (lib) + mkIf + mkOption + optional + types + ; +in +{ + options.services.tailscale = { + loginServer = mkOption { + type = types.str; + description = "The Tailscale login server to use."; + }; + enableSSH = mkOption { + type = types.bool; + default = false; + description = "Enable Tailscale SSH functionality."; + }; + acceptDNS = mkOption { + type = types.bool; + default = true; + description = "Enable Tailscale's MagicDNS and custom DNS configuration."; + }; + }; + + config = mkIf cfg.enable { + services.tailscale = { + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + extraSetFlags = optional cfg.enableSSH "--ssh" ++ optional cfg.acceptDNS "--accept-dns"; + extraUpFlags = [ + "--login-server=${cfg.loginServer}" + ] + ++ optional cfg.enableSSH "--ssh" + ++ optional cfg.acceptDNS "--accept-dns"; + }; + + environment.shellAliases = { + ts = "${cfg.package}/bin/tailscale"; + }; + + networking.firewall.trustedInterfaces = [ cfg.interfaceName ]; + + sops.secrets."tailscale/auth-key" = { }; + }; +} diff --git a/overlays/default.nix b/overlays/default.nix new file mode 100644 index 0000000..32635d4 --- /dev/null +++ b/overlays/default.nix @@ -0,0 +1,32 @@ +{ inputs, ... }: + +{ + # packages in `pkgs/` accessible through 'pkgs.local' + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; + + # https://nixos.wiki/wiki/Overlays + modifications = + final: prev: + let + files = [ + ]; + imports = builtins.map (f: import f final prev) files; + in + builtins.foldl' (a: b: a // b) { } imports; + + # old-stable nixpkgs accessible through 'pkgs.old-stable' + old-stable-packages = final: prev: { + old-stable = import inputs.nixpkgs-old-stable { + inherit (final) system; + inherit (prev) config; + }; + }; + + # unstable nixpkgs accessible through 'pkgs.unstable' + unstable-packages = final: prev: { + unstable = import inputs.nixpkgs-unstable { + inherit (final) system; + inherit (prev) config; + }; + }; +} diff --git a/pkgs/default.nix b/pkgs/default.nix new file mode 100644 index 0000000..2dadf8a --- /dev/null +++ b/pkgs/default.nix @@ -0,0 +1,8 @@ +{ + pkgs ? import , + ... +}: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..1f53b98 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +# NixOS install script + + +### VARIABLES ### + +ASK_VERIFICATION=1 # Default to ask for verification +CONFIG_DIR="/tmp/nixos" # Directory to copy flake to / clone flake into +GIT_BRANCH="master" # Default Git branch +GIT_REPO="" # Git repository URL +HOSTNAME="" # Hostname +MNT="/mnt" # root mount point +SEPARATOR="________________________________________" # line separator + +### FUNCTIONS ### + +# Function to display help information +Show_help() { + echo "Usage: $0 [-r REPO] [-n HOSTNAME] [-b BRANCH] [-y] [-h]" + echo + echo "Options:" + echo " -r, --repo REPO Your NixOS configuration Git repository URL" + echo " -n, --hostname HOSTNAME Specify the hostname for the NixOS configuration" + echo " -b, --branch BRANCH Specify the Git branch to use (default: $GIT_BRANCH)" + echo " -y, --yes Do not ask for user verification before proceeding" + echo " -h, --help Show this help message and exit" +} + +# Function to format, partition, and mount disks for $HOSTNAME using disko +Run_disko() { + echo "$SEPARATOR" + echo "Running disko..." + nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode disko "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.nix +} + +# Function to format, partition, and mount disks for $HOSTNAME using a partitioning script +Run_script() { + echo "$SEPARATOR" + echo "Running partitioning script..." + bash "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.sh +} + +# Function to check mount points and partitioning +Check_partitioning() { + echo "$SEPARATOR" + echo "Printing mount points and partitioning..." + mount | grep "$MNT" + lsblk -f + [[ "$ASK_VERIFICATION" == 1 ]] && read -rp "Verify the mount points and partitioning. Press Ctrl+c to cancel or Enter to continue..." +} + +# Function to generate hardware configuration +Generate_hardware_config() { + [[ "$ASK_VERIFICATION" == 1 ]] && read -rp "No hardware configuration found. Press Ctrl+c to cancel or Enter to generate one..." + + echo "$SEPARATOR" + echo "Generating hardware configuration..." + nixos-generate-config --root "$MNT" --show-hardware-config > "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix + + # Check if hardware configuration has been generated + if [[ ! -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix ]]; then + echo "Error: Hardware configuration cannot be generated." + exit 1 + fi + + # Add configuration to git + # TODO: get rid of cd + cd "$CONFIG_DIR"/hosts/"$HOSTNAME" || exit 1 + git add "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix + cd || exit 1 + + echo "Hardware configuration generated successfully." +}; + +# Function to install configuration for $HOSTNAME +Install() { + # Check if hardware configuration exists + [[ ! -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix ]] && Generate_hardware_config + + echo "$SEPARATOR" + echo "Installing NixOS..." + nixos-install --root "$MNT" --no-root-password --flake "$CONFIG_DIR"#"$HOSTNAME" && echo "You can reboot the system now." +} + +### PARSE ARGUMENTS ### + +while [[ "$#" -gt 0 ]]; do + case $1 in + -r|--repo) GIT_REPO="$2"; shift ;; + -b|--branch) GIT_BRANCH="$2"; shift ;; + -y|--yes) ASK_VERIFICATION=0 ;; + -h|--help) Show_help; exit 0 ;; + -n|--hostname) HOSTNAME="$2"; shift ;; + *) echo "Unknown option: $1"; Show_help; exit 1 ;; + esac + shift +done + +### PREREQUISITES ### + +echo "$SEPARATOR" +mkdir -p "$CONFIG_DIR" + +# Clone NixOS configuration from $GIT_REPO if provided +if [[ ! -z "$GIT_REPO" ]]; then + # Install git if not already installed + if ! command -v git &> /dev/null; then + echo "Git is not installed. Installing..." + nix-env -iA nixos.git + fi + + # Clone Git repo if directory is empty + if [[ -z "$(ls -A "$CONFIG_DIR" 2>/dev/null)" ]]; then + echo "Cloning NixOS configuration repo..." + git clone --depth 1 -b "$GIT_BRANCH" "$GIT_REPO" "$CONFIG_DIR" + + # Check if git repository has been cloned + if [[ ! -d "$CONFIG_DIR"/.git ]]; then + echo "Error: Git repository could not be cloned." + exit 1 + fi + else + echo "$CONFIG_DIR is not empty. Skip cloning $GIT_REPO." + fi +fi + +if [[ ! -f "$CONFIG_DIR"/flake.nix ]]; then + echo "Error: $CONFIG_DIR does not contain 'flake.nix'." + exit 1 +fi + +### CHOOSE CONFIG ### + +# If hostname is not provided via options, prompt the user +if [[ -z "$HOSTNAME" ]]; then + # Get list of available hostnames + HOSTNAMES=$(ls "$CONFIG_DIR"/hosts) + + echo "$SEPARATOR" + echo "Please choose a hostname to install its NixOS configuration." + echo "$HOSTNAMES" + read -rp "Enter hostname: " HOSTNAME + + # Check if hostname is empty + if [[ -z "$HOSTNAME" ]]; then + echo "Error: Hostname cannot be empty." + exit 1 + fi +fi + +### INSTALLATION ### + +# Check if NixOS configuration exists +if [[ -d "$CONFIG_DIR"/hosts/"$HOSTNAME" ]]; then + + # Check for existing disko configuration + if [[ -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.nix ]]; then + Run_disko || ( echo "Error: disko failed." && exit 1 ) + # Check for partitioning script + elif [[ -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.sh ]]; then + Run_script || ( echo "Error: Partitioning script failed." && exit 1 ) + else + echo "Error: No disko configuration (disks.nix) or partitioning script (disks.sh) found for host '$HOSTNAME'." + exit 1 + fi + + Check_partitioning + Install || ( echo "Error: Installation failed." && exit 1 ) +else + echo "Error: Configuration for host '$HOSTNAME' does not exist." + exit 1 +fi diff --git a/templates/generic-server/boot.nix b/templates/generic-server/boot.nix new file mode 100644 index 0000000..a459d88 --- /dev/null +++ b/templates/generic-server/boot.nix @@ -0,0 +1,8 @@ +{ + boot = { + loader = { + grub.enable = false; + generic-extlinux-compatible.enable = true; + }; + }; +} diff --git a/templates/generic-server/default.nix b/templates/generic-server/default.nix new file mode 100644 index 0000000..482c6a8 --- /dev/null +++ b/templates/generic-server/default.nix @@ -0,0 +1,21 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + outputs.nixosModules.common + outputs.nixosModules.nixvim + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/generic-server/disks.sh b/templates/generic-server/disks.sh new file mode 100644 index 0000000..5e06bc8 --- /dev/null +++ b/templates/generic-server/disks.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/templates/generic-server/flake.nix b/templates/generic-server/flake.nix new file mode 100644 index 0000000..fc47489 --- /dev/null +++ b/templates/generic-server/flake.nix @@ -0,0 +1,4 @@ +{ + description = "A generic x86_64 server client template"; + path = ./.; +} diff --git a/templates/generic-server/hardware.nix b/templates/generic-server/hardware.nix new file mode 100644 index 0000000..a0d751a --- /dev/null +++ b/templates/generic-server/hardware.nix @@ -0,0 +1,23 @@ +{ pkgs, lib, ... }: + +{ + boot = { + kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; + initrd.availableKernelModules = [ + "xhci_pci" + "usbhid" + "usb_storage" + ]; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + }; + }; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + hardware.enableRedistributableFirmware = true; +} diff --git a/templates/generic-server/networking.nix b/templates/generic-server/networking.nix new file mode 100644 index 0000000..c00bcf5 --- /dev/null +++ b/templates/generic-server/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "cryodev-pi"; + networking.domain = "cryodev.xyz"; +} diff --git a/templates/generic-server/packages.nix b/templates/generic-server/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/templates/generic-server/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/templates/generic-server/services/comin.nix b/templates/generic-server/services/comin.nix new file mode 100644 index 0000000..2317b06 --- /dev/null +++ b/templates/generic-server/services/comin.nix @@ -0,0 +1,24 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.comin + ]; + + services.comin = { + enable = true; + remotes = [ + { + name = "origin"; + url = "https://${constants.services.forgejo.fqdn}/steffen/cryodev-server.git"; + branches.main.name = "main"; + } + ]; + }; +} diff --git a/templates/generic-server/services/default.nix b/templates/generic-server/services/default.nix new file mode 100644 index 0000000..e4c52d5 --- /dev/null +++ b/templates/generic-server/services/default.nix @@ -0,0 +1,9 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ./tailscale.nix + ./netdata.nix + ./comin.nix + ]; +} diff --git a/templates/generic-server/services/netdata.nix b/templates/generic-server/services/netdata.nix new file mode 100644 index 0000000..9ff9a6c --- /dev/null +++ b/templates/generic-server/services/netdata.nix @@ -0,0 +1,31 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + services.netdata = { + enable = true; + config = { + stream = { + enabled = "yes"; + destination = "${constants.hosts.cryodev-main.ip}:${toString constants.services.netdata.port}"; + "api key" = config.sops.placeholder."netdata/stream/child-uuid"; + }; + }; + }; + + # Make sure sops is enabled/imported for this host to handle the secret + imports = [ outputs.nixosModules.sops ]; + + sops = { + defaultSopsFile = ../secrets.yaml; + secrets."netdata/stream/child-uuid" = { + owner = "netdata"; + group = "netdata"; + }; + }; +} diff --git a/templates/generic-server/services/nginx.nix b/templates/generic-server/services/nginx.nix new file mode 100644 index 0000000..00734e5 --- /dev/null +++ b/templates/generic-server/services/nginx.nix @@ -0,0 +1,14 @@ +{ + outputs, + ... +}: + +{ + imports = [ outputs.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/templates/generic-server/services/openssh.nix b/templates/generic-server/services/openssh.nix new file mode 100644 index 0000000..f71c084 --- /dev/null +++ b/templates/generic-server/services/openssh.nix @@ -0,0 +1,12 @@ +{ + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/templates/generic-server/services/tailscale.nix b/templates/generic-server/services/tailscale.nix new file mode 100644 index 0000000..b9ab502 --- /dev/null +++ b/templates/generic-server/services/tailscale.nix @@ -0,0 +1,28 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.tailscale + ]; + + services.tailscale = { + enable = true; + # Connect to our own headscale instance + loginServer = "https://${constants.services.headscale.fqdn}"; + # Allow SSH access over Tailscale + enableSSH = true; + # Use MagicDNS names + acceptDNS = true; + + # Auth key for automated enrollment + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + }; + + sops.secrets."tailscale/auth-key" = { }; +} diff --git a/templates/generic-server/users.nix b/templates/generic-server/users.nix new file mode 100644 index 0000000..3570761 --- /dev/null +++ b/templates/generic-server/users.nix @@ -0,0 +1,9 @@ +{ inputs, outputs, ... }: + +{ + imports = [ + outputs.nixosModules.normalUsers + ../../users/steffen + ../../users/cryotherm + ]; +} diff --git a/templates/raspberry-pi/boot.nix b/templates/raspberry-pi/boot.nix new file mode 100644 index 0000000..a459d88 --- /dev/null +++ b/templates/raspberry-pi/boot.nix @@ -0,0 +1,8 @@ +{ + boot = { + loader = { + grub.enable = false; + generic-extlinux-compatible.enable = true; + }; + }; +} diff --git a/templates/raspberry-pi/default.nix b/templates/raspberry-pi/default.nix new file mode 100644 index 0000000..482c6a8 --- /dev/null +++ b/templates/raspberry-pi/default.nix @@ -0,0 +1,21 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + outputs.nixosModules.common + outputs.nixosModules.nixvim + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/raspberry-pi/disks.sh b/templates/raspberry-pi/disks.sh new file mode 100644 index 0000000..5e06bc8 --- /dev/null +++ b/templates/raspberry-pi/disks.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/templates/raspberry-pi/flake.nix b/templates/raspberry-pi/flake.nix new file mode 100644 index 0000000..56389b8 --- /dev/null +++ b/templates/raspberry-pi/flake.nix @@ -0,0 +1,4 @@ +{ + description = "A Raspberry Pi 4 client template"; + path = ./.; +} diff --git a/templates/raspberry-pi/hardware.nix b/templates/raspberry-pi/hardware.nix new file mode 100644 index 0000000..a0d751a --- /dev/null +++ b/templates/raspberry-pi/hardware.nix @@ -0,0 +1,23 @@ +{ pkgs, lib, ... }: + +{ + boot = { + kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; + initrd.availableKernelModules = [ + "xhci_pci" + "usbhid" + "usb_storage" + ]; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + }; + }; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + hardware.enableRedistributableFirmware = true; +} diff --git a/templates/raspberry-pi/networking.nix b/templates/raspberry-pi/networking.nix new file mode 100644 index 0000000..c00bcf5 --- /dev/null +++ b/templates/raspberry-pi/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "cryodev-pi"; + networking.domain = "cryodev.xyz"; +} diff --git a/templates/raspberry-pi/packages.nix b/templates/raspberry-pi/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/templates/raspberry-pi/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/templates/raspberry-pi/services/comin.nix b/templates/raspberry-pi/services/comin.nix new file mode 100644 index 0000000..2317b06 --- /dev/null +++ b/templates/raspberry-pi/services/comin.nix @@ -0,0 +1,24 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.comin + ]; + + services.comin = { + enable = true; + remotes = [ + { + name = "origin"; + url = "https://${constants.services.forgejo.fqdn}/steffen/cryodev-server.git"; + branches.main.name = "main"; + } + ]; + }; +} diff --git a/templates/raspberry-pi/services/default.nix b/templates/raspberry-pi/services/default.nix new file mode 100644 index 0000000..e4c52d5 --- /dev/null +++ b/templates/raspberry-pi/services/default.nix @@ -0,0 +1,9 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ./tailscale.nix + ./netdata.nix + ./comin.nix + ]; +} diff --git a/templates/raspberry-pi/services/netdata.nix b/templates/raspberry-pi/services/netdata.nix new file mode 100644 index 0000000..9ff9a6c --- /dev/null +++ b/templates/raspberry-pi/services/netdata.nix @@ -0,0 +1,31 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + services.netdata = { + enable = true; + config = { + stream = { + enabled = "yes"; + destination = "${constants.hosts.cryodev-main.ip}:${toString constants.services.netdata.port}"; + "api key" = config.sops.placeholder."netdata/stream/child-uuid"; + }; + }; + }; + + # Make sure sops is enabled/imported for this host to handle the secret + imports = [ outputs.nixosModules.sops ]; + + sops = { + defaultSopsFile = ../secrets.yaml; + secrets."netdata/stream/child-uuid" = { + owner = "netdata"; + group = "netdata"; + }; + }; +} diff --git a/templates/raspberry-pi/services/nginx.nix b/templates/raspberry-pi/services/nginx.nix new file mode 100644 index 0000000..00734e5 --- /dev/null +++ b/templates/raspberry-pi/services/nginx.nix @@ -0,0 +1,14 @@ +{ + outputs, + ... +}: + +{ + imports = [ outputs.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/templates/raspberry-pi/services/openssh.nix b/templates/raspberry-pi/services/openssh.nix new file mode 100644 index 0000000..f71c084 --- /dev/null +++ b/templates/raspberry-pi/services/openssh.nix @@ -0,0 +1,12 @@ +{ + outputs, + ... +}: + +{ + imports = [ + outputs.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/templates/raspberry-pi/services/tailscale.nix b/templates/raspberry-pi/services/tailscale.nix new file mode 100644 index 0000000..b9ab502 --- /dev/null +++ b/templates/raspberry-pi/services/tailscale.nix @@ -0,0 +1,28 @@ +{ + config, + pkgs, + outputs, + constants, + ... +}: + +{ + imports = [ + outputs.nixosModules.tailscale + ]; + + services.tailscale = { + enable = true; + # Connect to our own headscale instance + loginServer = "https://${constants.services.headscale.fqdn}"; + # Allow SSH access over Tailscale + enableSSH = true; + # Use MagicDNS names + acceptDNS = true; + + # Auth key for automated enrollment + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + }; + + sops.secrets."tailscale/auth-key" = { }; +} diff --git a/templates/raspberry-pi/users.nix b/templates/raspberry-pi/users.nix new file mode 100644 index 0000000..3570761 --- /dev/null +++ b/templates/raspberry-pi/users.nix @@ -0,0 +1,9 @@ +{ inputs, outputs, ... }: + +{ + imports = [ + outputs.nixosModules.normalUsers + ../../users/steffen + ../../users/cryotherm + ]; +} diff --git a/users/cryotherm/default.nix b/users/cryotherm/default.nix new file mode 100644 index 0000000..7de4740 --- /dev/null +++ b/users/cryotherm/default.nix @@ -0,0 +1,7 @@ +{ + normalUsers.cryotherm = { + extraGroups = [ ]; + # No sshKeyFiles, so password login only (if allowed) or local access + sshKeyFiles = [ ]; + }; +} diff --git a/users/steffen/default.nix b/users/steffen/default.nix new file mode 100644 index 0000000..a7503e9 --- /dev/null +++ b/users/steffen/default.nix @@ -0,0 +1,10 @@ +{ outputs, ... }: + +{ + normalUsers.steffen = { + extraGroups = [ + "wheel" + ]; + sshKeyFiles = [ ./pubkeys/X670E.pub ]; + }; +} diff --git a/users/steffen/pubkeys/X670E.pub b/users/steffen/pubkeys/X670E.pub new file mode 100644 index 0000000..068fb57 --- /dev/null +++ b/users/steffen/pubkeys/X670E.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKNTpsF9Z313gWHiHi4SvjeXI4Mh80mtq0bR0AjsZr/SnPsXEiM8/ODbQNJ806qHLFSA4uA4vaevdZIJkpDqRIQviW7zHGp/weRh2+2ynH8RyFqJvsWIqWn8G5wXPYcRZ6eFjcqKraAQC46ITER4+NPgdC6Cr+dsHWyIroBep4m3EGhSLYNRaMYoKZ5aqD2jJLBolokVfseF06Y7tQ3QSwUioXgiodBdZ9hgXc/5AJdsXSxJMHmRArqbHwbWI0fhwkX+0jiUpOMXMGsJZx5G20X70mQpJu+UnQsGcw+ylQw6ZYtFmzNcYmOS//91DTzraHprnrENyb+pYV2UUZhKxjdkexpSBkkPoVEzMcw9+LCg4e/jsZ+urlRhdTPWW0/AaWJx3UJc1pHHu5UpIvQKfMdt9dZbgG7oYYE1JeCoTvtQKiBcdc54cmJuvwshaAkfN92tYGvj/L1Jeb06M34dycdCXGDGMIofMsZOsnDcHuY1CT82NlRjXmatAUOaO0rCbVNPluNmu4gmWhclQmhoUEmojBGaIXrcRuxrIJYZpWubQdBUCZiJFBJzEb2qnT0nFSe0Gu0tPOYdD/jcUVgYPRWggxQV6hssSlgERTJdzC5PhBnSe8Xi8W/rMgZA8+YBIKBJpJjF5HZTJ67EBZmNS3HWaZNIUmRXcgsONr41RCrw== steffen@X670E