NixOS Things

word of warning

I only just got started with NixOS, I have no idea what I'm doing. Stuff below might not be best practice, might not work, might be dangerous, idk. I'd love to hear improvements though, i'm reachable on matrix as @f0x:pixie.town, freenode f0x, email nixos@cthu.lu :)

Erase your darlings

My current setup is similar to https://grahamc.com/blog/erase-your-darlings, although the wipe-during-boot isn't currently actually active yet.

ZFS on LUKS with USB auto-decrypt

Boot order of these was kinda finnicky, otherwise it fails during boot.

  1. preLVM Mount USB with keyfile
1
2
3
4
5
6
preLVMCommands = pkgs.lib.mkBefore ''
    mkdir -m 0755 -p /key
    sleep 2
    echo "mounting /key"
    mount -n -t ext4 -o ro `findfs UUID=<USB_UUID_HERE>` /key
'';
  1. set ZFS disks to preLVM as well
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
luks.devices = {
    ssd1 = {
        name = "ssd1";
        device = "/dev/disk/by-uuid/SSD_UUID_HERE";
        allowDiscards = true;
        preLVM = true;
        keyFile = "/key/file";
        fallbackToPassword = true;
    };
    hdd1 = {
        name = "hdd1";
        device = "/dev/disk/by-uuid/HDD_UUID_HERE";
        preLVM = true;
        keyFile = "/key/file";
        fallbackToPassword = true;
    };

    ... etc
};

Morph to localhost

As the server is my most powerful machine, it makes sense for that to do all the building. Remote builds didn't work (and executing morph from my not-nixos desktop isn't great anyways). So I edit my morph stuff on desktop, with a deploy script that does:

rsync -avP . cosmos:/persist/morph
ssh cosmos morph deploy --upload-secrets /persist/morph/nodes.nix switch

So if something were to happen on cosmos making morph unworkable, I still have a local copy I can use to fix it, just have to change the localhost directive. The config for cosmos allows it's own ssh key access to root, to make the ssh localhost work.

Packaging simple things

Documentation: Nixpkgs

Packaging a simple utility, in this case galaxy, a combination of nodejs (no deps) and a bash script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
with import <nixpkgs> {};

stdenv.mkDerivation rec {
    pname = "galaxyMotd";
    version = "0.0.1";
    src = pkgs.fetchgit {
        url = "https://git.pixie.town/f0x/galaxy";
        rev = "96de8613458ef06185f2cec371556cd0fac26f35";
        sha256 = "1gcrinv14sbqybyvcfyq36mcfvq0lzqr3shmjw0cwl86s4ny6mm2";
    };

    buildInputs = [nodejs];

    dontBuild = true;

    installPhase = ''
        mkdir -p $out/bin
        mv galaxy.js $out/bin/
        mv motd $out/bin/galaxy_motd
    '';
}

You get the rev and sha256 through nix-prefetch-git <repo_url>. Basically just need to make sure ends up in $out/bin and then it's good

Throwing my scrapped ZSH config in

Split in a separate file, so

1
2
3
4
5
6
{pkgs, ...}:
{
    environment.etc.zshrc.text = pkgs.lib.mkAfter ''
        # ZSH config goes here
    ''
}

Escaping for ZSH stuff

My config uses a bunch of ${} or just $ in places, which have to be escaped inside the '' '' block by prefixing them with ''. With Vim: s/\$/''$$/g turns then into ''$.

Codimd / HedgeDoc

It's amazing how many services have Nix packages + service modules written for them, making them relatively easy to install. As a test I set up HedgeDoc;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services.hedgedoc = {
    enable = true;
    configuration = {
        allowOrigin = ["cosmos"];
        domain = "cosmos:3000";
        host = "0.0.0.0";
        #TODO: include this from .gitignore'd semi-secrets file 
        # (still ends up in nix store)
        sessionSecret = "<keysmash here>";
        db = {
            dialect = "postgres";
            host = "/var/run/postgresql";
            username = "codimd";
        };
    };
};

