November 29, 2023·6 min read·engineeringnixnixoswirelessupgrades

NixOS 23.11 gives wireless configuration a more explicit model

Tapir rewrites the hostapd module around radios and networks while changing authentication defaults. Its upgrade notes also distinguish package updates from compatibility with existing application state.

A single wireless-interface option cannot describe every access-point arrangement clearly. A physical radio can advertise multiple networks, and authentication belongs to each advertised network. NixOS 23.11 rewrites its hostapd module around those distinctions. The result is a more explicit configuration model with an incompatible change for existing declarations.

The November 29 release notes name the release Tapir. Their release-tag version highlights hostapd support for WiFi 6 and WPA3-SAE-PK alongside the module rewrite. It also documents changes to core packages and compatibility rules for services with existing data.

I value NixOS because a declaration can describe the system's intended arrangement directly. The hostapd change is interesting in that context. Its structure names the radio and network separately, so the declaration can express a relationship that a single top-level interface field could not capture as clearly.

Radios and advertised networks have different settings

The 23.11 hostapd module defines services.hostapd.radios. Each entry describes a radio, with a nested networks set defining its basic service sets. The module documentation describes a basic service set as what users commonly call a WiFi network.

Radio-level settings include the band and channel. A physical radio has one hardware mode under this model. A dual-band or tri-band access point therefore needs hardware with the appropriate multiple-radio capabilities. Adding another network beneath a radio does not turn that radio into an independent second band.

The module's examples show a radio with two advertised networks. One can require authentication while another has an explicitly open mode. Their SSIDs and authentication choices belong to separate network entries, while they share the enclosing radio's physical configuration.

This structure explains why the old services.hostapd.interface declaration cannot simply retain its original meaning. An existing configuration must identify the appropriate radio and network in the new hierarchy. That is a change to the model expressed by the options, not only a package update behind an unchanged interface.

The implementation also has concrete naming requirements. A network's name must begin with its radio name, and the module validates that relationship. With multiple networks on a radio, it currently requires explicit BSSIDs. These are operational constraints of the supplied configuration model, documented and checked by the module.

Authentication now defaults to WPA3-SAE

The release changes the default authentication mode to WPA3-SAE. Other modes remain available, including WPA2 using SHA-256 and a WPA3 transition mode. This default is therefore a policy decision within a configuration interface that still lets the operator state another supported choice.

Its mode descriptions give transition mode a specific meaning. It uses WPA3-Personal where possible and falls back to the documented WPA2 mode otherwise. The module requires both types of password configuration for that mode. Selecting a label without supplying the corresponding credentials is not enough.

The implementation checks these relationships through assertions. WPA3-SAE requires an SAE password source. WPA2 requires exactly one applicable WPA password source. Transition mode requires the SAE source and one WPA source. The evaluation checks make some invalid combinations fail before the service attempts to start.

These assertions do not establish that every client device can use the chosen mode. They check the declaration's internal requirements. Client compatibility and the radio's hardware support remain properties of the actual deployment, beyond what a Nix expression can establish from the selected options alone.

WiFi 6 support in the package is also distinct from the module's authentication default. A wireless protocol capability and a password-authentication choice describe different aspects of the access point. The release supplies both changes, but enabling one should not be explained as automatically selecting every other capability.

Password files separate secrets from declarations

The module rewrite adds file-based password options. Its examples recommend those over embedding password strings directly. The implementation creates runtime configuration before starting hostapd and incorporates the specified password data into that runtime arrangement.

A focused configuration fragment can state the hierarchy and authentication source without embedding the password itself:

{
  services.hostapd.enable = true;
  services.hostapd.radios.wlp2s0.networks.wlp2s0 = {
    ssid = "Example network";
    authentication = {
      mode = "wpa3-sae";
      saePasswordsFile = "/run/secrets/example-sae-passwords";
    };
  };
}

This fragment assumes that the named interface exists and that a separate mechanism supplies the password file in the format the module documents. It does not describe address assignment or a complete routed access point. Its purpose is to show where the network's identity and authentication source now belong.

The distinction between a string naming a runtime file and the file's contents matters here. The declaration selects the location to read. It does not need to carry the secret value as part of the option text. Provisioning that file remains a separate responsibility.

The source documents why that separation matters. Inline SAE password entries go into a world-readable file in the Nix store. Keeping a repository private does not change the permissions of that generated store file. The runtime-file option addresses a different stage of handling the value. SAE entries can also include a peer MAC address or identifier, and the documented selection rule uses the last matching entry. A password source can therefore describe a set of matching credentials, not necessarily one undifferentiated network password.

I find this a useful expression of declarative configuration's boundary. A repository can state which credential source a service should consume without pretending the repository itself must contain that credential. The module provides the service integration, while the deployment supplies the protected material.

Upgrading packages does not redefine existing data

The release also documents service changes where persisted application state constrains the update. One example is Nextcloud's default package selection, which depends on system.stateVersion. The notes say an explicit package takes precedence, and warn that major-version upgrades cannot skip the required intermediate version.

The state-version option documentation at this tag explains the reason for this mechanism. It records the first NixOS version installed on a machine and preserves compatibility with application data created under older defaults. It is not the selector for which Nixpkgs revision provides the operating system.

That distinction prevents a common misunderstanding of an upgrade. Keeping an older state version does not mean the system's packages remain frozen at that release. Changing the state version does not itself upgrade the system. It can instead change compatibility-sensitive defaults for applications that cannot migrate their data automatically.

The option documentation tells users to retain the value after an ordinary release upgrade. Changing it requires inspecting the resulting configuration changes and performing the relevant data migrations. The release's Nextcloud example supplies a concrete case where package selection and stored-state compatibility must be considered separately.

A declaration can therefore be current in its package source while deliberately retaining an older compatibility default. That is not an inconsistency. It represents the application's existing state as a constraint on the configuration produced by the newer module collection.

Runtime transitions remain separate from builds

Tapir updates systemd from 253 to 254 and glibc from 2.37 to 2.38. The notes recommend preparing the generation for boot and rebooting because a live switch can fail in rare cases involving missing mount units. That is a statement about activation of the running machine, beyond whether the new system output builds.

The notes also require boot.resumeDevice for hibernation outside EFI mode. This is a specific configuration requirement attached to the runtime transition. A successful evaluation of unrelated service declarations would not establish that hibernation has the device information it needs.

Package output structure changes too. getent moves into a dedicated glibc output, reducing the dependency size for consumers that need only that utility. The release recommends the getent alias rather than assuming the executable remains in glibc.bin. A dependency expression can stay explicit while the packaging arrangement becomes more selective.

The ROCm package set advances to 5.7.0, with versioned attribute sets. This is another case where package selection needs the release's actual names and versions. A declarative expression still depends on the interface supplied by the chosen package collection.

For systems using hostapd, the most substantial configuration work is the new radio/network hierarchy and its authentication requirements. For stateful services, the compatibility defaults remain a separate concern from the package-source upgrade. Tapir's notes connect those decisions to concrete options, letting the resulting declaration describe both the new implementation and the existing state it must continue to serve.