The `want_svc=0` root fetch presents `deploy.bao.clientCertFile`/ `clientKeyFile` to a listener that demands a client certificate. On a hive with neither set, the handshake is refused every time, and `swarm-services-cert` went from green to failed-and-retrying for 24h. The branch is now chosen at eval: with no reader identity, the unit logs that the store root was not staged and names the option, then exits 0, and no fetch is rendered. A hive with an identity fetches as before. `hivectl peer-config` already handles the absent file. Refs #4588 (review c88774, point 2).
192 lines
8.1 KiB
Nix
192 lines
8.1 KiB
Nix
# `checks.module-eval-hive-tls` — see ./lib.nix for the shared rationale (why
|
|
# this suite exists, naming convention, "evaluates not executes").
|
|
#
|
|
# The swarm-services leaf's renewal: a timer that really re-runs the
|
|
# issuance, at a threshold read off the store role's lifetime. And the
|
|
# store's root on a hive that asks it for no leaf.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
hive
|
|
runGroup
|
|
;
|
|
|
|
# Every service on one host, so the store's granting unit renders the role
|
|
# this leaf is issued through.
|
|
allLocal = hive {
|
|
deploy.singleHostSwarm = true;
|
|
};
|
|
|
|
# The same host with the role's lifetime moved, so a threshold that is a
|
|
# number of its own shows up as one that did not move with it.
|
|
shortTtl = hive {
|
|
deploy.singleHostSwarm = true;
|
|
deploy.bao.servicesPkiLeafTtlHours = 48;
|
|
};
|
|
|
|
# Fronting none of the swarm's names, reading a store it does not run,
|
|
# with the reader identity placed by hand.
|
|
noService = hive {
|
|
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
|
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
|
deploy.bao.serverCaFile = "/etc/pki/bao-ca.pem";
|
|
};
|
|
noSvcFetch = noService.systemd.services.swarm-services-cert.script;
|
|
|
|
# The same hive with no reader identity at all.
|
|
noIdentity = hive { };
|
|
noIdFetch = noIdentity.systemd.services.swarm-services-cert.script;
|
|
|
|
# The `want_svc=0` branch of a rendered script, up to its closing `fi`.
|
|
noSvcBranch =
|
|
script:
|
|
builtins.head (
|
|
lib.splitString "\nfi\n" (lib.last (lib.splitString ''if [ "$want_svc" = 0 ]; then'' script))
|
|
);
|
|
|
|
units = allLocal.systemd.services;
|
|
boot = units.swarm-services-cert;
|
|
timer = allLocal.systemd.timers.swarm-services-cert-renew;
|
|
|
|
# What the timer starts, read off the timer rather than assumed from its
|
|
# name: a timer's `Unit=` defaults to its own name, and pointing it at the
|
|
# boot unit is exactly the regression the cases below exist for.
|
|
triggeredName = lib.removeSuffix ".service" (
|
|
timer.timerConfig.Unit or "swarm-services-cert-renew.service"
|
|
);
|
|
triggered = units.${triggeredName};
|
|
|
|
remainsAfterExit = u: u.serviceConfig.RemainAfterExit or false;
|
|
|
|
renewAt = hours: "renewat=$(( ${toString hours} * 3600 / 2 ))";
|
|
|
|
cases = [
|
|
{
|
|
# Control: the boot issuance is the unit a timer cannot re-run. Were it
|
|
# not `RemainAfterExit`, the case after next would pass vacuously.
|
|
name = "the boot issuance renders, and stays active after it exits";
|
|
ok = lib.hasInfix "pki/issue/swarm-services" boot.script && remainsAfterExit boot;
|
|
}
|
|
{
|
|
name = "a timer for the services leaf is enabled and fires daily";
|
|
ok =
|
|
lib.elem "timers.target" timer.wantedBy
|
|
&& timer.timerConfig.OnCalendar == "daily"
|
|
&& timer.timerConfig.Persistent;
|
|
}
|
|
{
|
|
# Starting an active unit is a no-op, so a timer aimed at a
|
|
# `RemainAfterExit` unit fires on schedule and renews nothing.
|
|
name = "the timer starts a unit that re-runs the issuance and does not stay active";
|
|
ok =
|
|
units ? ${triggeredName}
|
|
&& !(remainsAfterExit triggered)
|
|
&& triggered.script == boot.script
|
|
# The whole set, `PATH` included: an environment copied off the
|
|
# boot unit's merged options renders a second `PATH` and fails.
|
|
&& triggered.environment == boot.environment;
|
|
}
|
|
{
|
|
# A restart of anything the gateway's cert import `Requires=` takes
|
|
# nginx down with it, so the renewal must be something nothing else
|
|
# depends on, and must run behind the boot issuance.
|
|
name = "nothing requires or waits on the renewal, and it runs after the boot issuance";
|
|
ok =
|
|
(triggered.requiredBy or [ ]) == [ ]
|
|
&& (triggered.wantedBy or [ ]) == [ ]
|
|
&& (triggered.before or [ ]) == [ ]
|
|
&& lib.elem "swarm-services-cert.service" triggered.after
|
|
&& !(lib.any (u: lib.elem "${triggeredName}.service" ((u.requires or [ ]) ++ (u.after or [ ]))) (
|
|
lib.attrValues (removeAttrs units [ triggeredName ])
|
|
));
|
|
}
|
|
{
|
|
# Moved with the role, in both places: the threshold is half of what the
|
|
# store grants, and the store grants what the option says.
|
|
name = "the leaf is renewed at half the role's lifetime, read from the option";
|
|
ok =
|
|
lib.hasInfix (renewAt 720) boot.script
|
|
&& lib.hasInfix (renewAt 48) shortTtl.systemd.services.swarm-services-cert.script
|
|
&& lib.hasInfix ''-in "$svcleaf" -noout -checkend "$renewat"'' boot.script
|
|
&& lib.hasInfix "max_ttl=48h" shortTtl.systemd.services.swarm-bao-controller-policy.script;
|
|
}
|
|
{
|
|
# The store replaces its root once it has less than one leaf lifetime
|
|
# left, and the hive asks for a fresh leaf, with the new root, at that
|
|
# same threshold. A number of its own here keeps a leaf whose root the
|
|
# store has already replaced.
|
|
name = "the services root is re-checked at the store's own replacement threshold";
|
|
ok =
|
|
let
|
|
shortBoot = shortTtl.systemd.services.swarm-services-cert.script;
|
|
shortStore = shortTtl.systemd.services.swarm-bao-controller-policy.script;
|
|
in
|
|
lib.hasInfix ''-in "$svcroot" -noout -checkend ${toString (720 * 3600)} '' boot.script
|
|
&& lib.hasInfix ''-in "$svcroot" -noout -checkend ${toString (48 * 3600)} '' shortBoot
|
|
&& lib.hasInfix "-checkend ${toString (48 * 3600)} <<<\"$root_ca\"" shortStore;
|
|
}
|
|
{
|
|
# The slot the bundle reads the store's root from, filled on a hive
|
|
# that asks the store for no leaf.
|
|
name = "a hive fronting no swarm name puts the store's pki root in its trust bundle";
|
|
ok =
|
|
noService.services.hyperhive.swarm.localServiceDomains == [ ]
|
|
&& lib.hasInfix ''"$BAO_ADDR/v1/pki/ca/pem"'' noSvcFetch
|
|
&& lib.hasInfix ''mv -f "$svcroot.new" "$svcroot"'' noSvcFetch
|
|
&& lib.hasInfix ''svcroot="$d/swarm-services-root.pem"'' noService.systemd.services.hive-tls-ca.script
|
|
&& lib.hasInfix ''if [ -s "$svcroot" ]; then anchors+=("$svcroot"); fi'' noService.systemd.services.hive-tls-ca.script;
|
|
}
|
|
{
|
|
name = "the root fetch reaches the store with the hive's own reader identity and server CA";
|
|
ok =
|
|
lib.hasInfix "--cacert ${lib.escapeShellArg "/etc/pki/bao-ca.pem"}" noSvcFetch
|
|
&& lib.hasInfix (
|
|
"--cert ${lib.escapeShellArg "/etc/pki/bao-client.pem"}"
|
|
+ " --key ${lib.escapeShellArg "/etc/pki/bao-client-key.pem"}"
|
|
) noSvcFetch;
|
|
}
|
|
{
|
|
# Nothing to present means no fetch to fail: the unit stays green and
|
|
# says why the root is missing. The control is the same hive with an
|
|
# identity, whose branch does fetch and can fail.
|
|
name = "a hive fronting no swarm name with no store identity skips the fetch and cannot fail on it";
|
|
ok =
|
|
noIdentity.services.hyperhive.swarm.localServiceDomains == [ ]
|
|
&& noIdentity.services.hyperhive.deploy.bao.clientCertFile == null
|
|
&& !(lib.hasInfix "/v1/pki/ca/pem" noIdFetch)
|
|
&& lib.hasInfix "store pki root not staged" (noSvcBranch noIdFetch)
|
|
&& lib.hasInfix "deploy.bao.clientCertFile" (noSvcBranch noIdFetch)
|
|
&& !(lib.hasInfix "exit 1" (noSvcBranch noIdFetch))
|
|
&& lib.hasInfix "exit 1" (noSvcBranch noSvcFetch)
|
|
&& lib.hasInfix "/v1/pki/ca/pem" (noSvcBranch noSvcFetch);
|
|
}
|
|
{
|
|
# The fetch retries against a sealed store; a gateway `Requires=` on it
|
|
# would go down with each attempt. The control is a host fronting
|
|
# swarm names, whose gateway does wait for its leaf.
|
|
name = "a hive fronting no swarm name does not hold its gateway on the store";
|
|
ok =
|
|
let
|
|
holds =
|
|
m:
|
|
lib.elem "hive-gateway-self-signed-cert.service" m.systemd.services.swarm-services-cert.requiredBy
|
|
|| lib.elem "hive-gateway-self-signed-cert.service" m.systemd.services.swarm-services-cert.before;
|
|
in
|
|
!(holds noService) && holds allLocal;
|
|
}
|
|
];
|
|
in
|
|
runGroup "hive-tls" cases
|