Rule progamming examples

This chapter deals with rules, and how they can be used to change requests and responses. The focus in this chapter is on real-world situations that are applicable to many Yxorp users. The examples given are only parts of a complete configuration; in most cases, an example of a rule is shown, to illustrate the subject of the paragraph.

If you run Yxorp as a security device, these rules may however not be complete; you should not just copy them and think your web server is secure. Remember, a reverse proxy is a complicated thing, and a minor mistake in the configuration may decrease your security instead of increasing it. Always be sure that you understand what you're dealing with, read up on RFC's and other documentation, make traces to see what is happening on the wire.

Enforcing SSL for certain requests

If you have a webserver with some unimportant content that you want to allow anyone access to (for instance, pictures of your pets), and also some restricted content (like your webmail application) you may want to make sure that logging in to your webmail server is only possible if SSL is used on the session - to to make sure your userid and password will be encrypted on the Internet. This can be done with a rule as follows:

<rule id="rule1" type="request">

<![CDATA[
if (uri -/^\/webmail/) {        // if uri starts with “webmail” in lower case
   if (!issslsession()) {
      reject(“Sorry, you're not using SSL”);
   }
}
]]>

</rule>

A more userfriendly way is also possible, in which the client is automatically redirected to the SSL variant of the URL that was attempted:

<rule id="rule1" type="request">

<![CDATA[
if (uri -/^\/webmail/) {        // if uri starts with “webmail” in lower case
   if (!issslsession()) {
      redirect(concat(“https://”, “www.yourdomain.whereever”, uri));
   }
}
]]>

</rule>

SSL to backend servers

Normally, Yxorp will use HTTP to backend servers (i.e. the servers that Yxorp is reverse-proxying for). In some cases however, it is desirable to connect to a backend server (i.e. a server that Yxorp is reverse-proxying for) using SSL.

To do this, some extra definitions are needed for the realserver objects, and a request rule must be used to enable SSL on the backend connection for the requests.

If your realserver definition is like:

<realserver id="sf" ip="192.0.2.1" port=”80” inservice="yes" />

change it to include the sslport tag as follows:

<realserver id="sf" ip="192.0.2.1" port=”80” sslport=”443” inservice="yes" />

Then, in your request rule, add a call to the function “setsslbackend()” as in the following example:

<rule id="rule1" type="request">

<![CDATA[

... code to determine if you want to reject the request straight away

if (... some code to see if you want to enable ssl backend encryption for this request) {
   setsslbackend();
}

]]>

</rule>

In the current versions, Yxorp does not by default check if the certificate that the backend server presents is valid. That means it could be a self-signed certificate, or it could be out of date, it could be issued for another domain name, etc. You could be accepting a certificate that is forged to make you think you are connecting to a verified site, while instead you are connecting to something else. If you are not in complete control of the server(s), use the isscertverified() function to verify the server certificate, and read the Advanced Concepts chapter on SSL processing.

Note that the setsslbackend function must be called for each request that you want to use backend SSL for.



Filtering on client IP address, range, or domain name

To allow access to some set of web resources based on the IP address of the client, use one of the examples below as a starting point.

Filtering on a single IP address

It is possible to filter on a single IP address as follows:

<rule id="rule1" type="request">

<![CDATA[

if (uri -/some part of the uri you want to filter on/) {
   if (!(getclientip() -/^127\.0\.0\.1$/)) {
      reject(“sorry, you don't have the right IP address”);
   }
}

]]>

</rule>

Note that the IP address is returned from the getclientip() function in its normalized textual form for the protocol in use (IPv4 or IPv6). In the example, the local host would have the address 127.0.0.1 for IPv4, but in IPv6 ::1, and ::ffff:127.0.0.1 may both occur.

Filtering on an IP range

It is possible to filter on a range of IP addresses as follows:

<rule id="rule1" type="request">

<![CDATA[

if (uri -/some part of the uri you want to filter on/) {
   if (!(clientinrange(“192.0.2.0/24”))) {
      reject(“sorry, you don't have the right IP address”);
   }
}

]]>

</rule>

The clientinrange() function determines if the client IP address is in the given range. The function can only process IPv4 ranges, however, there is an equivalent IPv6 function called clientinip6range().

Filtering on domain names

Filtering on client domain names can be done as well. The next example shows how to filter on the last two parts of the domain name:

<rule id="rule1" type="request">

<![CDATA[

if (uri -/some part of the uri you want to filter on/) {
   if (!(getclientdomainname() -/.*example\.com$/)) {
      reject(“sorry, you don't have the right domain name”);
   }
}

]]>

</rule>

The getclientdomainname() function returns the reverse DNS lookup for the client IP address. Note that in some cases, the reverse lookup of an address gives a different name than a normal lookup. Also, because Yxorp does not normally resolve the names of clients, using this function may impact performance.

Using black-, white- and greylisting

In filtering requests, a blacklist is used to specify what is not allowed. Everything that is not in the blacklist is allowed. In contrast, a whitelist specifies what is allowed; if something is not explicitly in the whitelist, it is not allowed.

Greylisting is a less well defined term, that has lately been associated with a technique for spam filtering. In the Yxorp context, we will use the term to define either black- or whitelists with exceptions for subsets of a list case.

Blacklisting

A typical blacklist rule is shown in the following example:



<rule id="rule1" type="request">

<![CDATA[

blacklist uri {
   -/\.exe/
   -/\.dll/
   -/^\/cgi-bin/
} if-failed {
   reject(“sorry, you did not pass the blacklist on this server”);
}

]]>

</rule>

In the example above, all requests containing the regular expressions anywhere in the URI will be rejected., except for the cgi-bin entry, which is anchored to the beginning of the URI.

In general, build your filters to anchor from the start of the URI; otherwise they may be too general (like the .exe and .dll patterns above). Don't anchor patterns only at the end, because webservers will accept several kinds of “parameters” to be passed in an URI (like: “http://localhost/?blablabla”, “http://localhost/#somereference” etc); you could use several constructions in your regular expressions to match in the desired way, but this may prove to be tricky to get right. Alternatively, an easy way out is to use one of the functions in the rule language to strip unwanted parameters off the URI before matching against the white- or blacklist.

Whitelisting

A typical whitelist rule is shown in the following example:

<rule id="rule1" type="request">

<![CDATA[

whitelist uri {
   -/^\/$/
   -/^\/index\.html$/
   -/^\/pictures/\.*\.jpg$/             
   -/^\/htmlfiles\/.*\.html$/
} if-failed {
   reject(“sorry, you did not pass the whitelist on this server”);
}

]]>

</rule>

In the example above, all requests not containing one of the regular expressions will be rejected. Note that the expressions are anchored at the beginning of the URI, to avoid the issues with parameters described above. Still, the URI “http://localhost/pictures/hacker.dll?whatever.jpg” would pass this whitelist.

So, how can you make a secure filter? Consider all of these measures:

  1. Be careful about what is on your web servers, where it is, and what it is. If you don't need it, it should not be on the server. Especially take care that updates, fixes, and patches do not (re-)install default content, samples, etc.

  2. Filter first using a blacklist, and in this way exclude all URI's that you do not want to make accessible. Things like executables for active content (cgi-bin, .php, .dll etc) can easily be excluded in this way.

  3. Then use a whitelist, and specify what you do want to make accessible. The whitelist is the most tricky part, because an expression that allows too much will give access to more resources than it should.

  4. Test the filter to see if it does what you expect!

Greylisting

Most blacklists and whitelists have a disadvantage: The filtering they provide is in some cases too black-and-white (pun intended... I mean too coarse-grained). For instance, in the whitelist example above, all files in the pictures directory are accessible. What if you have several directories there, but want to exclude one from the whitelist? This is where greylisting comes in: a whitelist (or blacklist) with exceptions. The following example shows this:

<rule id="rule1" type="request">

<![CDATA[

whitelist uri {
   -/^\/$/
   -/^\/index\.html$/
   -/^\/pictures\/\.*\.jpg$/ : if (uri -/^\/pictures\/secret/)) continue-list;
   -/^\/htmlfiles\/.*\.html$/
} if-failed {
   reject(“sorry, you did not pass the whitelist on this server”);
}

]]>

</rule>

The if-statement following the pictures regexp in this example examines the URI more closely; if the regexp in the if-statement matches, the continue-list statement causes the earlier match to be ignored.

This example uses a simple if-statement, however, all normal statements can be used within a white- or blacklist in this way. It is even possible to nest white- and blacklists, as shown in the following example:

<rule id="rule1" type="request">

<![CDATA[

whitelist uri {
   -/^\/$/
   -/^\/index\.html$/
   -/^\/pictures\/\.*\.jpg$/ : blacklist uri {
                                  -/^\.pictures\/secret/
                               } if-failed {
                                  reject(“sorry, your request is an exception to the whitelist”);
                               }
   -/^\/htmlfiles\/.*\.html$/
} if-failed {
   reject(“sorry, you did not pass the whitelist on this server”);
}

]]>

</rule>



Using an application to authenticate access to other URI's

Situation

Consider the following situation: You have an existing webmail program on your server (we'll use Squirrelmail in this example). You also have some other stuff on your server, that you do not want everyone to be able to see. Thirdly, you also have public content that anyone may see.

What Yxorp can do with this is using the login to Squirrelmail to authenticate users for the private pages; so, the private pages would only be accessible after a user has successfully logged in to Squirrelmail.

So, how does this work? Squirrelmail has a login page on which a user types userid and password. If the user clicks the login button (or presses enter), the browser will post the content (userid & password) to another URI, which is a Squirrelmail page that will verify userid and password. If successful, Squirrelmail will send a 302 “Found” status, with a redirect to the main webmail page, and set an authentication cookie (“key”). If unsuccesful, Squirrelmail will send a redirect to a page with a message that userid and password were invalid.

Yxorp needs to do the following to be able to use this:

  1. Track the client, i.e. setup a state cookie.

  2. Allow free access to the Squirrelmail login pages.

  3. Monitor the returned traffic from Squirrelmail to see if the authentication cookie is present

  4. Allow access to other pages based on if the authentication cookie was seen previously.

Note that the authentication cookie will only be sent once, as the result of the post. So, Yxorp must remember it. To do so, we will use the client state variable feature in Yxorp.

Example

The rule that processes requests coming in from clients is as follows:

<rule id="rule1" type="request">

// these uri will be served without authentication

whitelist uri {
   -/^/squirrelmail\/src\/login\.php/          // login page
   -/^/squirrelmail\/images\/sm_logo\.png/     // logo on login page
   -/^/squirrelmail\/src\/redirect\.php/)      // this processes the login

// add other public uri here

} if-failed {
   if (!getclientstatedvar("authenticated")) {      
      redirect("http://www.example.com/your-home-page");
      return;
   }
}

</rule>

In this example, note that the regexp patterns for the URI's are anchored at the start to prevent accidentally matching something else. The final if-statement in the example shows what to do if we have not (yet) seen a valid login; the most user-friendly action here is to send a redirect to your homepage or something, but in general you should not do this as a default action for all requests to your website, because this may give out unnecessary information to hackers. Since anything that is handled here should be a request to something the user is not authorized for, it may be a better idea to reject the request outright.

The second rule we need is the rule that processes the response coming back from the server. It is shown in the following example:

<rule id="rule2" type="response">
<![CDATA[
foreach i (enumerate_dupvar("Set_Cookie:")) {
   if (statuscode==302 && $i -/key=/) {
      if (uri -/^/squirrelmail\/src\/redirect\.php/) {
         trace("authenticated");
         setclientstatedvar("authenticated", "true");
      }
   } else if ($i -/key=deleted/) {
      trace("authentication cancelled");
      setclientstatedvar("authenticated", "");
   }
}
]]>
</rule>

In this rule example, note the use of duplicate variables and the foreach statement to process all of the variables in a dupvar group. This is necessary because more than one Set_Cookie header may be present in the response from the server, and the one we need (the one that sets “key=”) may not be the first one.

Also note the check for statuscode=302 (“Found”), which is the normal result from a POST request. In contrast, the else part of the if statement does not check for a statuscode, but just for the presence of a Set_Cookie header with a value of “key=deleted”. This is what happens if the user clicks the logout link within Squirrelmail; in this way the key value for the authentication is removed from the browser.

Especially note the explicit check for the URI causing the 302. If it was omitted, any active resource on the server could cause a 302 status code and a Set_Cookie, thereby potentially bypassing the userid and password check that we assume Squirrelmail to do for us.

Finally, the call setclientstatedvar(“authenticated”, ...) is used to set a client state variable called “authenticated” to the appropriate value. This is the variable we used in the request rule to determine if the request to the private pages are allowed.

Conclusion

So, what we have accomplished is a “login” to some arbitrary pages containing private content, without changing anything on the server. If the rules are carefully coded, and the application you use is solid, this can be a very secure way to control access.

However, you should understand that even though the authentication is performed, Yxorp does not know which user is logged on. As such, this example may be used to grant access to users, but it can not be used to give different access to different users.