keep the matrix appservice registration in bao, with the host file as the last copy
This commit is contained in:
parent
fb6cd36b5f
commit
d5c5862654
4 changed files with 1014 additions and 273 deletions
|
|
@ -137,11 +137,12 @@ let
|
|||
#
|
||||
# The homeserver's one appservice registration, and the swarm's identity on
|
||||
# it: `swarm-controller` creates every agent's account and every hive's
|
||||
# `@hive-<hive>` sender with its token. Minted INSIDE this container by
|
||||
# `swarm-matrix-ctl appservice render` and published to the store by
|
||||
# `appservice publish`, which is why it is gated on matrix-ctl's store
|
||||
# identity: with nobody to publish it, a registration here would be an
|
||||
# admin credential nobody reads.
|
||||
# `@hive-<hive>` sender with its token. The store holds its tokens;
|
||||
# `swarm-matrix-ctl appservice sync` keeps this container's copy in step with
|
||||
# them, which is why it is gated on matrix-ctl's store identity: with nobody
|
||||
# to sync it, a registration here would be an admin credential nobody reads.
|
||||
# `appservice render` minted a copy here only for a host that has none, and
|
||||
# renders the one the homeserver starts from.
|
||||
#
|
||||
# Its sender is promoted to homeserver admin at boot (`admin_execute`
|
||||
# below), so its token goes only to a store path no hive's policy reaches.
|
||||
|
|
@ -1002,9 +1003,11 @@ in
|
|||
# below is `requiredBy` it and local only.
|
||||
++ lib.optional ctlActive "${swarmAppserviceCredentialId}:${swarmAppserviceDir}/swarm.yaml";
|
||||
|
||||
# The swarm registration, minted and rendered before the homeserver
|
||||
# loads it. No network and no store: this is on tuwunel's start
|
||||
# path, and a store outage must not keep the homeserver down.
|
||||
# The swarm registration, rendered before the homeserver loads it from
|
||||
# the tokens in the state dir: the last copy of the store's that
|
||||
# `swarm-matrix-appservice-sync` took, or a minted pair on a host with
|
||||
# none. No network and no store: this is on tuwunel's start path, and
|
||||
# a store outage must not keep the homeserver down.
|
||||
systemd.services.swarm-matrix-appservice-render = lib.mkIf ctlActive {
|
||||
description = "render the swarm's appservice registration";
|
||||
before = [ "tuwunel.service" ];
|
||||
|
|
@ -1027,21 +1030,31 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
# Hand the swarm registration's token to swarm-controller, through
|
||||
# the store, under matrix-ctl's identity; the write lands at a path
|
||||
# only matrix-ctl and the controller are granted (./swarm-bao.nix).
|
||||
# The store holds the swarm registration; this keeps the state dir
|
||||
# the render above reads in step with it, and publishes the
|
||||
# `as_token` to the path `swarm-controller` reads, under matrix-ctl's
|
||||
# identity (the grants are in ./swarm-bao.nix). A store without the
|
||||
# registration is seeded from the state dir, so nothing rotates.
|
||||
#
|
||||
# ⚠️ Not `before`/`requiredBy` tuwunel, and nothing orders tuwunel
|
||||
# after it: the homeserver starts from the state dir's last copy
|
||||
# whether or not the store answers. When the registration is newer than
|
||||
# tuwunel's last start, this restarts tuwunel onto it.
|
||||
#
|
||||
# `Restart=on-failure` covers a sealed store or one still starting;
|
||||
# the start timeout is bounded for the reason
|
||||
# the timer below repeats the run, which a unit without `RemainAfterExit` allows.
|
||||
# The start timeout is bounded for the reason
|
||||
# ./swarm-secret-publisher.nix states: a sealed store answers on the
|
||||
# port and never answers the read.
|
||||
systemd.services.swarm-matrix-appservice-publish = lib.mkIf ctlActive {
|
||||
description = "publish the swarm's appservice token to the swarm secret store";
|
||||
systemd.services.swarm-matrix-appservice-sync = lib.mkIf ctlActive {
|
||||
description = "keep the swarm's appservice registration in step with the swarm secret store";
|
||||
after = [ "swarm-matrix-appservice-render.service" ];
|
||||
requires = [ "swarm-matrix-appservice-render.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [ pkgs.systemd ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${deployCfg.matrix.ctlPackage}/bin/swarm-matrix-ctl appservice publish";
|
||||
ExecStart = "${deployCfg.matrix.ctlPackage}/bin/swarm-matrix-ctl appservice sync";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 30;
|
||||
TimeoutStartSec = 60;
|
||||
|
|
@ -1053,12 +1066,19 @@ in
|
|||
BAO_CLIENT_KEY = deployCfg.matrix.ctlBaoClientKeyFile;
|
||||
MATRIX_APPSERVICE_CERT_ROLE = ctlCertRole;
|
||||
MATRIX_APPSERVICE_DIR = swarmAppserviceDir;
|
||||
MATRIX_APPSERVICE_SENDER = swarmSenderLocalpart;
|
||||
MATRIX_APPSERVICE_USER_REGEX = appserviceUserRegex;
|
||||
MATRIX_APPSERVICE_RESTART_UNIT = "tuwunel.service";
|
||||
}
|
||||
// lib.optionalAttrs (deployCfg.bao.serverCaFile != null) {
|
||||
BAO_CACERT = deployCfg.bao.serverCaFile;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.swarm-matrix-appservice-sync = lib.mkIf ctlActive (
|
||||
storeRefreshTimer "re-read the swarm's appservice registration from the swarm secret store"
|
||||
);
|
||||
|
||||
environment.systemPackages = [ deployCfg.matrix.package ];
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -618,7 +618,7 @@ let
|
|||
matrixCtlPolicyName = "swarm-matrix-ctl";
|
||||
matrixCtlCn = baoDeploy.matrixCtlCommonName;
|
||||
|
||||
# Three paths, each spelled to the leaf, and every narrowing in them is
|
||||
# Four paths, each spelled to the leaf, and every narrowing in them is
|
||||
# load-bearing.
|
||||
#
|
||||
# `secret/data/` is KV v2's ACL prefix, inserted by the engine rather than
|
||||
|
|
@ -628,18 +628,27 @@ let
|
|||
# to overwrite Grafana's OIDC client. The paths are spelled to the leaf for
|
||||
# that reason, not for tidiness.
|
||||
#
|
||||
# The first is the swarm appservice's token, which matrix-ctl mints inside
|
||||
# the container and publishes here for the controller. `read` as well as
|
||||
# write, unlike either sibling, because publish compares before it writes.
|
||||
# The first is the swarm appservice's token, which matrix-ctl derives from
|
||||
# the registration and publishes here for the controller. `read` as well as
|
||||
# write, unlike either sibling, because sync compares before it writes.
|
||||
#
|
||||
# The second is the homeserver's own OIDC client secret, read-only:
|
||||
# The second is the registration's tokens: the store's copy, which matrix-ctl
|
||||
# seeds from the container's state dir and then follows. `create` and `read`
|
||||
# only, never `update`: a registration that exists is replaced by an
|
||||
# operator, not by a unit.
|
||||
#
|
||||
# The third is the homeserver's own OIDC client secret, read-only:
|
||||
# `swarm-bao-matrix-oidc` on the homeserver's host reads it under this leaf.
|
||||
# The third, also read-only, is the client secret of the journal forwarder in
|
||||
# The fourth, also read-only, is the client secret of the journal forwarder in
|
||||
# the homeserver's container.
|
||||
matrixCtlPolicyText = ''
|
||||
path "${credentialMountPath}/data/${swarmAppserviceTokenLeaf}" {
|
||||
capabilities = ["create", "update", "read"]
|
||||
}
|
||||
|
||||
path "${credentialMountPath}/data/${swarmAppserviceRegistrationLeaf}" {
|
||||
capabilities = ["create", "read"]
|
||||
}
|
||||
''
|
||||
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.matrix.sso.clientId}/oidc/client"
|
||||
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.matrix.otel.clientId}/oidc/client";
|
||||
|
|
@ -650,6 +659,11 @@ let
|
|||
# homeserver's admin.
|
||||
swarmAppserviceTokenLeaf = "swarm/controller/swarm-controller/matrix/appservice-token";
|
||||
|
||||
# The swarm appservice registration's leaf, the nix half of
|
||||
# `swarm_secret_client::matrix::swarm_appservice_registration_path`. Only
|
||||
# matrix-ctl reaches it; the controller reads the token above, not this.
|
||||
swarmAppserviceRegistrationLeaf = "swarm/controller/swarm-controller/matrix/appservice-registration";
|
||||
|
||||
# The controller's own OIDC client secret, the nix half of
|
||||
# `swarm_secret_client::queue::controller_client_path`. The publisher writes
|
||||
# it; the controller reads it and holds no other copy.
|
||||
|
|
@ -1902,12 +1916,13 @@ in
|
|||
example = "swarm-matrix-ctl.svc";
|
||||
description = ''
|
||||
Subject the store's matrix-ctl cert-auth role accepts — the
|
||||
identity the matrix container's `swarm-matrix-appservice-publish`
|
||||
unit presents when it publishes the swarm appservice's token.
|
||||
identity the matrix container's `swarm-matrix-appservice-sync`
|
||||
unit presents when it syncs the swarm appservice's registration and
|
||||
publishes its token.
|
||||
|
||||
A **third** identity rather than reuse of either sibling above. Its
|
||||
grant is that credential itself, plus read on two OIDC client
|
||||
secrets: the homeserver's, fetched by `swarm-bao-matrix-oidc`, and
|
||||
grant is that credential and the registration it derives from, plus
|
||||
read on two OIDC client secrets: the homeserver's, fetched by `swarm-bao-matrix-oidc`, and
|
||||
that of the journal forwarder in the homeserver's container. The
|
||||
point of the whole arrangement is that the
|
||||
process holding the appservice token is not the hive, so handing it
|
||||
|
|
|
|||
|
|
@ -575,12 +575,13 @@ let
|
|||
# prefix the publisher holds would be a real loss even though it would
|
||||
# read as tidier.
|
||||
#
|
||||
# The first leaf is the swarm appservice token, which matrix-ctl mints
|
||||
# and publishes for the controller; the second and third are the
|
||||
# homeserver's own OIDC client secret and its container forwarder's,
|
||||
# read-only. The whole list, so a fourth stanza fails rather than riding
|
||||
# The first leaf is the swarm appservice token, which matrix-ctl derives
|
||||
# and publishes for the controller; the second is the registration it
|
||||
# derives that from, `create` and `read` only; the third and fourth are
|
||||
# the homeserver's own OIDC client secret and its container forwarder's,
|
||||
# read-only. The whole list, so a fifth stanza fails rather than riding
|
||||
# along beside correct ones.
|
||||
name = "matrix-ctl's grant is the swarm appservice token and the homeserver's and its forwarder's OIDC secrets, nothing else";
|
||||
name = "matrix-ctl's grant is the swarm appservice token and registration and the homeserver's and its forwarder's OIDC secrets, nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script;
|
||||
|
|
@ -594,6 +595,13 @@ let
|
|||
"read"
|
||||
];
|
||||
}
|
||||
{
|
||||
path = "secret/data/swarm/controller/swarm-controller/matrix/appservice-registration";
|
||||
caps = [
|
||||
"create"
|
||||
"read"
|
||||
];
|
||||
}
|
||||
{
|
||||
path = "secret/data/swarm/services/tuwunel/oidc/client";
|
||||
caps = [ "read" ];
|
||||
|
|
@ -611,8 +619,9 @@ let
|
|||
}
|
||||
{
|
||||
# 🩸 `read` is load-bearing here, unlike on the publisher: matrix-ctl's
|
||||
# `appservice publish` reads the swarm appservice token back and writes
|
||||
# only when the store's copy differs.
|
||||
# `appservice sync` reads the swarm appservice token back and writes
|
||||
# only when the store's copy differs, and reads the registration before
|
||||
# it creates it.
|
||||
name = "matrix-ctl may read back what it writes";
|
||||
ok =
|
||||
let
|
||||
|
|
@ -2238,7 +2247,9 @@ let
|
|||
# it: matrix-ctl is its one writer. Pinned as the whole stanza, so an
|
||||
# added capability fails.
|
||||
name = "the controller reads the swarm appservice token and cannot write it";
|
||||
ok = lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/matrix/appservice-token\" {\n capabilities = [\"read\"]\n}" baoGrantHere.systemd.services.swarm-bao-controller-policy.script;
|
||||
ok =
|
||||
lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/matrix/appservice-token\" {\n capabilities = [\"read\"]\n}" baoGrantHere.systemd.services.swarm-bao-controller-policy.script
|
||||
&& !(lib.hasInfix "appservice-registration" baoGrantHere.systemd.services.swarm-bao-controller-policy.script);
|
||||
}
|
||||
{
|
||||
# The controller's own OIDC client secret, which the publisher writes and
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ let
|
|||
ok =
|
||||
let
|
||||
env =
|
||||
baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-publish.environment;
|
||||
baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-sync.environment;
|
||||
hiveLeaf = baoWithMatrix.services.hyperhive.deploy.bao.clientCertFile;
|
||||
in
|
||||
env.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/matrix-ctl.pem" && env.BAO_CLIENT_CERT != hiveLeaf;
|
||||
|
|
@ -193,24 +193,26 @@ let
|
|||
# agreement the unit cannot get wrong, and the verb: a bare invocation
|
||||
# exits non-zero with clap's usage, a deploy-time failure with no local
|
||||
# signal.
|
||||
name = "the publish unit is handed the store role and invokes its verb";
|
||||
name = "the sync unit is handed the store role, the sender and the homeserver unit, and invokes its verb";
|
||||
ok =
|
||||
let
|
||||
u = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-publish;
|
||||
u = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-sync;
|
||||
in
|
||||
u.environment.MATRIX_APPSERVICE_CERT_ROLE == "swarm-matrix-ctl"
|
||||
&& lib.hasSuffix "/bin/swarm-matrix-ctl appservice publish" u.serviceConfig.ExecStart;
|
||||
&& u.environment.MATRIX_APPSERVICE_RESTART_UNIT == "tuwunel.service"
|
||||
&& u.environment.MATRIX_APPSERVICE_SENDER == "swarm"
|
||||
&& lib.hasSuffix "/bin/swarm-matrix-ctl appservice sync" u.serviceConfig.ExecStart;
|
||||
}
|
||||
{
|
||||
# 🩸 A secret is a path, never a value. Every variable the unit is given
|
||||
# names a file or an address; the token itself is read out of the state
|
||||
# dir at runtime, so nothing here can be a token and an environment block
|
||||
# is world-readable through `systemctl show`.
|
||||
name = "the publish unit's environment carries paths and addresses, never a token";
|
||||
name = "the sync unit's environment carries paths and addresses, never a token";
|
||||
ok =
|
||||
let
|
||||
env =
|
||||
baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-publish.environment;
|
||||
baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-sync.environment;
|
||||
in
|
||||
!(lib.any (v: lib.hasInfix "as_token" v || lib.hasInfix "syt_" v) (lib.attrValues env));
|
||||
}
|
||||
|
|
@ -272,6 +274,52 @@ let
|
|||
&& lib.hasSuffix "swarm-matrix-ctl appservice render" r.serviceConfig.ExecStart
|
||||
&& !(r.environment ? BAO_ADDR);
|
||||
}
|
||||
{
|
||||
# The start path stays free of the store. The render is what tuwunel
|
||||
# waits for and it carries no store address or identity; the sync unit
|
||||
# that does is neither before tuwunel nor required by it, and tuwunel
|
||||
# names it nowhere, so a store outage leaves the homeserver starting on
|
||||
# the state dir's last copy. The presence arms are the render's own
|
||||
# ordering (the same lookup finds `before`/`requiredBy` where they are)
|
||||
# and the sync's order after the render.
|
||||
name = "the sync unit is off tuwunel's start path and the render holds no store identity";
|
||||
ok =
|
||||
let
|
||||
c = baoWithMatrix.containers.hive-matrix.config.systemd;
|
||||
r = c.services.swarm-matrix-appservice-render;
|
||||
s = c.services.swarm-matrix-appservice-sync;
|
||||
t = c.services.tuwunel;
|
||||
names =
|
||||
u:
|
||||
lib.concatLists [
|
||||
(u.after or [ ])
|
||||
(u.requires or [ ])
|
||||
(u.wants or [ ])
|
||||
(u.bindsTo or [ ])
|
||||
];
|
||||
sync = "swarm-matrix-appservice-sync.service";
|
||||
in
|
||||
lib.elem "tuwunel.service" r.before
|
||||
&& lib.elem "swarm-matrix-appservice-render.service" s.after
|
||||
&& !(lib.elem "tuwunel.service" (s.before or [ ]))
|
||||
&& !(lib.elem "tuwunel.service" (s.requiredBy or [ ]))
|
||||
&& !(lib.elem "tuwunel.service" (s.wantedBy or [ ]))
|
||||
&& !(lib.elem sync (names t))
|
||||
&& !(lib.elem sync (r.before or [ ]))
|
||||
&& !(lib.any (lib.hasPrefix "BAO_") (lib.attrNames r.environment));
|
||||
}
|
||||
{
|
||||
# A timer repeats the sync, which needs a unit that does not stay
|
||||
# `active` after a run.
|
||||
name = "the sync unit is repeated by a timer and does not stay active";
|
||||
ok =
|
||||
let
|
||||
c = baoWithMatrix.containers.hive-matrix.config.systemd;
|
||||
in
|
||||
c.timers.swarm-matrix-appservice-sync.timerConfig.OnUnitInactiveSec == "10min"
|
||||
&& !(c.services.swarm-matrix-appservice-sync.serviceConfig.RemainAfterExit or false)
|
||||
&& (c.services.swarm-matrix-appservice-render.serviceConfig.RemainAfterExit or false);
|
||||
}
|
||||
{
|
||||
# The absence arm for the four above: with no matrix-ctl identity there
|
||||
# is nobody to publish the swarm token, so no admin, no registration and
|
||||
|
|
@ -284,7 +332,8 @@ let
|
|||
in
|
||||
!(g ? admin_execute)
|
||||
&& !(c.systemd.services ? swarm-matrix-appservice-render)
|
||||
&& !(c.systemd.services ? swarm-matrix-appservice-publish)
|
||||
&& !(c.systemd.services ? swarm-matrix-appservice-sync)
|
||||
&& !(c.systemd.timers ? swarm-matrix-appservice-sync)
|
||||
&& !(lib.any (lib.hasPrefix "swarm-appservice.yaml") c.systemd.services.tuwunel.serviceConfig.LoadCredential);
|
||||
}
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue