Watch
0
0
Fork
You've already forked hyperhive
0

add a per-agent egress log mode

This commit is contained in:
atlas 2026-10-30 02:18:39 +01:00 • committed by mara
commit e46ad07226
3 changed files with 637 additions and 31 deletions

View file

@ -7,7 +7,10 @@
# rules leave, or reaches only the listed names: the base list, the allow-list
# the swarm keeps plus this host's `allowedDomains` (rendered by hive-c0re
# beside the users file) and the names
# the swarm lists for that agent alone. An agent sends its credential
# the swarm lists for that agent alone. A third kind, set per agent
# (`internet_log`), reaches the same names but is also let through to any other
# destination the deny rules leave; the request is logged as `would_deny`.
# An agent sends its credential
# once it has fetched it (../agent-modules/egress-proxy.nix); the exporter,
# journal forwarding and the self-check below show the proxy works.
{
@ -81,8 +84,11 @@ let
# user is then let through to the base names, a user the swarm lists names
# for to those names (the include, whose rules hive-c0re generates and whose
# file may be empty), an unrestricted user to any other destination on a
# safe port, and every other user to the listed names. The `dst` denies cost an
# unlisted name a lookup on the host's resolver before it is refused.
# safe port, and every other user to the listed names. A `log`-mode agent's
# rule sits in the include after the agent's own names and before
# `deny !allowed_dst`, so it sees the same denies and only turns that last
# refusal into an allow. The `dst` denies cost an unlisted name a lookup on
# the host's resolver before it is refused.
squidConf = ''
http_port ${listen}
pid_filename /run/squid/squid.pid
@ -95,6 +101,15 @@ let
# Cachemgr scrapes are left out.
access_log syslog:daemon.info squid !manager
# A request that a `log`-mode agent's rule (hive-c0re's include, below)
# let through carries the annotation `would_deny=1`. It gets a second line
# in its own format, so the first line's format stays what the
# dashboards and lints parse, and `swarm-logs query '"would_deny"'` finds
# exactly these requests.
logformat would_deny would_deny user=%[un method=%rm host=%>rd port=%>rP result=%Ss/%03>Hs
acl would_denied note would_deny 1
access_log syslog:daemon.info logformat=would_deny would_denied
# The helper reads the users file on every request and reloads it when its
# mtime moves. It exits when the file is missing, which is why the file is
# created before squid starts. A positive answer is cached for a minute, so
@ -358,8 +373,10 @@ in
includes after every destination deny and `deny !authed`, where the
base list is allowed: for each agent of this hive the swarm lists
names for, an ACL for its user, an ACL for its names and the
`http_access allow` joining the two. Empty until the controller has a
list. Squid is reloaded when it changes. Owned by `hive-core` and
`http_access allow` joining the two. After those, for each agent in
`log` mode, an `http_access allow` for names `allowed_dst` lacks that
annotates the request `would_deny=1`. Empty until the controller has a
list or an agent is in `log` mode. Squid is reloaded when it changes. Owned by `hive-core` and
readable by the `squid` group.
'';
};

View file

@ -1670,7 +1670,13 @@ let
# The default render, pinned so moving the options changes none of it:
# the listen address squid binds and hive-c0re hands out, and the users
# file hive-c0re writes, on a hive holding a store identity.
name = "squid's default listen address and users file reach squid.conf and hive-c0re unchanged";
#
# Also the order hive-c0re's `log`-mode rules rely on: the include of
# `agent-allow.conf` follows every safety deny and `deny !authed`, and
# precedes `deny !allowed_dst`, so a log rule in it cannot let through
# what those refuse. The `would_deny` log line sits beside the unchanged
# request line.
name = "squid's default listen address and users file reach squid.conf and hive-c0re unchanged, and the include sits after the safety denies";
ok =
let
withIdentity = hiveWithController {
@ -1679,12 +1685,36 @@ let
};
e = withIdentity.systemd.services.hive-c0re.environment;
conf = withIdentity.services.squid.configText;
lines = map lib.strings.trim (lib.splitString "\n" conf);
at = line: lib.lists.findFirstIndex (l: l == line) null lines;
include = at "include /var/lib/hive-squid/agent-allow.conf";
before = [
"http_access deny !Safe_ports"
"http_access deny CONNECT !SSL_ports"
"http_access deny ip_v4_host"
"http_access deny ip_v6_host"
"http_access deny to_localhost"
"http_access deny to_linklocal"
"http_access deny to_bridge"
"http_access deny to_private"
"http_access deny !authed"
];
in
e.HIVE_SQUID_PROXY == "${withIdentity.services.hyperhive.network.bridgeIp}:3128"
&& e.HIVE_SQUID_USERS_FILE == "/var/lib/hive-squid/users"
&& lib.hasInfix "http_port ${e.HIVE_SQUID_PROXY}" conf
&& lib.hasInfix "basic_ncsa_auth /var/lib/hive-squid/users" conf
&& lib.hasInfix "acl allowed_dst dstdomain cache.nixos.org" conf;
&& lib.hasInfix "acl allowed_dst dstdomain cache.nixos.org" conf
# Presence controls: the include and the policy deny are found at all.
&& include != null
&& at "http_access deny !allowed_dst" != null
&& include < at "http_access deny !allowed_dst"
&& lib.all (rule: at rule != null && at rule < include) before
# The request line is unchanged; the would_deny line is a second one.
&& at "access_log syslog:daemon.info squid !manager" != null
&& at "acl would_denied note would_deny 1" != null
&& at "access_log syslog:daemon.info logformat=would_deny would_denied" != null
&& lib.hasInfix "logformat would_deny would_deny user=%[un " conf;
}
{
# A hive's rendered identity, pinned so moving the option changes none of

View file

@ -545,6 +545,34 @@ pkgs.testers.runNixOSTest {
wait_status("agentonly.t.local", ATLAS, "403")
assert connect("forge.t.local", RUTH) == "200", "the hive's own name after the swarm list is gone"
with subtest("a log-mode agent reaches an unlisted name and is logged as would_deny, and nothing else changes"):
# What hive-c0re renders for `ruth` in log mode, after the agent rules.
write_allow(
seeded,
"# source: swarm\n"
"acl log_mark annotate_transaction would_deny=1\n"
"acl agent_log_ruth proxy_auth ruth\n"
"http_access allow agent_log_ruth !allowed_dst log_mark\n",
)
wait_status("other.t.local", RUTH, "200")
proxy.wait_until_succeeds(
"journalctl -u squid.service -o cat | grep -E "
"'would_deny user=ruth method=CONNECT host=other\\.t\\.local port=443 result='"
)
assert connect("forge.t.local", RUTH) == "200", "a listed name"
assert connect("other.t.local", ATLAS) == "403", "control: a restricted agent is still refused"
assert connect("other.t.local") == "407", "no credential"
assert connect("other.t.local", "ruth:" + secrets.token_urlsafe(24)) == "407", "wrong password"
assert connect("deny-canary.invalid", SELFCHECK) == "403", "the self-check's deny canary"
for name in ["loop.t.local", "bridge.t.local", "meta.t.local", "127.0.0.1", "203.0.113.2", "priv10.t.local", "other.t.local:22", "other.t.local:80"]:
assert connect(name, RUTH) == "403", f"log mode still refuses {name}"
assert squid_saw(r"would_deny user=ruth method=CONNECT host=other\.t\.local port=443 "), "the unlisted name is marked"
assert not squid_saw(r"would_deny user=ruth .* host=forge\.t\.local"), "a listed name is not marked"
assert not squid_saw(r"would_deny user=atlas "), "a restricted agent is not marked"
assert not squid_saw(r"would_deny .* host=loop\.t\.local"), "a refused request is not marked"
write_allow(seeded, "")
wait_status("other.t.local", RUTH, "403")
with subtest("squid starts on a missing list, treats every user as restricted, and recovers when it appears"):
proxy.succeed(f"rm {LIST}")
proxy.succeed("systemctl restart squid.service")