apply each agent's internet flag to its squid credential
This commit is contained in:
parent
0b99f17bc3
commit
501445ebc3
1 changed files with 323 additions and 91 deletions
|
|
@ -112,13 +112,18 @@ pkgs.testers.runNixOSTest {
|
|||
];
|
||||
# Names the proxy resolves, each a different destination class.
|
||||
networking.hosts = {
|
||||
"192.168.2.2" = [ "forge.t.local" ];
|
||||
# `other.t.local` resolves, is reachable and is on no allow-list.
|
||||
"192.168.2.2" = [
|
||||
"forge.t.local"
|
||||
"other.t.local"
|
||||
];
|
||||
"127.0.0.1" = [ "loop.t.local" ];
|
||||
${bridgeIp} = [ "bridge.t.local" ];
|
||||
"169.254.169.254" = [ "meta.t.local" ];
|
||||
};
|
||||
# Renders the users file the way hive-c0re does.
|
||||
environment.systemPackages = [ pkgs.mkpasswd ];
|
||||
services.hyperhive.squid.selfcheck.unrestrictedCanary = "other.t.local";
|
||||
services.hyperhive.squid.allowedDomains = [
|
||||
"forge.t.local"
|
||||
"loop.t.local"
|
||||
|
|
@ -157,14 +162,24 @@ pkgs.testers.runNixOSTest {
|
|||
testScript = ''
|
||||
import re
|
||||
import secrets
|
||||
import time
|
||||
|
||||
# Drawn fresh each run. The users file holds only their hashes, as
|
||||
# hive-c0re renders it.
|
||||
PW = {u: secrets.token_urlsafe(24) for u in ("atlas", "selfcheck.squid", "ruth")}
|
||||
# hive-c0re renders it. `atlas` is a restricted agent and `uma` an
|
||||
# unrestricted one; the two self-check users share one secret.
|
||||
PW = {
|
||||
u: secrets.token_urlsafe(24)
|
||||
for u in ("atlas", "uma", "selfcheck.squid", "ruth")
|
||||
}
|
||||
PW["selfcheck-unrestricted.squid"] = PW["selfcheck.squid"]
|
||||
ATLAS = "atlas:" + PW["atlas"]
|
||||
UMA = "uma:" + PW["uma"]
|
||||
SELFCHECK = "selfcheck.squid:" + PW["selfcheck.squid"]
|
||||
SELFCHECK_U = "selfcheck-unrestricted.squid:" + PW["selfcheck.squid"]
|
||||
RUTH = "ruth:" + PW["ruth"]
|
||||
USERS = "/var/lib/hive-squid/users"
|
||||
LIST = "/var/lib/hive-squid/unrestricted"
|
||||
UNRESTRICTED = ["uma", "selfcheck-unrestricted.squid"]
|
||||
|
||||
start_all()
|
||||
proxy.wait_for_unit("squid.service")
|
||||
|
|
@ -194,17 +209,42 @@ pkgs.testers.runNixOSTest {
|
|||
f"&& chmod 640 {USERS}.new && mv {USERS}.new {USERS} && sleep 1"
|
||||
)
|
||||
|
||||
with subtest("squid starts on the empty users file and refuses every login"):
|
||||
def write_list(*names):
|
||||
# What hive-c0re does for the list. Squid reads it on a reload, which
|
||||
# squid-unrestricted-reload does when the file changes.
|
||||
body = "printf '%s\\n' " + " ".join(names) if names else ":"
|
||||
proxy.succeed(
|
||||
f"({body}) > {LIST}.new && chown hive-core:squid {LIST}.new "
|
||||
f"&& chmod 640 {LIST}.new && mv {LIST}.new {LIST}"
|
||||
)
|
||||
|
||||
def wait_status(host, cred, want):
|
||||
# The reload that follows a list write is asynchronous.
|
||||
got = None
|
||||
for _ in range(30):
|
||||
got = connect(host, cred)
|
||||
if got == want:
|
||||
return
|
||||
time.sleep(1)
|
||||
raise AssertionError(f"{host} as {cred.split(':')[0]}: wanted {want}, last got {got}")
|
||||
|
||||
with subtest("squid starts on the empty users file and list and refuses every login"):
|
||||
assert proxy.succeed(f"stat -c '%U:%G %a' {USERS}").strip() == "hive-core:squid 640"
|
||||
assert proxy.succeed(f"stat -c '%U:%G %a' {LIST}").strip() == "hive-core:squid 640"
|
||||
assert proxy.succeed("stat -c %a /var/lib/hive-squid").strip() == "2750"
|
||||
assert proxy.succeed(f"stat -c %s {USERS}").strip() == "0"
|
||||
assert proxy.succeed(f"stat -c %s {LIST}").strip() == "0"
|
||||
assert connect("forge.t.local", ATLAS) == "407", "no such user"
|
||||
assert connect("deny-canary.invalid", ATLAS) == "403", "an unlisted name needs no login"
|
||||
assert connect("other.t.local", UMA) == "407", "no such user, unlisted name"
|
||||
|
||||
with subtest("the users file holds hashes, never a password"):
|
||||
write_users(ATLAS, SELFCHECK)
|
||||
write_users(ATLAS, UMA, SELFCHECK, SELFCHECK_U)
|
||||
users = proxy.succeed(f"cat {USERS}")
|
||||
assert re.fullmatch(r"(atlas|selfcheck\.squid):\$6\$\S+\n(atlas|selfcheck\.squid):\$6\$\S+\n", users), users
|
||||
rows = [l.split(":", 1) for l in users.splitlines()]
|
||||
assert sorted(r[0] for r in rows) == sorted(
|
||||
["atlas", "uma", "selfcheck.squid", "selfcheck-unrestricted.squid"]
|
||||
), users
|
||||
assert all(re.fullmatch(r"\$6\$\S+", r[1]) for r in rows), users
|
||||
assert not any(p in users for p in PW.values()), users
|
||||
|
||||
with subtest("an allowed name is tunnelled with a credential and challenged without one"):
|
||||
|
|
@ -215,22 +255,31 @@ pkgs.testers.runNixOSTest {
|
|||
# DES would accept the first eight characters; the hash must not.
|
||||
assert connect("forge.t.local", "atlas:" + PW["atlas"][:8]) == "407", "a prefix of the password"
|
||||
|
||||
with subtest("an unlisted name is refused with or without a credential"):
|
||||
assert connect("deny-canary.invalid", ATLAS) == "403", "deny canary"
|
||||
assert connect("deny-canary.invalid") == "403", "deny canary, anonymous"
|
||||
with subtest("with an empty list every user is restricted"):
|
||||
assert connect("other.t.local", UMA) == "403", "uma, not listed yet"
|
||||
assert connect("other.t.local", ATLAS) == "403"
|
||||
assert connect("forge.t.local", UMA) == "200", "control: uma still reaches a listed name"
|
||||
|
||||
with subtest("a name resolving to a host-local destination is refused"):
|
||||
assert connect("loop.t.local", ATLAS) == "403", "loopback"
|
||||
assert connect("bridge.t.local", ATLAS) == "403", "bridge address"
|
||||
assert connect("meta.t.local", ATLAS) == "403", "link-local"
|
||||
with subtest("a listed user reaches an unlisted name and the others are refused"):
|
||||
write_list(*UNRESTRICTED)
|
||||
wait_status("other.t.local", UMA, "200")
|
||||
assert connect("other.t.local", ATLAS) == "403", "restricted"
|
||||
assert connect("forge.t.local", ATLAS) == "200", "restricted, listed name"
|
||||
assert connect("forge.t.local", UMA) == "200", "unrestricted, listed name"
|
||||
assert connect("other.t.local") == "407", "no credential"
|
||||
assert connect("other.t.local", "uma:" + secrets.token_urlsafe(24)) == "407", "wrong password"
|
||||
assert connect("deny-canary.invalid", ATLAS) == "403", "restricted, a name that does not resolve"
|
||||
|
||||
with subtest("IP literals are refused even when the address is reachable"):
|
||||
assert connect("127.0.0.1", ATLAS) == "403"
|
||||
assert connect("192.168.2.2", ATLAS) == "403"
|
||||
|
||||
with subtest("only a port-443 CONNECT is allowed"):
|
||||
assert connect("forge.t.local:22", ATLAS) == "403"
|
||||
assert connect("forge.t.local:80", ATLAS) == "403"
|
||||
with subtest("neither kind of user reaches a host-local destination or another port"):
|
||||
for cred in (ATLAS, UMA):
|
||||
who = cred.split(":")[0]
|
||||
assert connect("loop.t.local", cred) == "403", f"{who}: loopback"
|
||||
assert connect("bridge.t.local", cred) == "403", f"{who}: bridge address"
|
||||
assert connect("meta.t.local", cred) == "403", f"{who}: link-local"
|
||||
assert connect("127.0.0.1", cred) == "403", f"{who}: loopback literal"
|
||||
assert connect("192.168.2.2", cred) == "403", f"{who}: IP literal"
|
||||
assert connect("other.t.local:22", cred) == "403", f"{who}: port 22"
|
||||
assert connect("other.t.local:80", cred) == "403", f"{who}: CONNECT to port 80"
|
||||
|
||||
with subtest("an agent cannot read cachemgr"):
|
||||
agent.succeed(
|
||||
|
|
@ -247,32 +296,50 @@ pkgs.testers.runNixOSTest {
|
|||
"journalctl -u squid-selfcheck.service -o cat | "
|
||||
"grep -F 'squid-selfcheck ok=true allow_canary=forge.t.local "
|
||||
"allow_status=200 deny_canary=deny-canary.invalid deny_status=403 "
|
||||
"anon_status=407 credential=present'"
|
||||
"anon_status=407 credential=present "
|
||||
"unrestricted_canary=other.t.local unrestricted_status=200'"
|
||||
)
|
||||
|
||||
with subtest("the self-check fails, and says why, when the unrestricted user is not listed"):
|
||||
write_list()
|
||||
wait_status("other.t.local", UMA, "403")
|
||||
proxy.fail("systemctl start squid-selfcheck.service")
|
||||
proxy.succeed(
|
||||
"journalctl -u squid-selfcheck.service -o cat | "
|
||||
"grep -F 'squid-selfcheck ok=false allow_canary=forge.t.local "
|
||||
"allow_status=200 deny_canary=deny-canary.invalid deny_status=403 "
|
||||
"anon_status=407 credential=present "
|
||||
"unrestricted_canary=other.t.local unrestricted_status=403'"
|
||||
)
|
||||
write_list(*UNRESTRICTED)
|
||||
wait_status("other.t.local", UMA, "200")
|
||||
|
||||
with subtest("the self-check fails, and says why, without a credential or with the wrong one"):
|
||||
proxy.succeed("mv /run/credstore/hive-squid.selfcheck /run/credstore/kept")
|
||||
proxy.fail("systemctl start squid-selfcheck.service")
|
||||
proxy.succeed(
|
||||
"journalctl -u squid-selfcheck.service -o cat | "
|
||||
"grep -F 'squid-selfcheck ok=false allow_canary=forge.t.local "
|
||||
"allow_status=407 deny_canary=deny-canary.invalid deny_status=403 "
|
||||
"anon_status=407 credential=absent'"
|
||||
"allow_status=407 deny_canary=deny-canary.invalid deny_status=407 "
|
||||
"anon_status=407 credential=absent "
|
||||
"unrestricted_canary=other.t.local unrestricted_status=407'"
|
||||
)
|
||||
proxy.succeed(f"printf '%s\\n' {secrets.token_urlsafe(24)} > /run/credstore/hive-squid.selfcheck")
|
||||
proxy.fail("systemctl start squid-selfcheck.service")
|
||||
proxy.succeed(
|
||||
"journalctl -u squid-selfcheck.service -o cat | "
|
||||
"grep -F 'allow_status=407 deny_canary=deny-canary.invalid deny_status=403 "
|
||||
"anon_status=407 credential=present'"
|
||||
"grep -F 'allow_status=407 deny_canary=deny-canary.invalid deny_status=407 "
|
||||
"anon_status=407 credential=present "
|
||||
"unrestricted_canary=other.t.local unrestricted_status=407'"
|
||||
)
|
||||
proxy.succeed("mv -f /run/credstore/kept /run/credstore/hive-squid.selfcheck")
|
||||
|
||||
with subtest("squid's access log reaches the journal under squid.service, with the user"):
|
||||
journal = proxy.succeed("journalctl -u squid.service -o cat")
|
||||
assert re.search(r"192\.168\.1\.3 TCP_TUNNEL/200 .* CONNECT forge\.t\.local:443 atlas ", journal), journal
|
||||
assert re.search(r"192\.168\.1\.3 TCP_TUNNEL/200 .* CONNECT other\.t\.local:443 uma ", journal), journal
|
||||
assert re.search(r"192\.168\.1\.3 TCP_DENIED/407 .* CONNECT forge\.t\.local:443 - ", journal), journal
|
||||
assert re.search(r"192\.168\.1\.3 TCP_DENIED/403 .* CONNECT deny-canary\.invalid:443 ", journal), journal
|
||||
assert re.search(r"192\.168\.1\.3 TCP_DENIED/403 .* CONNECT other\.t\.local:443 atlas ", journal), journal
|
||||
assert re.search(r"192\.168\.1\.3 TCP_DENIED/403 .* CONNECT loop\.t\.local:443 ", journal), journal
|
||||
# Exporter scrapes are cachemgr requests and stay out of the log.
|
||||
assert "squid-internal-mgr" not in journal, journal
|
||||
|
|
@ -287,15 +354,36 @@ pkgs.testers.runNixOSTest {
|
|||
assert m and float(m.group(1)) >= 1, metrics
|
||||
proxy.fail("curl -sf -m 5 http://${bridgeIp}:9301/metrics")
|
||||
|
||||
with subtest("squid survives a reload"):
|
||||
with subtest("squid survives a reload and keeps the list"):
|
||||
proxy.succeed("systemctl reload squid.service")
|
||||
proxy.wait_for_unit("squid.service")
|
||||
assert connect("forge.t.local", ATLAS) == "200"
|
||||
assert connect("other.t.local", UMA) == "200"
|
||||
assert connect("other.t.local", ATLAS) == "403"
|
||||
|
||||
with subtest("a user added while squid runs can log in, with no reload"):
|
||||
write_users(ATLAS, SELFCHECK, RUTH)
|
||||
with subtest("a user added while squid runs can log in, with no reload, and is restricted"):
|
||||
write_users(ATLAS, UMA, SELFCHECK, SELFCHECK_U, RUTH)
|
||||
assert connect("forge.t.local", RUTH) == "200"
|
||||
assert connect("forge.t.local", ATLAS) == "200"
|
||||
assert connect("other.t.local", RUTH) == "403"
|
||||
|
||||
with subtest("a user the list gains is unrestricted after the reload and loses it again"):
|
||||
write_list(*UNRESTRICTED, "ruth")
|
||||
wait_status("other.t.local", RUTH, "200")
|
||||
assert connect("loop.t.local", RUTH) == "403", "still no loopback"
|
||||
write_list(*UNRESTRICTED)
|
||||
wait_status("other.t.local", RUTH, "403")
|
||||
assert connect("other.t.local", UMA) == "200", "control: uma is still listed"
|
||||
|
||||
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")
|
||||
proxy.wait_for_unit("squid.service")
|
||||
proxy.wait_for_open_port(3128, "${bridgeIp}")
|
||||
assert connect("forge.t.local", UMA) == "200", "logins work"
|
||||
assert connect("other.t.local", UMA) == "403", "no list, no unrestricted user"
|
||||
write_list(*UNRESTRICTED)
|
||||
wait_status("other.t.local", UMA, "200")
|
||||
|
||||
with subtest("squid starts on a missing users file, refuses logins, and recovers when it appears"):
|
||||
proxy.succeed(f"rm {USERS}")
|
||||
|
|
@ -303,8 +391,9 @@ pkgs.testers.runNixOSTest {
|
|||
proxy.wait_for_unit("squid.service")
|
||||
proxy.wait_for_open_port(3128, "${bridgeIp}")
|
||||
assert connect("forge.t.local", ATLAS) == "407", "no file, no login"
|
||||
assert connect("deny-canary.invalid", ATLAS) == "403", "an unlisted name still answers"
|
||||
write_users(ATLAS, SELFCHECK)
|
||||
assert connect("other.t.local", UMA) == "407", "no file, no login, even when listed"
|
||||
write_users(ATLAS, UMA, SELFCHECK, SELFCHECK_U)
|
||||
assert connect("forge.t.local", ATLAS) == "200", "the file appeared"
|
||||
assert connect("other.t.local", UMA) == "200", "listed, and now logged in"
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue