...
 
Commits (2)
......@@ -151,6 +151,22 @@ spec:
value:
type: string
minLength: 1
result-header:
type: object
required:
- header
- success
- failure
properties:
header:
type: string
pattern: "^req\\.http\\.[a-zA-Z0-9!#$%&'*+.^_`|~-]+$"
success:
type: string
minLength: 1
failure:
type: string
minLength: 1
vcl:
type: string
minLength: 1
......
......@@ -291,6 +291,10 @@ Optional fields for ``acl`` are:
executed, as detailed below. By default, ``conditions`` is empty,
in which case the match is executed for every client request.
* ``result-header``: specifies a client request header and values to
set for the header when the failure status is or is not invoked for
the ACL evaluation. By default, no header is set.
Each element of the ``addrs`` array may have these fields, of which
``addr`` is required:
......@@ -457,6 +461,26 @@ when the URL begins with "/tea", and the Host header is exactly
value: cafe.example.com
```
The ``result-header`` field specifies a client request header that is
set with a value for the "fail" or "success" results of the ACL
evaluation. If ``result-header`` is present, then all three of its
fields are required:
* ``header``: a string of the form ``req.http.$HEADER``, for the
client request header ``$HEADER``
* ``success``: a string that is assigned to the header if the failure
status is not invoked. So for ``type:whitelist``, the ``success``
string is assigned if the address matches the ACL; for
``type:blacklist``, it is assigned if the address does not match.
* ``failure``: a string that is assigned to the header if the failure
status is invoked.
The ``result-header`` makes it possible to check the result of the ACL
match at a later stage of request processing, for example to implement
logic that depends on the result.
See the [``examples/`` folder](/examples/acl) for working examples
of ACL configurations.
......@@ -543,7 +567,7 @@ Elements of the ``rewrites`` array may have these fields, of which
both backend context. In other words, both of them must begin
with either ``re`` or ``be``.
* ``rules``: an non-empty array of rules specifying conditions under
* ``rules``: a non-empty array of rules specifying conditions under
which a rewrite is executed, and strings used for the rewrites. If
the ``rules`` array is left out, then the rewrite is
unconditional. Each rule is an object with these two fields:
......
......@@ -78,6 +78,10 @@ The next example re-creates the ACL shown as an example in
value: ^/tea(/|$)
- comparand: req.http.Host
value: cafe.example.com
result-header:
header: req.http.X-Tea-Whitelisted
success: "true"
failure: "false"
```
Addresses that match the ACL are:
......@@ -117,10 +121,30 @@ requests are routed to the Service ``tea-svc`` under these
conditions. So ``conditions`` serves to restrict the ACL match to
requests for that Service.
The example also shows the use of the ``result-header`` field to
assign a value to a client request header, depending on the result of
the ACL match. In this case, the client request header
``X-Tea-Whitelisted`` is set to the string "true" if the address from
``X-Real-IP`` matches the ACL -- the string from the ``success`` field
is set when the failure status is not invoked, which in the case of a
whitelist means that the address under consideration matches the
ACL. If the address from ``X-Real-IP`` does not match the ACL, and
hence for a whitelist leads to the failure response, then the string
"false" from the ``failure`` field is assigned to
``X-Tea-Whitelisted``.
The use of ``request-header`` makes it possible to implement logic in
further request processing that depends on the ACL result. In this
case, the request header can be inspected for the result of
whitelisting. If the ``request-header`` field does not appear in the
ACL config, then no header is set as the result of the ACL match.
To verify this configuration with curl, we use the ``-x`` option (or
``--proxy``) set to ``$ADDR:$PORT``, where ``$ADDR`` is the external
address of the cluster, and ``$PORT`` is the port at which requests
are routed to the Ingress.
are routed to the Ingress. We also use ``varnishlog`` on the Pods
implementing the Ingress to verify that the ``X-Tea-Whitelisted``
is set according to the ``result-header`` config.
```
# Requests without an X-Real-IP header fail the ACL match, and get
......@@ -134,6 +158,19 @@ $ curl -v -x $ADDR:$PORT http://cafe.example.com/tea
< HTTP/1.1 403 Forbidden
[...]
# varnishlog shows that X-Tea-Whitelisted was set to false:
* << Request >> 33494
[...]
- ReqHeader Host: cafe.example.com
[...]
- ReqMethod GET
- ReqURL /tea
- ReqProtocol HTTP/1.1
[...]
- ReqHeader X-Tea-Whitelisted: false
[...]
# Request with the X-Real-IP header, but set to an IP that does not
# match the ACL:
$ curl -H 'X-Real-IP: 198.51.100.47' -v -x $ADDR:$PORT http://cafe.example.com/tea
......@@ -147,6 +184,21 @@ $ curl -H 'X-Real-IP: 198.51.100.47' -v -x $ADDR:$PORT http://cafe.example.com/t
< HTTP/1.1 403 Forbidden
[...]
# varnishlog:
* << Request >> 66076
[...]
- ReqHeader Host: cafe.example.com
[...]
- ReqMethod GET
- ReqURL /tea
- ReqProtocol HTTP/1.1
[...]
- ReqHeader X-Real-IP: 198.51.100.47
[...]
- ReqHeader X-Tea-Whitelisted: false
[...]
# Request with an X-Real-IP header that matches the ACL:
$ curl -H 'X-Real-IP: 192.0.2.120' -v -x $ADDR:$PORT http://cafe.example.com/tea
[...]
......@@ -158,6 +210,20 @@ $ curl -H 'X-Real-IP: 192.0.2.120' -v -x $ADDR:$PORT http://cafe.example.com/tea
< HTTP/1.1 200 OK
[...]
# varnishlog shows that X-Tea-Whitelisted was set to true:
* << Request >> 33592
[...]
- ReqHeader Host: cafe.example.com
[...]
- ReqMethod GET
- ReqURL /tea
- ReqProtocol HTTP/1.1
[...]
- ReqHeader X-Real-IP: 192.0.2.120
[...]
- ReqHeader X-Tea-Whitelisted: true
[...]
```
## Blacklist and use of X-Forwarded-For
......@@ -182,6 +248,10 @@ The next example defines a blacklist for the ranges 192.0.20/24 and
- comparand: req.http.Host
compare: equal
value: cafe.example.com
result-header:
header: req.http.X-Coffee-Blacklist
failure: "true"
success: "false"
```
Type ``blacklist`` means that the failure status is returned for IPs
......@@ -216,6 +286,17 @@ one, which restricts ACL matches to URLs beginning with
"/coffee". This is important to the logic of ACL matching -- the match
for the "more specific" URL range is executed first.
The example also shows that the sense of setting the ``result-header``
is reversed for blacklisting. In this case, the client request header
``X-Coffee-Blacklist`` is set to the string "true" if the address from
``X-Forwarded-For`` matches the ACL, since the string from the
``failure`` field is set when the failure status is invoked. For
blacklists, this means that the address under consideration matches
the ACL. If the address from ``X-Forwarded-For`` does not match the
ACL, and hence does not lead to the failure response due to
blacklisting, then the string "false" from the ``success`` field is
assigned to ``X-Coffee-Blacklist``.
Verifying the ACL with curl:
```
......@@ -232,6 +313,19 @@ $ curl -v -x $ADDR:$ADDR http://cafe.example.com/coffee/black
< HTTP/1.1 200 OK
[...]
# varnishlog shows the X-Coffee-Blacklist was set to "false":
* << Request >> 33704
[...]
- ReqHeader Host: cafe.example.com
[...]
- ReqMethod GET
- ReqURL /coffee/black
- ReqProtocol HTTP/1.1
[...]
- ReqHeader X-Coffee-Blacklist: false
[...]
# A request in which the first field of X-Forwarded-For does not match
# the blacklist is not blocked:
$ curl -H 'X-Forwarded-For: 203.0.113.47, 192.0.2.11' -v -x $ADDR:$PORT http://cafe.example.com/coffee/black
......@@ -245,6 +339,21 @@ $ curl -H 'X-Forwarded-For: 203.0.113.47, 192.0.2.11' -v -x $ADDR:$PORT http://c
< HTTP/1.1 200 OK
[...]
# varnishlog:
* << Request >> 33741
[...]
- ReqHeader Host: cafe.example.com
[...]
- ReqMethod GET
- ReqURL /coffee/black
- ReqProtocol HTTP/1.1
[...]
- ReqHeader X-Forwarded-For: 203.0.113.47, 192.0.2.11
[...]
- ReqHeader X-Coffee-Blacklist: false
[...]
# A request in which the first field of X-Forwarded-For matches the
# blacklist is blocked, and the client receives the 404 response
# as specified by fail-status:
......@@ -258,6 +367,20 @@ $ curl -H 'X-Forwarded-For: 192.0.2.11' -v -x $ADDR:$PORT http://cafe.example.co
< HTTP/1.1 404 Not Found
[...]
# varnishlog shows the X-Coffee-Blacklist was set to "true":
* << Request >> 163884
[...]
- ReqHeader Host: cafe.example.com
[...]
- ReqMethod GET
- ReqURL /coffee/black
- ReqProtocol HTTP/1.1
[...]
- ReqHeader X-Forwarded-For: 192.0.2.11
[...]
- ReqHeader X-Coffee-Blacklist: true
[...]
```
## Another use of X-Forwarded-For
......
......@@ -60,6 +60,11 @@ spec:
# - the Host header is exactly equal to "cafe.example.com"
# (According to the Ingress rules of the cafe example, requests
# are routed to the Service tea-svc under these conditions).
#
# The result-header config means that the client request header
# X-Tea-Whitelisted is set to "true" if the address from X-Real-IP
# is on the whitelist (the "success" string is assigned if the
# fail-status is not invoked); set to "false" otherwise.
- name: man-vcl-example
addrs:
- addr: localhost
......@@ -75,6 +80,10 @@ spec:
value: ^/tea(/|$)
- comparand: req.http.Host
value: cafe.example.com
result-header:
header: req.http.X-Tea-Whitelisted
success: "true"
failure: "false"
# This ACL defines a blacklist:
# 192.0.20/24 and 198.51.100.0/24
......@@ -93,6 +102,12 @@ spec:
# The conditions specify that the ACL match is executed when:
# - the URL path begins with "/coffee/black", and
# - the Host header is exactly equal to "cafe.example.com"
#
# The result-header config means that the client request header
# X-Coffee-Blacklist is set to "true" if the address from
# X-Forwarded-For is on the blacklist (the "failure" string is
# assigned if the fail-status is invoked); set to "false"
# otherwise.
- name: xff-first-example
addrs:
- addr: 192.0.2.0
......@@ -109,6 +124,10 @@ spec:
- comparand: req.http.Host
compare: equal
value: cafe.example.com
result-header:
header: req.http.X-Coffee-Blacklist
failure: "true"
success: "false"
# This ACL defines a blacklist for 203.0.113.0/24.
#
......
......@@ -107,12 +107,13 @@ type AuthCondition struct {
// ACLSpec specifies whitelisting or blacklisting IP addresses against
// an access control list.
type ACLSpec struct {
Name string `json:"name,omitempty"`
ACLType ACLType `json:"type,omitempty"`
Comparand string `json:"comparand,omitempty"`
FailStatus *int32 `json:"fail-status,omitempty"`
Addresses []ACLAddress `json:"addrs,omitempty"`
Conditions []Condition `json:"conditions,omitempty"`
Name string `json:"name,omitempty"`
ACLType ACLType `json:"type,omitempty"`
Comparand string `json:"comparand,omitempty"`
ResultHdr *ResultHdrType `json:"result-header,omitempty"`
FailStatus *int32 `json:"fail-status,omitempty"`
Addresses []ACLAddress `json:"addrs,omitempty"`
Conditions []Condition `json:"conditions,omitempty"`
}
// ACLAddress represents an entry in a VCL. If MaskBits is non-nil, it
......@@ -158,6 +159,12 @@ const (
NotMatch = "not-match"
)
type ResultHdrType struct {
Header string `json:"header"`
Success string `json:"success"`
Failure string `json:"failure"`
}
type RewriteRule struct {
Value string `json:"value,omitempty"`
Rewrite string `json:"rewrite,omitempty"`
......
......@@ -58,6 +58,11 @@ func (in *ACLAddress) DeepCopy() *ACLAddress {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ACLSpec) DeepCopyInto(out *ACLSpec) {
*out = *in
if in.ResultHdr != nil {
in, out := &in.ResultHdr, &out.ResultHdr
*out = new(ResultHdrType)
**out = **in
}
if in.FailStatus != nil {
in, out := &in.FailStatus, &out.FailStatus
*out = new(int32)
......@@ -331,6 +336,22 @@ func (in *ProbeSpec) DeepCopy() *ProbeSpec {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResultHdrType) DeepCopyInto(out *ResultHdrType) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResultHdrType.
func (in *ResultHdrType) DeepCopy() *ResultHdrType {
if in == nil {
return nil
}
out := new(ResultHdrType)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RewriteRule) DeepCopyInto(out *RewriteRule) {
*out = *in
return
......
......@@ -443,6 +443,13 @@ func (worker *NamespaceWorker) configACL(spec *vcl.Spec,
}
vclACL.Conditions[j] = vclMatch
}
if acl.ResultHdr != nil {
worker.log.Debugf("ACL %s: ResultHdr=%+v", acl.Name,
*acl.ResultHdr)
vclACL.ResultHdr.Header = acl.ResultHdr.Header
vclACL.ResultHdr.Success = acl.ResultHdr.Success
vclACL.ResultHdr.Failure = acl.ResultHdr.Failure
}
worker.log.Debugf("VarnishConfig %s/%s add VCL ACL config: "+
"%+v", vcfg.Namespace, vcfg.Name, vclACL)
spec.ACLs[i] = vclACL
......
......@@ -20,7 +20,15 @@ sub vcl_recv {
{{- end}}
{{aclCmp .Comparand}} {{if .Whitelist}}!{{end}}~ {{aclName .Name}}
) {
{{- if .ResultHdr.Header}}
set {{.ResultHdr.Header}} = "{{.ResultHdr.Failure}}";
{{- end}}
return(synth({{.FailStatus}}));
}
{{- if .ResultHdr.Header}}
else {
set {{.ResultHdr.Header}} = "{{.ResultHdr.Success}}";
}
{{- end}}
{{- end}}
}
......@@ -364,6 +364,18 @@ func (a byComparand) Less(i, j int) bool {
return a[i].Comparand < a[j].Comparand
}
type ResultHdrType struct {
Header string
Success string
Failure string
}
func (resultHdr ResultHdrType) hash(hash hash.Hash) {
hash.Write([]byte(resultHdr.Header))
hash.Write([]byte(resultHdr.Success))
hash.Write([]byte(resultHdr.Failure))
}
// ACL represents an Access Control List, derived from a
// VarnishConfig.
type ACL struct {
......@@ -373,6 +385,7 @@ type ACL struct {
Whitelist bool
Addresses []ACLAddress
Conditions []MatchTerm
ResultHdr ResultHdrType
}
func (acl ACL) hash(hash hash.Hash) {
......@@ -387,6 +400,7 @@ func (acl ACL) hash(hash hash.Hash) {
for _, cond := range acl.Conditions {
cond.hash(hash)
}
acl.ResultHdr.hash(hash)
}
// interface for sorting []ACL
......
import std;
acl vk8s_man_vcl_example_acl {
"localhost";
"192.0.2.0"/24;
! "192.0.2.23";
}
sub vcl_recv {
if (
client.ip !~ vk8s_man_vcl_example_acl
) {
set req.http.ACL-Whitelist = "fail";
return(synth(403));
}
else {
set req.http.ACL-Whitelist = "pass";
}
}
......@@ -472,6 +472,57 @@ func TestAclTemplate(t *testing.T) {
}
}
var aclResultHdr = Spec{
ACLs: []ACL{{
Name: "man_vcl_example",
Comparand: "client.ip",
FailStatus: 403,
Whitelist: true,
Addresses: []ACLAddress{
ACLAddress{
Addr: "localhost",
MaskBits: 255,
Negate: false,
},
ACLAddress{
Addr: "192.0.2.0",
MaskBits: 24,
Negate: false,
},
ACLAddress{
Addr: "192.0.2.23",
MaskBits: 255,
Negate: true,
},
},
ResultHdr: ResultHdrType{
Header: "req.http.ACL-Whitelist",
Success: "pass",
Failure: "fail",
},
}},
}
func TestAclResultHeader(t *testing.T) {
var buf bytes.Buffer
gold := "acl_result_hdr.golden"
if err := aclTmpl.Execute(&buf, aclResultHdr); err != nil {
t.Error("acls template Execute():", err)
return
}
ok, err := cmpGold(buf.Bytes(), gold)
if err != nil {
t.Fatalf("Reading %s: %v", gold, err)
}
if !ok {
t.Errorf("Generated VCL does not match gold file: %s", gold)
if testing.Verbose() {
t.Logf("Generated: %s", buf.String())
}
}
}
var customVCLSpec = Spec{
DefaultService: Service{},
Rules: []Rule{{
......