add a per-agent egress log mode
This commit is contained in:
parent
9140d063d7
commit
e46ad07226
3 changed files with 637 additions and 31 deletions
|
|
@ -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.
|
||||
'';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue