Commits
Completes the dendritic ilx library begun in a2fb8af.
- Server/infra aspects: server-base, web-gateway, nfs-{server,client},
monitoring, infra-networking, harden-{audit,disk,fail2ban,kernel,
no-defaults,noexec,sshd,sudo,usbguard}, restic-backup, postgres-server,
impermanence, mosquitto, zigbee2mqtt, atticd, auto-upgrade, dns, git-ssh,
locale, nix-defaults.
- Restore the treefmt formatter dropped in the conversion: re-add treefmt-nix
+ modules/formatter.nix (nixfmt + yamlfmt), and switch dendritic.nix to
flake-file's .dendritic flakeModule. It was .default, which regenerated a
broken `outputs = import ./outputs.nix` and failed check-flake-file.
- statix (empty-pattern, inherit) and nixfmt clean across all aspects.
- Drop the empty, unreferenced overlays/default.nix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the plain HM/NixOS/darwin module tree with a flake-parts + denful/den dendritic flake that exports the reusable `ilx` aspect namespace (flake.denful.ilx). Each aspect is a configured concern: helix is editor-base only with languages (cpp/nix/rust/python/harper) owning their toolchain + LSP; editors own their dotfiles; shells carry the tools their config references; cask/masApp apps are atomic and compose ilx.homebrew; ghostty/dock/desktoppr merge cask + config. Generic Determinate daemon policy lives in ilx.determinate (no FlakeHub input, so the producer stays sandbox-evaluable). The pre-den plain modules remain on the `noden` branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hosts using infra.profiles.networking are static-IPv4-only with no IPv6. Add
accept_ra=2 + use_tempaddr=0 on the LAN interface so each host SLAACs a GUA +
default route from its VLAN's prefix-delegated /64. =2 (not the default 1)
because a forwarding host (maloja runs the microVM bridge) disables RA
acceptance at =1; =2 accepts it even with forwarding on and is a harmless no-op
where no RA is present. mkDefault so a host can override.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
promtail's client URL was always built as http://<lokiHost>:<loki.port>/...
Add an optional lokiUrl that, when set, is used verbatim — letting a consumer
push to e.g. https://loki.<domain>/loki/api/v1/push through Caddy on 443 instead
of dialing the Loki port directly. lokiHost becomes nullable; an assertion
requires exactly one of lokiHost / lokiUrl when promtail is enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Repo guidance is consolidated into the workspace docs/repos.md, the
single source of truth across the three-repo setup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
services.fail2ban was previously inlined only in nix-config's moesa
host. Lift the pattern into a reusable hardening module so every NixOS
host opts in the same way (infra.profiles.fail2ban.enable = true).
The profile preserves moesa's prior settings (maxretry 5, 1h base
bantime with exponential multipliers capped at 168h, overalljails on)
and reads the always-ignored LAN range from
config.infra.network.ipv4IpRange rather than hardcoding it.
Also add the new file to the nixosModules.hardening bundle so future
hosts adopting the bundle inherit the option automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added speculatively in abbb737 for invariant predicates to filter hosts
by class. In practice every predicate hardcodes the hostname it cares
about, so the enum was never read. Removing reduces fleet-wide friction
(every new host had to declare class) without losing any behavior.
Adds a required host classification (no default) so consumers can scope
fleet-level invariants and policy to the right host shape. Values cover
the current consumer fleet:
- nixos-server, nixos-pi, nixos-cache, nixos-microvm, nixos-iso
- darwin-workstation
Option is read by invariant test harnesses; system builds don't depend
on it.
The writeShellScriptBin wrapper added nothing over the documented
nix run nixpkgs#{deadnix,statix} invocation and copied the whole tree
into the store on every run. Pre-commit hooks already run both tools.
Add two convenience bundles so consumers can import a single name
instead of listing individual modules:
- homeManagerModules.dev-tools: CLI, editor, shell, vcs programs
(git, fish, helix, direnv, fzf, lazygit, zsh)
- homeManagerModules.desktop: i3-based Linux desktop programs
(i3, alacritty, rofi, dunst, polybar, i3-status-rust)
Both bundles pull in infra-options so the global_variables option
declarations are always present.
Replace lib.mkForce with lib.mkDefault for AllowUsers so the value
from infra.profiles.sshd.allowUsers is a default that consumers can
override at normal priority rather than requiring a second mkForce
to beat the hardening module.
Add infra.profiles.monitoring.unboundSocketPath option (default:
/var/run/unbound/unbound.sock) so the monitoring module does not
require config.services.unbound to be evaluated on the same host.
Consumers that override the socket path can now set it explicitly
instead of relying on the unbound module being present in the same
config evaluation.
Move lint tooling out of checks into packages.<system>.lint so it
is not pulled in by `nix flake check`. Invoke with `nix run .#lint`.
Also export nixosModules.nixpkgs-config (modules/shared/default.nix)
so consumers can import the shared nixpkgs overlays/config in a
single module reference instead of a direct path import.
Previously lokiHost had to be a hostname key present in
infra.network.ipMap, which resolved to ipMap.<name>.ipv4. That doesn't
work for hosts living off the LAN ipMap — specifically microvm guests
that reach the loki host on a bridge address, not its fleet LAN IP.
Relax the resolution: try ipMap.<lokiHost>.ipv4 first, fall back to
treating the value as a raw IP literal. Same option type (string),
backwards-compatible with all existing callers (moesa, bernina pass
hostname keys).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
infra.services.<name> now carries port + web + storage + identity as
optional facets, replacing the previously-split:
- infra.services.<n>.port (this file)
- infra.storage.services.<n> (storage_locations.nix, deleted)
- infra.users.serviceToUserMap (users.nix, dropped)
web.{host,url} are derived from {subdomain, domain}, so consumers no
longer string-concatenate the URL. Domain itself is a free-form string
set by the deployment-routing module — not an enum, so service routing
(e.g. immich's eventual flip from internal to public) is a one-line
change without false swappability framing.
Storage facet collapses {dataDir, mediaDir, musicDir, stateDir} into
just {base, backupDir?, nfsExport, allowedIpRange}; per-service content
subdirs live in the service's own module, derived locally from base.
Duplicate-port assertion now ignores nulls; duplicate-uid/gid assertions
added (replaces the same check in nix-config's personal_users.nix).
nfs-server module + lib/nfs.nix updated to read from the new shape.
The hourly GitHub Actions cron mirroring from Tangled fails regularly.
Switch to a Tangled spindle workflow that pushes to GitHub on each push
to main. Requires GITHUB_TOKEN secret configured in Tangled repo settings.
Adds a LAN-only counterpart to publicDomain. Consumers route internal
service subdomains and their ACME wildcards under this domain; the
existing publicDomain stays reserved for CF-edge-terminated public
hostnames.
Splitting the two lets internal traffic stop relying on a domain that
also fronts public services, which is the precondition for moving
public auth and photos to dedicated microVMs.
The default GITHUB_TOKEN lacks the workflows scope, so mirror pushes
that touch .github/workflows/ are rejected. Swap to a fine-grained
PAT (MIRROR_TOKEN) with Contents + Workflows write.
Drop the "maloja" default on infra.profiles.monitoring.promtail.lokiHost
so the module carries no reference to any specific deployment. Consumers
must now set the loki host explicitly when enabling promtail (which is
the right ergonomics anyway — the only reason to enable promtail is
because you have a specific loki target in mind).
- modules/nixos/files.nix: never imported, also used builtins.getEnv
"HOME" which is illegal in pure flake evaluation. Stale leftover.
- modules/home-manager/programs/polybar.nix: drop the commented-out
ws-icon-* and label-* overrides that have sat dead since import.
No consumer drv changes (verified across plessur, maloja, bernina,
moesa, work).
No remaining consumer enables infra.git.useAdWrapper. Remove the
adCustomGitPackage shim and the matching option from global_variables.
Consumers that need it can re-add a local override; until then the
hardcoded x86_64-linux LD_PRELOAD path was dead code on every host.
The hardening baseline previously hardcoded PermitRootLogin = "no";
every consumer that wanted key-based root login (typical for remote
deployment via nixos-anywhere or auto-upgrade) had to override it
with lib.mkForce. Add infra.profiles.sshd.permitRootLogin (default
"no") so consumers can switch to "prohibit-password" cleanly.
Shared baseline for nix.settings.allowed-users plus an opt-in
enableGc flag covering automatic nix.gc (weekly, --delete-older-than
3d) and nix.optimise (Sun 03:00) — both extracted from duplicated
bernina/moesa blocks.
All settings use lib.mkDefault so consumers may still override.
Extracts the duplicated system.autoUpgrade pattern from bernina and moesa
into a reusable module with configurable inputsToUpdate, dates, and
randomizedDelaySec options.
Add three reusable modules to remove duplication in nix-config and
work-nix-config consumers:
- darwinModules.nix-homebrew-base: standardizes nix-homebrew wiring
(user from infra.host, mutableTaps=false, autoMigrate=true) with
taps as an attrset option, so consumers only supply flake inputs.
- homeManagerModules.git-sops-signing: wires infra.git.{signingKeyPath,
allowedSignersFile} from a sops-nix secret. Lives at the sibling
option infra.gitSopsSigning (not under infra.git) to avoid an
infinite recursion through the existing git submodule.
- darwinModules.fish-unstable: function module over nixpkgs-unstable
that overlays fish to work around the aarch64-darwin code-signature
bug in nixos-25.11.
All three are gated by mkIf so importing alone is a no-op. Drv-paths
in both consumers are unchanged after adoption.
Add basic CI on main:
- flake-check.yml: runs `nix flake check --all-systems`
- lint.yml: runs `statix check` and `deadnix --fail`
Also fix pre-existing statix W04/W10 findings so lint is clean:
- assignment-instead-of-inherit in infra-networking, sshd hardening,
infra-monitoring (3x)
- empty function pattern `{ ... }:` in darwin/power.nix and
nixos/files.nix (the latter from the recent flakeRoot removal)
Replace consumer-supplied flakeRoot specialArg with relative imports
for in-repo paths. shared_packages.nix now imports scripts.nix via
../shared/scripts.nix, and files.nix references ../../dotfiles/linux/*
directly. Removes a misleading default (flakeRoot ? inputs.self) that
only worked because every caller overrode it.
- Replace github URL with canonical git+https://tangled.org/ilak.ch/nix-modules
- Add infra-networking, infra-monitoring, mosquitto, zigbee2mqtt, persist
to nixosModules list
- Add power to darwinModules list
- Add vim to lib list
- Document the infra.profiles.* namespace introduced by the new modules
Z2M 2.x changed the default secrets file from secrets.yaml to
secret.yaml. Update the preStartScript to match.
nixpkgs zigbee2mqtt module now defaults homeassistant to an attrset
({ enabled = true; ... }), conflicting with our boolean. The default
already enables HA integration, so our explicit setting is unnecessary.
Completes the dendritic ilx library begun in a2fb8af.
- Server/infra aspects: server-base, web-gateway, nfs-{server,client},
monitoring, infra-networking, harden-{audit,disk,fail2ban,kernel,
no-defaults,noexec,sshd,sudo,usbguard}, restic-backup, postgres-server,
impermanence, mosquitto, zigbee2mqtt, atticd, auto-upgrade, dns, git-ssh,
locale, nix-defaults.
- Restore the treefmt formatter dropped in the conversion: re-add treefmt-nix
+ modules/formatter.nix (nixfmt + yamlfmt), and switch dendritic.nix to
flake-file's .dendritic flakeModule. It was .default, which regenerated a
broken `outputs = import ./outputs.nix` and failed check-flake-file.
- statix (empty-pattern, inherit) and nixfmt clean across all aspects.
- Drop the empty, unreferenced overlays/default.nix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the plain HM/NixOS/darwin module tree with a flake-parts + denful/den dendritic flake that exports the reusable `ilx` aspect namespace (flake.denful.ilx). Each aspect is a configured concern: helix is editor-base only with languages (cpp/nix/rust/python/harper) owning their toolchain + LSP; editors own their dotfiles; shells carry the tools their config references; cask/masApp apps are atomic and compose ilx.homebrew; ghostty/dock/desktoppr merge cask + config. Generic Determinate daemon policy lives in ilx.determinate (no FlakeHub input, so the producer stays sandbox-evaluable). The pre-den plain modules remain on the `noden` branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hosts using infra.profiles.networking are static-IPv4-only with no IPv6. Add
accept_ra=2 + use_tempaddr=0 on the LAN interface so each host SLAACs a GUA +
default route from its VLAN's prefix-delegated /64. =2 (not the default 1)
because a forwarding host (maloja runs the microVM bridge) disables RA
acceptance at =1; =2 accepts it even with forwarding on and is a harmless no-op
where no RA is present. mkDefault so a host can override.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
promtail's client URL was always built as http://<lokiHost>:<loki.port>/...
Add an optional lokiUrl that, when set, is used verbatim — letting a consumer
push to e.g. https://loki.<domain>/loki/api/v1/push through Caddy on 443 instead
of dialing the Loki port directly. lokiHost becomes nullable; an assertion
requires exactly one of lokiHost / lokiUrl when promtail is enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
services.fail2ban was previously inlined only in nix-config's moesa
host. Lift the pattern into a reusable hardening module so every NixOS
host opts in the same way (infra.profiles.fail2ban.enable = true).
The profile preserves moesa's prior settings (maxretry 5, 1h base
bantime with exponential multipliers capped at 168h, overalljails on)
and reads the always-ignored LAN range from
config.infra.network.ipv4IpRange rather than hardcoding it.
Also add the new file to the nixosModules.hardening bundle so future
hosts adopting the bundle inherit the option automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a required host classification (no default) so consumers can scope
fleet-level invariants and policy to the right host shape. Values cover
the current consumer fleet:
- nixos-server, nixos-pi, nixos-cache, nixos-microvm, nixos-iso
- darwin-workstation
Option is read by invariant test harnesses; system builds don't depend
on it.
Add two convenience bundles so consumers can import a single name
instead of listing individual modules:
- homeManagerModules.dev-tools: CLI, editor, shell, vcs programs
(git, fish, helix, direnv, fzf, lazygit, zsh)
- homeManagerModules.desktop: i3-based Linux desktop programs
(i3, alacritty, rofi, dunst, polybar, i3-status-rust)
Both bundles pull in infra-options so the global_variables option
declarations are always present.
Add infra.profiles.monitoring.unboundSocketPath option (default:
/var/run/unbound/unbound.sock) so the monitoring module does not
require config.services.unbound to be evaluated on the same host.
Consumers that override the socket path can now set it explicitly
instead of relying on the unbound module being present in the same
config evaluation.
Move lint tooling out of checks into packages.<system>.lint so it
is not pulled in by `nix flake check`. Invoke with `nix run .#lint`.
Also export nixosModules.nixpkgs-config (modules/shared/default.nix)
so consumers can import the shared nixpkgs overlays/config in a
single module reference instead of a direct path import.
Previously lokiHost had to be a hostname key present in
infra.network.ipMap, which resolved to ipMap.<name>.ipv4. That doesn't
work for hosts living off the LAN ipMap — specifically microvm guests
that reach the loki host on a bridge address, not its fleet LAN IP.
Relax the resolution: try ipMap.<lokiHost>.ipv4 first, fall back to
treating the value as a raw IP literal. Same option type (string),
backwards-compatible with all existing callers (moesa, bernina pass
hostname keys).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
infra.services.<name> now carries port + web + storage + identity as
optional facets, replacing the previously-split:
- infra.services.<n>.port (this file)
- infra.storage.services.<n> (storage_locations.nix, deleted)
- infra.users.serviceToUserMap (users.nix, dropped)
web.{host,url} are derived from {subdomain, domain}, so consumers no
longer string-concatenate the URL. Domain itself is a free-form string
set by the deployment-routing module — not an enum, so service routing
(e.g. immich's eventual flip from internal to public) is a one-line
change without false swappability framing.
Storage facet collapses {dataDir, mediaDir, musicDir, stateDir} into
just {base, backupDir?, nfsExport, allowedIpRange}; per-service content
subdirs live in the service's own module, derived locally from base.
Duplicate-port assertion now ignores nulls; duplicate-uid/gid assertions
added (replaces the same check in nix-config's personal_users.nix).
nfs-server module + lib/nfs.nix updated to read from the new shape.
Adds a LAN-only counterpart to publicDomain. Consumers route internal
service subdomains and their ACME wildcards under this domain; the
existing publicDomain stays reserved for CF-edge-terminated public
hostnames.
Splitting the two lets internal traffic stop relying on a domain that
also fronts public services, which is the precondition for moving
public auth and photos to dedicated microVMs.
Drop the "maloja" default on infra.profiles.monitoring.promtail.lokiHost
so the module carries no reference to any specific deployment. Consumers
must now set the loki host explicitly when enabling promtail (which is
the right ergonomics anyway — the only reason to enable promtail is
because you have a specific loki target in mind).
- modules/nixos/files.nix: never imported, also used builtins.getEnv
"HOME" which is illegal in pure flake evaluation. Stale leftover.
- modules/home-manager/programs/polybar.nix: drop the commented-out
ws-icon-* and label-* overrides that have sat dead since import.
No consumer drv changes (verified across plessur, maloja, bernina,
moesa, work).
The hardening baseline previously hardcoded PermitRootLogin = "no";
every consumer that wanted key-based root login (typical for remote
deployment via nixos-anywhere or auto-upgrade) had to override it
with lib.mkForce. Add infra.profiles.sshd.permitRootLogin (default
"no") so consumers can switch to "prohibit-password" cleanly.
Add three reusable modules to remove duplication in nix-config and
work-nix-config consumers:
- darwinModules.nix-homebrew-base: standardizes nix-homebrew wiring
(user from infra.host, mutableTaps=false, autoMigrate=true) with
taps as an attrset option, so consumers only supply flake inputs.
- homeManagerModules.git-sops-signing: wires infra.git.{signingKeyPath,
allowedSignersFile} from a sops-nix secret. Lives at the sibling
option infra.gitSopsSigning (not under infra.git) to avoid an
infinite recursion through the existing git submodule.
- darwinModules.fish-unstable: function module over nixpkgs-unstable
that overlays fish to work around the aarch64-darwin code-signature
bug in nixos-25.11.
All three are gated by mkIf so importing alone is a no-op. Drv-paths
in both consumers are unchanged after adoption.
Add basic CI on main:
- flake-check.yml: runs `nix flake check --all-systems`
- lint.yml: runs `statix check` and `deadnix --fail`
Also fix pre-existing statix W04/W10 findings so lint is clean:
- assignment-instead-of-inherit in infra-networking, sshd hardening,
infra-monitoring (3x)
- empty function pattern `{ ... }:` in darwin/power.nix and
nixos/files.nix (the latter from the recent flakeRoot removal)
Replace consumer-supplied flakeRoot specialArg with relative imports
for in-repo paths. shared_packages.nix now imports scripts.nix via
../shared/scripts.nix, and files.nix references ../../dotfiles/linux/*
directly. Removes a misleading default (flakeRoot ? inputs.self) that
only worked because every caller overrode it.
Z2M 2.x changed the default secrets file from secrets.yaml to
secret.yaml. Update the preStartScript to match.