# 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` # and `curl`. { lib }: { # Store paths in the form `bao kv` takes, mount first. jwtPath, pullPath, pushPath, # 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 # : 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 and the two tokens, 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" # 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 ''; }