Compare commits

..

3 commits

Author SHA1 Message Date
de1ee54b8b
feat(attic): extract attic to service module, add cache host, configure reverse proxy/DNS
Some checks failed
Build / build (Development) (push) Has been cancelled
Build / Determining hosts to build (push) Failing after 11m22s
Build / build (Testing) (push) Has been cancelled
2026-03-17 21:46:44 +01:00
ccfa328771
refactor(security): migrate hardcoded credentials and SSH keys to sops-nix
Some checks failed
Build / build (Development) (push) Has been cancelled
Build / Determining hosts to build (push) Failing after 13m25s
Build / build (Testing) (push) Has been cancelled
2026-03-17 21:45:56 +01:00
cbb70ab8bb
chore(agent): add bos55-nix-config skill and style rules
Some checks failed
Build / build (Development) (push) Has been cancelled
Build / Determining hosts to build (push) Failing after 14m25s
Build / build (Testing) (push) Has been cancelled
2026-03-17 21:44:54 +01:00
17 changed files with 260 additions and 16 deletions

View file

@ -47,6 +47,7 @@
Ingress.modules = [ ./hosts/Ingress ]; Ingress.modules = [ ./hosts/Ingress ];
Gitea.modules = [ ./hosts/Gitea ]; Gitea.modules = [ ./hosts/Gitea ];
Vaultwarden.modules = [ ./hosts/Vaultwarden ]; Vaultwarden.modules = [ ./hosts/Vaultwarden ];
BinaryCache.modules = [ ./hosts/BinaryCache ];
# Production multi-service # Production multi-service
Binnenpost.modules = [ ./hosts/Binnenpost ]; Binnenpost.modules = [ ./hosts/Binnenpost ];

View file

@ -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";
};
}

View file

@ -1,4 +1,4 @@
{ pkgs, ... }: { config, inputs, pkgs, ... }:
{ {
config = { config = {
@ -83,6 +83,14 @@
"traefik.http.routers.hugo.rule" = "Host(`hugo.depeuter.dev`)"; "traefik.http.routers.hugo.rule" = "Host(`hugo.depeuter.dev`)";
"traefik.http.services.hugo.loadbalancer.server.url" = "https://192.168.0.11:444"; "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"; system.stateVersion = "24.05";

View file

@ -3,6 +3,7 @@
{ {
config = { config = {
homelab = { homelab = {
networking.hostIp = "192.168.0.91";
apps = { apps = {
bind9.enable = true; bind9.enable = true;
homepage = { homepage = {
@ -13,6 +14,7 @@
plex.enable = true; plex.enable = true;
}; };
virtualisation.guest.enable = true; virtualisation.guest.enable = true;
users.deploy.enable = true;
}; };
networking = { networking = {
@ -36,7 +38,7 @@
interfaces.ens18 = { interfaces.ens18 = {
ipv4.addresses = [ ipv4.addresses = [
{ {
address = "192.168.0.91"; address = config.homelab.networking.hostIp;
prefixLength = 24; prefixLength = 24;
} }
]; ];
@ -59,7 +61,8 @@
environment = { environment = {
# NOTE Required # NOTE Required
# The email address used when setting up the initial administrator account to login to pgAdmin. # 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 # NOTE Required
# The password used when setting up the initial administrator account to login to pgAdmin. # The password used when setting up the initial administrator account to login to pgAdmin.
PGADMIN_DEFAULT_PASSWORD = "ChangeMe"; PGADMIN_DEFAULT_PASSWORD = "ChangeMe";

View file

@ -165,7 +165,7 @@ providers:
# Certificates # Certificates
"--certificatesresolvers.letsencrypt.acme.dnschallenge=true" "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
"--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" "--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" "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
# Additional routes # Additional routes
@ -176,8 +176,8 @@ providers:
# "8080:8080/tcp" # The Web UI (enabled by --api.insecure=true) # "8080:8080/tcp" # The Web UI (enabled by --api.insecure=true)
]; ];
environment = { environment = {
# TODO Hide this! # TODO Hugo: Populate 'cloudflare_dns_token' in sops.
"CLOUDFLARE_DNS_API_TOKEN" = "6Vz64Op_a6Ls1ljGeBxFoOVfQ-yB-svRbf6OyPv2"; "CLOUDFLARE_DNS_API_TOKEN" = config.sops.placeholder.cloudflare_dns_token or "CLOUDFLARE_TOKEN_PLACEHOLDER";
}; };
environmentFiles = [ environmentFiles = [
]; ];

View file

@ -1,6 +1,6 @@
$TTL 604800 $TTL 604800
@ IN SOA ns1 admin ( @ IN SOA ns1 admin (
15 ; Serial 16 ; Serial
604800 ; Refresh 604800 ; Refresh
86400 ; Retry 86400 ; Retry
2419200 ; Expire 2419200 ; Expire
@ -40,6 +40,9 @@ sonarr IN A 192.168.0.33
; Development VM ; Development VM
plex IN A 192.168.0.91 plex IN A 192.168.0.91
; Binary Cache (via Binnenpost proxy)
nix-cache IN A 192.168.0.89
; Catchalls ; Catchalls
*.production IN A 192.168.0.31 *.production IN A 192.168.0.31
*.development IN A 192.168.0.91 *.development IN A 192.168.0.91

View file

@ -496,7 +496,8 @@ in {
#FORGEJO__mailer__CLIENT_KEY_FILE = "custom/mailer/key.pem"; #FORGEJO__mailer__CLIENT_KEY_FILE = "custom/mailer/key.pem";
# Mail from address, RFC 5322. This can be just an email address, or the # Mail from address, RFC 5322. This can be just an email address, or the
# `"Name" <email@example.com>` format. # `"Name" <email@example.com>` format.
FORGEJO__mailer__FROM = ''"${title}" <git@depeuter.dev>''; # 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 # 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. # ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address.
#FORGEJO__mailer__ENVELOPE_FROM = ""; #FORGEJO__mailer__ENVELOPE_FROM = "";

View file

@ -72,7 +72,7 @@ in {
# Certificates # Certificates
"--certificatesresolvers.letsencrypt.acme.dnschallenge=true" "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
"--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" "--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" "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
]; ];
volumes = [ volumes = [

View file

@ -344,6 +344,7 @@ in {
# ORG_CREATION_USERS=none # ORG_CREATION_USERS=none
## A comma-separated list means only those users can create orgs: ## A comma-separated list means only those users can create orgs:
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com # 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 org admins to invite users, even when signups are disabled
# INVITATIONS_ALLOWED=true # INVITATIONS_ALLOWED=true
@ -590,7 +591,7 @@ in {
## To make sure the email links are pointing to the correct host, set the DOMAIN variable. ## 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 ## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
SMTP_HOST = "smtp.gmail.com"; 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_FROM_NAME = cfg.name;
# SMTP_USERNAME=username # SMTP_USERNAME=username
# SMTP_PASSWORD=password # SMTP_PASSWORD=password

View file

@ -1,8 +1,14 @@
{ {
imports = [
./secrets.nix
./substituters.nix
];
config = { config = {
homelab = { homelab = {
services.openssh.enable = true; services.openssh.enable = true;
users.admin.enable = true; users.admin.enable = true;
common.substituters.enable = true;
}; };
nix.settings.experimental-features = [ nix.settings.experimental-features = [
@ -12,5 +18,10 @@
# Set your time zone. # Set your time zone.
time.timeZone = "Europe/Brussels"; time.timeZone = "Europe/Brussels";
sops = {
defaultSopsFile = ../../secrets/secrets.yaml;
age.keyFile = "/var/lib/sops-nix/key.txt";
};
}; };
} }

View file

@ -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" = {};
};
}

View file

@ -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;
};
};
}

View file

@ -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 ];
};
}

View file

@ -1,6 +1,7 @@
{ {
imports = [ imports = [
./actions ./actions
./attic
./openssh ./openssh
]; ];
} }

View file

@ -26,7 +26,9 @@ in {
config.users.groups.wheel.name # Enable 'sudo' for the user. config.users.groups.wheel.name # Enable 'sudo' for the user.
]; ];
initialPassword = "ChangeMe"; initialPassword = "ChangeMe";
openssh.authorizedKeys.keys = cfg.authorizedKeys; openssh.authorizedKeys.keyFiles = [
config.sops.secrets.user_keys_admin.path
];
packages = with pkgs; [ packages = with pkgs; [
curl curl
git git

View file

@ -12,9 +12,8 @@ in {
extraGroups = [ extraGroups = [
"docker" # Allow access to the docker socket. "docker" # Allow access to the docker socket.
]; ];
openssh.authorizedKeys.keys = [ openssh.authorizedKeys.keyFiles = [
# Hugo config.sops.secrets.user_keys_backup.path
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICms6vjhE9kOlqV5GBPGInwUHAfCSVHLI2Gtzee0VXPh"
]; ];
}; };
}; };

View file

@ -15,8 +15,8 @@ in {
isSystemUser = true; isSystemUser = true;
home = "/var/empty"; home = "/var/empty";
shell = pkgs.bashInteractive; shell = pkgs.bashInteractive;
openssh.authorizedKeys.keys = [ openssh.authorizedKeys.keyFiles = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" config.sops.secrets.user_keys_deploy.path
]; ];
}; };
}; };