To keep things fully declarative, I added the Postgres info too. Doing it that way uses Peer Authentication. Unix user = access to the corresponding postgres user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services.postgresql = {
    enable = true;
    dataDir = "/persist/postgres";

    #TODO: make wrapper function to set easily add entries
    #      for database and user with same name
    ensureDatabases = ["codimd"];
    ensureUsers = [
        {   name = "codimd";
            ensurePermissions = {
                "DATABASE codimd" = "ALL PRIVILEGES";
            };
        }
    ];
};

And that's a fully working HedgeDoc installation.

Storing semi-secrets

Fixing the Hedgedoc TODO, I now have a semi-secrets.nix for things I don't want in my (eventually published) git repo, but that do need to be passed straight into Nix stuff. Hence also the term semi-secrets, because they end up in the /nix store, which is readable system-wide.

The secure thing to do (like happens with ssh private keys for example) is have the config refer to a file on disk, with proper permissions, optionally managed through Morph secrets management for example.

semi-secrets.nix:

1
2
3
{
    hedgedoc.sessionSecret = "<keysmash here>";
}

System config is now wrapped in a let/in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{ config, pkgs, ... }:

let 
    semiSecrets = (import ../../semi-secrets.nix);
in {
    ... system config stuff

    services = {
        hedgedoc = {
            configuration = {
                sessionSecret = semiSecrets.hedgedoc.sessionSecret;
            };
        };
    };

    ... more system config stuff
}

and a .gitignore that excludes semi-secrets.nix

Writing functions (for generating my zsh config)

I basically dumped my ZSH config in a string and called it a day, but I quite like using different color combinations for different hosts. For that, I converted zshConfig.nix into a function with 2 (optional) arguments;

1
2
3
4
5
6
{color1 ? "2", color2 ? "3"}: (''
# ZSH config

# prompt
PROMPT='%F{${color1}}[%m] %a%F{${color2}}%n [%3c] %F{15}'
'')

Actually the only place in that whole file where I do want $ to be used as a $, substituting the color numbers nicely into the config. It's called like this from my machine specific configs:

1
environment.etc.zshrc.text = pkgs.lib.mkAfter (zshConfig {color1 = "4"; color2 = "5";});

Writing functions (for generating postgres users with databases)

Most of my TODO's are where I notice I'm copy-pasting, but don't quite know yet how to abstract the functionality. Here I am generating Postgres users that have full access to a database with the same name;

1
2
3
4
5
6
7
8
9
services = {
    postgresql = ({
            enable = true;
            dataDir = "/persist/postgres";
        } // (postgresUsersWithDatabases
            ["codimd" "synapse"]
        )
    );
};

The // is a new operator I learned, which merges sets: a // b adds all the keys/values from b into a (overriding existing ones).
postgresUsersWithDatabases function:

1
2
3
4
5
6
7
8
9
10
11
12
databases: {
    ensureDatabases = databases;

    ensureUsers = (let 
        userWithPermissions = user: {
            name = user;
            ensurePermissions = {
                "DATABASE ${user}" = "ALL PRIVILEGES";
            };
        };
    in (map userWithPermissions databases));
}

Now I'm thinking with portals.

NixOS Installation on BuyVM

BuyVM needs static ip assignments, the details of which can all be found in the networking tab of the Stallion control panel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
networking = {
    useDHCP = false;
    defaultGateway = "";
    defaultGateway6 = "";
    interfaces.eth0 = {
        ipv4.addresses = [{
            address = "";
            prefixLength = 24;
        }];
        ipv6.addresses = [{
            address = "";
            prefixLength = 48;
        }];
    };
};

NixOS Installation on Hetzner Cloud VPS

Similar to BuyVM, it needs some manual settings (but only for ipv6), like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
networking = {
    usePredictableInterfaceNames = false; # if true/default, it's *probably* ens3 instead of eth0

    defaultGateway6 = {
        address = "fe80::1";
        interface = "eth0";
    };

    interfaces.eth0 = {
        useDHCP = true; # for ipv4
        ipv6.addresses = [{
            address = "YOUR:IPV6:ADDR::1";
            prefixLength = 64;
        }];
    };
};