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.
Look up the next element in the periodic table, and then create that under
nix/hosts.
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.
Instructions across various kinds of machines are not consistent due to bootstrapping. See also All Machine Types.
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.
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.
The Raspberry Pi uses its own proprietary boot system. Sometimes it can use
something called uboot, which may or may not be proprietary.
For these, use the following invocation:
rpi-host-new hostBut, 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.zstOnce 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.pubA quick git status will show the host pub key has changed.
agenix rekey -aThen 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'host='my-host'; ssh-keygen -t ed25519 -N "" -f secrets/$host-pub-keyYou will need to deploy to nickel to have it pull in the new host for both its
DNS settings, DHCP, and Prometheus exporting.
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.pubThis covers miscellaneous devices on the network which need special instruction or understanding.
Sometimes the windfall is pretty good!
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.usbmodemM1230887551Press enter to make it prompt you for credentials.
If you idle too long, it will log you out.
Coming soon!
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:
| Address | Role | Reachable from | Certificate source |
|---|---|---|---|
192.168.254.9 | Private | LAN only | Internal CA |
192.168.254.100 | Public | LAN + Internet | ACME (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.
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
versionandhashfor each pinned package. Both fields are updated together so the overlay always sees a consistent pair. overlays/<pkg>.nix- Calls
overrideAttrson the nixpkgs derivation, replacingversionandsrcwith the values fromstatic.nix. The overlay is wired intooverlays/default.nix. scripts/<pkg>-update- A shell script that fetches the latest upstream
version, prefetches its hash with
nix-prefetch-url, converts to SRI withnix hash convert, and writes both values back intostatic.nixvianix-editor.
To update a package, run its script and then redeploy:
scripts/firefox-bin-update
nix-darwin-switchCurrent packages using this pattern: claude-code, firefox-bin,
makemkv, signal-desktop-bin, yt-dlp, zoom-us.
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
optionsandconfigattributes, and declares its own options. Theconfigsection interprets those options into a configuration. Generally speaking, these modules will be ready to get contributed tonixpkgs. 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-modulescontains all modules for NixOS, and should be imported in./nixos-modules/linux-host.nix. There is also adarwin-moduleswhich 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
configdirectories:darwin-configs,home-configs, andnixos-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 samplenixos-configin./nixos-configs/sample.nix. It contains patterns we typically find in our configs.
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.
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.
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:
- Create a DNS A record at the registrar pointing
myapp.example.comto the router’s WAN IP address. - 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 are managed via agenix and agenix-rekey.
To generate secrets, and rekey them, use:
agenix rekey generate --rekey -aThis 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.
# 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/systemVersion �[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.
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 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 {}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 []
;
})
];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.enabletrue
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>"
}
]
Working in a cloned repo that has no flake.nix without polluting its git
history uses two complementary mechanisms:
.git/info/excludeis 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.lockmakes them visible tonix developwithout creating a commit.
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 allowUse 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- Email
./secrets/proton-ca.crtto the recipient. - Open the email in iOS.
- Tap the name of the file (not the download button).
- Choose the current device to install the profile.
- Open the Settings app.
- Profile Download should appear just below the user account (should be the second entry). Go there.
- “Not verified” is normal/expected to see. Tap Install.
- Tap install again.
- Tap install yet again.
- Go to the root of the Settings app.
- Navigate to General → About → Certificate Trust Settings. The last entry will be at the bottom.
- Turn on the domain for the network (
proton). - Press Continue.
- 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 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)
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.
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 | Host | Auth today | OIDC-capable | Migrate? | Notes |
|---|---|---|---|---|---|
| Immich | silicon | OIDC | ✓ | Done | Admin promoted manually in UI |
| Gitea | silicon | LDAP | ✓ | Yes | Needs per-user DB update [fn:gitea] |
| Matrix | silicon | LDAP | ✓ | Yes | ldap3 incompatible with MAS [fn:matrix] |
| Grafana | nickel | LDAP | ✓ | Yes | groups must be in ID token [fn:grafana] |
| Nextcloud | copper | LDAP | Partial | Neutral | Admin group sync bug [fn:nextcloud] |
| OpenHAB | bromine | — | via proxy | Planned | Forward-auth; currently disabled |
| MeTube | silicon | None | via proxy | N/A | Forward-auth; mind socket.io |
[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).
[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.
[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.
[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).
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.
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)
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.
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/chatdoes not work through the queue endpoint.
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_mbdeclared 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.
Models are configured in Nix by VRAM tier. Each tier imports the previous one:
| Tier | File | Adds |
|---|---|---|
| 8 GB | ollama-models-8gb-vram.nix | dolphin3:8b, gemma3:4b, llama3.1:8b, |
| llama3.2:3b, mistral:7b, phi4-mini | ||
| 12 GB | ollama-models-12gb-vram.nix | gemma3:12b, qwen2.5:14b |
A host imports the tier matching its GPU. The ollama-model-loader
systemd service pulls models on boot.
| File | Role |
|---|---|
nixos-configs/garage-queue-server.nix | Server + nginx on silicon |
nixos-configs/garage-queue-worker.nix | Worker config (all Ollama hosts) |
nixos-configs/ollama.nix | Ollama service + model loader |
nixos-configs/ollama-models-*-vram.nix | Per-tier model lists |
hosts/titanium.nix | 12 GB VRAM worker example |
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'GET /api/tagsis not supported throughollama.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/chatis not queued. Adding it would require a second queue definition mirroring the/api/generatesetup.- There is no transparent proxy fallback for non-queued routes.
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:
- This ensures the files are owned by Nextcloud - required for Nextcloud to manage them.
- The
--iconvinvocation 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.
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.
################################################################################