diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 0236ced..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Build - -on: - push: - branches: - - main - - 'test-*' - pull_request: - -jobs: - # Job to find all hosts that should be built - get-hosts: - runs-on: ubuntu-latest - container: catthehacker/ubuntu:act-24.04 - outputs: - hosts: ${{ steps.set-hosts.outputs.hosts }} - steps: - - uses: actions/checkout@v4 - - name: Install Nix - uses: cachix/install-nix-action@v27 - - id: set-hosts - run: | - # Extract host names from nixosConfigurations - HOSTS=$(nix eval .#nixosConfigurations --apply "builtins.attrNames" --json) - echo "hosts=$HOSTS" >> $GITHUB_OUTPUT - - build: - needs: get-hosts - runs-on: ubuntu-latest - container: catthehacker/ubuntu:act-24.04 - strategy: - fail-fast: false - matrix: - host: ${{ fromJson(needs.get-hosts.outputs.hosts) }} - steps: - - uses: actions/checkout@v4 - - name: Install Nix - uses: cachix/install-nix-action@v27 - - name: Build NixOS configuration - run: nix build .#nixosConfigurations.${{ matrix.host }}.config.system.build.toplevel diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml deleted file mode 100644 index 4cc892e..0000000 --- a/.github/workflows/check.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Check - -on: - push: - branches: - - '**' - pull_request: - -jobs: - check: - runs-on: ubuntu-latest - container: catthehacker/ubuntu:act-24.04 - steps: - - uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v27 - with: - extra_nix_config: | - experimental-features = nix-command flakes - access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - - name: Flake check - run: nix flake check diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index a037a7a..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Deploy - -on: - push: - branches: - - main - - 'test-*' - workflow_dispatch: - inputs: - mode: - description: 'Activation mode (switch, boot, test)' - default: 'switch' - required: true - -jobs: - deploy: - runs-on: ubuntu-latest - container: catthehacker/ubuntu:act-24.04 - steps: - - uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v27 - with: - extra_nix_config: | - experimental-features = nix-command flakes - - - name: Setup SSH - run: | - mkdir -p ~/.ssh - echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -H 192.168.0.0/24 >> ~/.ssh/known_hosts || true - # Disable strict host key checking for the local network if needed, - # or rely on known_hosts. For homelab, we can be slightly more relaxed - # but let's try to be secure. - echo "StrictHostKeyChecking no" >> ~/.ssh/config - - - name: Verify Commit Signature - if: github.event.sender.login != 'renovate[bot]' - run: | - # TODO Hugo: Export your public GPG/SSH signing keys to a runner secret named 'TRUSTED_SIGNERS'. - # For GPG: gpg --export --armor | base64 -w0 - - if [ -z "${{ secrets.TRUSTED_SIGNERS }}" ]; then - echo "::error::TRUSTED_SIGNERS secret is missing. Deployment aborted for safety." - exit 1 - fi - - # Implementation note: This step expects a keyring in the TRUSTED_SIGNERS secret. - # We use git to verify the signature of the current commit. - echo "${{ secrets.TRUSTED_SIGNERS }}" | base64 -d > /tmp/trusted_keys.gpg - gpg --import /tmp/trusted_keys.gpg - - if ! git verify-commit HEAD; then - echo "::error::Commit signature verification failed. Only signed commits from trusted maintainers can be deployed." - exit 1 - fi - echo "Commit signature verified successfully." - - - name: Install deploy-rs - run: nix profile install github:serokell/deploy-rs - - - name: Deploy to hosts - run: | - # Determine profile based on branch - if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then - # Main site: persistent deployment - deploy . --skip-checks --targets $(deploy . --list | grep '.system$' | tr '\n' ' ') - elif [[ "${{ github.ref }}" == "refs/heads/test-"* ]]; then - # Test branch: non-persistent deployment (test profile) - # The branch name should be test- - HOSTNAME="${GITHUB_REF#refs/heads/test-}" - deploy .#${HOSTNAME}.test --skip-checks - fi - - - name: Manual Deploy - if: github.event_name == 'workflow_dispatch' - run: | - # TODO: Implement manual dispatch logic if needed - deploy . --skip-checks diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 8cb0f4b..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Test" -on: - pull_request: - push: -jobs: - tests: - if: false - runs-on: ubuntu-latest - container: - image: catthehacker/ubuntu:act-latest - steps: - - uses: actions/checkout@v5 - - uses: https://github.com/cachix/install-nix-action@v31 - with: - nix_path: nixpkgs=channel:nixos-unstable - - name: "My custom step" - run: nix run nixpkgs#hello diff --git a/.gitignore b/.gitignore index 8daf605..485dee6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ .idea -result diff --git a/README.md b/README.md deleted file mode 100644 index 19f9892..0000000 --- a/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Bos55 NixOS Config - -Automated CI/CD deployment for NixOS homelab using `deploy-rs`. - -## Repository Structure - -- `hosts/`: Host-specific configurations. -- `modules/`: Shared NixOS modules. -- `users/`: User definitions (including the `deploy` user). -- `secrets/`: Encrypted secrets via `sops-nix`. - -## Deployment Workflow - -### Prerequisites -- SSH access to the `deploy` user on target hosts. -- `deploy-rs` installed locally (`nix profile install github:serokell/deploy-rs`). - -### Deployment Modes - -1. **Production Deployment (main branch):** - Triggered on push to `main`. Automatically builds and switches all hosts. bootloader is updated. - Manual: `deploy .` - -2. **Test Deployment (test- branch):** - Triggered on push to `test-`. Builds and activates the configuration on the specific host **without** updating the bootloader. Reboots will revert to the previous generation. - Manual: `deploy .#.test` - -3. **Kernel Upgrades / Maintenance:** - Use `deploy .#.system --boot` to update the bootloader without immediate activation, followed by a manual reboot. - -## Local Development - -### 1. Developer Shell -This repository includes a standardized development environment containing all necessary tools (`deploy-rs`, `sops`, `age`, etc.). -```bash -nix develop -# or if using direnv -direnv allow -``` - -### 2. Build a host VM -You can build a QEMU VM for any host configuration to test changes locally: -```bash -nix build .#nixosConfigurations..config.system.build.vm -./result/bin/run--vm -``` - -> [!WARNING] -> **Network Conflict**: Default VMs use user-mode networking (NAT) which is safe. However, if you configure the VM to use bridge networking, it will attempt to use the static IP defined in `hostIp`. Ensure you do not have a physical host with that IP active on the same bridge to avoid network interference. - -### 3. Run Integration Tests -Run the automated test suite: -```bash -nix-build test/vm-test.nix -``` - -### 3. Test CI Workflows Locally -Use `act` to test the GitHub Actions workflows: -```bash -act -W .github/workflows/check.yml -``` - -## Security -See [SECURITY.md](SECURITY.md) for details on the trust model and secret management. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index ca1c208..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,93 +0,0 @@ -# Security and Trust Model - -This document outlines the security architecture, trust boundaries, and assumptions of the Bos55 NixOS deployment pipeline. This model is designed to support a multi-member infrastructure team and remains secure even if the repository is published publicly. - -## Trust Zones - -The system is partitioned into three distinct trust zones, each with specific controls to prevent lateral movement and privilege escalation. - -### 🔴 Zone 1: Trusted Maintainers (Source of Truth) -* **Actors:** Infrastructure Team / Maintainers. -* **Capabilities:** - * Full access to the Git repository. - * Ownership of `sops-nix` master keys (GPG or Age). - * Direct root access to NixOS hosts via personal SSH keys for emergency maintenance. -* **Trust:** Root of trust. All changes must originate from or be approved by a Trusted Maintainer. -* **Security Controls:** - * **Signed Commits:** All contributions must be cryptographically signed by a trusted GPG/SSH key to be eligible for deployment. - - **MFA:** Hardware-based multi-factor authentication for repository access. - - **Metadata Redaction:** Sensitive identifiers like SSH `authorizedKeys` are stored in `sops-nix`. This prevents **infrastructure fingerprinting**, where an attacker could link your public keys to your personal identities or other projects. - -### 🟡 Zone 2: CI/CD Pipeline (Automation Layer) -* **Actor:** GitHub Actions / Forgejo Runners. -* **Capabilities:** - * Builds Nix derivations from the repository. - * Access to the `DEPLOY_SSH_KEY` (allowing SSH access to the `deploy` user on target hosts). - * **Trusted Signers:** The public keys for verifying signatures are stored as a **Runner Secret** (`TRUSTED_SIGNERS`). This hides the identities of the infrastructure team even in a public repository. - * **NO ACCESS** to `sops-nix` decryption keys. Secrets remain encrypted during the build. -* **Security Controls:** - * **Signature Enforcement:** The `deploy.yml` workflow verifies the cryptographic signature of every maintainer commit. Deployment is aborted if the signature is missing or untrusted. - * **Sandboxing:** Runners execute in ephemeral, isolated containers. - * **Branch Protection:** Deployments to production (`main`) require approved Pull Requests. - * **Fork Protection:** CI workflows (and secrets) are explicitly disabled for forks. - -### 🟢 Zone 3: Target NixOS Hosts (Runtime) -* **Actor:** Production, Testing, and Service nodes. -* **Capabilities:** Decrypt secrets locally using host-specific `age` keys. -* **Trust:** Consumers of builds. They trust Zone 2 only for the pushing of store paths and triggering activation scripts. -* **Security Controls:** - * **Restricted `deploy` User:** The SSH user for automation is non-root. Sudo access is strictly policed via `sudoers` rules to allow only `nix-env` and `switch-to-configuration`. - * **Immutable Store:** Building on Nix ensures that the system state is derived from a cryptographically hashed store, preventing unauthorized local modifications from persisting across reboots. - ---- - -## Security Assumptions & Policies - -### 1. Public Repository Safety -The repository is designed to be safe for public viewing. No unencrypted secrets should ever be committed. The deployment pipeline is protected against "malicious contributors" via: -- **Mandatory PR Reviews:** No code can reach the `main` branch without peer review. -- **Secret Scoping:** Deployment keys are only available to authorized runs on protected branches. - -### 2. Supply Chain & Dependencies -- **Flake Lockfiles:** All dependencies (Nixpkgs, `deploy-rs`, etc.) are pinned to specific git revisions. -- **Renovate Bot:** Automated version upgrades allow for consistent tracking of upstream changes, though they require manual review or successful status checks for minor/patch versions. - -### 3. Signed Commit Enforcement -To prevent "force-push" attacks or runner compromises from injecting malicious code into the history, the pipeline should be configured to only deploy commits signed by a known "Trusted Maintainer" key. This ensures that even if a git account is compromised, the attacker cannot deploy code without the physical/cryptographic signing key. - ---- - -## Trust Boundary Diagram - -```mermaid -graph TD - subgraph "Zone 1: Trusted Workstations" - DEV["Maintainers (Team)"] - SOPS_KEYS["Master SOPS Keys"] - SIGN_KEYS["Signing Keys (GPG/SSH)"] - end - - subgraph "Zone 2: CI/CD Runner (Sandboxed)" - CI["Automated Runner"] - SSH_KEY["Deploy SSH Key"] - end - - subgraph "Zone 3: NixOS Target Hosts" - HOST["Target Host"] - HOST_AGE["Host Age Key"] - end - - DEV -- "Signed Push / PR" --> CI - CI -- "Push Store Paths & Activate" --> HOST - HOST_AGE -- "Local Decrypt" --> HOST - - style DEV fill:#f96,stroke:#333 - style CI fill:#ff9,stroke:#333 - style HOST fill:#9f9,stroke:#333 -``` - -## Security Best Practices for Maintainers - -1. **Keep Master Keys Offline:** Never store `sops-nix` master keys on the CI runner or public servers. -2. **Audit Runner Logs:** Periodically review CI execution logs for unexpected behavior. -3. **Rotate Deployment Keys:** Rotate the `DEPLOY_SSH_KEY` if maintainer membership changes significantly. diff --git a/flake.lock b/flake.lock index da5c167..67df8c4 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772624091, - "narHash": "sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN+UHzW1jc=", + "lastModified": 1760524057, + "narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "80bdc1e5ce51f56b19791b52b2901187931f5353", + "rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1772495394, - "narHash": "sha256-hmIvE/slLKEFKNEJz27IZ8BKlAaZDcjIHmkZ7GCEjfw=", + "lastModified": 1760393368, + "narHash": "sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E=", "owner": "Mic92", "repo": "sops-nix", - "rev": "1d9b98a29a45abe9c4d3174bd36de9f28755e3ff", + "rev": "ab8d56e85b8be14cff9d93735951e30c3e86a437", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7e7e68c..446f4ce 100644 --- a/flake.nix +++ b/flake.nix @@ -13,85 +13,52 @@ url = "github:gytis-ivaskevicius/flake-utils-plus"; inputs.flake-utils.follows = "flake-utils"; }; - deploy-rs = { - url = "github:serokell/deploy-rs"; - inputs.nixpkgs.follows = "nixpkgs"; - }; }; outputs = inputs@{ self, nixpkgs, - flake-utils, sops-nix, utils, deploy-rs, + flake-utils, sops-nix, utils, ... }: let system = utils.lib.system.x86_64-linux; - lib = nixpkgs.lib; in - utils.lib.mkFlake { - inherit self inputs; + utils.lib.mkFlake { + inherit self inputs; - hostDefaults.modules = [ + hostDefaults = { + inherit system; + + modules = [ ./modules ./users + sops-nix.nixosModules.sops - ({ self, ... }: { - sops.defaultSopsFile = "${self}/secrets/secrets.yaml"; - sops.age.keyFile = "/var/lib/sops-nix/key.txt"; - }) - ({ self, ... }: { - sops.defaultSopsFile = "${self}/secrets/secrets.yaml"; - sops.age.keyFile = "/var/lib/sops-nix/key.txt"; - }) ]; - - hosts = { - # Infrastructure - Niko.modules = [ ./hosts/Niko ]; - Ingress.modules = [ ./hosts/Ingress ]; - Gitea.modules = [ ./hosts/Gitea ]; - Vaultwarden.modules = [ ./hosts/Vaultwarden ]; - - # Production - Binnenpost.modules = [ ./hosts/Binnenpost ]; - Production.modules = [ ./hosts/Production ]; - ProductionGPU.modules = [ ./hosts/ProductionGPU ]; - ProductionArr.modules = [ ./hosts/ProductionArr ]; - ACE.modules = [ ./hosts/ACE ]; - - # Lab - Template.modules = [ ./hosts/Template ]; - Development.modules = [ ./hosts/Development ]; - Testing.modules = [ ./hosts/Testing ]; - }; - - deploy.nodes = let - pkg = deploy-rs.lib.${system}; - isDeployable = nixos: (nixos.config.homelab.users.deploy.enable or false) && (nixos.config.homelab.networking.hostIp != null); - in - builtins.mapAttrs (_: nixos: { - hostname = nixos.config.homelab.networking.hostIp; - sshUser = "deploy"; - user = "root"; - profiles.system.path = pkg.activate.nixos nixos; - profiles.test.path = pkg.activate.custom nixos.config.system.build.toplevel '' - $PROFILE/bin/switch-to-configuration test - ''; - }) (lib.filterAttrs (_: isDeployable) self.nixosConfigurations); - - checks = builtins.mapAttrs (_: lib: lib.deployChecks self.deploy) deploy-rs.lib; - - outputsBuilder = channels: { - formatter = channels.nixpkgs.alejandra; - devShells.default = channels.nixpkgs.mkShell { - name = "homelab-dev"; - buildInputs = [ - deploy-rs.packages.${system}.deploy-rs - channels.nixpkgs.sops - channels.nixpkgs.age - ]; - shellHook = "echo '🛡️ Homelab Development Shell Loaded'"; - }; - }; }; + + hosts = { + # Physical hosts + Niko.modules = [ ./hosts/Niko ]; + + # Virtual machines + + # Single-service + Ingress.modules = [ ./hosts/Ingress ]; + Gitea.modules = [ ./hosts/Gitea ]; + Vaultwarden.modules = [ ./hosts/Vaultwarden ]; + + # Production multi-service + Binnenpost.modules = [ ./hosts/Binnenpost ]; + Production.modules = [ ./hosts/Production ]; + ProductionGPU.modules = [ ./hosts/ProductionGPU ]; + ProductionArr.modules = [ ./hosts/ProductionArr ]; + ACE.modules = [ ./hosts/ACE ]; + + # Others + Template.modules = [ ./hosts/Template ]; + Development.modules = [ ./hosts/Development ]; + Testing.modules = [ ./hosts/Testing ]; + }; + }; } diff --git a/hosts/ACE/default.nix b/hosts/ACE/default.nix index 094b077..04aa284 100644 --- a/hosts/ACE/default.nix +++ b/hosts/ACE/default.nix @@ -1,12 +1,10 @@ -{ config, pkgs, ... }: +{ pkgs, ... }: { config = { homelab = { - networking.hostIp = "192.168.0.41"; services.actions.enable = true; virtualisation.guest.enable = true; - users.deploy.enable = true; }; networking = { @@ -26,7 +24,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.41"; prefixLength = 24; } ]; diff --git a/hosts/Binnenpost/default.nix b/hosts/Binnenpost/default.nix index 624608b..561fbe1 100644 --- a/hosts/Binnenpost/default.nix +++ b/hosts/Binnenpost/default.nix @@ -1,4 +1,4 @@ -{ config, pkgs, ... }: +{ pkgs, ... }: { config = { @@ -13,14 +13,12 @@ }; homelab = { - networking.hostIp = "192.168.0.89"; apps = { speedtest.enable = true; technitiumDNS.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; - users.deploy.enable = true; }; networking = { @@ -45,7 +43,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.89"; prefixLength = 24; } ]; diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix index 68c5fea..77f6758 100644 --- a/hosts/Development/default.nix +++ b/hosts/Development/default.nix @@ -3,7 +3,6 @@ { config = { homelab = { - networking.hostIp = "192.168.0.91"; apps = { bind9.enable = true; homepage = { @@ -12,9 +11,9 @@ }; traefik.enable = true; plex.enable = true; + solidtime.enable = true; }; virtualisation.guest.enable = true; - users.deploy.enable = true; }; networking = { @@ -38,7 +37,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.91"; prefixLength = 24; } ]; @@ -61,8 +60,7 @@ environment = { # NOTE Required # The email address used when setting up the initial administrator account to login to pgAdmin. - # TODO Hugo: Populate 'pgadmin_email' in sops. - PGADMIN_DEFAULT_EMAIL = config.sops.placeholder.pgadmin_email or "pgadmin-admin@example.com"; + PGADMIN_DEFAULT_EMAIL = "kmtl.hugo+pgadmin@gmail.com"; # NOTE Required # The password used when setting up the initial administrator account to login to pgAdmin. PGADMIN_DEFAULT_PASSWORD = "ChangeMe"; diff --git a/hosts/Gitea/default.nix b/hosts/Gitea/default.nix index d6996b2..c6c9b43 100644 --- a/hosts/Gitea/default.nix +++ b/hosts/Gitea/default.nix @@ -3,12 +3,9 @@ { config = { homelab = { - networking.hostIp = "192.168.0.24"; apps.gitea.enable = true; virtualisation.guest.enable = true; - users.deploy.enable = true; - users.admin = { enable = true; authorizedKeys = [ @@ -31,7 +28,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.24"; prefixLength = 24; } ]; diff --git a/hosts/Ingress/default.nix b/hosts/Ingress/default.nix index c16f151..c0a3ac9 100644 --- a/hosts/Ingress/default.nix +++ b/hosts/Ingress/default.nix @@ -2,11 +2,7 @@ { config = { - homelab = { - networking.hostIp = "192.168.0.10"; - virtualisation.guest.enable = true; - users.deploy.enable = true; - }; + homelab.virtualisation.guest.enable = true; networking = { hostName = "Ingress"; @@ -23,8 +19,8 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; - prefixLength = 24; + address = "192.168.0.10"; +prefixLength = 24; } ]; }; @@ -43,7 +39,6 @@ }; }; - security.acme = { acceptTerms = true; defaults = { @@ -51,7 +46,7 @@ dnsPropagationCheck = true; dnsProvider = "cloudflare"; dnsResolver = "1.1.1.1:53"; - email = config.sops.placeholder.acme_email or "acme-email@example.com"; + email = "tibo.depeuter@telenet.be"; credentialFiles = { CLOUDFLARE_DNS_API_TOKEN_FILE = "/var/lib/secrets/depeuter-dev-cloudflare-api-token"; }; diff --git a/hosts/Isabel/default.nix b/hosts/Isabel/default.nix index f275b0e..0a1f50f 100644 --- a/hosts/Isabel/default.nix +++ b/hosts/Isabel/default.nix @@ -165,7 +165,7 @@ providers: # Certificates "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" - "--certificatesresolvers.letsencrypt.acme.email=${config.sops.placeholder.acme_email or "acme-email@example.com"}" + "--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be" "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" # Additional routes @@ -176,8 +176,8 @@ providers: # "8080:8080/tcp" # The Web UI (enabled by --api.insecure=true) ]; environment = { - # TODO Hugo: Populate 'cloudflare_dns_token' in sops. - "CLOUDFLARE_DNS_API_TOKEN" = config.sops.placeholder.cloudflare_dns_token or "CLOUDFLARE_TOKEN_PLACEHOLDER"; + # TODO Hide this! + "CLOUDFLARE_DNS_API_TOKEN" = "6Vz64Op_a6Ls1ljGeBxFoOVfQ-yB-svRbf6OyPv2"; }; environmentFiles = [ ]; diff --git a/hosts/Niko/default.nix b/hosts/Niko/default.nix index c08ccdc..910f325 100644 --- a/hosts/Niko/default.nix +++ b/hosts/Niko/default.nix @@ -7,7 +7,6 @@ ]; homelab = { - networking.hostIp = "192.168.0.11"; apps = { technitiumDNS.enable = true; traefik.enable = true; diff --git a/hosts/Production/default.nix b/hosts/Production/default.nix index a4ebc75..9bb565d 100644 --- a/hosts/Production/default.nix +++ b/hosts/Production/default.nix @@ -3,13 +3,11 @@ { config = { homelab = { - networking.hostIp = "192.168.0.31"; apps = { calibre.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; - users.deploy.enable = true; }; networking = { @@ -33,7 +31,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.31"; prefixLength = 24; } ]; diff --git a/hosts/ProductionArr/default.nix b/hosts/ProductionArr/default.nix index 1168bc8..ff4f4c2 100644 --- a/hosts/ProductionArr/default.nix +++ b/hosts/ProductionArr/default.nix @@ -3,13 +3,11 @@ { config = { homelab = { - networking.hostIp = "192.168.0.33"; apps = { arr.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; - users.deploy.enable = true; }; networking = { @@ -33,7 +31,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.33"; prefixLength = 24; } ]; diff --git a/hosts/ProductionGPU/default.nix b/hosts/ProductionGPU/default.nix index 5f8ad82..fa9ca8c 100644 --- a/hosts/ProductionGPU/default.nix +++ b/hosts/ProductionGPU/default.nix @@ -3,10 +3,8 @@ { config = { homelab = { - networking.hostIp = "192.168.0.94"; apps.jellyfin.enable = true; virtualisation.guest.enable = true; - users.deploy.enable = true; }; networking = { @@ -30,7 +28,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.94"; prefixLength = 24; } ]; diff --git a/hosts/Testing/default.nix b/hosts/Testing/default.nix index cc4efcf..cc353f6 100644 --- a/hosts/Testing/default.nix +++ b/hosts/Testing/default.nix @@ -3,13 +3,11 @@ { config = { homelab = { - networking.hostIp = "192.168.0.92"; apps = { freshrss.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; - users.deploy.enable = true; }; networking = { @@ -34,7 +32,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.92"; prefixLength = 24; } ]; diff --git a/hosts/Vaultwarden/default.nix b/hosts/Vaultwarden/default.nix index b24ef6d..5ded575 100644 --- a/hosts/Vaultwarden/default.nix +++ b/hosts/Vaultwarden/default.nix @@ -3,7 +3,6 @@ { config = { homelab = { - networking.hostIp = "192.168.0.22"; apps.vaultwarden = { enable = true; domain = "https://vault.depeuter.dev"; @@ -11,15 +10,11 @@ }; virtualisation.guest.enable = true; - users = { - deploy.enable = true; - - admin = { - enable = true; - authorizedKeys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJnihoyozOCnm6T9OzL2xoMeMZckBYR2w43us68ABA93" - ]; - }; + users.admin = { + enable = true; + authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJnihoyozOCnm6T9OzL2xoMeMZckBYR2w43us68ABA93" + ]; }; }; @@ -37,7 +32,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = config.homelab.networking.hostIp; + address = "192.168.0.22"; prefixLength = 24; } ]; diff --git a/modules/apps/default.nix b/modules/apps/default.nix index f62dca7..385f915 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -9,6 +9,7 @@ ./homepage ./jellyfin ./plex + ./solidtime ./speedtest ./technitium-dns ./traefik diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix index 34d52e4..0361bd5 100644 --- a/modules/apps/gitea/default.nix +++ b/modules/apps/gitea/default.nix @@ -496,8 +496,7 @@ in { #FORGEJO__mailer__CLIENT_KEY_FILE = "custom/mailer/key.pem"; # Mail from address, RFC 5322. This can be just an email address, or the # `"Name" ` format. - # TODO Hugo: Populate 'gitea_mailer_from' in sops. - FORGEJO__mailer__FROM = config.sops.placeholder.gitea_mailer_from or "git@example.com"; + FORGEJO__mailer__FROM = ''"${title}" ''; # Sometimes it is helpful to use a different address on the envelope. Set this to use # ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address. #FORGEJO__mailer__ENVELOPE_FROM = ""; diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix new file mode 100644 index 0000000..725d32d --- /dev/null +++ b/modules/apps/solidtime/default.nix @@ -0,0 +1,278 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.solidtime; + + networkName = "solidtime"; + internalNetworkName = "solidtime-internal"; + proxyNet = config.homelab.apps.traefiik.sharedNetworkName; + + user = "1000:1000"; + + # dbExternalPort = ...; + dbInternalPort = 5432; + + gotenbergPort = 3000; + + inherit (config.virtualisation.oci-containers) containers; + + solidtimeImageName = "solidtime/solidtime"; + version = "0.10.0"; + solidtimeImage = "${solidtimeImageName}:${version}"; + solidtimeImageFile = pkgs.dockerTools.pullImage { + imageName = solidtimeImageName; + finalImageTag = version; + imageDigest = "sha256:817d3a366ecc39f0473d7154372afa82dd4e6e50c66d70be45804892c8421cbb"; + sha256 = "sha256-h5aCKaquUF/EVsOHaLOHrn1HAoXZYPhAbJ+e4cmjSA8="; + }; + + volumes = [ + "solidtime-storage:/var/www/html/storage" + "solidtime-logs:/var/www/html/storage/logs" + "solidtime-app:/var/www/html/storage/app" + ]; + + # laravel.env + laravelEnv = { + APP_NAME = "Solidtime"; + VITE_APP_NAME = laravelEnv.APP_NAME; + APP_ENV = "production"; + APP_DEBUG = "false"; + APP_URL = "http://localhost:${toString cfg.port}"; + APP_FORCE_HTTPS = "false"; + APP_ENABLE_REGISTRATION = "false"; + TRUSTED_PROXIES = "0.0.0.0/0,2000:0:0:0:0:0:0:0/3"; + + # Logging + LOG_CHANNEL = "stderr_daily"; + LOG_LEVEL = "debug"; + + # Database + DB_CONNECTION = "pgsql"; + DB_HOST = containers.solidtimeDb.hostname; + DB_PORT = toString dbInternalPort; + DB_SSL_MODE = "require"; + DB_DATABASE = "solidtime"; + DB_USERNAME = "solidtime"; + DB_PASSWORD = "ChangeMe"; + + # Mail + #MAIL_MAILER = "smtp"; + #MAIL_HOST = "smtp.gmail.com"; + #MAIL_PORT = "465"; + #MAIL_ENCRYPTION = "tls"; + #MAIL_FROM_ADDRESS = "no-reply@time.depeuter.dev"; + MAIL_FROM_NAME = laravelEnv.APP_NAME; + #MAIL_USERNAME = "kmtl.hugo@gmail.com"; + #MAIL_PASSWORD = "fhfxoequhhqidrhd"; + + # Queue + QUEUE_CONNECTION = "database"; + + # File storage + FILESYSTEM_DISK = "local"; + PUBLIC_FILESYSTEM_DISK = "public"; + + # Services + GOTENBERG_URL = "http://${containers.solidtimeGotenberg.hostname}:${toString gotenbergPort}"; + }; + +in { + options.homelab.apps.solidtime = { + enable = lib.mkEnableOption "Solidtime time tracker using Docker"; + port = lib.mkOption { + type = lib.types.int; + default = 8000; + description = "Solidtime WebUI port"; + }; + exposePort = lib.mkEnableOption "Expose Soldtime port"; + }; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + # Make sure the Docker network exists. + systemd.services = { + "docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "${containers.solidtime.serviceName}.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + "docker-${internalNetworkName}-create-network" = { + description = "Create Docker network for ${internalNetworkName}"; + requiredBy = [ + "${containers.solidtime.serviceName}.service" + "${containers.solidtimeScheduler.serviceName}.service" + "${containers.solidtimeQueue.serviceName}.service" + "${containers.solidtimeDb.serviceName}.service" + "${containers.solidtimeGotenberg.serviceName}.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${internalNetworkName}; then + ${pkgs.docker}/bin/docker network create ${internalNetworkName} + fi + ''; + }; + }; + + virtualisation.oci-containers.containers = { + solidtime = { + hostname = "solidtime"; + image = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; + ports = [ + # Open ports if you don't use Traefik + "${toString cfg.port}:8000" + ]; + networks = [ + networkName + internalNetworkName + ]; + extraOptions = [ + # Healthecks + # test: [ "CMD", "curl", "--fail", "http://localhost:8000/health-check/up" ] + ''--health-cmd=curl --fail http://localhost:8000/health-check/up'' + ]; + inherit volumes; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.solidtime.rule" = "Host(`time.${config.networking.hostName}.depeuter.dev`)"; + "traefik.http.services.solidtime.loadbalancer.server.port" = toString cfg.port; + }; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "http"; + }; + }; + solidtimeScheduler = { + hostname = "scheduler"; + image = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' + ]; + inherit volumes; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "scheduler"; + }; + }; + solidtimeQueue = { + hostname = "queue"; + image = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "worker"; + WORKER_COMMAND = "php /var/www/html/artisan queue:work"; + }; + }; + solidtimeDb = let + imageName = "postgres"; + finalImageTag = "15"; + in { + hostname = "database"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:98fe06b500b5eb29e45bf8c073eb0ca399790ce17b1d586448edc4203627d342"; + sha256 = "sha256-AZ4VkOlROX+nR/MjDjsA4xdHzmtKjiBAtsp2Q6IdOvg="; + }; + autoStart = true; + ports = [ + # "${toString dbExternalPort}:${toString dbInternalPort}" + ]; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: - CMD - pg_isready - '-q' - '-d' - '${DB_DATABASE}' - '-U' - '${DB_USERNAME}' retries: 3 timeout: 5s + ''--health-cmd="pg_isready -q -d ${laravelEnv.DB_DATABASE} -U ${laravelEnv.DB_USERNAME}"'' + "--health-retries=3" + "--health-timeout=5s" + ]; + volumes = [ + "solidtime-db:/var/lib/postgresql/data" + ]; + environment = { + PGPASSWORD = laravelEnv.DB_PASSWORD; + POSTGRES_DB = laravelEnv.DB_DATABASE; + POSTGRES_USER = laravelEnv.DB_USERNAME; + POSTGRES_PASSWORD = laravelEnv.DB_PASSWORD; + }; + }; + solidtimeGotenberg = let + imageName = "gotenberg/gotenberg"; + finalImageTag = "8.26.0"; + in { + hostname = "gotenberg"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:328551506b3dec3ff6381dd47e5cd72a44def97506908269e201a8fbfa1c12c0"; + sha256 = "sha256-1zz4xDAgXxHUnkCVIfjHTgXb82EFEx+5am6Cu9+eZj4="; + }; + autoStart = true; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "curl", "--silent", "--fail", "http://localhost:3000/health" ] + ''--health-cmd="curl --silent --fail http://localhost:${toString gotenbergPort}/health"'' + ]; + }; + }; + }; +} + diff --git a/modules/apps/traefik/default.nix b/modules/apps/traefik/default.nix index 54588c1..7f6ce38 100644 --- a/modules/apps/traefik/default.nix +++ b/modules/apps/traefik/default.nix @@ -72,7 +72,7 @@ in { # Certificates "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" - "--certificatesresolvers.letsencrypt.acme.email=${config.sops.placeholder.acme_email or "acme-email@example.com"}" + "--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be" "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ]; volumes = [ diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix index 6c55d0a..4510299 100644 --- a/modules/apps/vaultwarden/default.nix +++ b/modules/apps/vaultwarden/default.nix @@ -13,12 +13,12 @@ in { description = "Vaultwarden WebUI port"; }; domain = lib.mkOption { - type = lib.types.str; + type = lib.types.string; example = "https://vault.depeuter.dev"; description = "Domain to configure Vaultwarden on"; }; name = lib.mkOption { - type = lib.types.str; + type = lib.types.string; example = "Hugo's Vault"; description = "Service name to use for invitations and mail"; }; @@ -77,7 +77,7 @@ in { dataDir = "/data"; in { hostname = "vaultwarden"; - image = "vaultwarden/server:1.35.4-alpine"; + image = "vaultwarden/server:1.34.3-alpine"; autoStart = true; ports = [ "${toString cfg.port}:80/tcp" @@ -344,7 +344,6 @@ in { # ORG_CREATION_USERS=none ## A comma-separated list means only those users can create orgs: # ORG_CREATION_USERS=admin1@example.com,admin2@example.com - # TODO Hugo: Redact org creation users if needed. ## Invitations org admins to invite users, even when signups are disabled # INVITATIONS_ALLOWED=true @@ -591,7 +590,7 @@ in { ## To make sure the email links are pointing to the correct host, set the DOMAIN variable. ## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory SMTP_HOST = "smtp.gmail.com"; - SMTP_FROM = config.sops.placeholder.vaultwarden_smtp_from or "vaultwarden@example.com"; + SMTP_FROM = "vault@depeuter.dev"; SMTP_FROM_NAME = cfg.name; # SMTP_USERNAME=username # SMTP_PASSWORD=password diff --git a/modules/common/default.nix b/modules/common/default.nix index e8c60a6..44309f5 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -1,9 +1,4 @@ { - imports = [ - ./networking.nix - ./secrets.nix - ]; - config = { homelab = { services.openssh.enable = true; diff --git a/modules/common/networking.nix b/modules/common/networking.nix deleted file mode 100644 index 837684e..0000000 --- a/modules/common/networking.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ config, lib, ... }: - -{ - options.homelab.networking = { - hostIp = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = '' - The primary IP address of the host. - Used for automated deployment and internal service discovery. - ''; - }; - }; - - config = lib.mkIf (config.homelab.networking.hostIp != null) { - # If a hostIp is provided, we can potentially use it to configure - # networking interfaces or firewall rules automatically here in the future. - }; -} diff --git a/modules/common/secrets.nix b/modules/common/secrets.nix deleted file mode 100644 index 10b6473..0000000 --- a/modules/common/secrets.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ config, lib, ... }: - -{ - sops.secrets = { - # -- User Public Keys (Anti-Fingerprinting) -- - "user_keys_admin" = { neededForUsers = true; }; - "user_keys_deploy" = { neededForUsers = true; }; - "user_keys_backup" = { neededForUsers = true; }; - - # -- Infrastructure Metadata -- - # Hugo TODO: Populate these in your .sops.yaml / secrets file - "acme_email" = {}; - "cloudflare_dns_token" = {}; - "pgadmin_email" = {}; - "gitea_mailer_from" = {}; - "vaultwarden_smtp_from" = {}; - }; -} diff --git a/users/admin/default.nix b/users/admin/default.nix index 14766da..dc01c81 100644 --- a/users/admin/default.nix +++ b/users/admin/default.nix @@ -26,9 +26,7 @@ in { config.users.groups.wheel.name # Enable 'sudo' for the user. ]; initialPassword = "ChangeMe"; - openssh.authorizedKeys.keyFiles = [ - config.sops.secrets.user_keys_admin.path - ]; + openssh.authorizedKeys.keys = cfg.authorizedKeys; packages = with pkgs; [ curl git diff --git a/users/backup/default.nix b/users/backup/default.nix index 8c20374..acae033 100644 --- a/users/backup/default.nix +++ b/users/backup/default.nix @@ -12,8 +12,9 @@ in { extraGroups = [ "docker" # Allow access to the docker socket. ]; - openssh.authorizedKeys.keyFiles = [ - config.sops.secrets.user_keys_backup.path + openssh.authorizedKeys.keys = [ + # Hugo + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICms6vjhE9kOlqV5GBPGInwUHAfCSVHLI2Gtzee0VXPh" ]; }; }; diff --git a/users/deploy/default.nix b/users/deploy/default.nix index 5c28561..0509d1e 100644 --- a/users/deploy/default.nix +++ b/users/deploy/default.nix @@ -3,19 +3,7 @@ let cfg = config.homelab.users.deploy; in { - options.homelab.users.deploy = { - enable = lib.mkEnableOption "user Deploy"; - - authorizedKeys = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = []; - description = '' - Additional SSH public keys authorized for the deploy user. - The CI runner key should be provided as a base key; personal - workstation keys can be appended here per host or globally. - ''; - }; - }; + options.homelab.users.deploy.enable = lib.mkEnableOption "user Deploy"; config = lib.mkIf cfg.enable { users = { @@ -27,15 +15,12 @@ in { isSystemUser = true; home = "/var/empty"; shell = pkgs.bashInteractive; - openssh.authorizedKeys.keyFiles = [ - config.sops.secrets.user_keys_deploy.path + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" ]; }; }; - # Allow the deploy user to push closures to the nix store - nix.settings.trusted-users = [ "deploy" ]; - security.sudo.extraRules = [ { groups = [