diff --git a/flake.nix b/flake.nix index 446f4ce..aa18c00 100644 --- a/flake.nix +++ b/flake.nix @@ -47,6 +47,7 @@ Ingress.modules = [ ./hosts/Ingress ]; Gitea.modules = [ ./hosts/Gitea ]; Vaultwarden.modules = [ ./hosts/Vaultwarden ]; + BinaryCache.modules = [ ./hosts/BinaryCache ]; # Production multi-service Binnenpost.modules = [ ./hosts/Binnenpost ]; diff --git a/hosts/BinaryCache/default.nix b/hosts/BinaryCache/default.nix new file mode 100644 index 0000000..e651599 --- /dev/null +++ b/hosts/BinaryCache/default.nix @@ -0,0 +1,49 @@ +{ config, pkgs, lib, system, ... }: + +let + hostIp = "192.168.0.25"; +in { + config = { + homelab = { + services.attic = { + enable = true; + enableRemoteBuilder = true; + openFirewall = true; + }; + virtualisation.guest.enable = true; + }; + + networking = { + hostName = "BinaryCache"; + hostId = "100002500"; + domain = "depeuter.dev"; + + useDHCP = false; + + enableIPv6 = true; + + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + + interfaces.ens18 = { + ipv4.addresses = [ + { + address = hostIp; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + # Sops configuration for this host is now handled by the common module + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Binnenpost/default.nix b/hosts/Binnenpost/default.nix index 561fbe1..e1325ba 100644 --- a/hosts/Binnenpost/default.nix +++ b/hosts/Binnenpost/default.nix @@ -1,4 +1,4 @@ -{ pkgs, ... }: +{ config, inputs, pkgs, ... }: { config = { @@ -83,6 +83,14 @@ "traefik.http.routers.hugo.rule" = "Host(`hugo.depeuter.dev`)"; "traefik.http.services.hugo.loadbalancer.server.url" = "https://192.168.0.11:444"; + + "traefik.http.routers.attic.rule" = "Host(`${inputs.self.nixosConfigurations.BinaryCache.config.homelab.services.attic.domain}`)"; + "traefik.http.services.attic.loadbalancer.server.url" = + let + bcConfig = inputs.self.nixosConfigurations.BinaryCache.config; + bcIp = (pkgs.lib.head bcConfig.networking.interfaces.ens18.ipv4.addresses).address; + bcPort = bcConfig.homelab.services.attic.port; + in "http://${bcIp}:${toString bcPort}"; }; system.stateVersion = "24.05"; diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix index fda8e57..68c5fea 100644 --- a/hosts/Development/default.nix +++ b/hosts/Development/default.nix @@ -3,6 +3,7 @@ { config = { homelab = { + networking.hostIp = "192.168.0.91"; apps = { bind9.enable = true; homepage = { @@ -13,6 +14,7 @@ plex.enable = true; }; virtualisation.guest.enable = true; + users.deploy.enable = true; }; networking = { @@ -36,7 +38,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.91"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; @@ -59,7 +61,8 @@ environment = { # NOTE Required # The email address used when setting up the initial administrator account to login to pgAdmin. - PGADMIN_DEFAULT_EMAIL = "kmtl.hugo+pgadmin@gmail.com"; + # TODO Hugo: Populate 'pgadmin_email' in sops. + PGADMIN_DEFAULT_EMAIL = config.sops.placeholder.pgadmin_email or "pgadmin-admin@example.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/Isabel/default.nix b/hosts/Isabel/default.nix index 0a1f50f..f275b0e 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=tibo.depeuter@telenet.be" + "--certificatesresolvers.letsencrypt.acme.email=${config.sops.placeholder.acme_email or "acme-email@example.com"}" "--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 Hide this! - "CLOUDFLARE_DNS_API_TOKEN" = "6Vz64Op_a6Ls1ljGeBxFoOVfQ-yB-svRbf6OyPv2"; + # TODO Hugo: Populate 'cloudflare_dns_token' in sops. + "CLOUDFLARE_DNS_API_TOKEN" = config.sops.placeholder.cloudflare_dns_token or "CLOUDFLARE_TOKEN_PLACEHOLDER"; }; environmentFiles = [ ]; diff --git a/modules/apps/bind9/db.depeuter.dev b/modules/apps/bind9/db.depeuter.dev index 72f3825..413adfe 100644 --- a/modules/apps/bind9/db.depeuter.dev +++ b/modules/apps/bind9/db.depeuter.dev @@ -1,6 +1,6 @@ $TTL 604800 @ IN SOA ns1 admin ( - 15 ; Serial + 16 ; Serial 604800 ; Refresh 86400 ; Retry 2419200 ; Expire @@ -40,6 +40,9 @@ sonarr IN A 192.168.0.33 ; Development VM plex IN A 192.168.0.91 +; Binary Cache (via Binnenpost proxy) +nix-cache IN A 192.168.0.89 + ; Catchalls *.production IN A 192.168.0.31 *.development IN A 192.168.0.91 diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix index 0361bd5..34d52e4 100644 --- a/modules/apps/gitea/default.nix +++ b/modules/apps/gitea/default.nix @@ -496,7 +496,8 @@ 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. - FORGEJO__mailer__FROM = ''"${title}" ''; + # TODO Hugo: Populate 'gitea_mailer_from' in sops. + FORGEJO__mailer__FROM = config.sops.placeholder.gitea_mailer_from or "git@example.com"; # 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/traefik/default.nix b/modules/apps/traefik/default.nix index 7f6ce38..54588c1 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=tibo.depeuter@telenet.be" + "--certificatesresolvers.letsencrypt.acme.email=${config.sops.placeholder.acme_email or "acme-email@example.com"}" "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ]; volumes = [ diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix index 907dda4..6c55d0a 100644 --- a/modules/apps/vaultwarden/default.nix +++ b/modules/apps/vaultwarden/default.nix @@ -344,6 +344,7 @@ 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 @@ -590,7 +591,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 = "vault@depeuter.dev"; + SMTP_FROM = config.sops.placeholder.vaultwarden_smtp_from or "vaultwarden@example.com"; SMTP_FROM_NAME = cfg.name; # SMTP_USERNAME=username # SMTP_PASSWORD=password diff --git a/modules/common/default.nix b/modules/common/default.nix index 44309f5..dc6cb5f 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -1,8 +1,14 @@ { + imports = [ + ./secrets.nix + ./substituters.nix + ]; + config = { homelab = { services.openssh.enable = true; users.admin.enable = true; + common.substituters.enable = true; }; nix.settings.experimental-features = [ @@ -12,5 +18,10 @@ # Set your time zone. time.timeZone = "Europe/Brussels"; + + sops = { + defaultSopsFile = ../../secrets/secrets.yaml; + age.keyFile = "/var/lib/sops-nix/key.txt"; + }; }; } diff --git a/modules/common/secrets.nix b/modules/common/secrets.nix new file mode 100644 index 0000000..10b6473 --- /dev/null +++ b/modules/common/secrets.nix @@ -0,0 +1,18 @@ +{ 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/modules/common/substituters.nix b/modules/common/substituters.nix new file mode 100644 index 0000000..bb8e564 --- /dev/null +++ b/modules/common/substituters.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, inputs, ... }: + +let + cfg = config.homelab.common.substituters; +in { + options.homelab.common.substituters = { + enable = lib.mkEnableOption "Binary cache substituters"; + domain = lib.mkOption { + type = lib.types.str; + default = inputs.self.nixosConfigurations.BinaryCache.config.homelab.services.attic.domain; + description = "The domain name of the binary cache."; + }; + publicKey = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The public key of the Attic cache (e.g., 'homelab:...')"; + }; + }; + + config = lib.mkIf cfg.enable { + nix.settings = { + substituters = [ + "https://${cfg.domain}" + ]; + trusted-public-keys = lib.optional (cfg.publicKey != null) cfg.publicKey; + }; + }; +} diff --git a/modules/services/attic/default.nix b/modules/services/attic/default.nix new file mode 100644 index 0000000..6241748 --- /dev/null +++ b/modules/services/attic/default.nix @@ -0,0 +1,119 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.services.attic; +in { + options.homelab.services.attic = { + enable = lib.mkEnableOption "Attic binary cache server"; + domain = lib.mkOption { + type = lib.types.str; + default = "nix-cache.depeuter.dev"; + description = "The domain name for the Attic server."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "The port Attic server listens on."; + }; + databaseName = lib.mkOption { + type = lib.types.str; + default = "attic"; + description = "The name of the PostgreSQL database."; + }; + dbContainerName = lib.mkOption { + type = lib.types.str; + default = "attic-db"; + description = "The name of the PostgreSQL container."; + }; + storagePath = lib.mkOption { + type = lib.types.str; + default = "/var/lib/atticd/storage"; + description = "The path where Attic store's its blobs."; + }; + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to open the firewall port for Attic."; + }; + enableRemoteBuilder = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to enable remote build capabilities on this host."; + }; + }; + + config = lib.mkIf cfg.enable { + sops.secrets = { + "attic/db-password" = { }; + "attic/server-token-secret" = { }; + }; + + services.atticd = { + enable = true; + environmentFile = config.sops.secrets."attic/server-token-secret".path; + + settings = { + listen = "[::]:${toString cfg.port}"; + allowed-hosts = [ cfg.domain ]; + api-endpoint = "https://${cfg.domain}/"; + + database.url = "postgresql://${cfg.databaseName}@${cfg.dbContainerName}:5432/${cfg.databaseName}"; + + storage = { + type = "local"; + path = cfg.storagePath; + }; + + chunking = { + min-size = 16384; # 16 KiB + avg-size = 65536; # 64 KiB + max-size = 262144; # 256 KiB + }; + }; + }; + + homelab.virtualisation.containers.enable = true; + + virtualisation.oci-containers.containers."${cfg.dbContainerName}" = { + image = "postgres:15-alpine"; + autoStart = true; + # We still map it to host for Attic (running on host) to connect to it via bridge IP or name + # if we set up networking/DNS correctly. + ports = [ + "5432:5432/tcp" + ]; + environment = { + POSTGRES_USER = cfg.databaseName; + POSTGRES_PASSWORD_FILE = config.sops.secrets."attic/db-password".path; + POSTGRES_DB = cfg.databaseName; + }; + volumes = [ + "attic-db:/var/lib/postgresql/data" + ]; + }; + + # Map the container name to localhost if Attic is on the host + networking.extraHosts = '' + 127.0.0.1 ${cfg.dbContainerName} + ''; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; + + # Remote build host configuration + nix.settings.trusted-users = lib.mkIf cfg.enableRemoteBuilder [ "root" "@wheel" "builder" ]; + + users.users.builder = lib.mkIf cfg.enableRemoteBuilder { + isNormalUser = true; + group = "builder"; + openssh.authorizedKeys.keys = [ + # Placeholders - user should provide actual keys + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFrp6aM62Bf7bj1YM5AlAWuNrANU3N5e8+LtbbpmZPKS" + ]; + }; + users.groups.builder = lib.mkIf cfg.enableRemoteBuilder {}; + + # Only open SSH if remote builder is enabled + services.openssh.ports = lib.mkIf cfg.enableRemoteBuilder [ 22 ]; + networking.firewall.allowedTCPPorts = lib.mkIf cfg.enableRemoteBuilder [ 22 ]; + }; +} diff --git a/modules/services/default.nix b/modules/services/default.nix index f70bc54..81e0042 100644 --- a/modules/services/default.nix +++ b/modules/services/default.nix @@ -1,6 +1,7 @@ { imports = [ ./actions + ./attic ./openssh ]; } diff --git a/users/admin/default.nix b/users/admin/default.nix index dc01c81..14766da 100644 --- a/users/admin/default.nix +++ b/users/admin/default.nix @@ -26,7 +26,9 @@ in { config.users.groups.wheel.name # Enable 'sudo' for the user. ]; initialPassword = "ChangeMe"; - openssh.authorizedKeys.keys = cfg.authorizedKeys; + openssh.authorizedKeys.keyFiles = [ + config.sops.secrets.user_keys_admin.path + ]; packages = with pkgs; [ curl git diff --git a/users/backup/default.nix b/users/backup/default.nix index acae033..8c20374 100644 --- a/users/backup/default.nix +++ b/users/backup/default.nix @@ -12,9 +12,8 @@ in { extraGroups = [ "docker" # Allow access to the docker socket. ]; - openssh.authorizedKeys.keys = [ - # Hugo - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICms6vjhE9kOlqV5GBPGInwUHAfCSVHLI2Gtzee0VXPh" + openssh.authorizedKeys.keyFiles = [ + config.sops.secrets.user_keys_backup.path ]; }; }; diff --git a/users/deploy/default.nix b/users/deploy/default.nix index 0509d1e..93505fc 100644 --- a/users/deploy/default.nix +++ b/users/deploy/default.nix @@ -15,8 +15,8 @@ in { isSystemUser = true; home = "/var/empty"; shell = pkgs.bashInteractive; - openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" + openssh.authorizedKeys.keyFiles = [ + config.sops.secrets.user_keys_deploy.path ]; }; };