Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Intro

Noxa is a tool to ease the configuration of multi-host NixOS configurations. Starting with the motivation, design goals, and design decisions, this handbook guides you through setting up a multi-host NixOS configuration.

Motivation

NixOS is a Linux distribution configuration solution composed of modules and packages NixOS Manual. While nixpkgs provides many modules for configuring NixOS machines, it lacks fundamental support for configuration beyond the scope of a single host.

In practice, some hosts share configuration settings. Imagine that you configure four different NixOS machines and would like to set up a WireGuard overlay network between them. You will likely configure networking.wireguard.interfaces.<name> on each host, but there are several open questions:

  1. Is specifying each network redundantly on each host necessary? Boiling it down: specifying a network (and members) once globally should be enough, right?
  2. How do you manage secrets efficiently? Preshared connection keys, for example, are owned not by one but by two hosts. Should you specify them on several hosts redundantly, or would it be sufficient to declare the network topology globally?

The problem we face here is, that the existance/declaration of the overlay network is conceptually not part any host specific configuration. Instead only the membership to this network might be a host specific setting.

Noxa builds another layer on top of the “normal” per-system nixos configuration by providing inter host dependencies and configuration options; a higher-abstraction module system environment (see the Examples below).

Goal

The goal of Noxa is to fill the gap of nixos-modules limited to only a single host, providing a framework to support the configuration of multiple hosts that:

  1. Depend on each other
  2. Depend on a global configuration that is not part of any specific host
  3. Automate as many tasks that multi-host management demands (like exchanging secrets, overlay network configuration, inter device SSH connection setup, …).

How does it work

At the highest level, Noxa uses the nixpkgs module system to declare global settings and entities that do not belong to any specific host and the hosts itself. Settings specified here (in noxa modules) do not belong to the configuration of any specific host unless specified under nodes.<name>.configuration, which holds the configuration of the host <name>.

For example, the following Noxa module declares two hosts, while hostB depends on the configuration values of hostA.

{config, ...}: {
    # Declaration of `hostA`
    nodes.hostA.configuration = {
        # Add prometheus node exporter on non-default port
        services.prometheus.exporters.node = {
            enable = true;
            openFirewall = true;
            port = 6000;
        };

        /* Other NixOS configuration options */
    };

    # Declaration of `hostB`
    nodes.hostB.configuration = {
        # Import some nixos module for this host only
        imports = [
            ./some-nixos-module.nix
        ];

        # Configure prometheus collector
        services.prometheus = {
            enable = true;
            scrapeConfigs = [{
                job_name = "node";
                targets = [
                    # access config of `hostA`
                    "hostA:${config.nodes.hostA.configuration.services.prometheus.exporters.node.port}"
                ]
            }];
        };
    };

    imports = [
        ./some-other-noxa-module.nix
    ];

    /* Other Noxa configuration options */
}

For a reference, which options are available to noxa modules checkout Modules.

Getting started

To quick start, initialize a new noxa/nixos project via:

nix flake init --template github:0xccf4/noxa

Compiling and deploying a new configuration, is done via the “normal” nixos build commands, e.g.:

nixos-rebuild switch --flake .#<hostname>

A more detailed guide is found in Getting Started

Contributing

Contributions are welcome. Suggest new features and bugs via issues. Pull requests are welcome.

List of other multi-host configuration frameworks (feel free to add others):

Why another framework?

Initially @0xCCF4 started the project to learn Nix in-depth, but since it has grown into a usable multi-host configuration framework that is used on a daily basis.

Getting Started

Quick start

Create a new noxa project via:

nix flake init --template github:0xccf4/noxa

Which will set you up with a basic template project structured as follows:

  • hardware contains the hardware-configuration.nix files of your hosts.
  • hosts contains the toplevel definitions of your machines/hosts
  • modules/home contains home-manager modules
  • modules/nixos contains nixos modules
  • modules/noxa contains noxa modules
  • packages contains custom nix derivation which will be available to your hosts
  • parts contains flake-parts modules to build up your flake.nix
  • users contains the definition of your users, with their respective home-manager modules
  • secrets will contain the secrets of your hosts

Of course feel free to change, restructure and adapt the project to your needs. The above is just a template to get you started fast. For additional information see also the README.md of the template.

Setting up noxa

When using the template project, skip this step.

To add noxa to your flake, 1. add it as an input to your flake.nix, then, 2. instantiate it via noxa.lib.noxa-instantiate.

Below you will find an example flake.nix:

