the in-swarm cache stores its address, name and public key beside its tokens, create-only. swarm-controller copies the four cache facts to swarm/hives/<hive>/attic/* every five minutes, with read on the sources and write on the copies. part 1 of 3: nothing reads the copies until the hive reader and probe land.
184 lines
6.6 KiB
Nix
184 lines
6.6 KiB
Nix
# The shell for the in-swarm cache's first start: what its two host units run
|
|
# after they log in to the secret store under the cache's own certificate.
|
|
# Each secret is created only while the store holds none, so a second run reads
|
|
# what the first one stored and mints nothing.
|
|
#
|
|
# Pure function — NOT a NixOS module. Call it from a module's `let`:
|
|
#
|
|
# atticInit = import ./lib/attic-init.nix { inherit lib; } { … };
|
|
# script = ''
|
|
# ${atticInit.prelude}
|
|
# ${atticInit.ensureJwt}
|
|
# '';
|
|
#
|
|
# `prelude` expects `./atomic-write-secret.nix` and `./refresh-consumer.nix`
|
|
# above it, and `bao`, `coreutils` and `systemd` on the caller's `path`.
|
|
# `ensureJwt` also wants `openssl` there; `provision` wants `atticadm`, `attic`,
|
|
# `curl` and `sed`.
|
|
{ lib }:
|
|
{
|
|
# Store paths in the form `bao kv` takes, mount first.
|
|
jwtPath,
|
|
pullPath,
|
|
pushPath,
|
|
# The cache's public facts, created only while absent: the server's address
|
|
# (scheme included), the cache's name, and the key its narinfo are signed
|
|
# with. Hives pull with them.
|
|
urlPath,
|
|
cachePath,
|
|
publicKeyPath,
|
|
# The address `urlPath` holds, which clients reach the server at.
|
|
url,
|
|
# The cache's name on the server.
|
|
cache,
|
|
# Where the host reaches atticd, scheme included.
|
|
endpoint,
|
|
# The server's config file, which the host's `atticadm` reads as well.
|
|
configFile,
|
|
# The file atticd loads its signing key from, and the container that does.
|
|
envFile,
|
|
machine,
|
|
unit,
|
|
}:
|
|
let
|
|
q = lib.escapeShellArg;
|
|
in
|
|
{
|
|
prelude = ''
|
|
err="$(mktemp)"
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$err" "$work"' EXIT
|
|
|
|
if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then
|
|
echo "could not log in to swarm-bao with the cache's certificate; leaving the cache as it is." >&2
|
|
cat "$err" >&2
|
|
exit 1
|
|
fi
|
|
export BAO_TOKEN
|
|
|
|
# <store path> <mint function>: the stored value, created with the
|
|
# function's output first while the store holds none. `bao kv get` exits 2
|
|
# both for "nothing stored" and for a refusal, so a failed read always
|
|
# tries the create, which `-cas=0` refuses when the path exists, and the
|
|
# second read decides. The value reaches `bao` on stdin, never in an
|
|
# argument.
|
|
ensure_secret() {
|
|
local path="$1" mint="$2" value minted
|
|
if ! value="$(bao kv get -field=value "$path" 2>"$err")"; then
|
|
minted="$("$mint")" || return 1
|
|
if [ -z "$minted" ]; then
|
|
echo "minting $path produced nothing." >&2
|
|
return 1
|
|
fi
|
|
if ! printf '%s' "$minted" | bao kv put -cas=0 "$path" value=- >/dev/null 2>"$err"; then
|
|
echo "could not create $path; reading it back in case another run created it." >&2
|
|
cat "$err" >&2
|
|
fi
|
|
if ! value="$(bao kv get -field=value "$path" 2>"$err")"; then
|
|
echo "swarm-bao did not return $path." >&2
|
|
cat "$err" >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
if [ -z "$value" ]; then
|
|
echo "swarm-bao returned an empty $path." >&2
|
|
return 1
|
|
fi
|
|
printf '%s' "$value"
|
|
}
|
|
'';
|
|
|
|
# The signing key atticd verifies tokens with, and the file it loads it from.
|
|
# The key is a base64 RSA PEM in PKCS#1 form, which is what `-traditional`
|
|
# asks for.
|
|
ensureJwt = ''
|
|
mint_jwt() {
|
|
openssl genrsa -traditional 4096 2>/dev/null | base64 -w0
|
|
}
|
|
|
|
jwt="$(ensure_secret ${q jwtPath} mint_jwt)" || exit 1
|
|
line="ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=$jwt"
|
|
if secret_differs ${q envFile} "$line"; then
|
|
atomic_write_secret 0400 "" ${q envFile} "$line"
|
|
fi
|
|
|
|
# atticd reads the key as it starts, and only then.
|
|
refresh_consumer ${q machine} ${q unit} ${q envFile}
|
|
'';
|
|
|
|
# The cache, its two tokens and its public facts, once atticd answers. The
|
|
# tokens are signed here with the key the store holds; the cache is created
|
|
# through the API by a token that lives for five minutes and is never stored.
|
|
provision = ''
|
|
up=0
|
|
for _ in $(seq 1 30); do
|
|
if curl -s -o /dev/null --max-time 2 ${q endpoint}; then
|
|
up=1
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
if [ "$up" != 1 ]; then
|
|
echo "atticd did not answer at ${endpoint}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! jwt="$(bao kv get -field=value ${q jwtPath} 2>"$err")" || [ -z "$jwt" ]; then
|
|
echo "swarm-bao did not return ${jwtPath}; the cache's tokens are not minted." >&2
|
|
cat "$err" >&2
|
|
exit 1
|
|
fi
|
|
export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$jwt"
|
|
|
|
# <subject> <validity> <grant…>
|
|
mint_token() {
|
|
local sub="$1" validity="$2"
|
|
shift 2
|
|
atticadm -f ${q configFile} make-token --sub "$sub" --validity "$validity" "$@"
|
|
}
|
|
mint_pull() { mint_token swarm-hives-pull 10y --pull ${q cache}; }
|
|
mint_push() { mint_token swarm-builder-push 10y --pull ${q cache} --push ${q cache}; }
|
|
|
|
pull="$(ensure_secret ${q pullPath} mint_pull)" || exit 1
|
|
ensure_secret ${q pushPath} mint_push >/dev/null || exit 1
|
|
|
|
# The client reads its token from a file, so the token is in no argument.
|
|
export XDG_CONFIG_HOME="$work"
|
|
use_token() {
|
|
install -d -m 0700 "$work/attic"
|
|
(umask 077 && printf '%s' "$1" >"$work/token")
|
|
printf 'default-server = "local"\n\n[servers.local]\nendpoint = "%s"\ntoken-file = "%s"\n' \
|
|
${q endpoint} "$work/token" >"$work/attic/config.toml"
|
|
}
|
|
|
|
# The pull token reads the cache's configuration when the cache exists, so
|
|
# a cache that is there costs no token. The server's database can be empty
|
|
# while the store still holds both tokens.
|
|
use_token "$pull"
|
|
if ! attic cache info ${q "local:${cache}"} >/dev/null 2>"$err"; then
|
|
admin="$(mint_token swarm-attic-init 5m --create-cache ${q cache} --pull ${q cache})" || exit 1
|
|
use_token "$admin"
|
|
if ! attic cache create ${q "local:${cache}"} >/dev/null 2>"$err"; then
|
|
echo "could not create the cache ${cache}; checking whether another run did." >&2
|
|
cat "$err" >&2
|
|
fi
|
|
use_token "$pull"
|
|
if ! attic cache info ${q "local:${cache}"} >/dev/null 2>"$err"; then
|
|
echo "the cache ${cache} is not there after creating it." >&2
|
|
cat "$err" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# What hives pull with. The key is the one the server reports for the cache;
|
|
# `attic` prints it on stderr.
|
|
mint_url() { printf '%s' ${q url}; }
|
|
mint_cache() { printf '%s' ${q cache}; }
|
|
mint_public_key() {
|
|
attic cache info ${q "local:${cache}"} 2>&1 | sed -n 's/^ *Public Key: *//p'
|
|
}
|
|
ensure_secret ${q urlPath} mint_url >/dev/null || exit 1
|
|
ensure_secret ${q cachePath} mint_cache >/dev/null || exit 1
|
|
ensure_secret ${q publicKeyPath} mint_public_key >/dev/null || exit 1
|
|
'';
|
|
}
|