nix: re-run the host's store fetches on a ten-minute timer
Seven host oneshots fetch a secret from the swarm secret store into a file their consumer loads at start: swarm-bao-grafana-oidc, swarm-bao-otel-oidc, swarm-bao-matrix-oidc, swarm-bao-ci-runner, swarm-bao-forge-oidc, swarm-bao-hive-otel-oidc and swarm-bao-forwarder-oidc. Each was RemainAfterExit with no timer, so it ran at boot or deploy and never again: a secret re-minted in the store reached its consumer only through an operator restarting the fetch. lib/store-refresh-timer.nix gives each one a timer (OnUnitInactiveSec 10min, RandomizedDelaySec 1min, the agent's own store re-read interval), and each drops RemainAfterExit so a timer start is not a no-op. Each script already writes the file only when secret_differs, and refresh_consumer/refresh_unit restart the consumer only when the file is newer than its last start, so a run reading an unchanged value restarts nothing. Two docs said these units do not run on a timer; credentials.md's ci-runner row said the runner token is not re-pulled. Part of #5017 (slice T).
This commit is contained in:
parent
c2dc18413b
commit
2f05e2db28
12 changed files with 146 additions and 13 deletions
|
|
@ -68,6 +68,7 @@ let
|
|||
|
||||
baoCfg = config.services.hyperhive.swarm.bao;
|
||||
storeRetry = import ../lib/store-retry.nix { };
|
||||
storeRefreshTimer = import ../lib/store-refresh-timer.nix { };
|
||||
atomicWriteSecret = import ../lib/atomic-write-secret.nix { };
|
||||
refreshConsumer = import ../lib/refresh-consumer.nix { };
|
||||
havePublisherIdentity =
|
||||
|
|
@ -1033,8 +1034,9 @@ in
|
|||
# later attempt lands re-runs `forgejo-sso-source` (below).
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
# No `RemainAfterExit`: the timer below starts this again, and
|
||||
# starting an active unit is a no-op.
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
SyslogIdentifier = "swarm-bao-forge-oidc";
|
||||
TimeoutStartSec = 30;
|
||||
};
|
||||
|
|
@ -1113,6 +1115,8 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
systemd.timers.swarm-bao-forge-oidc = storeRefreshTimer "re-read the forge's OIDC client secret from the swarm secret store";
|
||||
|
||||
# The reverse direction of the unit above: publish the swarm-controller's
|
||||
# freshly-minted forge token OUT of the container into the swarm secret
|
||||
# store, where the controller reads it wherever it runs. On the host and
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ let
|
|||
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
|
||||
refreshConsumer = import ./lib/refresh-consumer.nix { };
|
||||
storeRetry = import ./lib/store-retry.nix { };
|
||||
storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
|
||||
|
||||
# ⚠️ tuwunel does NOT read the host path directly, and this indirection
|
||||
# is not ceremony. Upstream's own words: "under systemd the path must be
|
||||
|
|
@ -617,8 +618,9 @@ in
|
|||
# later attempt lands restarts tuwunel (below).
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
# No `RemainAfterExit`: the timer below starts this again, and
|
||||
# starting an active unit is a no-op.
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
SyslogIdentifier = "swarm-bao-matrix-oidc";
|
||||
TimeoutStartSec = 30;
|
||||
};
|
||||
|
|
@ -689,6 +691,8 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
systemd.timers.swarm-bao-matrix-oidc = storeRefreshTimer "re-read the homeserver's OIDC client secret from the swarm secret store";
|
||||
|
||||
# ⚠️ Deliberately NO `networking.hosts` entry for authelia's name. This
|
||||
# container resolves through the hive's dnsmasq at `bridgeIp` (see the
|
||||
# static resolv.conf below), and every `gateway.localNames` entry —
|
||||
|
|
|
|||
27
nix/host-modules/lib/store-refresh-timer.nix
Normal file
27
nix/host-modules/lib/store-refresh-timer.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Timer for a host oneshot that fetches a credential from the swarm secret
|
||||
# store: it starts the oneshot again 10 minutes after each run, so a value
|
||||
# re-minted in the store lands on the host and restarts its consumer with no
|
||||
# operator step. Ten minutes matches the agent's own store re-read
|
||||
# (../../agent-modules/bao.nix).
|
||||
#
|
||||
# ⚠️ The oneshot must not be `RemainAfterExit`: starting a unit that stays
|
||||
# `active (exited)` is a no-op, so the timer would fire and fetch nothing.
|
||||
# A run that reads an unchanged value writes nothing and restarts nothing —
|
||||
# its `secret_differs` gate and ./refresh-consumer.nix see to that.
|
||||
#
|
||||
# Gate the timer exactly as its service is gated: a timer whose unit does
|
||||
# not exist fails to load.
|
||||
#
|
||||
# Pure function — NOT a NixOS module. Call it from a module's `let`:
|
||||
#
|
||||
# storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
|
||||
# systemd.timers.swarm-bao-foo = storeRefreshTimer "re-read foo's secret from the swarm secret store";
|
||||
{ }:
|
||||
description: {
|
||||
inherit description;
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnUnitInactiveSec = "10min";
|
||||
RandomizedDelaySec = "1min";
|
||||
};
|
||||
}
|
||||
|
|
@ -348,6 +348,7 @@ in
|
|||
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
|
||||
refreshConsumer = import ./lib/refresh-consumer.nix { };
|
||||
storeRetry = import ./lib/store-retry.nix { };
|
||||
storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
|
||||
in
|
||||
{
|
||||
# Reachable from agent containers and nowhere else: this opens
|
||||
|
|
@ -766,8 +767,9 @@ in
|
|||
# a later attempt lands restarts it (below).
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
# No `RemainAfterExit`: the timer below starts this again, and
|
||||
# starting an active unit is a no-op.
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
SyslogIdentifier = fetchUnit;
|
||||
TimeoutStartSec = 30;
|
||||
};
|
||||
|
|
@ -828,6 +830,10 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
systemd.timers.${fetchUnit} = lib.mkIf fetches (
|
||||
storeRefreshTimer "re-read this hive's telemetry OIDC client secret from the swarm secret store"
|
||||
);
|
||||
|
||||
assertions = [
|
||||
{
|
||||
# A hive authenticates to the swarm's collector as itself — there
|
||||
|
|
|
|||
|
|
@ -1367,6 +1367,7 @@ let
|
|||
# rather than a convenience for nginx.
|
||||
privateNetwork = false;
|
||||
storeRetry = import ./lib/store-retry.nix { };
|
||||
storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
|
|
@ -2566,8 +2567,9 @@ in
|
|||
# which this boot waits on.
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
# No `RemainAfterExit`: the timer below starts this again, and
|
||||
# starting an active unit is a no-op.
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
SyslogIdentifier = "swarm-bao-forwarder-oidc";
|
||||
TimeoutStartSec = 30;
|
||||
};
|
||||
|
|
@ -2639,6 +2641,8 @@ in
|
|||
refresh_consumer ${lib.escapeShellArg cfg.machine} opentelemetry-collector.service ${lib.escapeShellArg forwarderHostSecretPath}
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.swarm-bao-forwarder-oidc = storeRefreshTimer "re-read the secret store forwarder's OIDC client secret from the store";
|
||||
})
|
||||
|
||||
(lib.mkIf (deployCfg.bao.enable && haveServerTls) {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ let
|
|||
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
|
||||
refreshConsumer = import ./lib/refresh-consumer.nix { };
|
||||
storeRetry = import ./lib/store-retry.nix { };
|
||||
storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
|
||||
|
||||
privateNetwork = true;
|
||||
in
|
||||
|
|
@ -206,8 +207,9 @@ in
|
|||
];
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
# No `RemainAfterExit`: the timer below starts this again, and
|
||||
# starting an active unit is a no-op.
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
SyslogIdentifier = "swarm-bao-ci-runner";
|
||||
TimeoutStartSec = 30;
|
||||
};
|
||||
|
|
@ -268,6 +270,8 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
systemd.timers.swarm-bao-ci-runner = storeRefreshTimer "re-read the CI runner's registration token from the swarm secret store";
|
||||
|
||||
systemd.services."container@${machine}" = caTrust.containerOrdering;
|
||||
|
||||
containers.${machine} = {
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ let
|
|||
# reaches this at 127.0.0.1:<port>.
|
||||
privateNetwork = false;
|
||||
storeRetry = import ./lib/store-retry.nix { };
|
||||
storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
|
||||
in
|
||||
{
|
||||
# What the service IS to every hive is `swarm.grafana` in
|
||||
|
|
@ -576,8 +577,9 @@ in
|
|||
# Grafana (below).
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
# No `RemainAfterExit`: the timer below starts this again, and
|
||||
# starting an active unit is a no-op.
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
SyslogIdentifier = "swarm-bao-grafana-oidc";
|
||||
# What actually bounds each attempt below. Stated here rather than left
|
||||
# to systemd's default, so the number a boot waits on is in the file
|
||||
|
|
@ -665,6 +667,8 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
systemd.timers.swarm-bao-grafana-oidc = storeRefreshTimer "re-read Grafana's OIDC client secret from the swarm secret store";
|
||||
|
||||
# The admin password: created in the store if it holds none, then copied
|
||||
# into the container, under Grafana's own leaf. Same shape as
|
||||
# `swarm-bao-nats-callout-xkey` in ./swarm-nats.nix. The grant is
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ let
|
|||
# its own trust material.
|
||||
privateNetwork = false;
|
||||
storeRetry = import ./lib/store-retry.nix { };
|
||||
storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
|
||||
in
|
||||
{
|
||||
# What the collector IS to the swarm — the client it is registered as, where
|
||||
|
|
@ -453,8 +454,9 @@ in
|
|||
# the collector (below).
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
# No `RemainAfterExit`: the timer below starts this again, and
|
||||
# starting an active unit is a no-op.
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
SyslogIdentifier = "swarm-bao-otel-oidc";
|
||||
# What actually bounds each attempt below. Stated here rather than left
|
||||
# to systemd's default, so the number a boot waits on is in the file
|
||||
|
|
@ -532,6 +534,10 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
systemd.timers.swarm-bao-otel-oidc = lib.mkIf haveClientIdentity (
|
||||
storeRefreshTimer "re-read the swarm collector's OIDC client secret from the swarm secret store"
|
||||
);
|
||||
|
||||
# Where the delivery unit above lands the secret. `mkDefault`, so a
|
||||
# deployment delivering it some other way just sets the option — and
|
||||
# `mkIf haveClientIdentity` so a host with no store identity is left with
|
||||
|
|
|
|||
Loading…
Reference in a new issue