{
  inputs = {
    # Nixpkgs
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

    # Noxa
    noxa = {
      url = "github:0xCCF4/noxa";
      inputs.nixpkgs.follows = "nixpkgs";
      inputs.agenix.follows = "agenix";
      inputs.agenix-rekey.follows = "agenix-rekey";
      inputs.home-manager.follows = "home-manager";
    };

    # Secret management
    agenix = {
      url = "github:ryantm/agenix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # Secret management
    agenix-rekey = {
      url = "github:oddlama/agenix-rekey";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # Home Manager
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    { self
    , nixpkgs
    , agenix
    , agenix-rekey
    , noxa
    , home-manager
    , ...
    }:
      with nixpkgs.lib; with builtins;
      {
        # Noxa configuration
        noxaConfiguration = noxa.lib.noxa-instantiate {
          modules = [ ./config.nix ];
          specialArgs = {
            inherit agenix;
            inherit agenix-rekey;
            inherit home-manager;
          };
        };
      };
}

The function noxa.lib.noxa-instantiate will instantiate your configuration and accepts as arguments:

  • modules: A list of noxa modules which will be used to build up your configuration. In here, you will add your hosts and other configuration options.
  • specialArgs: A set of arguments which will be passed to all noxa modules

Noxa uses the same module system as nixos and home-manager, so all concept of options, modules, etc. apply to noxa modules as well.

Adding a new host

When using the template project, navigate to hosts/, just add a new file with the name of your host, e.g. vm-alice.nix and add the configuration of your host there. This file will the the toplevel declaration of your host, like the normal configuration.nix file of a nixos configuration. (To let nix know about the new file, you will have to git add it.)

Adding a host can be done via declaring it inside a noxa module, e.g.:

{...}: {
  nodes.hostA = {
    # Host specific configuration options, e.g.:
    configuration = {
      # Add prometheus node exporter on non-default port
      services.ssh.enable = true;

      /* Other NixOS configuration options */
    };
  };

  nodeNames = [ "hostA" ];
}

Setting up secrets

Noxa uses by default agenix and agenix-rekey to manage secrets. To get started using secrets, you will need to generate a master key and include the public SSH host keys of your hosts in their hosts configuration.

Generating a master key

Generate a master key to encrypt secrets with.

mkdir -p $HOME/.noxa
nix shell "nixpkgs#age" -c age-keygen -o $HOME/.noxa/master.key
# note down the public key, you will need it below

# Recommended: Encrypt the master key with a password
nix run "nixpkgs#age" -- --encrypt --armor --passphrase -o $HOME/.noxa/master.key.age $HOME/.noxa/master.key
rm $HOME/.noxa/master.key

Adding a master key to the configuration

When using the template project, navigate to modules/nixos/secrets.nix, you will find placeholders to add your previously generated master key to the configuration.

Create a entry in noxa.secrets.options.masterIdentities with the path and public key of your master key, e.g.:

noxa.secrets.options.masterIdentities = [
  {
    identity = "/home/user/.noxa/master.key.age";
    pubkey = "age1qql...";
  }
];

Adding a new host or adding the public SSH host to the configuration

Secrets will be stored encrypted in the nix store, such that only the host they are meant for can decrypt them using their private SSH host key. Therefore, you will have to add the public SSH host key of your hosts to the respective host configuration, e.g.:

{...}: {
  # Inside host configuration of `hostA`
  config = {
    # Add the public SSH host key of `hostA` to the configuration
    noxa.secrets.options.hostPubkey = "ssh-ed25519 AAAAC...";
  };
}

> When using the template project, navigate to `hosts/vm-bob.nix`, you will find a placeholder to add the public SSH host key of `vm-bob` to the configuration.

Further information about secrets

Further information about secrets and how to use them can be found in the Secrets module documentation.

Compiling and deploying a configuration

When using the template project, you may use any tool to build and deploy the configuration since noxa’s output will be available via the nixosConfigurations attribute of the flake, e.g. nixosConfigurations.vm-bob.

Compiling and deploying a new configuration, is done via the “normal” nixos and build commands by:

nixos-rebuild switch --flake .#<hostname>

When you are not using the template project, you might want to add compatibility for the these tools, by, e.g. adding the following to your flake.nix outputs section:

# NixOS tool compatibility
nixosConfigurations = mapAttrs
  (name: value: {
    config = value.configuration;
    options = value.options;
  })
  self.noxaConfiguration.config.nodes;

Further reading

You may continue reading on the architecture and instantiation of the module system in the Architecture or jump to the explanation of a specific module in the Modules section.

Architecture

Below we explain internal concepts of noxa.

Nodes

When we talk about the machines that are configured via noxa we use the term “node”. A node has a single unique name, which is used to identify it. This name is specified when declaring a node

{
    nodes."my-node" = { ... };
}

Note that: the name of the node is not necessarily the same as the hostname of the machine. When referring nodes in the configuration, we always use the node name, never the hostname.

Declaring node configuration

The configuration of a node is comprised of the default configuration, shared among all nodes, and the node-specific configuration. The default configuration is specified using the defaults attribute, while the node-specific configuration is specified under the node name. For example:

{
    defaults.configuration = {
        networking.hostName = mkDefault "abc";
    };

    nodes."my-node" = {
        # node-specific configuration
        networking.hostName = "my-node";
    };

    nodes."other-node" = {
        # node-specific configuration
        # inherits the default hostName "abc"
    };

    nodeNames = [ "my-node" "other-node" ];
}

Global imports and modules might, therefore, be placed in the defaults configuration, while node-specific imports and modules can be placed in the node configuration.

Note the nodeNames attribute, which is a list of all node names. This is currently required, when you write noxa modules that go over all nodes, and will set properties on them. To prevent infinite recursion on module evaluation, we added this attribute, so that the module can know which nodes there are, without having to look at the nodes attribute itself.

If you have suggestion on how to circumvent recursion in this case, please let us know via an issue or PR.

Nixpkgs versions

By default, all nodes share the same nixpkgs version, which is inherited from the nixpkgs version you specified for noxa itself.

{ # flake configuration
  inputs = {
    # Nixpkgs
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

    # Nixpkgs stable
    nixpkgs-stable.url = "github:nixos/nixpkgs/release-25.11";

    # Noxa
    noxa = {
      url = "github:0xCCF4/noxa";
      inputs.nixpkgs.follows = "nixpkgs"; # use the nixpkgs version above
    };
  };
}

However, you can specify that a single node use a different nixpkgs version, by setting the nixpkgs attribute in the node configuration, e.g.:

{nixpkgs-stable, ...}: {
    nodes."my-node" = {
        nixpkgs = nixpkgs-stable; # use a different nixpkgs version for this node
    };
}

This node will then use the nixpkgs-stable version of pkgs and module system.

Module system

Internally noxa uses the same module system to evaluate the configuration files like nixos does; via the nixpkgs.lib.evalModules function. You can, therefore, specify your (initial) modules and special args, when calling the noxa-instantiate function.

To evaluate the configuration of a single node noxa will call the <nixpkgs>/nixos/lib/eval-config.nix which would normally be called via calling <nixpkgs>.lib.nixosSystem.

By default, we pass down the following additional special args to noxa modules:

  • noxa: The noxa flake itself
  • noxa.nixpkgs: The nixpkgs, noxa uses during its module system evaluation.
  • noxa.nixosModules/noxa.noxaModules: The list of modules specified in the noxa flake
  • noxa.lib: Utility functions provided by noxa.
  • noxa.lib.net: Network related utility functions, vendored from nix-net-lib

To each nixos host evaluation, noxa will pass down the following additional special args:

  • agenix: The agenix module specified in the noxa flake
  • agenix-rekey: The agenix-rekey module specified in the noxa flake
  • noxa: See above, minus the nixpkgs
  • noxaHost/name: The node name of the host being evaluated
  • noxaConfig: The top-level config attribute of the noxa module system evaluation
  • nodes: The nodes.<name>.configuration attribute of the noxa module system evaluation

Modules

Below you find a short summary of functionality of the different modules comprising noxa.

Nodes

The core framework consists of the Nodes module, providing definitions of hosts.

SSH

The SSH module allows automatic configuration of SSH authorized keys between different hosts. You might for example want that some user (bob@source) can log in to a specific host (alice@destination) to forward the port 5555. This can be achieved by the following configuration, automatically creating required SSH keys, setting up authorized keys with connection restrictions, and configuration of the SSH config files.

nodes.source.configuration.ssh.grants = {

 # name of the grant, can be anything
 portForwarding = {
      
   # connection settings
   from = "bob";
   to.node = "target";
   to.user = "alice";

   # configure client side options
   to.extraOptions = {

     # extra option to the home manager SSH module
     localForward = [
       bind.port = 5555;
       host.address = "127.0.0.1";
       host.port = 5555;
     ];

     extraOptions = {
       # dont open a shell, only port forwarding
       SessionType = "none";
     };

   };

   # configure server side options
   options.open = ["127.0.0.1:5555"];
   
   commands = {pkgs}: [ # only allow port forwarding, no shell or other commands
     "${pkgs.coreutils}/bin/false"
   ];
 };
};

Now run agenix generate, then agenix rekey and voila.

Secrets

This module allows configuring of host specific and shared secrets.

For example when setting up a wireguard network, connection keys are neither owned by any of both peers. noxa will manage these shared secrets and make them available to the respective hosts. Under the hood, noxa uses agenix and agenix-rekey to manage these secrets, but adds required functionality for above mentioned use cases.

nodes.example.configuration = {
  noxa.secrets.def = [
    
    { # shared secret between "source" and "target"
      ident = "connection-psk-example";
      module = "noxa.wireguard";
      hosts = [ "source" "target" ];
      generator.script = "wireguard-psk";
    }
    
    { # host specific secret
      ident = "interface-key-example";
      module = "noxa.wireguard";
      generator.script = "wireguard-key";
    }

    { # instance wide secret, not assigned to any host
      ident = "instance-wide-secret";
      module = "something";
      global = true;
    }

  ];
};

Wireguard

This module allows the declaration of wireguard overlay networks to connect several hosts together. It uses the secrets module to automatically exchange secrets between the hosts, configures gateways and routing in case of hosts being behind NAT/without public IPs, while also allowing direct peer to peer connections for hosts reachable by each other.

wireguard.overlay-lan = {
  networkAddress = "10.0.0.0/24";

  members = {
    # define a host "someHost" as a member of this wireguard network 
    someHost.deviceAddresses = "10.0.0.1/32";
    someHost.backend = "wg-quick";
    someHost.advertise.server.listenPort = 51823;
    someHost.advertise.server.firewallAllow = true;

    # define another host "otherHost" as a member of this wireguard network
    otherHost.deviceAddresses = "10.0.0.2/32";
    otherHost.keepAlive = 30;
  };
};

Now run agenix generate, then agenix rekey and voila.

SSH Module

The SSH module automated the distribution of SSH host config between managed hosts.

By specifying which user on which node has access to which user on a different node, SSH configuration and authorized SSH keys can be generated and distributed automatically.

Internal workings

The SSH module automates the following steps:

  1. Generation and distribution of SSH keypairs (via Secrets module).
  2. Configuration of authorizedKey on the target node for the targetUser (via users.users..openssh.authorizedKeys).
  3. Configuration of the .ssh/config file on the source node to allow easy access to the target node (via Home Manager).

Usage

When adding a new grant, renaming a grant, new secrets need to be rolled out to the affected nodes. This can be done by running agenix generate (to generate new secrets) and agenix rekey (to distribute the new secrets to the affected nodes).

For a detailed list of options, see the SSH module reference. Below are some common examples of usage.

Examples

Simple connection

Allow bob@source to SSH into alice@target:

nodes.source.configuration.ssh.grants = {

 # name of the grant, can be anything
 access = {
      
   # connection settings
   from = "bob";
   to.node = "target";
   to.user = "alice";

 };
};

Restrict commands on the target node

Allow bob@source to SSH into alice@target but only allow running the uptime or date command:

nodes.source.configuration.ssh.grants = {

 # name of the grant, can be anything
 uptime = {
      
   # connection settings
   from = "bob";
   to.node = "target";
   to.user = "alice";
   
   commands = {pkgs}: [
     "${pkgs.coreutils}/bin/uptime"
     "${pkgs.coreutils}/bin/date"
   ];

   # provide a list of allowed commands when no valid command is provided
   showAvailableCommands = true;
 };
};

Port forwarding

Allow bob@source to SSH into alice@target but only allow port forwarding on port 5555, no shell access:

nodes.source.configuration.ssh.grants = {

 # name of the grant, can be anything
 portForwarding = {
      
   # connection settings
   from = "bob";
   to.node = "target";
   to.user = "alice";

   # configure client side options
   to.extraOptions = {

     # extra option to the home manager SSH module
     localForward = [
       bind.port = 5555;
       host.address = "127.0.0.1";
       host.port = 5555;
     ];

     extraOptions = {
       # dont open a shell, only port forwarding
       SessionType = "none";
     };

   };

   # configure server side options
   options.open = ["127.0.0.1:5555"];
   
   commands = {pkgs}: [ # only allow port forwarding, no shell or other commands
     "${pkgs.coreutils}/bin/false"
   ];
 };
};

SSH ⚙️

ssh.grants

Grant SSH access from from node users to to node users.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

ssh.grants.<name>.commands

Function that evaluates to a list of commands the user is allowed to execute on the target node. If empty, all commands are allowed.

This function will be called with the pkgs.callPackage function taken from the target node.

Type: function that evaluates to a(n) list of ((submodule) or package convertible to it)

Default:

<function>

Declared by:

ssh.grants.<name>.commands.<function body>.*.aliases

The SSH command that is requested by the user, mapping to this command.

Type: list of string

Default:

[ ]

Declared by:

ssh.grants.<name>.commands.<function body>.*.command

The command to allow.

Type: string or package convertible to it

Declared by:

ssh.grants.<name>.commands.<function body>.*.passParameters

Whether to pass any parameters given by the user to the command.

Type: boolean

Default:

false

Declared by:

ssh.grants.<name>.extraConnectionOptions

Additional SSH connection options to use when connecting to the target node.

View man SSH(8) - AUTHORIZED_KEYS

Type: list of string

Default:

[ ]

Declared by:

ssh.grants.<name>.from

Source user name.

Type: string

Declared by:

ssh.grants.<name>.name

Alias name under which the user can ssh {alias} to the target.

Type: string

Default:

"<name>"

Declared by:

ssh.grants.<name>.options.agentForwarding

Apply the “agent-forwarding” option to this SSH key, allowing SSH agent forwarding.

Type: boolean

Default:

false

Declared by:

ssh.grants.<name>.options.allowPortForwarding

Whether to set the port-forwarding option for this SSH key.

Type: boolean

Default:

false

Declared by:

ssh.grants.<name>.options.extraOptions

Additional configuration options to add authorized_key for this grant.

Type: list of string

Default:

[ ]

Declared by:

ssh.grants.<name>.options.listen

Apply the “permitlisten” option to this SSH key, remote listening and forwarding of ports to local ports.

Type: list of string

Default:

[ ]

Declared by:

ssh.grants.<name>.options.open

Apply the “permitopen” option to this SSH key, allowing to open specific host:port combinations.

Type: list of string

Default:

[ ]

Declared by:

ssh.grants.<name>.options.pty

Apply the “pty” option to this SSH key, allowing to allocate a pseudo-terminal.

Type: boolean

Default:

false

Declared by:

ssh.grants.<name>.options.restrict

Apply the “restrict” option to this SSH key, disabling every feature except executing commands. Disabling this option, will circumvent all other options set via .options .

Type: boolean

Default:

true

Declared by:

ssh.grants.<name>.options.x11Forwarding

Apply the “x11-forwarding” option to this SSH key, allowing X11 forwarding.

Type: boolean

Default:

false

Declared by:

ssh.grants.<name>.resolvedCommands

The resolved commands after evaluating the commands function.

Type: (list of ((submodule) or package convertible to it)) or string (read only)

Declared by:

ssh.grants.<name>.showAvailableCommands

If set to true, when the user tries to execute an unauthorized command, the list of available commands will be shown.

Type: boolean

Default:

true

Declared by:

ssh.grants.<name>.sshGenKeyType

When generating SSH keys use this key type.

Type: one of “ed25519”, “rsa”

Default:

"ed25519"

Declared by:

ssh.grants.<name>.to

Destination node and user.

Type: submodule

Default:

"<to>"

Declared by:

ssh.grants.<name>.to.extraOptions

Additional configuration options to add to the SSH config for this grant.

Type: anything

Default:

{ }

Declared by:

ssh.grants.<name>.to.hostname

Hostname or IP address of the target node.

Multiple addresses may be specified by providing a executable each that when exiting with code 0 selects the corresponding address, see the example value.

Type: string or attribute set of (submodule)

Default:

"<to.node>"

Example:

{
  local = {
    command = "ping -c 1 -W 1 192.168.0.55 > /dev/null";
    host = "192.168.0.55";
    priority = 10;
  };
  public = {
    command = "true";
    host = "host.example.com";
    priority = 20;
  };
}

Declared by:

ssh.grants.<name>.to.node

Destination node name.

Type: string

Declared by:

ssh.grants.<name>.to.port

SSH port of the target node.

Type: signed integer

Default:

22

Declared by:

ssh.grants.<name>.to.sshFingerprint

Expected SSH host key fingerprint of the destination node.

Type: null or string

Default:

null

Declared by:

ssh.grants.<name>.to.user

Destination user name.

Type: string

Declared by:

NixOS Secrets

NixOS Secrets ⚙️

age.rekey.initialRollout

Indicates whether this is the initial rollout. Secrets will not be available on the target host yet.

Type: boolean (read only)

Declared by:

age.secrets

Extension of the age (agenix) secrets module to provide secrets for multi-host NixOs configurations.

Type: attribute set of (submodule)

Declared by:

age.secrets.<name>.global

Whether this secret is global, i.e., a host might subscribe to it without owning it.

Type: boolean

Default:

false

Declared by:

age.secrets.<name>.hosts

The hosts that have access to this secret.

Type: unique list of string

Default:

[
  "<noxa-host-id>"
]

Example:

[
  "host1"
  "host2"
]

Declared by:

age.secrets.<name>.ident

The name of the secret.

This is the name of the secret, e.g. “wg-interface-key”.

Type: string

Example:

"wg-interface-key"

Declared by:

age.secrets.<name>.identifier

A unique identifier for the secret, derived from the module and name. This may be used to name the secret.

Type: string (read only)

Example:

"host:noxa.wireguard.interfaces.some-interface::wg-interface-key"

Declared by:

age.secrets.<name>.module

The owning module of that secret.

Typically this is the name of module declaring the secret, e.g. “noxa.wireguard.interfaces.<name>”.

Type: string

Example:

"services.openssh"

Declared by:

noxa.secrets.enable

Enables the secrets module, multi-host secret management.

Type: boolean

Default:

true

Declared by:

noxa.secrets.def

A list of secrets that are managed by the noxa secrets module.

Each secret is either a host specific secret or a shared secret. Host specific secrets are only available on the host that owns them, while shared secrets are available on all hosts that declare them.

The options provided will be passed to the agenix module, by using the identifier as the name of the secret. The identifier is derived from the module and name of the secret, e.g. “host:noxa.wireguard.interfaces.some-interface::wg-interface-key” or “shared:noxa.wireguard.interfaces.some-interface:host1,host2:wg-preshared-connection-key”.

Type: list of (submodule)

Default:

[ ]

Declared by:

noxa.secrets.def.*.generator

The generator configuration for this secret. See agenix-rekey documentation.

Type: null or (submodule)

Default:

null

Declared by:

noxa.secrets.def.*.generator.dependencies

Other secrets on which this secret depends. See agenix-rekey documentation.

Type: null or (list of unspecified value) or attribute set of unspecified value

Default:

null

Example:

[ config.age.secrets.basicAuthPw1 nixosConfigurations.machine2.config.age.secrets.basicAuthPw ]

Declared by:

noxa.secrets.def.*.generator.script

Generator script, see agenix-rekey documentation.

Type: null or string or function that evaluates to a(n) string

Default:

null

Declared by:

noxa.secrets.def.*.generator.tags

Optional list of tags that may be used to refer to secrets that use this generator.

See agenix-rekey documentation for more information.

Type: null or (list of string)

Default:

null

Example:

[
  "wireguard"
]

Declared by:

noxa.secrets.def.*.global

Whether this secret is global, i.e., a host might subscribe to it without owning it.

Type: boolean

Default:

false

Declared by:

noxa.secrets.def.*.group

The group to set on the secret file when it is created.

Type: null or string

Default:

null

Declared by:

noxa.secrets.def.*.hosts

The hosts that have access to this secret.

Type: unique list of string

Default:

[
  "<noxa-host-id>"
]

Example:

[
  "host1"
  "host2"
]

Declared by:

noxa.secrets.def.*.ident

The name of the secret.

This is the name of the secret, e.g. “wg-interface-key”.

Type: string

Example:

"wg-interface-key"

Declared by:

noxa.secrets.def.*.identifier

A unique identifier for the secret, derived from the module and name. This may be used to name the secret.

Type: string (read only)

Example:

"host:noxa.wireguard.interfaces.some-interface::wg-interface-key"

Declared by:

noxa.secrets.def.*.mode

The file mode to set on the secret file when it is created.

Type: null or string

Default:

null

Declared by:

noxa.secrets.def.*.module

The owning module of that secret.

Typically this is the name of module declaring the secret, e.g. “noxa.wireguard.interfaces.<name>”.

Type: string

Example:

"services.openssh"

Declared by:

noxa.secrets.def.*.owner

The owner to set on the secret file when it is created.

Type: null or string

Default:

null

Declared by:

noxa.secrets.def.*.rekeyFile

The path to the rekey file for this secret. This is used by the agenix-rekey module to rekey the secret.

Type: absolute path (read only)

Declared by:

noxa.secrets.hostSecretsPath

The path where host secrets are stored. This is the path where noxa will look for (encrypted) host specific secrets.

This directory contains encrypted secrets for each host. Secrets in this directory are host specific, at least the secret part of the secret is owned by a single host and only published to that host.

An example secret would be the private wireguard key for an interface. Still the public key might be shared with other hosts.

ATTENTION: Since this path is copied to the nix store, it must not contain any secrets that are not encrypted.

Type: absolute path

Declared by:

noxa.secrets.instanceSecretsPath

The path where instance-wide secrets are stored. This is the path where noxa will look for (encrypted) instance-wide secrets.

This directory contains encrypted secrets that are shared between potentially all hosts. Secrets in this directory are not host specific, each host might subscribe to them.

An example secret would be a shared API key or password.

Since this path is used by multiple hosts, it is recommended to set this path once for all hosts, instead of setting it per host.

ATTENTION: Since this path is copied to the nix store, it must not contain any secrets that are not encrypted.

Type: absolute path

Declared by:

noxa.secrets.options.enable

Enables the ‘simple’ options, by providing settings proxy, a user can set the options, inside the noxa.secrets.options module that will provide sensible defaults for the agenix and agenix-rekey module.

If this is set to false, the user must set-up the agenix and agenix-rekey modules manually.

Type: boolean

Default:

true

Declared by:

noxa.secrets.options.hostPubkey

The public key of the host that is used to encrypt the secrets for this host.

Type: null or string

Default:

null

Declared by:

noxa.secrets.options.masterIdentities

A list of identities that are used to decrypt encrypted secrets for rekeying.

Type: list of (submodule)

Declared by:

noxa.secrets.options.masterIdentities.*.identity

The identity that is used to encrypt and store secrets as .age files. This must be an absolute path, given as string to not publish keys to the nix store.

This is the private key file used.

Type: null or string

Declared by:

noxa.secrets.options.masterIdentities.*.pubkey

The identity that is used to encrypt and store secrets as .age files. This is the age public key of the identity, used to encrypt the secrets.

This is the public key file used.

Type: null or string

Declared by:

noxa.secrets.options.rekeyDirectory

The directory where the rekey files are stored. This is used by the agenix-rekey module to rekey the secrets. This directory must be writable by the user that runs the agenix-rekey module and added to the git repo.

It is recommended to use $\{noxaHost} to create a unique directory for each host.

Type: absolute path

Declared by:

noxa.secrets.secretsPath

The path where all secrets are stored. Subfolders are created for host specific, shared, and instance secrets.

Type: null or absolute path

Declared by:

noxa.secrets.sharedSecretsPath

The path where secrets shared between several hosts are stored. This is the path where noxa will look for (encrypted) shared secrets.

This directory contains encrypted secrets that are shared between several hosts. Secrets in this directory are not host specific, they are not owned by a single host, but an group of hosts.

An example secret would be the pre-shared symmetric key for a wireguard interface peer.

Since this path is used by multiple hosts, it is recommended to set this path once for all hosts, instead of setting it per host.

ATTENTION: Since this path is copied to the nix store, it must not contain any secrets that are not encrypted.

Type: absolute path

Declared by:

noxa.sshHostKeys.generate

Generates SSH host keys on boot even if the openssh service is not enabled.

Type: boolean

Default:

false

Declared by:

noxa.sshHostKeys.hostKeysPrivate

List of SSH private host keys, accessible during runtime.

Type: list of string (read only)

Declared by:

noxa.sshHostKeys.impermanencePathOverride

Override the storage location for the ssh keys. Since some modules, like the noxa.secrets module, depend on the keys being stored on a mounted disk during configuration activation, and not expose functionality of systemd orderings, this option can be used to override the storage location of the keys; useful when using impermanence setups.

Type: null or string

Default:

null

Declared by:

Nodes

The nodes module allows definition of hosts and their NixOS configuration.

Defining nodes

A node is defined via

nodes.<name>.configuration = {
  # (NixOS) configuration of the node <name>
  # ...
};
nodeNames = [ "<name>" ];

If you use the template, the nodes are automatically added to the nodeNames list.

Default values for all nodes

Default nixos configuration values for all nodes (or imports) may be specified via the defaults attribute, which is inherited by all nodes.

defaults.configuration = {
  # default configuration for all nodes
  # ...
};

Nixpkgs versions

Each host may use its own version of nixpkgs which can be specified via the nixpkgs attribute. If not specified, the node inherits the nixpkgs version from the noxa framework.

Nodes ⚙️

defaults

Default options applied to all nodes.

Type: submodule

Default:

{ }

Declared by:

defaults.build.toplevel

Build this node’s configuration into a NixOS system package.

Alias to config.system.build.toplevel.

Type: package (read only)

Declared by:

defaults.build.vm

Build this node’s configuration into a VM testing package.

Alias to config.system.build.vm.

Type: package (read only)

Declared by:

defaults.nixpkgs

The nixpkgs version to use when building this node.

By default, if not explicitly set, it uses the same version than the Noxa flake itself.

Type: absolute path

Default:

"<nixpkgs>"

Declared by:

defaults.options

The contents of the options defined by the nixpkgs module for this node.

Type: raw value (read only)

Default:

"<options>"

Declared by:

defaults.pkgs

The pkgs set with overlays and for the target system of this node.

Type: raw value (read only)

Default:

"<pkgs>"

Declared by:

defaults.reachable.allowHostConfiguration

Allow the host to configure its own reachable addresses. If set to false, values can only be set on the Noxa module level.

Type: boolean

Default:

false

Declared by:

defaults.reachable.internet

List of external IP addresses this host is reachable at via using a public IP address.

Type: list of (IPv4 address or IPv6 address)

Default:

[ ]

Declared by:

defaults.reachable.wireguardNetwork

List of IP addresses this host is reachable at via WireGuard (specified via name).

Type: attribute set of list of (IPv4 address or IPv6 address)

Default:

{ }

Declared by:

defaults.specialArgs

Special arguments passed to the host modules.

Type: attribute set of anything

Default:

{ }

Declared by:

nodeNames

A list of node names managed by Noxa. Due to the architecture of Noxa, noxa modules might unwillingly create new nodes, this list contains the name of all nodes that are currently managed by Noxa. Noxa modules can check this list to see if a node was created by themselves.

  The user must set this to the listOf all nodes they want to manage, otherwise if you
  don't care, set this to `attrNames config.nodes`.

Type: list of string

Default:

[ ]

Declared by:

nodes

A set of nixos hosts managed by Noxa.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

nodes.<name>.build.toplevel

Build this node’s configuration into a NixOS system package.

Alias to config.system.build.toplevel.

Type: package (read only)

Declared by:

nodes.<name>.build.vm

Build this node’s configuration into a VM testing package.

Alias to config.system.build.vm.

Type: package (read only)

Declared by:

nodes.<name>.nixpkgs

The nixpkgs version to use when building this node.

By default, if not explicitly set, it uses the same version than the Noxa flake itself.

Type: absolute path

Default:

"<nixpkgs>"

Declared by:

nodes.<name>.options

The contents of the options defined by the nixpkgs module for this node.

Type: raw value (read only)

Default:

"<options>"

Declared by:

nodes.<name>.pkgs

The pkgs set with overlays and for the target system of this node.

Type: raw value (read only)

Default:

"<pkgs>"

Declared by:

nodes.<name>.reachable.allowHostConfiguration

Allow the host to configure its own reachable addresses. If set to false, values can only be set on the Noxa module level.

Type: boolean

Default:

false

Declared by:

nodes.<name>.reachable.internet

List of external IP addresses this host is reachable at via using a public IP address.

Type: list of (IPv4 address or IPv6 address)

Default:

[ ]

Declared by:

nodes.<name>.reachable.wireguardNetwork

List of IP addresses this host is reachable at via WireGuard (specified via name).

Type: attribute set of list of (IPv4 address or IPv6 address)

Default:

{ }

Declared by:

nodes.<name>.specialArgs

Special arguments passed to the host modules.

Type: attribute set of anything

Default:

{ }

Declared by:

Getting Started

todo

Wireguard (Node Config) ⚙️

noxa.wireguard.enable

Enables the WireGuard module, which a cross-host VPN setup utility for wireguard.

Type: boolean

Default:

false

Declared by:

noxa.wireguard.interfaces

A set of WireGuard interfaces to configure. Each interface is defined by its name and contains its private key, public key, and listen port.

Type: lazy attribute set of (submodule)

Default:

{ }

Declared by:

noxa.wireguard.interfaces.<name>.advertise.keepAlive

The keep alive interval remote peers should use when communicating with this interface.

Type: null or signed integer

Default:

null

Declared by:

noxa.wireguard.interfaces.<name>.advertise.server

Options for wireguard servers. If a wireguard interface is regarded as a server (e.g. since it has a public IP address), it may advertise its service via the server.advertise option.

If set, all peers that would like to connect to that peer will use the advertised listen port and address as means of directly connecting to the server.

Further, if server.defaultGateway is set, all peers that do not advertise listen port and address will be reached via the server marked as default gateway. Therefore, only one interface may be marked as default gateway at any time.

Type: null or (submodule)

Default:

null

Declared by:

noxa.wireguard.interfaces.<name>.advertise.server.defaultGateway

If set, this server will be the default gateway for clients.

Type: boolean

Default:

false

Declared by:

noxa.wireguard.interfaces.<name>.advertise.server.firewallAllow

If set, the nixos firewall will allow incoming connections to the advertised listen port.

Type: boolean

Default:

true

Declared by:

noxa.wireguard.interfaces.<name>.advertise.server.listenAddress

The address this server will listen on for incoming connections.

Type: IPv4 address or IPv6 address

Declared by:

noxa.wireguard.interfaces.<name>.advertise.server.listenPort

The port this server will listen on for incoming connections.

Type: signed integer

Declared by:

noxa.wireguard.interfaces.<name>.autostart

Specifies whether to autostart the WireGuard interface.

Only relevant if the backend is set to wg-quick.

Type: boolean

Default:

true

Declared by:

noxa.wireguard.interfaces.<name>.backend

The backend to use for WireGuard config generation.

  • wireguard: Uses the networking.wireguard.interfaces module to generate the configuration.
  • wg-quick: Uses the networking.wg-quick.interfaces module to generate the configuration.

Type: one of “wireguard”, “wg-quick”

Default:

"wireguard"

Declared by:

noxa.wireguard.interfaces.<name>.deviceAddresses

List of ip addresses to assign to this interface. The server will forward traffic to these addresses.

Type: list of (IPv4 address or IPv6 address)

Declared by:

noxa.wireguard.interfaces.<name>.gatewayOverride

If set, this interface will use the specified hostname as the gateway for connecting to the wireguard network.

Type: null or string

Default:

null

Declared by:

noxa.wireguard.interfaces.<name>.keepAlive

The default keep alive interval this interface will use when communicating with remote peers.

If the remote end uses advertise.keepAlive, the minimum value of both will be used.

Type: null or signed integer

Default:

null

Declared by:

noxa.wireguard.interfaces.<name>.kind.isClient

This interface has the role of a client, meaning it does not advertise other peers to connect to it. Instead, it connects to other peers, initiating the connection.

Type: boolean (read only)

Default:

true

Declared by:

noxa.wireguard.interfaces.<name>.kind.isGateway

This interface is a server and is marked as the default gateway for clients. This means that clients will use this interface to reach other peers that do not advertise their listen port and address.

Type: boolean (read only)

Default:

false

Declared by:

noxa.wireguard.interfaces.<name>.kind.isServer

This interface has the role of a server, meaning it advertises its listen port and address to peers.

Type: boolean (read only)

Default:

false

Declared by:

noxa.wireguard.interfaces.<name>.networkAddress

The network IP addresses. On clients, traffic of this network will be routed through the WireGuard interface.

Type: IPv4 address, normalized network address, or (IPv6 address, normalized network address)

Declared by:

noxa.wireguard.routes

A set of intermediary connection information, automatically computed from the nixos configurations.

Type: lazy attribute set of (submodule) (read only)

Declared by:

noxa.wireguard.routes.<name>.neighbors

A set of connections for this interface, automatically computed from the nixos configurations.

Type: lazy attribute set of (submodule) (read only)

Declared by:

noxa.wireguard.routes.<name>.neighbors.<name>.keepAlive

The keep-alive interval for this connection, in seconds. If set to null, no keep-alive is configured.

Type: null or signed integer (read only)

Declared by:

noxa.wireguard.routes.<name>.participants.clients

A list of clients for this interface. Automatically populated.

Type: list of string (read only)

Declared by:

noxa.wireguard.routes.<name>.participants.gateways

A list of gateways for this interface. Automatically populated.

Type: list of string (read only)

Declared by:

noxa.wireguard.routes.<name>.participants.servers

A list of servers for this interface. Automatically populated.

Type: list of string (read only)

Declared by:

noxa.wireguard.routes.<name>.peers

A list of peers for this interface. Automatically populated.

Type: list of (submodule) (read only)

Declared by:

noxa.wireguard.routes.<name>.peers.*.target

The target hostname of the connection.

Type: string (read only)

Declared by:

noxa.wireguard.routes.<name>.peers.*.via

The hostname of the peer this connection is routed through.

Type: null or string (read only)

Declared by:

noxa.wireguard.secrets

A set of wireguard secrets. When using the .interfaces options, this set is automatically populated. Each peer will own its own set of secrets

Type: lazy attribute set of (submodule)

Default:

{ }

Declared by:

noxa.wireguard.secrets.<name>.presharedKeyFiles

The pre-shared key file for each peer (by hostname) of the wireguard interface

Type: lazy attribute set of string (read only)

Declared by:

noxa.wireguard.secrets.<name>.privateKeyFile

The private key file of the wireguard interface

Type: string (read only)

Declared by:

noxa.wireguard.secrets.<name>.publicKey

The public key the wireguard interface

Type: string (read only)

Declared by:

Wireguard (Noxa Config) ⚙️

wireguard

Type: attribute set of (submodule)

Default:

{ }

Declared by:

wireguard.<name>.allowNodesToJoin

If set to true, nodes may join this network by declaring noxa.wireguard.interfaces.<interface>. If set to false, nodes may only join this network if declared in this network configuration.

Type: boolean

Default:

false

Declared by:

wireguard.<name>.autoConfigureGateway

If set to true, the gateways for this network will be automatically configured based on the nodes.<name>.reachable.internet attribute.

Type: boolean

Default:

true

Declared by:

wireguard.<name>.members

Configuration of the wireguard members.

Type: attribute set of (submodule)

Declared by:

wireguard.<name>.members.<name>.advertise.keepAlive

The keep alive interval remote peers should use when communicating with this interface.

If set to any other value than null, the value will be applied to the nodes configuration.

Type: null or signed integer

Default:

null

Declared by:

wireguard.<name>.members.<name>.advertise.server

Options for wireguard servers. If a wireguard interface is regarded as a server (e.g. since it has a public IP address), it may advertise its service via the server.advertise option.

If set, all peers that would like to connect to that peer will use the advertised listen port and address as means of directly connecting to the server.

Further, if server.defaultGateway is set, all peers that do not advertise listen port and address will be reached via the server marked as default gateway. Therefore, only one interface may be marked as default gateway at any time.

If set to any other value than null, the value will be applied to the node configuration.

Type: null or (submodule)

Default:

null

Declared by:

wireguard.<name>.members.<name>.advertise.server.defaultGateway

If set, this server will be the default gateway for clients.

Type: null or boolean

Default:

null

Declared by:

wireguard.<name>.members.<name>.advertise.server.firewallAllow

If set, the nixos firewall will allow incoming connections to the advertised listen port.

If set to any other value than null, the value will be applied to the node configuration.

Type: null or boolean

Default:

null

Declared by:

wireguard.<name>.members.<name>.advertise.server.listenAddress

The address this server will listen on for incoming connections.

Type: null or IPv4 address or IPv6 address

Default:

null

Declared by:

wireguard.<name>.members.<name>.advertise.server.listenPort

The port this server will listen on for incoming connections.

Type: null or signed integer

Default:

null

Declared by:

wireguard.<name>.members.<name>.autostart

Specifies whether to autostart the WireGuard interface.

Only relevant if the backend is set to wg-quick.

If set to any other value than null, the value will be applied to the node configuration.

Type: null or boolean

Default:

null

Declared by:

wireguard.<name>.members.<name>.backend

The backend to use for WireGuard config generation.

  • wireguard: Uses the networking.wireguard.interfaces module to generate the configuration.
  • wg-quick: Uses the networking.wg-quick.interfaces module to generate the configuration.

If set to any other value than null, the value will be applied to the node configuration.

Type: null or one of “wireguard”, “wg-quick”

Default:

"wireguard"

Declared by:

wireguard.<name>.members.<name>.deviceAddresses

List of ip addresses to assign to this interface. The server will forward traffic to these addresses.

Type: null or ((list of (IPv4 address or IPv6 address)) or (IPv4 address or IPv6 address) convertible to it)

Default:

null

Declared by:

wireguard.<name>.members.<name>.gatewayOverride

If set, this interface will use the specified node as the gateway for connecting to the wireguard network.

If set to any other value than null, the value will be applied to the nodes configuration.

Type: null or string

Default:

null

Declared by:

wireguard.<name>.members.<name>.keepAlive

The default keep alive interval this interface will use when communicating with remote peers.

If the remote end uses advertise.keepAlive, the minimum value of both will be used.

If set to any other value than null, the value will be applied to the nodes configuration.

Type: null or signed integer

Default:

null

Declared by:

wireguard.<name>.members.<name>.onlySpecifiedDeviceAddresses

If set to true, device addresses set by the node configuration are rejected. If set to false, the node might add additional device addresses to the interface.

Type: boolean

Default:

true

Declared by:

wireguard.<name>.networkAddress

The network address of the wireguard network.

Type: IPv4 address, normalized network address, or (IPv6 address, normalized network address)

Declared by: