A hive fronting no swarm name fetches the store's pki root presenting its reader certificate. When deploy.bao.clientCertFile or clientKeyFile is unset, or either named file is not readable, the unit now exits 1 with a message naming both options and their paths, instead of exiting 0 with the root missing from the trust bundle. The module-eval cases that asserted the quiet skip assert the failure instead, and a new case covers the named-but-absent files.
206 lines
8.9 KiB
Nix
206 lines
8.9 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 the `fi` that closes it (the
|
|
# only one followed by a blank line).
|
|
noSvcBranch =
|
|
script:
|
|
builtins.head (
|
|
lib.splitString "\nfi\n\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;
|
|
}
|
|
{
|
|
# No identity to present: the unit fails, naming both options, rather
|
|
# than leaving the bundle without the root behind a green unit. The
|
|
# control is the same hive with an identity, whose branch fetches.
|
|
name = "a hive fronting no swarm name with no store identity fails the unit, naming both options";
|
|
ok =
|
|
noIdentity.services.hyperhive.swarm.localServiceDomains == [ ]
|
|
&& noIdentity.services.hyperhive.deploy.bao.clientCertFile == null
|
|
&& !(lib.hasInfix "/v1/pki/ca/pem" noIdFetch)
|
|
&& lib.hasInfix "services.hyperhive.deploy.bao.clientCertFile" (noSvcBranch noIdFetch)
|
|
&& lib.hasInfix "services.hyperhive.deploy.bao.clientKeyFile" (noSvcBranch noIdFetch)
|
|
&& lib.hasSuffix "exit 1" (noSvcBranch noIdFetch)
|
|
&& !(lib.hasInfix "exit 0" (noSvcBranch noIdFetch))
|
|
&& !(lib.hasInfix "not staged" noIdFetch)
|
|
&& lib.hasInfix "/v1/pki/ca/pem" (noSvcBranch noSvcFetch);
|
|
}
|
|
{
|
|
# An identity named but not on disk fails before curl, naming the paths.
|
|
name = "a hive fronting no swarm name whose identity files are absent fails the unit, naming the paths";
|
|
ok =
|
|
lib.hasInfix "for f in /etc/pki/bao-client.pem /etc/pki/bao-client-key.pem; do" (
|
|
noSvcBranch noSvcFetch
|
|
)
|
|
&& lib.hasInfix ''[ -r "$f" ] || missing="$missing $f"'' (noSvcBranch noSvcFetch)
|
|
&& lib.hasInfix "cannot read$missing" (noSvcBranch noSvcFetch)
|
|
&& lib.hasInfix "clientCertFile (/etc/pki/bao-client.pem)" (noSvcBranch noSvcFetch)
|
|
&& lib.hasInfix "clientKeyFile (/etc/pki/bao-client-key.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
|