# The root-side copy of the cache facts the host's nix daemon and hive-priv # use: the key a downloaded closure must be signed by and the netrc the pull # token goes out in. The script runs as root on the hive host under the hive's # own store identity and writes: # # - the key into `dir`, which only root can read; # - the netrc at `netrcFile`, root 0600: the pull token's entry for `host` # followed by the entries of the host's own netrc (`hostNetrc`) for other # machines. `netrc-file` is one path, so a fragment that names this file must # carry what the host already keeps there; # - a nix.conf fragment at `confFile` that the host's nix.conf includes, with # `extra-substituters` (the cache's address), `extra-trusted-public-keys` and # `netrc-file` (that merged netrc), so the daemon # substitutes from the cache and its other substituters. It holds no secret. # # Nothing is read from a file hive-c0re (unprivileged) writes: the key and the # token come from the store. # # A store that holds no facts yet, or cannot be reached, leaves what is in # `dir`, `netrcFile` and `confFile` as they are. Nothing here removes a file. # # Pure function — NOT a NixOS module. Call it from a unit's `script`, with # `bao`, `coreutils`, `gnugrep` and `systemd` on its `path` and the `BAO_*` # client environment set: # # script = import ./lib/attic-root-facts.nix { inherit lib; } { # hive = "alpha"; host = "attic.example.org"; dir = "/run/hyperhive-priv/attic"; # url = "https://attic.example.org/swarm"; confFile = "/run/hyperhive-nix/cache.conf"; # netrcFile = "/run/hyperhive-nix/netrc"; hostNetrc = "/etc/nix/netrc"; # }; { lib }: { # The hive's name in the store: facts are read from # `secret/swarm/hives//attic/`. hive, # The cache server's host name, which the netrc line names. host, # Where the key goes. dir, # The cache's address as the daemon lists it in `extra-substituters`. url, # Where the nix.conf fragment goes; its directory is root's and readable. confFile, # Where the merged netrc goes, and the netrc the host uses without this # module, which is merged in and never written. netrcFile, hostNetrc, }: '' set -euo pipefail ${import ./store-seeded-secret.nix { }} ${import ./atomic-write-secret.nix { }} ${import ./refresh-consumer.nix { }} dir=${lib.escapeShellArg dir} conf=${lib.escapeShellArg confFile} netrc_file=${lib.escapeShellArg netrcFile} host_netrc=${lib.escapeShellArg hostNetrc} install -d -m 0700 "$dir" install -d -m 0755 "$(dirname -- "$conf")" "$(dirname -- "$netrc_file")" store_init store_login if [ "$STORE_UP" != 1 ]; then echo "could not read the cache facts; $dir stays as it is." >&2 exit 1 fi # . Sets the variable to the leaf's value. A leaf the store # does not hold sets `missing`; any other failure ends the run. missing=0 fetch() { local value if value="$(store_bao kv get -field=value ${lib.escapeShellArg "secret/swarm/hives/${hive}/attic"}/"$1" 2>"$STORE_ERR")"; then printf -v "$2" '%s' "$value" elif grep -qi 'no value found' "$STORE_ERR"; then missing=1 else cat "$STORE_ERR" >&2 echo "could not read the cache fact $1; $dir stays as it is." >&2 exit 1 fi } key="" token="" fetch public-key key fetch pull-token token if [ "$missing" = 1 ]; then echo "the swarm has not given this hive its cache facts yet; $dir stays as it is." exit 0 fi # One token each: both land in a file nix and hive-priv parse by whitespace. if [[ ! "$key" =~ ^[^[:space:]:]+:[^[:space:]]+$ ]] || [[ ! "$token" =~ ^[^[:space:]]+$ ]]; then echo "the stored cache facts are not a : and a single token; $dir stays as it is." >&2 exit 1 fi # Written only when the value changed, so the files keep their mtime between # runs. The owner is the running user, root, and the mode is 0600. if secret_differs "$dir/public-key" "$key"; then atomic_write_secret 0600 "" "$dir/public-key" "$key" fi # The host's netrc without its lines for this cache's host: an entry runs from # a `machine` or `default` line to the next one. A missing file is no entries. # Our entry goes first, which is the one curl takes when an entry the filter # does not recognise names the cache's host too. other_netrc_entries() { local line skip=0 tok kept="" [ -f "$host_netrc" ] || return 0 while IFS= read -r line || [ -n "$line" ]; do read -ra tok <<<"$line" case "''${tok[0]-}" in machine) if [ "''${tok[1]-}" = ${lib.escapeShellArg host} ]; then skip=1; else skip=0; fi ;; default) skip=0 ;; esac [ "$skip" = 1 ] || kept+="$line"$'\n' done <"$host_netrc" printf '%s' "$kept" } netrc="machine ${host} password $token" others="$(other_netrc_entries)" if [ -n "$others" ]; then netrc="$netrc"$'\n'"$others"; fi # The mode too: an earlier deploy left this path 0640 for a group. if secret_differs "$netrc_file" "$netrc" || [ "$(stat -c %a "$netrc_file")" != 600 ]; then atomic_write_secret 0600 "" "$netrc_file" "$netrc" fi # Last, so a daemon that reads the fragment finds the netrc in place. The # mtime moves only with the content. Lix re-reads nix.conf for every # connection, CppNix only at start, so a CppNix daemon that started before # this write is restarted. printf -v fragment '%s\n%s\n%s' \ ${lib.escapeShellArg "extra-substituters = ${url}"} \ "extra-trusted-public-keys = $key" \ "netrc-file = $netrc_file" if secret_differs "$conf" "$fragment"; then atomic_write_secret 0644 "" "$conf" "$fragment" fi refresh_unit nix-daemon.service "$conf" ''