Skip to content

LoganBarnett/dotfiles

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3,453 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nix

Here I store my notes regarding Nix as it pertains to this repository. This will include some findings and also projects I’m working on. I do have separate notes for Nix in my private notes repository, and I haven’t yet come up with a method of consolidating them.

This helps me close the many tabs I have open. This is very incomplete and I have many more tabs to go. I think I like this as a general “wut I do in Nix” scratch pad though.

creating new hosts

create Nix host file

Look up the next element in the periodic table, and then create that under nix/hosts.

deploy tools

proton-deploy is vastly preferred for all deployments. It uses Nix’s copy-closure mechanism to transfer store paths to the remote host securely.

remote-deploy is a break-glass script for working around limitations of the copy-closure mechanism. Known cases where it may be necessary are documented in the troubleshooting section. After using remote-deploy to recover, return to proton-deploy for subsequent deployments.

remote-deploy works by rsyncing the entire repo to ~~/proton-nix~ on the remote host. Sensitive files from the repo may be present there. By default, ~~/proton-nix~ is removed after deployment completes. Pass --no-cleanup to retain it.

by machine type

Instructions across various kinds of machines are not consistent due to bootstrapping. See also All Machine Types.

BIOS systems

Not a lot must be done here to accommodate BIOS, since a bootable partition is simply bootable. There might have to be some adjustments made in the BIOS settings. Despite how simple this process is, it is essentially dead already. New systems will use UEFI and thus see UEFI systems.

UEFI systems

Unfortunately you cannot create a bootable image and simply slap it into a UEFI system, because that UEFI system has to be told where to boot. I’ve read that one can simply create a boot partition and put certain files in the right place. Nix does this, or can be taught to do this. In any case, I have verified the magic files went into the magic locations, but still no joy.

Instead, boot into the system using the detachable USB drive. The host is called nucleus.proton on boot. From there, you should be able to do a remote deployment. Unfortunately proton-deploy does not accommodate this activity yet. Until that feature is supported, you can use remote-deploy --enter-via nucleus.proton .... It would be desirable to use proton-deploy though, due to security concerns.

Once the machine state has been written, remove the USB drive and reboot the system.

Raspberry Pi

The Raspberry Pi uses its own proprietary boot system. Sometimes it can use something called uboot, which may or may not be proprietary.

modules

initial deployment steps

For these, use the following invocation:

rpi-host-new host

But, you know, change host.

Once that’s complete, use image-deploy thusly, with your SD card plugged in:

image-deploy --image result/sd-image/*.img.zst

Once it’s done, and the partition table looks fleshed out, remove the SD card (no eject/unmount needed). Plug it into the Pi and boot the Pi.

agenix has some trouble with this configuration and needs some help getting bootstrapped. Use proton-deploy to the image to help it lay down everything needed. I though I read this needed a reboot, but I was incorrect about that - the reboot does nothing.

Part of the problem is that the host key that was laid down differs from the one used in the initial build. I haven’t figured out how to get around this yet, and from my recollection of reading oddllama’s dotfiles, it’s not possible yet.

So scan the host key into the right location:

host='host'; ssh-keyscan $host.proton | grep -o 'ssh-ed25519.*' > secrets/$host-pub-key.pub

A quick git status will show the host pub key has changed.

agenix rekey -a

Then do another proton-deploy switch $host. This should fix everything.

I don’t think this works actually. I get:

$ proton-deploy switch bromine

building the system configuration...
warning: Git tree '/Users/logan/dev/dotfiles' is dirty
[1/0/3063 copied (1.1/9.4 MiB)] copying path '/nix/store/rp3ac35r3pfb3fqh3rdqzlzsq67jpnws-source' to 'ssh://bromine.proton'error: cannot add path '/nix/store/rp3ac35r3pfb3fqh3rdqzlzsq67jpnws-source' because it lacks a signature by a trusted key
error (ignored): error: writing to file: Broken pipe
error (ignored): error: writing to file: Broken pipe
error: unexpected end-of-file

But using remote-deploy $host works fine.

I recently added my deploy user to the trusted users list, so let’s see if that helps. Why this is an issue for the initial deploy but not after a local switch is done is not well understood.

Hey that seemed to work as of [2025-09-13 Sat].

To help bootstrap the system, it needs the remote builder key so it can utilize the rpi-build host.

host='rpi-installer'
sudo cat /run/agenix/builder-key \
  | ssh $host.proton 'sudo tee /run/agenix/builder-key' \
  > /dev/null
ssh $host.proton 'sudo chmod 400 /run/agenix/builder-key'

Container Guests

host='my-host'; ssh-keygen -t ed25519 -N "" -f secrets/$host-pub-key

All Machine Types

You will need to deploy to nickel to have it pull in the new host for both its DNS settings, DHCP, and Prometheus exporting.

public key

You’ll need a public key to do rekeying of secrets. Unfortunately due to bootstrapping, you’ll need to generate a dummy key.

host='host'; ssh-keygen -t ed25519 -N "" -f secrets/$host-pub-key
git add secrets/$host-pub-key*
host='host'; ssh-keyscan $host.proton | grep -o 'ssh-ed25519.*' > secrets/$host-pub-key.pub

devices

This covers miscellaneous devices on the network which need special instruction or understanding.

Aruba 6300M

Sometimes the windfall is pretty good!

credentials and connection

The user name is admin, and the password can be found at ./secrets/aruba-admin-pass.age.

If the machine is reset, the username is still admin.

Don’t use screen, since it either has no options to make the serial connection work properly, or I don’t know of them. picocom does the trick though. Connect thusly:

picocom --baud 115200 --flow none /dev/tty.usbmodemM1230887551

Press enter to make it prompt you for credentials.

If you idle too long, it will log you out.

Configuring

Coming soon!

Topology

Public/Private Service Split

All services, public and private, run on a single physical host (silicon) with a single network interface. Rather than exposing the host’s primary IP to the internet, a secondary IP address is bound to the same interface and the router is configured to port-forward external traffic only to that address.

The two addresses serve completely separate roles:

AddressRoleReachable fromCertificate source
192.168.254.9PrivateLAN onlyInternal CA
192.168.254.100PublicLAN + InternetACME (DNS-01)

nginx listens on each address independently. A virtual host bound to the private address is unreachable from the internet even if the router were misconfigured, because an nftables rule drops any packet arriving from a non-RFC-1918 source address destined for port 443 on .9. The rule runs at a higher priority than the default NixOS firewall chain, so it takes effect before any ACCEPT rule could apply.

The port-forward mapping on the router is maintained automatically via UPnP. A systemd timer fires shortly after boot and every twelve hours thereafter, re-registering the mapping so it survives router reboots.

The diagram below illustrates the traffic paths. The named source block is tangled to docs/topology.d2; the two shell blocks that follow it regenerate the light and dark SVGs in docs/ — execute those to refresh the diagram after any topology change.

Nix

Rapid Package Updates

Some packages need to be updated independently of nixpkgs — for example, to pick up a bug fix that landed upstream before the pinned nixpkgs revision caught up. Bumping all of nixpkgs for a single package is risky, so instead we pin just the affected package’s version and hash in static.nix and override it in an overlay.

The three parts of this pattern are:

static.nix
Holds the version and hash for each pinned package. Both fields are updated together so the overlay always sees a consistent pair.
overlays/<pkg>.nix
Calls overrideAttrs on the nixpkgs derivation, replacing version and src with the values from static.nix. The overlay is wired into overlays/default.nix.
scripts/<pkg>-update
A shell script that fetches the latest upstream version, prefetches its hash with nix-prefetch-url, converts to SRI with nix hash convert, and writes both values back into static.nix via nix-editor.

To update a package, run its script and then redeploy:

scripts/firefox-bin-update
nix-darwin-switch

Current packages using this pattern: claude-code, firefox-bin, makemkv, signal-desktop-bin, yt-dlp, zoom-us.

Adding Services

Short of any community standing on what these should be called, we have two rough categories of Nix files we use for host configurations: A “module” and a “config”.

module
A module has options and config attributes, and declares its own options. The config section interprets those options into a configuration. Generally speaking, these modules will be ready to get contributed to nixpkgs. Importing such a module, sans any configuration for it, does nothing but makes the options available. So we gather up all modules and include them in the relevant host. nixos-modules contains all modules for NixOS, and should be imported in ./nixos-modules/linux-host.nix. There is also a darwin-modules which gets included in ./darwin.nix.
config
A config is relative inert, and it contains settings. It could dynamically determine settings and that’s fine, but it doesn’t declare options. Generally, a config is a place where we put the specifics for a service. Oftentimes a module will have an accompanying config. There is a series of config directories: darwin-configs, home-configs, and nixos-configs. They should go in there, and hosts or home profiles should pull them in as needed. Importing such a config applies its settings. You can find a sample nixos-config in ./nixos-configs/sample.nix. It contains patterns we typically find in our configs.

Exposing External Services Safely

Some services are meant to be reachable from the public internet, while others should stay private to the LAN. Both categories run on the same host, but the configuration described here keeps them completely separated at the network level. For the big picture of how the two IPs are structured, see Public/Private Service Split.

Configuring the Host

The split relies on two distinct IP addresses bound to the same network interface. The primary address carries private services; a secondary address is reserved for anything that should be externally reachable. The secondary address is declared directly on the host’s facts entry so that the DNS server and interface configuration both derive from a single source of truth:

silicon = {
  ipv4 = 9;                  # primary, private
  extraAddresses = {
    silicon-external = 100;  # secondary, public — gets a DNS A record
  };
  # ...
};

With the secondary IP in place, declare a domain-suffix policy that maps a real TLD to the public address. Every FQDN registered under that suffix inherits the address and certificate source automatically:

services.https.domains = {
  # Private services — all *.proton FQDNs listen on the LAN-only IP.
  "proton" = {
    addr       = "192.168.254.9";
    certSource = "internal-ca";
  };
  # Public services — all *.example.com FQDNs listen on the public IP.
  "example.com" = {
    addr       = "192.168.254.100";
    certSource = "acme";
  };
};

Using certSource = "acme" requires the ACME module to be configured with a DNS provider capable of DNS-01 challenges (we use Porkbun). With DNS-01 the host does not need to be reachable on port 80 to obtain a certificate, which means certs can be provisioned before the domain is publicly live.

The router’s port-forward mapping needs to stay registered. We use the UPnP port-forward module to automate this; it registers the mapping on boot and refreshes it on a timer so it survives router reboots:

services.upnp-portforward = {
  enable       = true;
  addr         = "192.168.254.100";  # secondary IP to forward to
  externalPort = 443;
  localPort    = 443;
};

If the router does not support UPnP IGD, the port forward must be created manually in the router’s administration UI instead.

Configuring the Service

Once the domain policy is in place, adding a service for public access looks identical to adding a private one. Declare the FQDN under services.https.fqdns; the domain-suffix policy determines which IP and certificate source to use:

services.https.fqdns."myapp.example.com" = {
  internalPort = 8080;
};

Because the FQDN ends in .example.com, the system uses the policy declared above: nginx listens on 192.168.254.100:443, and an ACME certificate is requested automatically via DNS-01 the next time the ACME renewal runs.

Two external steps remain outside of NixOS configuration:

  1. Create a DNS A record at the registrar pointing myapp.example.com to the router’s WAN IP address.
  2. Ensure the router forwards TCP 443 to 192.168.254.100 — either through the UPnP module above or by a static rule in the router’s UI.

Secrets

Secrets are managed via agenix and agenix-rekey.

To generate secrets, and rekey them, use:

agenix rekey generate --rekey -a

This must be run by a human. If you’re not a human, ask the human to run it.

If you must regenerate secrets, remove the base secret file, and then ask your human to run the above invocation.

Generations

List generations

# Alas, this isn't supported yet: https://github.com/NixOS/nix/pull/6911
export NO_COLOR=1
nix profile history --profile /nix/var/nix/profiles/system

Version �[1m114�[0m (2024-03-07) <- 113: No changes.

Version �[1m115�[0m (2024-03-07) <- 114: No changes.

Version �[1m116�[0m (2024-03-07) <- 115: No changes.

Version �[1m117�[0m (2024-03-07) <- 116: No changes.

Version �[1m118�[0m (2024-03-20) <- 117: No changes.

Version �[1m119�[0m (2024-03-31) <- 118: No changes.

Version �[1m120�[0m (2024-03-31) <- 119: No changes.

Version �[1m121�[0m (2024-03-31) <- 120: No changes.

Version �[1m122�[0m (2024-03-31) <- 121: No changes.

Version �[1m123�[0m (2024-03-31) <- 122: No changes.

Version �[1m124�[0m (2024-04-02) <- 123: No changes.

Version �[1m125�[0m (2024-04-02) <- 124: No changes.

Version �[1m126�[0m (2024-04-05) <- 125: No changes.

Version �[1m127�[0m (2024-05-14) <- 126: No changes.

Version �[1m128�[0m (2024-05-14) <- 127: No changes.

Version �[1m129�[0m (2024-05-14) <- 128: No changes.

Version �[1m130�[0m (2024-05-15) <- 129: No changes.

Version �[1m131�[0m (2024-05-15) <- 130: No changes.

Version �[1m132�[0m (2024-05-15) <- 131: No changes.

Version �[1m133�[0m (2024-05-15) <- 132: No changes.

Version �[1m134�[0m (2024-05-15) <- 133: No changes.

Version �[1m135�[0m (2024-05-15) <- 134: No changes.

Version �[1m136�[0m (2024-05-18) <- 135: No changes.

Version �[1m137�[0m (2024-05-21) <- 136: No changes.

Version �[1m138�[0m (2024-05-21) <- 137: No changes.

Version �[1m139�[0m (2024-05-21) <- 138: No changes.

Version �[1m140�[0m (2024-05-21) <- 139: No changes.

Version �[1m141�[0m (2024-05-21) <- 140: No changes.

Version �[1m142�[0m (2024-05-21) <- 141: No changes.

Version �[1m143�[0m (2024-05-21) <- 142: No changes.

Version �[1m144�[0m (2024-05-22) <- 143: No changes.

Version �[1m145�[0m (2024-05-22) <- 144: No changes.

Version �[1m146�[0m (2024-05-22) <- 145: No changes.

Version �[1m147�[0m (2024-05-22) <- 146: No changes.

Version �[1m148�[0m (2024-05-22) <- 147: No changes.

Version �[1m149�[0m (2024-05-23) <- 148: No changes.

Version �[1m150�[0m (2024-05-23) <- 149: No changes.

Version �[1m151�[0m (2024-05-23) <- 150: No changes.

Version �[1m152�[0m (2024-05-23) <- 151: No changes.

Version �[1m153�[0m (2024-05-23) <- 152: No changes.

Version �[1m154�[0m (2024-05-23) <- 153: No changes.

Version �[1m155�[0m (2024-05-23) <- 154: No changes.

Version �[1m156�[0m (2024-05-23) <- 155: No changes.

Version �[1m157�[0m (2024-05-23) <- 156: No changes.

Version �[1m158�[0m (2024-05-23) <- 157: No changes.

Version �[1m159�[0m (2024-05-23) <- 158: No changes.

Version �[1m160�[0m (2024-05-23) <- 159: No changes.

Version �[1m161�[0m (2024-05-23) <- 160: No changes.

Version �[1m162�[0m (2024-05-26) <- 161: No changes.

Version �[1m163�[0m (2024-05-26) <- 162: No changes.

Version �[1m164�[0m (2024-05-27) <- 163: No changes.

Version �[1m165�[0m (2024-05-27) <- 164: No changes.

Version �[1m166�[0m (2024-05-27) <- 165: No changes.

Version �[1m167�[0m (2024-05-27) <- 166: No changes.

Version �[1m168�[0m (2024-05-27) <- 167: No changes.

Version �[1m169�[0m (2024-05-27) <- 168: No changes.

Version �[1m170�[0m (2024-05-27) <- 169: No changes.

Version �[1m171�[0m (2024-05-27) <- 170: No changes.

Version �[1m172�[0m (2024-05-27) <- 171: No changes.

Version �[1m173�[0m (2024-05-27) <- 172: No changes.

Version �[1m174�[0m (2024-05-27) <- 173: No changes.

Version �[1m175�[0m (2024-05-27) <- 174: No changes.

Version �[1m176�[0m (2024-05-27) <- 175: No changes.

Version �[1m177�[0m (2024-05-27) <- 176: No changes.

Version �[1m178�[0m (2024-05-27) <- 177: No changes.

Version �[1m179�[0m (2024-05-27) <- 178: No changes.

Version �[1m180�[0m (2024-05-28) <- 179: No changes.

Version �[1m181�[0m (2024-05-28) <- 180: No changes.

Version �[1m182�[0m (2024-05-28) <- 181: No changes.

Version �[1m183�[0m (2024-05-28) <- 182: No changes.

Version �[1m184�[0m (2024-05-28) <- 183: No changes.

Version �[1m185�[0m (2024-05-28) <- 184: No changes.

Version �[1m186�[0m (2024-05-28) <- 185: No changes.

Version �[1m187�[0m (2024-05-31) <- 186: No changes.

Version �[1m188�[0m (2024-05-31) <- 187: No changes.

Version �[1m189�[0m (2024-05-31) <- 188: No changes.

Version �[1m190�[0m (2024-06-02) <- 189: No changes.

Version �[1m191�[0m (2024-06-02) <- 190: No changes.

Version �[1m192�[0m (2024-06-02) <- 191: No changes.

Version �[1m193�[0m (2024-06-03) <- 192: No changes.

Version �[1m194�[0m (2024-06-03) <- 193: No changes.

Version �[1m195�[0m (2024-06-03) <- 194: No changes.

Version �[1m196�[0m (2024-06-06) <- 195: No changes.

Version �[1m197�[0m (2024-06-06) <- 196: No changes.

Version �[1m198�[0m (2024-06-06) <- 197: No changes.

Version �[1m199�[0m (2024-06-06) <- 198: No changes.

Version �[1m200�[0m (2024-06-07) <- 199: No changes.

Version �[1m201�[0m (2024-06-07) <- 200: No changes.

Version �[1m202�[0m (2024-06-07) <- 201: No changes.

Version �[1m203�[0m (2024-06-07) <- 202: No changes.

Version �[1m204�[0m (2024-06-07) <- 203: No changes.

Version �[1m205�[0m (2024-06-07) <- 204: No changes.

Version �[1m206�[0m (2024-06-10) <- 205: No changes.

Version �[1m207�[0m (2024-06-10) <- 206: No changes.

Version �[1m208�[0m (2024-06-10) <- 207: No changes.

Version �[1m209�[0m (2024-06-10) <- 208: No changes.

Version �[1m210�[0m (2024-06-11) <- 209: No changes.

Version �[1m211�[0m (2024-06-14) <- 210: No changes.

Version �[1m212�[0m (2024-06-14) <- 211: No changes.

Version �[1m213�[0m (2024-06-14) <- 212: No changes.

Version �[1m214�[0m (2024-06-18) <- 213: No changes.

Version �[1m215�[0m (2024-06-22) <- 214: No changes.

Version �[1m216�[0m (2024-06-29) <- 215: No changes.

Version �[1m217�[0m (2024-06-29) <- 216: No changes.

Version �[1m218�[0m (2024-06-29) <- 217: No changes.

Version �[1m219�[0m (2024-06-29) <- 218: No changes.

Version �[1m220�[0m (2024-06-29) <- 219: No changes.

Version �[1m221�[0m (2024-06-29) <- 220: No changes.

Version �[1m222�[0m (2024-06-29) <- 221: No changes.

Version �[1m223�[0m (2024-06-29) <- 222: No changes.

Version �[1m224�[0m (2024-06-29) <- 223: No changes.

Version �[1m225�[0m (2024-06-29) <- 224: No changes.

Version �[1m226�[0m (2024-06-29) <- 225: No changes.

Version �[1m227�[0m (2024-06-30) <- 226: No changes.

Version �[1m228�[0m (2024-07-03) <- 227: No changes.

Version �[1m229�[0m (2024-07-03) <- 228: No changes.

Version �[1m230�[0m (2024-07-03) <- 229: No changes.

Version �[1m231�[0m (2024-07-03) <- 230: No changes.

Version �[1m232�[0m (2024-07-03) <- 231: No changes.

Version �[1m233�[0m (2024-07-03) <- 232: No changes.

Version �[1m234�[0m (2024-07-04) <- 233: No changes.

Version �[1m235�[0m (2024-07-04) <- 234: No changes.

Version �[1m236�[0m (2024-07-04) <- 235: No changes.

Version �[1m237�[0m (2024-07-04) <- 236: No changes.

Version �[1m238�[0m (2024-07-04) <- 237: No changes.

Version �[1m239�[0m (2024-07-06) <- 238: No changes.

Version �[1m240�[0m (2024-07-06) <- 239: No changes.

Version �[1m241�[0m (2024-07-09) <- 240: No changes.

Version �[1m242�[0m (2024-07-09) <- 241: No changes.

Version �[1m243�[0m (2024-07-10) <- 242: No changes.

Version �[1m244�[0m (2024-07-14) <- 243: No changes.

Version �[1m245�[0m (2024-07-24) <- 244: No changes.

Version �[1m246�[0m (2024-07-24) <- 245: No changes.

Version �[1m247�[0m (2024-07-24) <- 246: No changes.

Version �[1m248�[0m (2024-07-24) <- 247: No changes.

Version �[1m249�[0m (2024-07-24) <- 248: No changes.

Version �[1m250�[0m (2024-08-09) <- 249: No changes.

Version �[1m251�[0m (2024-08-10) <- 250: No changes.

Version �[1m252�[0m (2024-08-13) <- 251: No changes.

Version �[1m253�[0m (2024-08-13) <- 252: No changes.

Version �[1m254�[0m (2024-08-21) <- 253: No changes.

Version �[1m255�[0m (2024-08-24) <- 254: No changes.

Version �[1m256�[0m (2024-08-24) <- 255: No changes.

Version �[1m257�[0m (2024-08-24) <- 256: No changes.

Version �[1m258�[0m (2024-08-28) <- 257: No changes.

Version �[1m259�[0m (2024-08-29) <- 258: No changes.

Version �[1m260�[0m (2024-08-31) <- 259: No changes.

Version �[1m261�[0m (2024-09-03) <- 260: No changes.

Version �[1m262�[0m (2024-09-03) <- 261: No changes.

Version �[1m263�[0m (2024-09-03) <- 262: No changes.

Version �[1m264�[0m (2024-09-03) <- 263: No changes.

Version �[1m265�[0m (2024-09-03) <- 264: No changes.

Version �[1m266�[0m (2024-09-03) <- 265: No changes.

Version �[1m267�[0m (2024-09-03) <- 266: No changes.

Version �[1m268�[0m (2024-09-03) <- 267: No changes.

Version �[1m269�[0m (2024-09-03) <- 268: No changes.

Version �[1m270�[0m (2024-09-03) <- 269: No changes.

Version �[1m271�[0m (2024-09-03) <- 270: No changes.

Version �[1m272�[0m (2024-09-03) <- 271: No changes.

Version �[1m273�[0m (2024-09-04) <- 272: No changes.

Version �[1m274�[0m (2024-09-05) <- 273: No changes.

Version �[1m275�[0m (2024-09-05) <- 274: No changes.

Version �[1m276�[0m (2024-09-05) <- 275: No changes.

Version �[1m277�[0m (2024-09-05) <- 276: No changes.

Version �[1m278�[0m (2024-09-05) <- 277: No changes.

Version �[1m279�[0m (2024-09-05) <- 278: No changes.

Version �[1m280�[0m (2024-09-05) <- 279: No changes.

Version �[1m281�[0m (2024-09-05) <- 280: No changes.

Version �[1m282�[0m (2024-09-05) <- 281: No changes.

Version �[1m283�[0m (2024-09-05) <- 282: No changes.

Version �[1m284�[0m (2024-09-05) <- 283: No changes.

Version �[1m285�[0m (2024-09-05) <- 284: No changes.

Version �[1m286�[0m (2024-09-05) <- 285: No changes.

Version �[1m287�[0m (2024-09-05) <- 286: No changes.

Version �[1m288�[0m (2024-09-05) <- 287: No changes.

Version �[1m289�[0m (2024-09-05) <- 288: No changes.

Version �[1m290�[0m (2024-09-05) <- 289: No changes.

Version �[1m291�[0m (2024-09-05) <- 290: No changes.

Version �[1m292�[0m (2024-09-06) <- 291: No changes.

Version �[1m293�[0m (2024-09-06) <- 292: No changes.

Version �[1m294�[0m (2024-09-06) <- 293: No changes.

Version �[1m295�[0m (2024-09-06) <- 294: No changes.

Version �[1m296�[0m (2024-09-06) <- 295: No changes.

Version �[1m297�[0m (2024-09-06) <- 296: No changes.

Version �[1m298�[0m (2024-09-06) <- 297: No changes.

Version �[1m299�[0m (2024-09-06) <- 298: No changes.

Version �[1m300�[0m (2024-09-06) <- 299: No changes.

Version �[1m301�[0m (2024-09-06) <- 300: No changes.

Version �[1m302�[0m (2024-09-06) <- 301: No changes.

Version �[1m303�[0m (2024-09-06) <- 302: No changes.

Version �[1m304�[0m (2024-09-06) <- 303: No changes.

Version �[1m305�[0m (2024-09-06) <- 304: No changes.

Version �[1m306�[0m (2024-09-06) <- 305: No changes.

Version �[1m307�[0m (2024-09-06) <- 306: No changes.

Version �[1m308�[0m (2024-09-06) <- 307: No changes.

Version �[1m309�[0m (2024-09-06) <- 308: No changes.

Version �[1m310�[0m (2024-09-06) <- 309: No changes.

Version �[1m311�[0m (2024-09-06) <- 310: No changes.

Version �[1m312�[0m (2024-09-06) <- 311: No changes.

Version �[1m313�[0m (2024-09-06) <- 312: No changes.

Version �[1m314�[0m (2024-09-06) <- 313: No changes.

Version �[1m315�[0m (2024-09-06) <- 314: No changes.

Version �[1m316�[0m (2024-09-06) <- 315: No changes.

Version �[1m317�[0m (2024-09-06) <- 316: No changes.

Version �[1m318�[0m (2024-09-06) <- 317: No changes.

Version �[1m319�[0m (2024-09-06) <- 318: No changes.

Version �[1m320�[0m (2024-09-06) <- 319: No changes.

Version �[1m321�[0m (2024-09-06) <- 320: No changes.

Version �[1m322�[0m (2024-09-06) <- 321: No changes.

Version �[1m323�[0m (2024-09-06) <- 322: No changes.

Version �[1m324�[0m (2024-09-06) <- 323: No changes.

Version �[1m325�[0m (2024-09-06) <- 324: No changes.

Version �[1m326�[0m (2024-09-06) <- 325: No changes.

Version �[1m327�[0m (2024-09-06) <- 326: No changes.

Version �[1m328�[0m (2024-09-06) <- 327: No changes.

Version �[1m329�[0m (2024-09-06) <- 328: No changes.

Version �[1m330�[0m (2024-09-06) <- 329: No changes.

Version �[1m331�[0m (2024-09-06) <- 330: No changes.

Version �[1m332�[0m (2024-09-06) <- 331: No changes.

Version �[1m333�[0m (2024-09-06) <- 332: No changes.

Version �[1m334�[0m (2024-09-06) <- 333: No changes.

Version �[1m335�[0m (2024-09-06) <- 334: No changes.

Version �[1m336�[0m (2024-09-06) <- 335: No changes.

Version �[1m337�[0m (2024-09-06) <- 336: No changes.

Version �[1m338�[0m (2024-09-06) <- 337: No changes.

Version �[1m339�[0m (2024-09-06) <- 338: No changes.

Version �[1m340�[0m (2024-09-06) <- 339: No changes.

Version �[1m341�[0m (2024-09-06) <- 340: No changes.

Version �[1m342�[0m (2024-09-13) <- 341: No changes.

Version �[1m343�[0m (2024-09-14) <- 342: No changes.

Version �[1m344�[0m (2024-09-14) <- 343: No changes.

Version �[1m345�[0m (2024-09-16) <- 344: No changes.

Version �[1m346�[0m (2024-09-16) <- 345: No changes.

Version �[1m347�[0m (2024-09-19) <- 346: No changes.

Version �[1m348�[0m (2024-09-19) <- 347: No changes.

Version �[1m349�[0m (2024-09-20) <- 348: No changes.

Version �[1m350�[0m (2024-09-20) <- 349: No changes.

Version �[1m351�[0m (2024-09-20) <- 350: No changes.

Version �[1m352�[0m (2024-09-20) <- 351: No changes.

Version �[1m353�[0m (2024-09-20) <- 352: No changes.

Version �[1m354�[0m (2024-09-20) <- 353: No changes.

Version �[1m355�[0m (2024-09-20) <- 354: No changes.

Version �[1m356�[0m (2024-09-20) <- 355: No changes.

Version �[1m357�[0m (2024-09-20) <- 356: No changes.

Version �[1m358�[0m (2024-09-20) <- 357: No changes.

Version �[1m359�[0m (2024-09-20) <- 358: No changes.

Version �[1m360�[0m (2024-09-20) <- 359: No changes.

Version �[1m361�[0m (2024-09-20) <- 360: No changes.

Version �[1m362�[0m (2024-09-20) <- 361: No changes.

Version �[1m363�[0m (2024-09-20) <- 362: No changes.

Version �[1m364�[0m (2024-09-20) <- 363: No changes.

Version �[1m365�[0m (2024-09-20) <- 364: No changes.

Version �[1m366�[0m (2024-09-20) <- 365: No changes.

Version �[1m367�[0m (2024-09-20) <- 366: No changes.

Version �[1m368�[0m (2024-09-20) <- 367: No changes.

Version �[1m369�[0m (2024-09-20) <- 368: No changes.

Version �[1m370�[0m (2024-09-20) <- 369: No changes.

Version �[1m371�[0m (2024-09-20) <- 370: No changes.

Version �[1m372�[0m (2024-09-23) <- 371: No changes.

Version �[1m373�[0m (2024-09-23) <- 372: No changes.

Version �[1m374�[0m (2024-09-23) <- 373: No changes.

Version �[1m375�[0m (2024-09-25) <- 374: No changes.

Version �[1m376�[0m (2024-09-25) <- 375: No changes.

Version �[32;1m377�[0m (2024-09-26) <- 376: No changes.

conditional values

lib.mkIf

lib.mkIf either includes the value given or an empty attrset depending on the evaluation of the condition. In other words, this is for any attrset. For lists, see lib.optionals.

let
  a = lib.mkIf true { foo = "bar"; } # Returns { foo = "bar"; }.
  b = lib.mkIf false { foo = "bar"; } # Returns {}.
  # Returns { foo = "bar"; baz = "qux";  }
  c = { foo = "bar" } // (lib.mkIf true { baz = "qux"; })
  # Returns { foo = "bar"; }
  d = { foo = "bar" } // (lib.mkIf false { baz = "qux"; })
in {}

lib.optionals

lib.optionals includes the provided list if the condition is true. If false, an empty list is given. For an attrset, see lib.mkIf.

let
  a = lib.optionals true [ "foo" ] # Returns [ "foo" ].
  b = lib.optionals false [ "foo" ] # Returns [].
  # Returns [ "foo" "bar" ].
  c = [ "foo" ] // (lib.optionals true [ "bar" ])
  # Returns [ "foo" ].
  d = [ "foo" ] // (lib.optionals false [ "bar" ])
in {}

options that may not exist

While I was working on comfyui and started using different nixpkgs versions across hosts, I started running into problems with shared modules. Some would set services.comfyui and attributes under it, and this would cause Nix evaluation failures for hosts that didn’t know about it in their nixpkgs.

First, find or create your imports for the module in question. Next, use lib.mkIf and check for the existence of the option with builtins.hasAttr "comfyui" options.services). Then, in the key, allow the key name to evaluate to null based on a check.

{ lib, options, ... }: {
  imports = [
    (lib.mkIf (builtins.hasAttr "comfyui" options.services) {
      # This is kind of magical.  See
      # https://nix.dev/manual/nix/2.17/language/values.html?highlight=coerced#attribute-set
      # but basically if the attribute name evaluates to null then the attribute
      # won't exist.  Without this hack, we get `The option `services.comfyui'
      # does not exist.`.  This is a special case and one cannot use null as a
      # key name.
      services.${
        if (builtins.hasAttr "comfyui" options.services)
        then "comfyui"
        else null
      } = {
        package = pkgs ? comfyui-rocm;
        rocmSupport = true;
      };
    })
  ];
}

This is the best way to avoid config.modules and lib.mkMerge while also keeping things relatively simple. I should make a helper for this though.

To include a package conditionally, use this:

imports = [
  # cyme isn't available on all versions of nixpkgs I use.
  (lib.mkIf (builtins.hasAttr "cyme" pkgs) {
    environment.systemPackages =
      if (builtins.hasAttr "cyme" pkgs)
      then [
        # Allows us to query the status of USB devices.  This uses lsusb or
        # systemprofile -json under the hood in a cross-platform manner.
        # Unfortunately it does not work on non-USB devices (like SD cards)
        # like one might think.  This is _not_ for storage devices (many
        # things imply it will work, but it won't).
        pkgs.cyme
      ]
      else []
    ;
  })
];

Options and Config

Utilities

Looking up NixOS config

This is for showing the current configuration values tied to a host.

This is a pure Flake environment, so nixos-options will never work. Instead you can query the Flake itself thusly:

host='silicon'
nix eval \
  --impure \
  --json \
  .#nixosConfigurations.$host.config.services.openssh.enable
true

Looking up NixOS options

This is for showing the documentation of options available for a host.

Note, while this does print something, it won’t show you that you’re looking at nested options (I don’t think it even prints them). Right now ([2026-01-19 Mon]) there is no solid utility for doing this short of going to https://search.nixos.org.

host='silicon'
nix eval \
  --impure \
  --json \
  --apply '
    opts:
      builtins.listToAttrs (map
        (n: {
          name = n;
          value = {
            description = (opts.${n}.description or null);
            type = (opts.${n}.type.name or null);
          };
        })
        (builtins.attrNames opts))
  ' \
  .#nixosConfigurations.$host.options.services.nginx \
  | jq '
  to_entries
  | map({
      name: .key,
      description: (.value.description // "<no description>"),
      type: (.value.type // "<unknown>"),
      default:
        (if .value.hasDefault
         then (.value.default // "<unset>")
         else "<no default>"
         end),
    })
  | sort_by(.name)
'
[
  {
    "name": "additionalModules",
    "description": "Additional [third-party nginx modules](https://www.nginx.com/resources/wiki/modules/)\nto install. Packaged modules are available in `pkgs.nginxModules`.\n",
    "type": "listOf",
    "default": "<no default>"
  },
  {
    "name": "appendConfig",
    "description": "Configuration lines appended to the generated Nginx\nconfiguration file. Commonly used by different modules\nproviding http snippets. {option}`appendConfig`\ncan be specified more than once and its value will be\nconcatenated (contrary to {option}`config` which\ncan be set only once).\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "appendHttpConfig",
    "description": "Configuration lines to be appended to the generated http block.\nThis is mutually exclusive with using config and httpConfig for\nspecifying the whole http block verbatim.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "clientMaxBodySize",
    "description": "Set nginx global client_max_body_size.",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "commonHttpConfig",
    "description": "With nginx you must provide common http context definitions before\nthey are used, e.g. log_format, resolver, etc. inside of server\nor location contexts. Use this attribute to set these definitions\nat the appropriate location.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "config",
    "description": "Verbatim {file}`nginx.conf` configuration.\nThis is mutually exclusive to any other config option for\n{file}`nginx.conf` except for\n- [](#opt-services.nginx.appendConfig)\n- [](#opt-services.nginx.httpConfig)\n- [](#opt-services.nginx.logError)\n\nIf additional verbatim config in addition to other options is needed,\n[](#opt-services.nginx.appendConfig) should be used instead.\n",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "defaultHTTPListenPort",
    "description": "If vhosts do not specify listen.port, use these ports for HTTP by default.\n",
    "type": "unsignedInt16",
    "default": "<no default>"
  },
  {
    "name": "defaultListen",
    "description": "If vhosts do not specify listen, use these addresses by default.\nThis option takes precedence over {option}`defaultListenAddresses` and\nother listen-related defaults options.\n",
    "type": "listOf",
    "default": "<no default>"
  },
  {
    "name": "defaultListenAddresses",
    "description": "If vhosts do not specify listenAddresses, use these addresses by default.\nThis is akin to writing `defaultListen = [ { addr = \"0.0.0.0\" } ]`.\n",
    "type": "listOf",
    "default": "<no default>"
  },
  {
    "name": "defaultMimeTypes",
    "description": "Default MIME types for NGINX, as MIME types definitions from NGINX are very incomplete,\nwe use by default the ones bundled in the mailcap package, used by most of the other\nLinux distributions.\n",
    "type": "path",
    "default": "<no default>"
  },
  {
    "name": "defaultSSLListenPort",
    "description": "If vhosts do not specify listen.port, use these ports for SSL by default.\n",
    "type": "unsignedInt16",
    "default": "<no default>"
  },
  {
    "name": "enable",
    "description": "Whether to enable Nginx Web Server.",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "enableQuicBPF",
    "description": "Enables routing of QUIC packets using eBPF. When enabled, this allows\nto support QUIC connection migration. The directive is only supported\non Linux 5.7+.\nNote that enabling this option will make nginx run with extended\ncapabilities that are usually limited to processes running as root\nnamely `CAP_SYS_ADMIN` and `CAP_NET_ADMIN`.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "enableReload",
    "description": "Reload nginx when configuration file changes (instead of restart).\nThe configuration file is exposed at {file}`/etc/nginx/nginx.conf`.\nSee also `systemd.services.*.restartIfChanged`.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "eventsConfig",
    "description": "Configuration lines to be set inside the events block.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "experimentalZstdSettings",
    "description": "Enable alpha quality zstd module with recommended settings.\nLearn more about compression in Zstd format [here](https://github.com/tokers/zstd-nginx-module).\n\nThis adds `pkgs.nginxModules.zstd` to `services.nginx.additionalModules`.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "gitweb",
    "description": "<no description>",
    "type": "<unknown>",
    "default": "<no default>"
  },
  {
    "name": "group",
    "description": "Group account under which nginx runs.",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "httpConfig",
    "description": "Configuration lines to be set inside the http block.\nThis is mutually exclusive with the structured configuration\nvia virtualHosts and the recommendedXyzSettings configuration\noptions. See appendHttpConfig for appending to the generated http block.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "logError",
    "description": "Configures logging.\nThe first parameter defines a file that will store the log. The\nspecial value stderr selects the standard error file. Logging to\nsyslog can be configured by specifying the “syslog:” prefix.\nThe second parameter determines the level of logging, and can be\none of the following: debug, info, notice, warn, error, crit,\nalert, or emerg. Log levels above are listed in the order of\nincreasing severity. Setting a certain log level will cause all\nmessages of the specified and more severe log levels to be logged.\nIf this parameter is omitted then error is used.\n",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "mapHashBucketSize",
    "description": "Sets the bucket size for the map variables hash tables. Default\nvalue depends on the processor’s cache line size.\n\nRefer to [the nginx docs on hashes](https://nginx.org/en/docs/hash.html)\nfor more information.\n",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "mapHashMaxSize",
    "description": "Sets the maximum size of the map variables hash tables.\n",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "package",
    "description": "Nginx package to use. This defaults to the stable version. Note\nthat the nginx team recommends to use the mainline version which\navailable in nixpkgs as `nginxMainline`.\nSupported Nginx forks include `angie`, `openresty` and `tengine`.\n",
    "type": "package",
    "default": "<no default>"
  },
  {
    "name": "preStart",
    "description": "Shell commands executed before the service's nginx is started.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "prependConfig",
    "description": "Configuration lines prepended to the generated Nginx\nconfiguration file. Can for example be used to load modules.\n{option}`prependConfig` can be specified more than once\nand its value will be concatenated (contrary to {option}`config`\nwhich can be set only once).\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "proxyCache",
    "description": "<no description>",
    "type": "<unknown>",
    "default": "<no default>"
  },
  {
    "name": "proxyCachePath",
    "description": "Configure a proxy cache path entry.\nSee <https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path> for documentation.\n",
    "type": "attrsOf",
    "default": "<no default>"
  },
  {
    "name": "proxyResolveWhileRunning",
    "description": "Resolves domains of proxyPass targets at runtime and not only at startup.\nThis can be used as a workaround if nginx fails to start because of not-yet-working DNS.\n\n:::{.warn}\n`services.nginx.resolver` must be set for this option to work.\n:::\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "proxyTimeout",
    "description": "Change the proxy related timeouts in recommendedProxySettings.\n",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "recommendedBrotliSettings",
    "description": "Enable recommended brotli settings.\nLearn more about compression in Brotli format [here](https://github.com/google/ngx_brotli/).\n\nThis adds `pkgs.nginxModules.brotli` to `services.nginx.additionalModules`.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedGzipSettings",
    "description": "Enable recommended gzip settings.\nLearn more about compression in Gzip format [here](https://docs.nginx.com/nginx/admin-guide/web-server/compression/).\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedOptimisation",
    "description": "Enable recommended optimisation settings.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedProxySettings",
    "description": "Whether to enable recommended proxy settings if a vhost does not specify the option manually.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedTlsSettings",
    "description": "Enable recommended TLS settings.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedUwsgiSettings",
    "description": "Whether to enable recommended uwsgi settings if a vhost does not specify the option manually.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedZstdSettings",
    "description": "<no description>",
    "type": "unspecified",
    "default": "<no default>"
  },
  {
    "name": "resolver",
    "description": "Configures name servers used to resolve names of upstream servers into addresses\n",
    "type": "submodule",
    "default": "<no default>"
  },
  {
    "name": "serverNamesHashBucketSize",
    "description": "Sets the bucket size for the server names hash tables. Default\nvalue depends on the processor’s cache line size.\n",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "serverNamesHashMaxSize",
    "description": "Sets the maximum size of the server names hash tables.\n",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "serverTokens",
    "description": "Show nginx version in headers and error pages.",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "sslCiphers",
    "description": "Ciphers to choose from when negotiating TLS handshakes.",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "sslDhparam",
    "description": "Path to DH parameters file.",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "sslProtocols",
    "description": "Allowed TLS protocol versions.",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "sso",
    "description": "<no description>",
    "type": "<unknown>",
    "default": "<no default>"
  },
  {
    "name": "stateDir",
    "description": "<no description>",
    "type": "unspecified",
    "default": "<no default>"
  },
  {
    "name": "statusPage",
    "description": "Enable status page reachable from localhost on http://127.0.0.1/nginx_status.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "streamConfig",
    "description": "Configuration lines to be set inside the stream block.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "tailscaleAuth",
    "description": "<no description>",
    "type": "<unknown>",
    "default": "<no default>"
  },
  {
    "name": "typesHashMaxSize",
    "description": "Sets the maximum size of the types hash tables (`types_hash_max_size`).\nIt is recommended that the minimum size possible size is used.\nIf {option}`recommendedOptimisation` is disabled, nginx would otherwise\nfail to start since the mailmap `mime.types` database has more entries\nthan the nginx default value 1024.\n",
    "type": "positiveInt",
    "default": "<no default>"
  },
  {
    "name": "upstreams",
    "description": "Defines a group of servers to use as proxy target.\n",
    "type": "attrsOf",
    "default": "<no default>"
  },
  {
    "name": "user",
    "description": "User account under which nginx runs.",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "uwsgiResolveWhileRunning",
    "description": "Resolves domains of uwsgi targets at runtime\nand not only at start, you have to set\nservices.nginx.resolver, too.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "uwsgiTimeout",
    "description": "Change the uwsgi related timeouts in recommendedUwsgiSettings.\n",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "validateConfigFile",
    "description": "Whether to enable validating configuration with pkgs.writeNginxConfig.",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "virtualHosts",
    "description": "Declarative vhost config",
    "type": "attrsOf",
    "default": "<no default>"
  }
]

Developing on Third-Party Repositories

Working in a cloned repo that has no flake.nix without polluting its git history uses two complementary mechanisms:

  • .git/info/exclude is a per-clone gitignore that is never committed. Add local-only files here so they cannot accidentally be staged or committed upstream.
  • Nix’s flake evaluator only requires files to be staged (not committed), so git add flake.nix flake.lock makes them visible to nix develop without creating a commit.

Full per-project devShell

Use this when the project warrants its own pinned nixpkgs revision or project-specific tools.

echo "flake.nix" >> .git/info/exclude
echo "flake.lock" >> .git/info/exclude
echo ".envrc"    >> .git/info/exclude
# Write flake.nix, then stage it so Nix can evaluate it.
git add flake.nix flake.lock
echo "use flake ." > .envrc
direnv allow

Reusable devShell from dotfiles

Use this for a quick environment (e.g. “I just need Rust tooling”) without authoring a per-repo flake at all. Define named devShells in this flake and reference them by name.

echo ".envrc" >> .git/info/exclude
echo "use flake path:/Users/logan/dev/dotfiles#rust" > .envrc
direnv allow

TLS Certificate Trusting

iOS

  1. Email ./secrets/proton-ca.crt to the recipient.
  2. Open the email in iOS.
  3. Tap the name of the file (not the download button).
  4. Choose the current device to install the profile.
  5. Open the Settings app.
  6. Profile Download should appear just below the user account (should be the second entry). Go there.
  7. “Not verified” is normal/expected to see. Tap Install.
  8. Tap install again.
  9. Tap install yet again.
  10. Go to the root of the Settings app.
  11. Navigate to General → About → Certificate Trust Settings. The last entry will be at the bottom.
  12. Turn on the domain for the network (proton).
  13. Press Continue.
  14. Verify by navigating to a resource such as https://home-assistant.proton - it doesn’t matter if you can’t log in. You just need the page to load without a security warning.

Authentication

Architecture

Authentication is centralized: OpenLDAP holds the identity store, and Authelia is the OIDC/OAuth2 provider for browser-based SSO. Services either authenticate directly against LDAP (older pattern) or use Authelia as an OIDC identity provider (preferred going forward).

Browser / Client
     │
     ▼
Authelia (silicon, authelia.proton)
├─ OIDC authorization server
├─ Session management & access-control policy
└─ Delegates identity to OpenLDAP
     │
     ▼
OpenLDAP (silicon, ldap.proton:636, LDAPS only)
├─ dc=proton,dc=org
├─ ou=users  (person accounts + service accounts)
└─ ou=groups (groupOfNames; memberOf overlay)

Identity Store: OpenLDAP

The LDAP tree is facts-driven. nixos-modules/facts.nix is the single source of truth for human accounts, group memberships, and service endpoints. A Rust tool (ldap-reconciler) runs hourly on silicon, reading a JSON projection of facts.nix and reconciling the live directory against it.

  • Service accounts are declared inline in each service’s NixOS config via auth.ldap.users."<host>-<service>-service". agenix-rekey auto-generates plaintext and hashed password secrets.
  • Person account passwords are set once on creation and left alone by the reconciler (managed = false).
  • Secrets reach services via systemd LoadCredential, never via filesystem ownership.

OIDC Provider: Authelia

Authelia is configured at nixos-configs/authelia.nix. It reads users and groups from LDAP and issues OIDC tokens.

OIDC clients are generated dynamically from facts.network.services: any service with authentication = "oidc" gets a client entry. Confidential clients receive a client_secret_post credential; public clients (e.g. OpenHAB) use PKCE.

Access-control policy is also facts-driven: each service’s fqdn and groups list produce an Authelia rule granting one_factor to members of those groups.

Key limitation: Authelia can copy static LDAP attribute values into claims but has no expression engine to compute derived values (e.g. “is user in group X? emit role=admin”). Services that need role-in-claim must either accept manual post-login promotion or add a custom attribute to the LDAP schema.

Service Authentication Inventory

ServiceHostAuth todayOIDC-capableMigrate?Notes
ImmichsiliconOIDCDoneAdmin promoted manually in UI
GiteasiliconLDAPYesNeeds per-user DB update [fn:gitea]
MatrixsiliconLDAPYesldap3 incompatible with MAS [fn:matrix]
GrafananickelLDAPYesgroups must be in ID token [fn:grafana]
NextcloudcopperLDAPPartialNeutralAdmin group sync bug [fn:nextcloud]
OpenHABbrominevia proxyPlannedForward-auth; currently disabled
MeTubesiliconNonevia proxyN/AForward-auth; mind socket.io

Gitea migration notes

[fn:gitea] Existing accounts must be linked manually: set login_name to the OIDC sub value, login_type to 6 (OAuth), and login_source to the new authentication source ID in the database. Without this, OIDC logins create duplicate accounts. Upstream bug: admin/restricted status from the groups claim is only applied on the second login (gitea#32566).

Matrix/Synapse migration notes

[fn:matrix] The matrix-synapse-ldap3 plugin is incompatible with Matrix Authentication Service (MAS / MSC3861), the direction Synapse is heading. Migrate to the built-in oidc_providers with allow_existing_users: true and localpart_template: "{{ user.preferred_username }}" (Authelia exposes LDAP uid as preferred_username). Both LDAP and OIDC auth can coexist during the transition; remove ldap3 only after all users have logged in via OIDC at least once.

Grafana migration notes

[fn:grafana] Grafana evaluates role_attribute_path (JMESPath) against the ID token. Authelia 4.39+ puts groups in the UserInfo endpoint by default, not the ID token. If groups is absent and the JMESPath expression has a || 'Viewer' fallback, Grafana resolves it as Viewer without ever calling UserInfo. Fix: add a claims_policies entry for the Grafana client in authelia.nix that explicitly includes groups in the ID token.

Nextcloud migration notes

[fn:nextcloud] The user_oidc app can provision groups from the groups claim, but the admin group cannot be managed this way — members get removed on sync (open upstream issue as of 2025). A hybrid approach (LDAP for identity, OIDC for login) is the most practical path. Full cut-over requires accepting manual admin management. If Server-Side Encryption is ever enabled, OIDC is completely blocked (SSE requires cleartext passwords at login time).

Ollama

LLM inference is available at ollama.proton but the endpoint is not a direct Ollama server — it’s garage-queue, a capability-aware work queue that distributes inference requests across all hosts running Ollama.

Architecture

client (curl, gold-dig, etc.)
  │
  │  HTTPS
  ▼
ollama.proton (DNS → silicon)
  │
  │  nginx → Unix socket
  ▼
garage-queue-server (silicon)
  │
  │  NATS JetStream
  ▼
garage-queue-worker (titanium, arsenic, mac, …)
  │
  │  HTTP localhost:11434
  ▼
Ollama (local to worker)

What gets queued

Only POST /api/generate is routed through the queue. The server matches the incoming path against configured queue routes, extracts capability requirements from the JSON body (model tag + estimated VRAM), and enqueues the item. The connection is held open until a worker returns a result.

What does not work

Every other Ollama endpoint (GET /api/tags, POST /api/chat, etc.) returns a 405 (Method Not Allowed for GET) or 404 (no queue mapped for POST to unmapped paths) from garage-queue. There is currently no proxy pass-through for non-queued routes.

This means:

  • Clients cannot discover available models via ollama.proton/api/tags.
  • POST /api/chat does not work through the queue endpoint.

Workers and capabilities

Each host that runs Ollama also runs a garage-queue-worker. The worker advertises:

  • Tags — the model names from services.ollama.loadModels (e.g. llama3.1:8b, qwen2.5:14b).
  • Scalars — numeric capacities like vram_mb declared per-host.

When an inference request arrives, the server finds a worker whose advertised tags include the requested model and whose VRAM scalar meets the estimated requirement.

Model tiers

Models are configured in Nix by VRAM tier. Each tier imports the previous one:

TierFileAdds
8 GBollama-models-8gb-vram.nixdolphin3:8b, gemma3:4b, llama3.1:8b,
llama3.2:3b, mistral:7b, phi4-mini
12 GBollama-models-12gb-vram.nixgemma3:12b, qwen2.5:14b

A host imports the tier matching its GPU. The ollama-model-loader systemd service pulls models on boot.

Relevant files

FileRole
nixos-configs/garage-queue-server.nixServer + nginx on silicon
nixos-configs/garage-queue-worker.nixWorker config (all Ollama hosts)
nixos-configs/ollama.nixOllama service + model loader
nixos-configs/ollama-models-*-vram.nixPer-tier model lists
hosts/titanium.nix12 GB VRAM worker example

Direct access

To bypass the queue and hit Ollama directly (e.g. for /api/tags or debugging), SSH to the host and use localhost:11434:

ssh titanium.proton curl -s http://localhost:11434/api/tags | jq '.models[].name'

Known limitations

  • GET /api/tags is not supported through ollama.proton. garage-queue would need to synthesize this response by aggregating the model tags that workers have advertised, which is not yet implemented.
  • POST /api/chat is not queued. Adding it would require a second queue definition mirroring the /api/generate setup.
  • There is no transparent proxy fallback for non-queued routes.

Operations

File syncing

Example invocation to sync over my music files. Note that this preserves the directory, so music is one layer deep (not nested), and ensured to exist.

rsync \
  --archive \
  --verbose \
  --human-readable \
  --iconv=utf-8-mac,utf-8 \
  --chown=nextcloud:nextcloud \
  ~/Dropbox/music \
  --rsync-path='sudo rsync' 'silicon.proton:/tank/data/nextcloud/data/logan/files/'

Some notable things:

  1. This ensures the files are owned by Nextcloud - required for Nextcloud to manage them.
  2. The --iconv invocation converts from the macOS (see https://serverfault.com/a/427200 for details). Basically macOS uses UTF-8 NFC and Linux uses UTF-8 NFD. Linux seems to tolerate it, but the PHP runtime does not.

Graveyard

The graveyard/ directory preserves configurations for services that are no longer active. Files land here when the configuration could be useful as a future reference, when the service might be revisited, or when the history of how something was set up is worth retaining.

The directory structure mirrors the root of the repository. For example, a NixOS config that lived at nixos-configs/foo.nix moves to graveyard/nixos-configs/foo.nix.

Every file in the graveyard must have a TOMBSTONE comment block at the top of the file explaining why it was retired. The format is:

################################################################################
# TOMBSTONE — Short description of why this is no longer active.
#
# What the service was and what it did.
#
# Reason for decommission.  Where its import was removed from, if applicable.
# This file is kept for historical reference.
################################################################################

About

Config files I use

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Nix 76.9%
  • Shell 16.5%
  • Ruby 4.6%
  • Python 1.4%
  • Vim Script 0.3%
  • Standard ML 0.2%
  • Other 0.1%