swarm-logs-auth: check agent tokens on the log store's query route
A new `auth_request` responder on the log store's host. nginx sends it every `/select/logsql/` request whose `Authorization` is `Bearer swarm-agent.<agent>.<secret>`; it answers 200 when the secret is the one stored at `swarm/agents/<agent>/queue` (`swarm-agent-verify`), 401 otherwise. Every other bearer still goes to authelia, so agents still presenting the hive's shared OIDC client keep working. `auth_request` takes a fixed URI, so the split is a `map` on `$http_authorization` plus an internal location that rewrites to the map's pick. Checked against nginx 1.30.5 with stub upstreams: an agent token only the stub validator accepts gets 200, a token only the stub authelia accepts gets 200, every wrong one 401, and the internal locations 404 from outside. The responder serves a unix socket in its own traverse-only runtime directory and reads the store under its own leaf (`swarm-logs-auth`), whose policy is `swarm/agents/+/queue`, read-only. glue-bao-tls mints the leaf where the store runs; elsewhere the operator places it, and eval refuses a log store host without one. Part of #4459.
This commit is contained in:
parent
e7dbb94f28
commit
1f4736e79b
16 changed files with 515 additions and 9 deletions
|
|
@ -169,7 +169,7 @@ let
|
|||
_: u: (u.environment.BAO_CLIENT_CERT or null) == granterCertFile
|
||||
) baoGrantWithConsumers.systemd.services;
|
||||
|
||||
# The sixteen units that write a `swarm-*` grant, by name, for the discovery
|
||||
# The seventeen units that write a `swarm-*` grant, by name, for the discovery
|
||||
# control below.
|
||||
grantingUnitNames = [
|
||||
"swarm-bao-controller-policy"
|
||||
|
|
@ -186,6 +186,7 @@ let
|
|||
"swarm-bao-operator-viewer-policy"
|
||||
"swarm-bao-hive-leaf-policy"
|
||||
"swarm-bao-ci-runner-policy"
|
||||
"swarm-bao-logs-auth-policy"
|
||||
"swarm-bao-forge-publisher-policy"
|
||||
"swarm-bao-review-classifier-policy"
|
||||
];
|
||||
|
|
@ -704,6 +705,31 @@ let
|
|||
in
|
||||
lib.hasInfix "/nats-auth.pem ]" s && lib.hasInfix "swarm-nats-auth \"\" clientAuth" s;
|
||||
}
|
||||
{
|
||||
# The same `+` leaf as the queue responder, read-only, and nothing else:
|
||||
# this principal checks a presented secret and does nothing with it.
|
||||
name = "the log store responder's grant is every agent's queue secret, read-only, and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-logs-auth-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/agents/+/queue\" {\n capabilities = [\"read\"]\n}" s
|
||||
&& lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 1
|
||||
&& !(lib.hasInfix "secret/data/swarm/agents/*" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/services" s)
|
||||
&& lib.hasInfix "auth/cert/certs/swarm-logs-auth" s
|
||||
&& lib.hasInfix "allowed_common_names=swarm-logs-auth" s
|
||||
&& lib.hasInfix "token_policies=swarm-logs-auth" s;
|
||||
}
|
||||
{
|
||||
name = "the PKI unit signs the log store responder's leaf under its own subject";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-pki.script;
|
||||
in
|
||||
lib.hasInfix "/logs-auth.pem ]" s && lib.hasInfix "swarm-logs-auth \"\" clientAuth" s;
|
||||
}
|
||||
{
|
||||
# 🩸 The half that makes the policies above bind: a policy grants only
|
||||
# through a token that carries it, and a token is minted by a cert-auth
|
||||
|
|
@ -915,24 +941,24 @@ let
|
|||
}
|
||||
{
|
||||
# A store host without the granter's pair writes its grants some other
|
||||
# way, so none of the sixteen units may exist. Without this arm
|
||||
# way, so none of the seventeen units may exist. Without this arm
|
||||
# `lib.mkIf haveGranter` could be dropped from any of them and every other
|
||||
# case here would still pass.
|
||||
name = "without the granter's pair none of the sixteen granting units render";
|
||||
name = "without the granter's pair none of the seventeen granting units render";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterOptOut.systemd.services;
|
||||
in
|
||||
lib.all (unit: !(s ? ${unit})) (grantingUnitNames ++ [ "swarm-bao-granter-role" ])
|
||||
# The control: the same store with the pair renders all sixteen.
|
||||
# The control: the same store with the pair renders all seventeen.
|
||||
&& lib.all (unit: baoGrantHere.systemd.services ? ${unit}) grantingUnitNames;
|
||||
}
|
||||
{
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the sixteen still
|
||||
# 🩸 What replaced the silent skip. With no bootstrap token the seventeen still
|
||||
# render, and a refused granter fails them with the step that fixes it.
|
||||
# A store host that never named a token is told to name one, since the
|
||||
# unit that sets the granter up renders only where it has.
|
||||
name = "a store host without a bootstrap token renders the sixteen, each failing loudly with the one-time step";
|
||||
name = "a store host without a bootstrap token renders the seventeen, each failing loudly with the one-time step";
|
||||
ok =
|
||||
let
|
||||
s = baoGranterNoToken.systemd.services;
|
||||
|
|
@ -1695,8 +1721,8 @@ let
|
|||
}
|
||||
{
|
||||
# What makes the case above mean something: discovery by the granter's
|
||||
# certificate reaches all sixteen units, and each yields calls.
|
||||
name = "the granter-policy check sees all sixteen granting units, and parses calls from each";
|
||||
# certificate reaches all seventeen units, and each yields calls.
|
||||
name = "the granter-policy check sees all seventeen granting units, and parses calls from each";
|
||||
ok =
|
||||
lib.sort lib.lessThan (lib.attrNames granterUnits) == lib.sort lib.lessThan grantingUnitNames
|
||||
&& lib.all (u: baoCalls u.script != [ ]) (lib.attrValues granterUnits)
|
||||
|
|
|
|||
|
|
@ -504,6 +504,44 @@ let
|
|||
&& !(lib.hasInfix "$args" q.extraConfig)
|
||||
&& !(lib.hasInfix "?" q.proxyPass);
|
||||
}
|
||||
{
|
||||
# `auth_request` takes a fixed URI, so the split between the agents'
|
||||
# responder and authelia is a map plus a rewrite in an internal
|
||||
# location. Every link of that chain is pinned: the map keys on the
|
||||
# agent-token prefix and falls back to authelia, the query location
|
||||
# names the switch, the switch rewrites to the map's pick, and the
|
||||
# agent target reaches the responder's socket, which the unit binds.
|
||||
name = "the machine query location sends agent bearers to swarm-logs-auth and the rest to authelia";
|
||||
ok =
|
||||
let
|
||||
l = allLocal.services.nginx.virtualHosts."logs.t.local".locations;
|
||||
http = allLocal.services.nginx.appendHttpConfig;
|
||||
unit = allLocal.systemd.services.swarm-logs-auth;
|
||||
in
|
||||
lib.hasInfix "map \$http_authorization \$swarm_logs_auth_target" http
|
||||
&& lib.hasInfix "\"~^Bearer swarm-agent\\.\" /__swarm_logs_agent_auth;" http
|
||||
&& lib.hasInfix "default /__hive_authelia;" http
|
||||
&& lib.hasInfix "auth_request /__swarm_logs_auth;" l."^~ /select/logsql/".extraConfig
|
||||
&& lib.hasInfix "rewrite ^ \$swarm_logs_auth_target last;" l."= /__swarm_logs_auth".extraConfig
|
||||
&& lib.hasInfix "internal;" l."= /__swarm_logs_agent_auth".extraConfig
|
||||
&& l."= /__swarm_logs_agent_auth".proxyPass == "http://unix:/run/swarm-logs-auth/auth.sock:/"
|
||||
&& lib.hasInfix "--socket /run/swarm-logs-auth/auth.sock" unit.serviceConfig.ExecStart
|
||||
&& l ? "= /__hive_authelia";
|
||||
}
|
||||
{
|
||||
# The responder reads the store as itself, with the leaf the store's
|
||||
# host mints for it.
|
||||
name = "swarm-logs-auth presents its own store leaf";
|
||||
ok =
|
||||
let
|
||||
v = allLocal.services.hyperhive.deploy.victorialogs;
|
||||
lc = allLocal.systemd.services.swarm-logs-auth.serviceConfig.LoadCredential;
|
||||
in
|
||||
lib.hasSuffix "/logs-auth.pem" v.authBaoClientCertFile
|
||||
&& lib.hasSuffix "/logs-auth-key.pem" v.authBaoClientKeyFile
|
||||
&& builtins.elem "bao-client.pem:${v.authBaoClientCertFile}" lc
|
||||
&& builtins.elem "bao-client-key.pem:${v.authBaoClientKeyFile}" lc;
|
||||
}
|
||||
{
|
||||
# The absence arm for the case above, and the option's own rule — a
|
||||
# service declares its entry under its own `enable` — made checkable.
|
||||
|
|
|
|||
Loading…
Reference in a new issue