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
|
|
@ -1,9 +1,12 @@
|
|||
# Egress proxy for this hive's agents: squid on the host, bound to the bridge
|
||||
# IP, default-deny on the destination hostname. It filters CONNECT targets and
|
||||
# plain-HTTP hosts and never decrypts, so it holds no signing key. A request
|
||||
# for a listed name must carry a per-agent proxy credential, checked against the
|
||||
# users file hive-c0re renders. No agent sends one yet; the exporter, journal
|
||||
# forwarding and the self-check below are how a deploy shows it works.
|
||||
# must carry a per-agent proxy credential, checked against the users file
|
||||
# hive-c0re renders. A credential's agent either has the swarm's
|
||||
# `unrestricted_internet` flag, and may then reach any destination the deny
|
||||
# rules leave, or reaches only the listed names. No agent sends a credential
|
||||
# yet; the exporter, journal forwarding and the self-check below are how a
|
||||
# deploy shows it works.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
|
|
@ -24,18 +27,21 @@ let
|
|||
|
||||
# The squid user the self-check logs in as: `swarm_secret_client::proxy::SELFCHECK_USER`.
|
||||
selfcheckUser = "selfcheck.squid";
|
||||
# The same string in `swarm_secret_client::proxy::SELFCHECK_UNRESTRICTED_USER`.
|
||||
selfcheckUnrestrictedUser = "selfcheck-unrestricted.squid";
|
||||
usersDir = builtins.dirOf cfg.usersFile;
|
||||
|
||||
# Evaluated top to bottom, first match wins. The three destination denies
|
||||
# sit before the only allow: squid resolves names on the host, so a name
|
||||
# that answers 127.0.0.1, 169.254.169.254 or a bridge address would
|
||||
# otherwise reach services the bridge firewall keeps agents out of. The
|
||||
# unlisted-name deny comes first because `dstdomain` needs no lookup while
|
||||
# the `dst` ACLs do: an unlisted name is refused at once instead of waiting
|
||||
# on the host resolver, and costs a client no DNS query. The credential
|
||||
# check sits right before the allow, so a refused destination answers 403
|
||||
# to a client with or without a credential, and only a request that would
|
||||
# otherwise be let through gets the 407 challenge.
|
||||
# sit before every allow, `allow unrestricted` included: squid resolves
|
||||
# names on the host, so a name that answers 127.0.0.1, 169.254.169.254 or a
|
||||
# bridge address would otherwise reach services the bridge firewall keeps
|
||||
# agents out of, and an unrestricted agent is still an agent. The IP-literal
|
||||
# denies come first because `dstdomain` on an IP literal does a reverse
|
||||
# lookup. The credential check sits right before the first allow, so only a
|
||||
# request that would otherwise be let through gets the 407 challenge. An
|
||||
# unrestricted user is then let through 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.
|
||||
squidConf = ''
|
||||
http_port ${netCfg.bridgeIp}:${toString port}
|
||||
pid_filename /run/squid/squid.pid
|
||||
|
|
@ -59,6 +65,10 @@ let
|
|||
acl SSL_ports port 443
|
||||
acl Safe_ports port 80 443
|
||||
acl authed proxy_auth REQUIRED
|
||||
# The users whose agent holds the flag. Squid reads the file when it parses
|
||||
# its configuration, so `squid-unrestricted-reload` reloads squid when
|
||||
# hive-c0re rewrites it. An empty or missing file lists no one.
|
||||
acl unrestricted proxy_auth "${cfg.unrestrictedFile}"
|
||||
|
||||
# Host-originated requests: the self-check and the exporter. A local
|
||||
# connect to the bridge IP takes the bridge IP as its source.
|
||||
|
|
@ -82,11 +92,12 @@ let
|
|||
http_access deny CONNECT !SSL_ports
|
||||
http_access deny ip_v4_host
|
||||
http_access deny ip_v6_host
|
||||
${lib.optionalString (allowed != [ ]) "http_access deny !allowed_dst"}
|
||||
http_access deny to_localhost
|
||||
http_access deny to_linklocal
|
||||
http_access deny to_bridge
|
||||
http_access deny !authed
|
||||
http_access allow unrestricted
|
||||
${lib.optionalString (allowed != [ ]) "http_access deny !allowed_dst"}
|
||||
${lib.optionalString (allowed != [ ]) "http_access allow allowed_dst"}
|
||||
http_access deny all
|
||||
'';
|
||||
|
|
@ -128,30 +139,32 @@ let
|
|||
secret=$(< "$credential_file")
|
||||
fi
|
||||
|
||||
# <host> <auth|anon>: the status squid answers a CONNECT with; 000 when
|
||||
# the proxy is down.
|
||||
# <host> <user|anon>: the status squid answers a CONNECT with; 000 when
|
||||
# the proxy is down. Both users log in with the one secret.
|
||||
connect_status() {
|
||||
{
|
||||
if [ "$2" = auth ] && [ -n "$secret" ]; then
|
||||
printf 'proxy-user = "%s:%s"\n' ${selfcheckUser} "$secret"
|
||||
if [ "$2" != anon ] && [ -n "$secret" ]; then
|
||||
printf 'proxy-user = "%s:%s"\n' "$2" "$secret"
|
||||
fi
|
||||
} | curl -sS -m 10 -K - --interface ${netCfg.bridgeIp} -p -x ${proxyUrl} \
|
||||
-o /dev/null -w '%{http_connect}' "https://$1/" 2>/dev/null || true
|
||||
}
|
||||
|
||||
allow_status=$(connect_status ${cfg.selfcheck.canary} auth)
|
||||
deny_status=$(connect_status ${denyCanary} auth)
|
||||
allow_status=$(connect_status ${cfg.selfcheck.canary} ${selfcheckUser})
|
||||
deny_status=$(connect_status ${denyCanary} ${selfcheckUser})
|
||||
anon_status=$(connect_status ${cfg.selfcheck.canary} anon)
|
||||
unrestricted_status=$(connect_status ${cfg.selfcheck.unrestrictedCanary} ${selfcheckUnrestrictedUser})
|
||||
|
||||
ok=false
|
||||
if [ "$allow_status" = 200 ] && [ "$deny_status" = 403 ] && [ "$anon_status" = 407 ]; then
|
||||
if [ "$allow_status" = 200 ] && [ "$deny_status" = 403 ] && [ "$anon_status" = 407 ] \
|
||||
&& [ "$unrestricted_status" = 200 ]; then
|
||||
ok=true
|
||||
fi
|
||||
credential=absent
|
||||
if [ -n "$secret" ]; then
|
||||
credential=present
|
||||
fi
|
||||
echo "squid-selfcheck ok=$ok allow_canary=${cfg.selfcheck.canary} allow_status=$allow_status deny_canary=${denyCanary} deny_status=$deny_status anon_status=$anon_status credential=$credential"
|
||||
echo "squid-selfcheck ok=$ok allow_canary=${cfg.selfcheck.canary} allow_status=$allow_status deny_canary=${denyCanary} deny_status=$deny_status anon_status=$anon_status credential=$credential unrestricted_canary=${cfg.selfcheck.unrestrictedCanary} unrestricted_status=$unrestricted_status"
|
||||
[ "$ok" = true ]
|
||||
'';
|
||||
};
|
||||
|
|
@ -186,6 +199,23 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
unrestrictedFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "${builtins.dirOf cfg.usersFile}/unrestricted";
|
||||
readOnly = true;
|
||||
defaultText = lib.literalMD "`unrestricted`, in the directory of `usersFile`";
|
||||
description = ''
|
||||
The user names, one per line, that squid lets reach any destination the
|
||||
deny rules leave open: the users of the agents that hold the swarm's
|
||||
`unrestricted_internet` flag (set unless the swarm records it off), and
|
||||
the self-check's unrestricted user. hive-c0re renders it beside
|
||||
`usersFile`, and squid is reloaded when it changes. Every other user
|
||||
reaches only `allowedDomains`. An empty file, which is what exists
|
||||
until hive-c0re writes one, makes every user a restricted one.
|
||||
Owned by `hive-core` and readable by the `squid` group.
|
||||
'';
|
||||
};
|
||||
|
||||
selfcheck.credentialName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "hive-squid.selfcheck";
|
||||
|
|
@ -198,6 +228,17 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
selfcheck.unrestrictedCanary = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "example.com";
|
||||
description = ''
|
||||
Hostname outside `allowedDomains` that the self-check CONNECTs to on
|
||||
port 443, as its unrestricted user, and expects squid to let through.
|
||||
Squid resolves it with this host's resolver and connects to it, so a
|
||||
host with no route to it reports `ok=false`.
|
||||
'';
|
||||
};
|
||||
|
||||
selfcheck.canary = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = hyperhiveCfg.swarm.forge.domain;
|
||||
|
|
@ -230,11 +271,43 @@ in
|
|||
};
|
||||
|
||||
# The directory is setgid so the file hive-c0re renames into it is the
|
||||
# squid group's. The file exists before squid starts, empty: the helper
|
||||
# exits on a missing file and squid then rejects every login.
|
||||
# squid group's. The users file exists before squid starts, empty: the
|
||||
# helper exits on a missing file and squid then rejects every login. The
|
||||
# unrestricted list is created the same way, though squid treats a missing
|
||||
# one as empty.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${usersDir} 2750 hive-core squid - -"
|
||||
"f ${cfg.usersFile} 0640 hive-core squid - -"
|
||||
"f ${cfg.unrestrictedFile} 0640 hive-core squid - -"
|
||||
];
|
||||
|
||||
# Squid reads the unrestricted list when it parses its configuration, not
|
||||
# when it changes. hive-c0re writes the list only when its contents change,
|
||||
# so each change is one reload. Nothing happens to a squid that is not
|
||||
# running; it reads the list when it starts.
|
||||
systemd.paths.squid-unrestricted-reload = {
|
||||
description = "Watch squid's unrestricted-users list";
|
||||
wantedBy = [ "paths.target" ];
|
||||
pathConfig.PathChanged = cfg.unrestrictedFile;
|
||||
};
|
||||
|
||||
systemd.services.squid-unrestricted-reload = {
|
||||
description = "Reload squid to read its unrestricted-users list";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${config.systemd.package}/bin/systemctl try-reload-or-restart squid.service";
|
||||
};
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = !(builtins.elem cfg.selfcheck.unrestrictedCanary allowed);
|
||||
message = ''
|
||||
services.hyperhive.squid.selfcheck.unrestrictedCanary must be a name outside
|
||||
allowedDomains, or the self-check cannot tell an unrestricted user from a
|
||||
restricted one.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
systemd.services.squid = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue