swarm-archive: back up every agent's state to an append-only restic repository per agent
swarm-archive serves restic repositories with rclone, append-only, one login and one repository per agent. swarm-controller writes each placed agent's login and restic password to the store, replaces them on a move and removes them when the agent is destroyed. Every hive backs its agents up hourly under its own store identity. The archive host prunes by retention.
This commit is contained in:
parent
409e7ee5a0
commit
306aae0378
3 changed files with 2214 additions and 5 deletions
|
|
@ -195,6 +195,7 @@ let
|
|||
"swarm-bao-claude-login-policy"
|
||||
"swarm-bao-notify-router-policy"
|
||||
"swarm-bao-attic-policy"
|
||||
"swarm-bao-archive-policy"
|
||||
];
|
||||
|
||||
# Comment lines dropped first: both the HCL and the scripts explain
|
||||
|
|
@ -2166,6 +2167,30 @@ let
|
|||
in
|
||||
lib.hasInfix "/notify-router.pem ]" s && lib.hasInfix "swarm-notify-router \"\" clientAuth" s;
|
||||
}
|
||||
{
|
||||
# The archive reads the users and the repositories' passwords and writes
|
||||
# nothing; the hive-side copies of the REST password are not its to read.
|
||||
# The controller is the only writer: the control is its own grant.
|
||||
name = "the archive's grant is read on its users and restic passwords, list on its users, and nothing else, and only the controller writes them";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-archive-policy.script;
|
||||
grants = grantsIn s;
|
||||
c = grantsIn baoGrantHere.systemd.services.swarm-bao-controller-policy.script;
|
||||
in
|
||||
(grantFor grants "secret/data/swarm/services/archive/users/atlas").caps == [ "read" ]
|
||||
&& (grantFor grants "secret/data/swarm/services/archive/restic/atlas").caps == [ "read" ]
|
||||
&& (grantFor grants "secret/metadata/swarm/services/archive/users/").caps == [ "list" ]
|
||||
&& grantFor grants "secret/data/swarm/hives/h1/archive/atlas" == null
|
||||
&& lib.length grants == 3
|
||||
&& lib.hasInfix "allowed_common_names=swarm-archive" s
|
||||
&& lib.hasInfix "token_policies=swarm-archive" s
|
||||
&& lib.elem "create" (grantFor c "secret/data/swarm/services/archive/users/atlas").caps
|
||||
&& lib.elem "create" (grantFor c "secret/data/swarm/hives/h1/archive/atlas").caps
|
||||
&& (grantFor c "secret/metadata/swarm/hives/h1/archive/atlas").caps == [ "delete" ]
|
||||
&& (grantFor c "secret/metadata/swarm/services/archive/users/atlas").caps == [ "delete" ]
|
||||
&& !(lib.elem "delete" (grantFor c "secret/data/swarm/services/archive/restic/atlas").caps);
|
||||
}
|
||||
{
|
||||
# The cache's host units create each of its six secrets and read them
|
||||
# back, and replace only the public key: `update` there and nowhere else.
|
||||
|
|
|
|||
|
|
@ -561,6 +561,45 @@ let
|
|||
&& names atticExternal == names bare
|
||||
&& !(lib.elem "cache.example.org" (names atticExternal));
|
||||
}
|
||||
{
|
||||
# The archive on the host that hosts the shared services. Append-only and
|
||||
# private repos are what make one hive's compromise and one agent's
|
||||
# login small; the control is the bare host, which runs no archive.
|
||||
name = "the archive serves append-only with private repositories behind a TLS-only vhost, and a host without it renders none";
|
||||
ok =
|
||||
let
|
||||
a = atticHere.systemd.services.swarm-archive;
|
||||
users = atticHere.systemd.services.swarm-archive-users;
|
||||
prune = atticHere.systemd.services.swarm-archive-prune;
|
||||
vhost = atticHere.services.hyperhive.gateway.vhosts."archive.t.local";
|
||||
cfg = atticHere.services.hyperhive.deploy.archive;
|
||||
names = m: m.services.hyperhive.swarm.serviceDomains;
|
||||
in
|
||||
atticHere.services.hyperhive.deploy.archive.enable
|
||||
&& lib.hasInfix "--append-only" a.serviceConfig.ExecStart
|
||||
&& lib.hasInfix "--private-repos" a.serviceConfig.ExecStart
|
||||
&& lib.hasInfix "--htpasswd /run/swarm-archive-auth/htpasswd" a.serviceConfig.ExecStart
|
||||
&& lib.hasInfix "/var/lib/swarm-archive/repos" a.serviceConfig.ExecStart
|
||||
&& a.serviceConfig.User == "swarm-archive"
|
||||
&& vhost.forceSSL
|
||||
&& !(lib.hasInfix "auth_request" vhost.locations."/".extraConfig)
|
||||
&& vhost.locations."/".proxyPass == "http://127.0.0.1:8911"
|
||||
&& lib.elem "archive.t.local" (names atticHere)
|
||||
&& names atticHere == names bare
|
||||
&& cfg.retention.keepWithin == "3d"
|
||||
&& cfg.retention.keepLast == 3
|
||||
&& lib.hasInfix "--keep-last 3 --keep-within 3d --prune" prune.script
|
||||
&& prune.serviceConfig.User == "swarm-archive"
|
||||
&& lib.hasInfix "RESTIC_PASSWORD_COMMAND=" prune.script
|
||||
&& lib.hasInfix "secret/swarm/services/archive/users" users.script
|
||||
&& users.environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/archive.pem"
|
||||
&& !(lib.hasInfix "password" (lib.toLower (builtins.toJSON a.environment)))
|
||||
&& atticHere.systemd.timers ? swarm-archive-users
|
||||
&& atticHere.systemd.timers.swarm-archive-prune.timerConfig.OnCalendar == "daily"
|
||||
&& lib.elem "swarm-bao-archive-policy.service" users.after
|
||||
&& !(bare.systemd.services ? swarm-archive)
|
||||
&& !(bare.systemd.services ? swarm-archive-users);
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "builder" cases
|
||||
|
|
|
|||
|
|
@ -802,6 +802,31 @@ let
|
|||
noService.systemd.services.hive-priv-cache-facts.environment.BAO_CLIENT_CERT
|
||||
== "/etc/pki/bao-hive.pem";
|
||||
}
|
||||
{
|
||||
# ./hive-archive-backup.nix. The grants are read under this hive's own
|
||||
# identity and only from its own prefix; the swarm's password lives in no
|
||||
# file and no unit text. The no-identity host is the control that the
|
||||
# lookup is not always empty.
|
||||
name = "the hive backs up its agents to the archive under its own identity, hourly, reading only its own grants";
|
||||
ok =
|
||||
let
|
||||
u = units.hive-archive-backup;
|
||||
t = allLocal.systemd.timers.hive-archive-backup;
|
||||
in
|
||||
lib.hasInfix "secret/swarm/hives/h1/archive" u.script
|
||||
&& !(lib.hasInfix "swarm/services" u.script)
|
||||
&& !(lib.hasInfix "swarm/hives/h2" u.script)
|
||||
&& lib.hasInfix "rest:https://archive.t.local/" u.script
|
||||
&& lib.hasInfix "RESTIC_PASSWORD_COMMAND=" u.script
|
||||
&& lib.hasInfix "--exclude-caches" u.script
|
||||
&& lib.hasInfix "for target in state claude harness" u.script
|
||||
&& u.environment.BAO_CLIENT_CERT == units.hive-priv-cache-facts.environment.BAO_CLIENT_CERT
|
||||
&& u.serviceConfig.Type == "oneshot"
|
||||
&& t.timerConfig.OnCalendar == "hourly"
|
||||
&& lib.elem "timers.target" t.wantedBy
|
||||
&& !(noIdentity.systemd.services ? hive-archive-backup)
|
||||
&& !(noIdentity.systemd.timers ? hive-archive-backup);
|
||||
}
|
||||
{
|
||||
# Control: the lookup finds the sibling root unit of the same shape. The
|
||||
# squid self-check's password is read under this hive's own identity and
|
||||
|
|
|
|||
Loading…
Reference in a new issue