NixOS 19.09 separates configuration from privileged runtime state
Loris changes installer privileges and moves GitLab secrets into runtime files. Its service migrations show why a declarative upgrade still needs explicit treatment of persistent state.
A declarative operating system still has information that should not become part of an ordinary build artifact. NixOS 19.09 makes that distinction concrete by replacing GitLab's literal secret options with paths to secret files. The October 9 Loris announcement opens the new stable branch, and the release-tag notes explain why these values must remain outside the world-readable Nix store.
The same release changes the installation environment to log in as a less privileged nixos user. It also adjusts service identities and migrates persistent state for the new systemd version. These changes concern different operations, but each requires a declaration to account for a privilege or state boundary.
This is the part of NixOS that interests me most. Describing a system in source is useful only when the model remains clear about what belongs in the built configuration and what must be supplied or maintained at runtime. A release that exposes those distinctions gives the administrator a more accurate description of the machine.
A secret path replaces a secret value
The GitLab module previously accepted literal values for database credentials, SMTP authentication and several application secrets. Loris replaces those options with file-based versions. The release notes say the purpose is to avoid storing secret values in the world-readable store.
For a database password, the new form is a string naming the runtime file.
services.gitlab.databasePasswordFile = "/run/secrets/gitlab-database";
The path in this example is illustrative. The file must actually exist and contain the password expected by the deployment. Writing the option does not provision the file, choose its contents or decide who can read it.
The distinction between the path and its contents is essential. The module needs a reference through which runtime setup can obtain the credential. It does not need to embed that credential as a literal value in the configuration used to build the system. The release's migration instructions ask for a file containing the same exact string as the former option.
This is a compatibility change for an existing configuration. Merely adding File to the option name while leaving the password as its value would supply a password-shaped filename instead of a usable reference. Both the option's name and the meaning of its value change.
The new form also leaves secret delivery as an operational responsibility. It defines how the module receives a value without specifying every possible way an administrator might provide that file. That is an appropriate separation for a module used in different deployment arrangements.
Directory ownership also belongs to the contract
The release adds an ownership restriction to services.gitlab.statePath. Every parent directory must belong either to root or to the configured GitLab user. The notes give the example of a state path below /var/lib/gitlab and apply the restriction through its parent directories.
That requirement concerns the path used to reach state, not only the final directory's mode. A service's state location depends on the hierarchy containing it. Checking the leaf alone would omit the ownership relationships the release now requires along that path.
This is a different condition from keeping the database password out of the store. The secret-file change governs where confidential content is supplied. The state-path restriction governs the filesystem ownership surrounding mutable application state. A deployment needs to satisfy both where it uses both mechanisms.
My preference for declarative systems includes making these requirements inspectable. A path written in configuration is not a complete description of the authority surrounding it. Naming the required ownership relationship gives the path an operational contract that can be checked against the machine.
The installer changes its ordinary login identity
Loris's installation environment now uses the nixos account instead of logging into root by default. The installation-device profile at the release tag defines a normal user and configures automatic console login for that account.
The profile includes wheel membership and enables passwordless sudo. The release notes give sudo -i as the way to obtain a root shell. Ordinary commands therefore begin with the less privileged account, while installation tasks that require root can still acquire those privileges explicitly.
This is a change to the default execution identity, not a claim that the live installer establishes a strong authentication barrier against someone at its console. The profile assigns empty initial passwords to the installer accounts and configures the escalation path for that environment.
The profile's SSH setup makes another distinction. It provides the service but removes its ordinary wantedBy activation so it is not started by default. Its help text says to start the daemon manually and set a password for an account before logging in. Local console convenience and remote access are configured separately.
An installation image has a particular job and lifetime. These settings describe that image, not a blanket account policy that every installed NixOS machine should inherit. The point of the release change is that the user's initial work no longer starts in a root shell automatically.
Service setup moves under service identities
The MySQL service now runs as the mysql user. Previously systemd started it as root and MySQL dropped privileges itself. Loris applies the service user to the pre-start and post-start phases as well.
The notes say runtime and data-directory setup moves to RuntimeDirectory and tmpfiles to support that arrangement. This identifies the necessary companion change. Removing root from the service command is incomplete if its setup code still assumes it can create privileged directories directly.
Prometheus exporters also change their identity handling. They use DynamicUser by default. Exporters that need a persistent real user instead receive dedicated user and group names following an exporter-specific pattern, replacing the earlier nobody and nogroup defaults for those cases.
These are concrete decisions about process ownership. They should not be summarized as every service adopting the same identity model. MySQL gets its service user throughout its phases, while exporters choose between dynamic and dedicated identities according to their module requirements.
Existing state needs a migration condition
The upgrade to systemd 242 changes systemd-timesyncd so it no longer uses DynamicUser=yes. Its old state directory is /var/lib/private/systemd/timesync, and the new location is /var/lib/systemd/timesync. An activation script performs the move when required.
The release notes tie that migration to the system's state version. They tell a machine upgrading from 19.03 to retain a system.stateVersion of 19.03, or an earlier value appropriate to an older installation. Incorrectly changing that setting can prevent the required state migration.
This is a useful distinction between selecting new software and declaring the history of persistent state. The new systemd package belongs to the upgraded system. The existing state still needs treatment appropriate to where the previous installation stored it.
A declaration that only says the target release is 19.09 does not supply that history. The state-version condition lets activation distinguish an older state layout from one already created under the newer assumptions. That is why changing the number to match the release can have consequences beyond selecting packages.
Dependencies become specific to the required resource
The release also changes how nginx depends on ACME certificates. It no longer waits on a catch-all certificate target. Instead it depends on the particular certificates it manages, and the old aggregate target is removed.
The notes connect this to a certificate-renewal ordering race that could leave nginx in a broken state during a restart. Depending on an unrelated aggregate does not express the exact relationship between a service and the files it needs. The new dependency identifies those files' own lifecycle more closely.
NixOps key dependencies receive a similar adjustment. Services should depend on the relevant key-specific service instead of keys.target, which represented all uploaded keys. These changes reduce unrelated coupling in the declared service graph.
The practical benefit is a more accurate prerequisite. A service should wait for the credential or certificate it needs, with ordering that follows that resource's setup. Waiting for an entire collection can impose unrelated dependencies while still failing to express the precise transition the service cares about.
Other defaults make selection more explicit
Loris adds controls for GNOME's core services, shell and utilities, with games separately selectable. It also adds declarative printer configuration through ensurePrinters and ensureDefaultPrinter. The printer notes explicitly say that ensuring the declared printers does not delete existing ones.
That qualification matters to the meaning of desired state. An option that ensures the presence of listed resources is not necessarily an instruction to remove every unlisted resource. The documented behavior lets existing printers coexist with the declared set.
The release packages Nix 2.3 as its stable Nix. The operating-system changes go beyond that package update. Secret references, service users and state migrations each have their own contract. For an existing GitLab deployment, the immediate result is that the declaration names a file containing the credential, while the credential itself remains a runtime input outside the ordinary store artifact.