Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
de1ee54b8b
feat(attic): extract attic to service module, add cache host, configure reverse proxy/DNS 2026-03-17 21:46:44 +01:00
ccfa328771
refactor(security): migrate hardcoded credentials and SSH keys to sops-nix 2026-03-17 21:45:56 +01:00
cbb70ab8bb
chore(agent): add bos55-nix-config skill and style rules 2026-03-17 21:44:54 +01:00
5a031b48ed
fix(vaultwarden): Update image - fix webui not loading 2026-03-05 20:26:33 +01:00
47245b2b96
flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/544961dfcce86422ba200ed9a0b00dd4b1486ec5?narHash=sha256-EVAqOteLBFmd7pKkb0%2BFIUyzTF61VKi7YmvP1tw4nEw%3D' (2025-10-15)
  → 'github:NixOS/nixpkgs/80bdc1e5ce51f56b19791b52b2901187931f5353?narHash=sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN%2BUHzW1jc%3D' (2026-03-04)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/ab8d56e85b8be14cff9d93735951e30c3e86a437?narHash=sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E%3D' (2025-10-13)
  → 'github:Mic92/sops-nix/1d9b98a29a45abe9c4d3174bd36de9f28755e3ff?narHash=sha256-hmIvE/slLKEFKNEJz27IZ8BKlAaZDcjIHmkZ7GCEjfw%3D' (2026-03-02)
2026-03-05 20:17:50 +01:00
c04bce06b6
feat: Test Action
Including commits:
* chore: Disable test workflow
* Determine hosts
* Build each host
* Add NixOS to build step as well
* More specific hostnames
* fix json elements
* try different way
* Debug matrix
* run on push
* use var directly
* fix mappings
* fix mappings
* add toolcache
* Change names and ordere
* Debugging
* Debugging extra step
* Debugging needs outputs
* Debugging needs outputs hosts
* Preserve quotes
* printf escaped
* Using EOF
* test
* escape?
* without json parse
* hardcoding
* change build command
* Testing
2026-03-05 20:09:05 +01:00
23 changed files with 420 additions and 25 deletions

View file

@ -0,0 +1,39 @@
# Bos55 NixOS Configuration Style Guide
Follow these rules when modifying or extending the Bos55 NixOS configuration.
## 1. Network & IP Management
- **Local Ownership**: Define host IP addresses only within their respective host configuration files (e.g., `hosts/BinaryCache/default.nix`).
- **Dynamic Discovery**: Do NOT use global IP mapping modules. Instead, use inter-host evaluation to resolve IPs and ports at build time:
```nix
# In another host's config
let
bcConfig = inputs.self.nixosConfigurations.BinaryCache.config;
bcIp = (pkgs.lib.head bcConfig.networking.interfaces.ens18.ipv4.addresses).address;
in "http://${bcIp}:8080"
```
## 2. Modular Service Design
- **Encapsulation**: Services must be self-contained. Options like `openFirewall`, `port`, and `enableRemoteBuilder` should live in the service module (`modules/services/<service>/default.nix`).
- **Firewall Responsibility**: The service module is responsible for opening firewall ports (e.g., TCP 8080, SSH 22) based on its own options. Do not open ports manually in host files if the service provides an option.
- **Remote Builders**: If a service like Attic supports remote building, include the `builder` user, trusted-users, and SSH configuration within that module's options.
## 3. Container Networking
- **Discovery by Name**: Host services should connect to their companion containers (e.g., PostgreSQL) using the container name rather than `localhost` or bridge IPs.
- **Host Resolution**: Use `networking.extraHosts` in the service module to map the container name to `127.0.0.1` on the host for seamless traffic routing.
## 4. Secrets Management (sops-nix)
- **Centralized Config**: Fleet-wide `sops-nix` settings (like `defaultSopsFile` and `age.keyFile`) must live in `modules/common/default.nix`.
- **No Hardcoded Paths**: Always use `config.sops.secrets."path/to/secret".path` to reference credentials.
## 5. DNS & DNS Zone Files
- **Serial Increment**: Every change to a Bind9 zone file (e.g., `db.depeuter.dev`) MUST increment the `Serial` number in the SOA record.
- **Specific Domains**: Prefer a single, well-defined domain (e.g., `nix-cache.depeuter.dev`) over multiple aliases or magic values.
## 6. CI/CD Robustness
- **IP-Based Login**: When CI runners (Gitea Actions) need to interact with internal services, use direct IP addresses (e.g., `192.168.0.25`) for login/auth to bypass potential DNS resolution issues in the runner environment.
## 8. Git Workflow & Commits
- **Atomic Commits**: Each commit should represent a single logical change and be easily revertible. Split docs, metadata, and core code changes into separate commits.
- **Conventional Commits**: Use conventional commit messages (e.g., `feat:`, `fix:`, `docs:`, `refactor:`, `ci:`, `meta:`).
- **Branching**: Always work in feature branches and push to origin to create pull requests.

View file

@ -0,0 +1,51 @@
---
name: bos55-nix-config
description: Best practices and codestyle for the Bos55 NixOS configuration project.
---
# Bos55 NixOS Configuration Skill
This skill provides the core principles and implementation patterns for the Bos55 NixOS project. Use this skill when adding new hosts, services, or networking rules.
## Core Principles
### 1. Minimal Hardcoding
- **Host IPs**: Always define IPv4/IPv6 addresses within the host configuration (`hosts/`).
- **Options**: Prefer `lib.mkOption` over hardcoded strings for ports, domain names, and database credentials.
- **Unified Variables**: If a value is shared (e.g., between a PG container and a host service), define a local variable (e.g., `let databaseName = "attic"; in ...`) to ensure consistency.
### 2. Service-Driven Configuration
- **Encapsulation**: Service modules should manage their own firewall rules, users/groups, and SSH settings.
- **Trusted Access**: Use the service module to define `nix.settings.trusted-users` for things like remote builders.
### 3. Build-Time Discovery
- **Inter-Host Evaluation**: To avoid magic values, resolve a host's IP or port by evaluating its configuration in the flake's output:
```nix
bcConfig = inputs.self.nixosConfigurations.BinaryCache.config;
```
- **Domain Deferral**: Client modules should defer their default domain settings from the server module's domain option.
## Implementation Patterns
### Container-Host Connectivity
- **Pattern**: `Service` on host -> `Container` via bridge mapping.
- **Rule**: Map the container name to `127.0.0.1` using `networking.extraHosts` to allow the host service to resolve the container by name without needing the bridge IP.
### Secrets Management
- **Rule**: Standardize all secrets via `sops-nix`.
- **Common Module**: Ensure `modules/common/default.nix` handles the default `sopsFile` and `age` key configuration.
### Bind9 Management
- **Rule**: **ALWAYS** increment the serial when editing zone records.
### CI/CD Networking
- **Rule**: Use direct IPs for machine-to-machine login steps in Actions workflows to ensure reliability across different runner environments.
## 4. Security & Documentation
- **Supply Chain Protection**: Always verify and lock Nix flake inputs. Use fixed-output derivations for external resource downloads.
- **Assumptions Documentation**: Clearly document environment assumptions (e.g., Proxmox virtualization, Tailscale networking, and specific IP ranges) in host or service READMEs.
- **Project Structure**: Maintain the separation of `hosts`, `modules`, `users`, and `secrets` to ensure clear ownership and security boundaries.
### 5. Git Standards
- **Rule**: Follow **Conventional Commits** (e.g., `feat:`, `refactor:`, `docs:`, `meta:`).
- **Rule**: Keep commits **atomic** and **revertible**. Never mix documentation, infrastructure, and style guide changes in a single commit.

43
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: "Build"
on:
pull_request:
push:
jobs:
determine-hosts:
name: "Determining hosts to build"
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-24.04
outputs:
hosts: ${{ steps.hosts.outputs.hostnames }}
steps:
- uses: actions/checkout@v5
- uses: https://github.com/cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: "Determine hosts"
id: hosts
run: |
hostnames="$(nix eval .#nixosConfigurations --apply builtins.attrNames --json)"
printf "hostnames=%s\n" "${hostnames}" >> "${GITHUB_OUTPUT}"
build:
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-24.04
needs: determine-hosts
strategy:
matrix:
hostname: [
Development,
Testing
]
steps:
- uses: actions/checkout@v5
- uses: https://github.com/cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: "Build host"
run: |
nix build ".#nixosConfigurations.${{ matrix.hostname }}.config.system.build.toplevel" --verbose

17
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,17 @@
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

1
.gitignore vendored
View file

@ -1 +1,2 @@
.idea .idea
result

12
flake.lock generated
View file

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1760524057, "lastModified": 1772624091,
"narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=", "narHash": "sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN+UHzW1jc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5", "rev": "80bdc1e5ce51f56b19791b52b2901187931f5353",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -48,11 +48,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760393368, "lastModified": 1772495394,
"narHash": "sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E=", "narHash": "sha256-hmIvE/slLKEFKNEJz27IZ8BKlAaZDcjIHmkZ7GCEjfw=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "ab8d56e85b8be14cff9d93735951e30c3e86a437", "rev": "1d9b98a29a45abe9c4d3174bd36de9f28755e3ff",
"type": "github" "type": "github"
}, },
"original": { "original": {

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

@ -13,12 +13,12 @@ in {
description = "Vaultwarden WebUI port"; description = "Vaultwarden WebUI port";
}; };
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.string; type = lib.types.str;
example = "https://vault.depeuter.dev"; example = "https://vault.depeuter.dev";
description = "Domain to configure Vaultwarden on"; description = "Domain to configure Vaultwarden on";
}; };
name = lib.mkOption { name = lib.mkOption {
type = lib.types.string; type = lib.types.str;
example = "Hugo's Vault"; example = "Hugo's Vault";
description = "Service name to use for invitations and mail"; description = "Service name to use for invitations and mail";
}; };
@ -77,7 +77,7 @@ in {
dataDir = "/data"; dataDir = "/data";
in { in {
hostname = "vaultwarden"; hostname = "vaultwarden";
image = "vaultwarden/server:1.34.3-alpine"; image = "vaultwarden/server:1.35.4-alpine";
autoStart = true; autoStart = true;
ports = [ ports = [
"${toString cfg.port}:80/tcp" "${toString cfg.port}:80/tcp"
@ -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
]; ];
}; };
}; };