Declarative configuration for my macOS machines, built on
nix-darwin for system settings and
home-manager for the user environment, with
secrets encrypted through sops-nix. Each machine gets its
own entry under darwinConfigurations in flake.nix; everything they share
lives in the common directories described in
Directory Structure.
- Add the host's base configuration under
./hosts, named after the machine's hostname — either a flat./hosts/<hostname>.nixor a./hosts/<hostname>/default.nixfolder if the host needs several files. - Add the matching Home Manager configuration at
./home/<username>/<hostname>.nix, following the same convention. - Register both in
flake.nix: add them todarwinModulesandhomeModules, then add adarwinConfigurations.<hostname>entry that composes the modules.
- Run
curl -L https://nixos.org/nix/install | shto install Nix. On macOS this performs a multi-user installation, whichnix-darwinrequires: it creates thenixbldgroup and thenix-daemonservice, and leaves/nix/storeowned byroot:nixbld. - In a new terminal, verify installation with
nix --version - Enable flake support, so the
nixcommands in the next steps can evaluate this repository:
mkdir -p ~/.config/nix
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.confOnce nix-darwin is activated it manages /etc/nix/nix.conf from the nix.settings options in
hosts/common/global/nix.nix, which is where
experimental-features is set from then on. The user-level file written above is still read in
addition to it, so it does no harm, but it is only needed to bootstrap the first build and can
be removed afterwards.
See the Nix installation guide for the full documentation.
If ~/.gnupg folder doesn't exist, create it: mkdir ~/.gnupg.
This is to work around a minor gnupg agent issue that prints warnings if folder doesn't exist.
- From the password manager, create the
~/.ssh/id_ed25519and~/.ssh/id_ed25519.pubfiles. - Create the AGE keys file by running
ssh-to-age -i ~/.ssh/id_ed25519 -private-key > ~/.config/sops/age/keys.txt
If at some point the SSH keys need to be recreated, run ssh-keygen -t ed25519 -C "<email-here>".
Keep in mind to not set a passphrase for the key in order to be used from home-manager.
- Clone this repository in the folder:
~/.config - In the cloned folder, run
sudo make install - Run
direnv allowto set up the project locally for development - Run
sudo make switchto apply the configuration after the initial setup
Use make install only for that first activation: darwin-rebuild is not on PATH yet, so the
target fetches it with nix run nix-darwin/master#darwin-rebuild instead. Activation installs it
at /run/current-system/sw/bin/darwin-rebuild, and every later change goes through
sudo make switch, which calls that binary directly.
Run gpg --list-secret-keys --keyid-format=long and verify that no unsafe permissions on homedir
warning is printed. If a warning is printed, run the following commands to fix the permissions:
find ~/.gnupg -type f -exec chmod 600 {} \; # Set 600 for files
find ~/.gnupg -type d -exec chmod 700 {} \; # Set 700 for directoriesRun gpgconf --kill gpg-agent to restart the GPG agent and apply the new configurations.
Follow the steps in this guide to set up commit signing and signature verification in Git and GitHub.
- Apply changes from configuration:
sudo make switch. The available system options are documented in thenix-darwinmanual. - Format the project's source code:
make fmt. - Edit secrets:
make sops-*commands provided inMakefile. - Build for Linux:
make darwin-run-builderruns a remote Linux builder for macOS on demand, for building Linux derivations from this host. The declarativenix.linux-builderoption inhosts/common/presets/darwin.nixis disabled.
See Makefile for other commands.
The directory structure and documentation is inspired and based of off the Effect-TS/infra repository.
The directory structure of this project is optimized for sharing configuration as much as possible.
The ./hosts directory contains all systems that are managed by Nix Darwin. Each host is named
after the machine's hostname, and can be either a flat <hostname>.nix file or a <hostname>/
directory with a default.nix when the host's configuration warrants splitting up.
In addition, the ./hosts directory contains a ./common subdirectory. This directory contains
configuration that can be shared across all hosts. Within the ./common subdirectory, we can have
the following:
./common/global-> global configuration that is shared between all hosts and presets./common/presets-> presets that are applied to specific host types (i.e.nixos,darwin,desktop, etc.)./common/users-> user-specific configuration that is shared between hosts
The /home directory contains per-user, per-host Home Manager configurations. The directory
hierarchy corresponds to the user-specific Home Manager configurations for a particular host,
and follows the same flat-file-or-folder convention as ./hosts:
./home/<username>/<hostname>.nix
./home/<username>/<hostname>/default.nix
In addition, the ./home directory contains a ./common subdirectory. This directory
contains Home Manager configurations that can be shared across all hosts. Within the ./common
subdirectory, we have the following:
./common/global-> global configuration that is shared between all hosts./common/presets-> presets that are applied to specific host types (i.e.nixos,darwin,desktop, etc.)
The ./modules directory contains configuration to be used from the system configuration from
the flake (i.e. darwinSystem, linuxSystem, etc.).
This project makes use of Mozilla SOPS (Secrets OPerationS)
The .sops.yaml file at the root of the repository defines creation rules for
secrets to be encrypted with sops. Any files matching the defined creation rule paths will be
encrypted with the specified public keys.
To update secrets, run sops <path> to open the secrets file in unencrypted edit mode,
and just save after updating. Helper make commands were added for ease of use:
make sops-secrets edits hosts/common/global/secrets/secrets.yaml, and make sops-awscli
edits hosts/common/global/secrets/awscli.yaml.
If the file doesn't exist, it will be created. Make sure that a valid path regex exists in the
.sops.yaml file for the new file. The current rules only match files inside a secrets/
directory under hosts/ or home/ — a file placed elsewhere is silently left unencrypted, so
add the rule before writing any secret into it.
The project uses sops-nix for automatically decrypting
and injecting secrets into our Nix configurations.
To re-encrypt every secret file after making changes to the .sops.yaml file, run the snippet
below. It uses find -E for extended regular expressions, since the BSD find shipped with
macOS defaults to basic ones and won't match the creation rules otherwise:
find -E . -regex "$(yq -r '[.creation_rules[] | "./" + .path_regex] | join("|")' "$(pwd)/.sops.yaml")" | \
xargs -I {} sops updatekeys -y {}Secrets are encrypted to age recipients. Each entry under keys in .sops.yaml is the age
public key derived from the Ed25519 SSH key of a user on a given machine — the same key pair set
up in Setup encryption keys, not an SSH host key.
To derive the age public key, convert the SSH public key with ssh-to-age:
ssh-to-age -i ~/.ssh/id_ed25519.pubThen add it to the keys list in .sops.yaml, reference it from the desired key groups, and
re-encrypt the secret files (see Updating Secrets Configuration).
The re-encryption has to be run from a machine that can already decrypt them, since sops
decrypts each file before writing it back out to the new recipient list.