174 lines
7.3 KiB
Nix
174 lines
7.3 KiB
Nix
# The root-side copy of the cache facts the host's nix daemon and hive-priv use:
|
|
# the key a closure must be signed by, the netrc the pull token goes out in and
|
|
# the CA bundle that trusts the cache. 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` then
|
|
# the entries of the host's own netrc (`hostNetrc`), as `netrc-file` is one path;
|
|
# - the CA bundle at `caBundleFile`, root 0644: the host's own bundle
|
|
# (`baseBundle`) then the swarm's root (`swarmCa`), when the host holds it,
|
|
# which the cache's certificate chains to; `ssl-cert-file` is one path too;
|
|
# - a nix.conf fragment at `confFile` that the host's nix.conf includes, with
|
|
# `extra-substituters` (the cache's address), `extra-trusted-public-keys`,
|
|
# `netrc-file` and `ssl-cert-file`. 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`, `caBundleFile` 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`, `systemd` on its `path` and the `BAO_*` client 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";
|
|
# caBundleFile = "/run/hyperhive-nix/ca-bundle.crt"; baseBundle = "/etc/ssl/certs/ca-certificates.crt";
|
|
# swarmCa = "/var/lib/hive-tls/swarm-services-root.pem";
|
|
# };
|
|
{ lib }:
|
|
{
|
|
# The hive's name in the store: facts are read from
|
|
# `secret/swarm/hives/<hive>/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,
|
|
# Where the CA bundle goes.
|
|
caBundleFile,
|
|
# The CA bundle nix uses without this module, copied in and never written:
|
|
# `ssl-cert-file` is one path, so a fragment that names the new bundle must
|
|
# carry the host's CAs.
|
|
baseBundle,
|
|
# The swarm's root certificate. The file may be absent: the bundle then holds
|
|
# the host's CAs alone until a later run finds it.
|
|
swarmCa,
|
|
}:
|
|
''
|
|
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}
|
|
ca_bundle_file=${lib.escapeShellArg caBundleFile}
|
|
base_bundle=${lib.escapeShellArg baseBundle}
|
|
swarm_ca=${lib.escapeShellArg swarmCa}
|
|
install -d -m 0700 "$dir"
|
|
install -d -m 0755 "$(dirname -- "$conf")" "$(dirname -- "$netrc_file")" "$(dirname -- "$ca_bundle_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
|
|
|
|
# <leaf> <variable>. 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 <name>:<key> 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
|
|
|
|
# The host's CAs, then the swarm's root. Nix reads the file for each
|
|
# download, so a root that lands after the fragment reaches the next one
|
|
# without a restart. A file without a certificate is not appended: the bundle
|
|
# serves every https fetch of every nix client.
|
|
if [ ! -r "$base_bundle" ]; then
|
|
echo "cannot read the host's CA bundle $base_bundle; $ca_bundle_file stays as it is." >&2
|
|
exit 1
|
|
fi
|
|
bundle="$(< "$base_bundle")"
|
|
if [ -s "$swarm_ca" ] && grep -q -- '-----BEGIN CERTIFICATE-----' "$swarm_ca"; then
|
|
bundle+=$'\n'"$(< "$swarm_ca")"
|
|
else
|
|
echo "the swarm's root $swarm_ca is not on this host yet; $ca_bundle_file holds the host's CAs only."
|
|
fi
|
|
if secret_differs "$ca_bundle_file" "$bundle" || [ "$(stat -c %a "$ca_bundle_file")" != 644 ]; then
|
|
atomic_write_secret 0644 "" "$ca_bundle_file" "$bundle"
|
|
fi
|
|
|
|
# Last, so a daemon that reads the fragment finds the netrc and the bundle 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\n%s' \
|
|
${lib.escapeShellArg "extra-substituters = ${url}"} \
|
|
"extra-trusted-public-keys = $key" \
|
|
"netrc-file = $netrc_file" \
|
|
"ssl-cert-file = $ca_bundle_file"
|
|
if secret_differs "$conf" "$fragment"; then
|
|
atomic_write_secret 0644 "" "$conf" "$fragment"
|
|
fi
|
|
refresh_unit nix-daemon.service "$conf"
|
|
''
|