[RFC] IP-less ACLs (technically named listen addresses)
Dridi Boukelmoune
dridi at varni.sh
Tue Apr 25 15:35:37 CEST 2017
Hello everyone,
This is an idea that has been rejected once, I think before we had the
VIP process in place. PHK agreed to revisit this feature request in
exchange of a thorough specification, which is the object of this thread.
It also relates to VIP 17 (unix domain sockets support) and I will mention
why too. I will however also comment on the VIP 17 thread and don't wish
to discuss VIP 17 here.
# Current status
We already have mechanisms available in VCL to restrict or harden part
of a cache policy. For example, one can (and is strongly encouraged)
to prevent anyone from performing cache invalidation.
## ban via the varnish-cli
I'm mentioning this because I find interesting how a combination of
factors enables access control.
To use the ban outside of VCL, you need to connect to an administration
socket. However with our default packaging this socket is bound to the
loopback and you are welcome with a challenge that requires knowing a
shared secret. This shared secret is stored in a file accessible only to root.
We virtually have an ACL in this case: you can only perform such a ban
if you have local privileges on the system (by default). This is of course
true for any operation via the varnish-cli.
## client.ip + acl
Probably the most common construct among varnish users. Illustrated in
the vcl(7) manual.
## server.ip + acl
If varnishd is listening to different network interfaces (multiple -a
options) you can instead let the "network hardening" happen outside of
Varnish and allow operations when performed on an authorized/relevant
address.
Example for varnishd -a 1.2.3.4 -a 5.6.7.8 [...non -a opts...]:
acl admin {
"6.0.0.0"/8;
}
sub vcl_recv {
if (req.method == "PURGE") {
if (server.ip ~ admin) {
return (purge);
} else {
return (synth(405));
}
}
}
## server.ip + std.port
It's worth mentioning at this point that the VCL IP type (VCL_IP in C)
contains both an IP address and a port number. To my knowledge ACLs
only match the IP address.
This is a variant of the previous use case, where the port is discriminant
instead of the IP address. Once again leaving the hardening as someone
else's problem. Let's say typically a team of network folks (firewall rules
and whatnot).
Example for varnishd -a :80 -a :9080 [...non -a opts...]:
sub vcl_recv {
if (req.method == "PURGE") {
if (std.port(server.ip) == 8080) {
return (purge);
} else {
return (synth(405));
}
}
}
## req.*
You may rely on a cookie-based, token-based or anything coming from
the HTTP request to restrict an operation. VCL provides no specific
construct for that besides access to the headers and pseudo headers of
the request.
sub vcl_recv {
if (req.method == "PURGE") {
if (req.http.some-header == "some psk") {
return (purge);
} else {
return (synth(405));
}
}
}
# A note on PROXY protocol
You may prefer the remote&local variables over client&server depending
on your use case and/or for security reasons.
# IP-less ACLs
Or more accurately transport-independent ACLs, decoupled from IP
addresses or port numbers in VCL. In the case of VIP 17, it would also
be applicable if varnishd were to listen to a unix domain socket.
## How?
Give a name to listen addresses that can be used in VCL to make
decisions.
The name is passed in the -a optarg, ideally with the same syntax as
storage backends:
varnishd -a public=1.2.3.4 -a admin=5.6.7.8
varnishd -a public=:80 -a admin=:9080
You may change how varnishd is deployed without having to change the
VCL, provided that the names remain between varnishd instances (much
like storage backends).
An alternate syntax in case '=' is problematic:
varnishd -a :80,HTTP/1,public -a :9080,HTTP/1,admin
The latter forces you to pick a protocol if you wish to name a listen
address. Addresses not explicitly named are called "a0", "a1" and
so on (like "s0", "s1" etc for storage backends).
A new VCL_LISTEN_ADDRESS [1] type is introduced.
Each listen address has a symbol of this type, mapped to its name from
the command line. A new `local.listen_address` [2] variable is introduced
and can be used for access control.
Example for varnishd -a public=:80 -a admin=:9080 [...non -a opts...]:
sub vcl_recv {
if (req.method == "PURGE") {
if (local.listen_address == admin) {
return (purge);
} else {
return (synth(405));
}
}
}
## Why
The name is abstract and makes the VCL more portable in environments
where varnishd instances are scheduled dynamically with unpredictable
IP addresses or port numbers (without having to pre-process VCL).
Like `storage_hint` vs `storage`, having a type-checked symbol in VCL
removes the risk of not matching the right thing. Did you notice that
both my acl check and port check in the previous example had typos in
them that would _not_ prevent VCL from compiling?
This abstraction is also future proof, as new types of listen
addresses like unix socket domains could also be named and used as
such.
## Testing
Add new macros in varnishtest to make use of named listen addresses.
Example for varnish v1 [...] -start:
${v1_addr} for the first address, same as today
${v1_addr_a0} for the first address [3]
${v1_addr_*} for additional addresses
Possibly a `varnish v1 -addr <name>` used at launch time to easily
bind additional random ports (like -arg or -jail that can only be used
before the manager is started).
# A note on security
This does not improve nor worsen security, AFAICT.
I would argue that a type-checked `local.listen_address` slightly
improves things in this area. Unlike an ACL, it has to hard-match
something from the command line and typos are less likely to occur
(you could always confuse two names but you could also mix up two
ACLs).
Besides this tiny point, transport-independent ACLs only move the
problem outside of Varnish. For example, a compromised host may
perform restricted operations if its IP matches an ACL or if the
compromised host has access to a privileged local.ip and relying on
type-checked names doesn't solve that.
# Closing words
This in essence very similar to the varnish-cli pseudo ACL, sort of
restricting access to the root user with our packages default
configuration.
You could even remove the authorization challenge if the connection
happened on a unix domain socket and ugo permissions were
enough.
Cheers,
Dridi
[1] akin to VCL_STEVEDORE
[2] all names for types or variables are only suggestions
[3] because the -a arg is hard-coded anyway
More information about the varnish-dev
mailing list