Attic on Nix Darwin

Published: September 7, 2024, updated: January 6, 2025

Here are some snippets of code required to configure and run attic on macOS with nix-darwin, and use it as an optional cache.

Files required

To make nix-darwin configure attic correctly, you need a configuration moduleĢµ attic.nix and a configuration atticd.toml for attic to use during runtime to use the correct paths for storage.

User creation

First, create a user and group called attic to host the attic server on your machine. In the attic.nix file, add the following:

# attic.nix
{ config, pkgs, ... }:
{
  users.groups.attic = {
    # Adjust the gid to your liking
    gid = 603;
  };
  users.users.attic = {
    createHome = false;
    description = "attic user";
    gid = 603;
    # Adjust the uid to your liking
    uid = 603;
    isHidden = true;
  };
  users.knownGroups = [ "attic" ];
  users.knownUsers = [ "attic" ];
}

This Nix configuration hides the attic user, and it can’t log in since shell set to sbin/nologin.

Attic configuration

Create a configuration file atticd.toml and add the following contents:

# atticd.toml
# Socket address to listen on, you might want to adjust the port used.
listen = "127.0.0.1:18080"

# Optionally, configure allowed hosts here
allowed-hosts = []

[database]
# Attic's database is located in /var/attic/db.sqlite
url = "sqlite:///var/attic/db.sqlite?mode=rwc"

# Whether to enable sending on periodic heartbeat queries
#
# If enabled, a heartbeat query will be sent every minute
#heartbeat = false

[storage]
# Store everything locally in /var/attic/storage
type = "local"
path = "/var/attic/storage"

# Default values from
# https://github.com/zhaofengli/attic/blob/main/server/src/config-template.toml
[chunking]
nar-size-threshold = 65536 # chunk files that are 64 KiB or larger
min-size = 16384            # 16 KiB
avg-size = 65536            # 64 KiB
max-size = 262144           # 256 KiB

[compression]
type = "zstd"
#level = 8

[garbage-collection]
# The frequency to run garbage collection at
interval = "12 hours"

Write the attic configuration to /etc/attic/atticd.toml using the following Nix snippet:

# attic.nix
{ config, pkgs, ... }:
{
  # ...
  environment.etc = {
    atticd = {
      source = ./atticd.toml;
      target = "attic/atticd.toml";
    };
  };
  # ...
}

Credentials file

Next, feed 32 bytes of random data into base64, and store them in a secret, read-only file only accessible by attic:

openssl rand 32 |
  base64 |
  sudo tee /etc/attic/secret.base64 > /dev/null
sudo chown attic:attic /etc/attic/secret.base64
sudo chmod 400 /etc/attic/secret.base64

Attic service file

Next, tell nix-darwin to add a launchd service using the following snippet in the same attic.nix file:

# attic.nix
{ config, pkgs, ... }:
let
  logPath = "/var/log/atticd";
  attic-client = pkgs.attic-client;
  attic-server = pkgs.attic-server;
in
{
  environment.systemPackages = [
    attic-client
    attic-server
  ];
  launchd.daemons.attic = {
    script = ''
      ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="$(cat /etc/attic/secret.base64)"
      export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64
      exec ${attic-server}/bin/atticd --config /etc/attic/atticd.toml
    '';
    serviceConfig = {
      KeepAlive = true;
      StandardOutPath = "${logPath}/attic.stdout.log";
      StandardErrorPath = "${logPath}/attic.stderr.log";
      UserName = "attic";
    };
  };
}

The preceding launchd daemon script reads the attic secret token into an environment variable and starts the attic server using the configuration stored in atticd.toml.

Ensure that you have a attic runtime directory:

sudo mkdir -m700 /var/attic
sudo chown attic:attic /var/attic

Configuring the client

Import attic.nix into your main nix-darwin configuration:

# darwin-configuration.nix
{ config, pkgs, ... }:
{
  imports = [
    # ...
    ./attic.nix
    # ...
  ];
}

Then, rebuild your nix-darwin system using darwin-rebuild switch. On my system, I use the following invocation:

darwin-rebuild switch --flake $DOTFILES/nix/generic

Next, create a JWT for a cache named after your computer’s name. You can use this JWT with your local Nix builder:

sudo -u attic \
  ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="$(sudo -u attic cat /etc/attic/secret.base64)" \
  atticadm make-token \
  --config /etc/attic/atticd.toml \
  --sub "$(hostname)" \
  --validity "1 month" \
  --pull "$(hostname)-*" \
  --push "$(hostname)-*" \
  --delete "$(hostname)-*" \
  --create-cache "$(hostname)-*" \
  --configure-cache "$(hostname)-*" \
  --configure-cache-retention "$(hostname)-*" \
  --destroy-cache "$(hostname)-*"

The JWT created in his preceding make-token command has broad permissions. Please adjust permissions to your liking. attic outputs the JWT token in your shell session.

Finally, try logging in with the generated token:

# Port configured in atticd.toml
attic login "$(hostname)" http://127.0.0.1:18080 "$YOUR_TOKEN"

Did it work? Great. To tell the Nix builder to use attic as its cache, it needs to have credentials available in a netrc file. Furthermore, you need to add the cache’s public key as a trusted key.

First, retrieve the public key and netrc file. The preceding attic login invocation created a config.toml file in $HOME/.config/attic, which conveniently contains the JWT token for a netrc file.

# This will try to grab the netrc information that attic created after logging
# in
sed -n -E -e 's/token = "(.+)"/machine .+\npassword \1/p' \
  $HOME/.config/attic/config.toml |
  sudo tee /etc/nix/netrc
sudo chmod 440 /etc/nix/netrc
# Let me know if this sed expression worked

You can show the public key using attic cache info:

attic cache info "$(hostname)-default"

You should see the following output:

               Public: false
           Public Key: XXXXXXX-default:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Binary Cache Endpoint: https://XXXXXXXXXXXXX:18080/XXXXXXX-default
         API Endpoint: https://XXXXXXXXXXXXX:18080/
      Store Directory: /nix/store
             Priority: 41
  Upstream Cache Keys: ["cache.nixos.org-1"]
     Retention Period: Global Default

Store the preceding information in attic.nix:

# attic.nix
{ ... } :
let
  # make sure to insert the correct hostname here:
  hostname = "your-hostname";
  # Insert the public key that you have created in the previous step
  public-key = "";
  # Make sure the hostname, port, and cache name are correct
  cache-url = "http://127.0.0.1:18080/${hostname}-default";
in
{
  nix.settings.substituters = [ cache-url ];
  nix.settings.trusted-public-keys = [

    "${hostname}-default:${public-key}"
  ];
  nix.settings.trusted-substituters = [ cache-url ];
  # This file was created using the preceding sed script
  nix.settings.netrc-file = "/etc/nix/netrc";
}

Testing the cache

Now, rebuild nix-darwin one more time. Every time you run Nix commands after that, Nix consults the local attic cache first. You can try this with any command:

# This will look up hello in your local cache first
nix run nixpkgs#hello

Troubleshooting

Does Nix complain that your local cache isn’t a binary cache? Check that you can access the attic cache using curl first:

# Might have copy the netrc file somewhere user-readable
curl --netrc-file /etc/nix/netrc -v -n \
  "http://localhost:18080/$(hostname)-default/nix-cache-info"

You should be able to see the following result:

[...]
< content-type: text/x-nix-cache-info
< date: Sat, 07 Sep 2024 08:15:50 GMT
< content-length: 51
<
WantMassQuery: 1
StoreDir: /nix/store
Priority: 41
[...]

This way, you can see if the credentials are correct or not, and if attic is accessible at all.

Furthermore, watch the attic logs under /var/log/atticd for any error messages. You should be able to observe the following log output:

==> /var/log/atticd/attic.stderr.log <==
[...]
Attic Server 0.1.0 (release)
Running migrations...
Starting API server...
Listening on 127.0.0.1:18080...

==> /var/log/atticd/attic.stdout.log <==

Further reading

Tags

I would be thrilled to hear from you! Please share your thoughts and ideas with me via email.

Back to Index