Showing posts with label security. Show all posts
Showing posts with label security. Show all posts

Wednesday, October 4, 2023

LXD Containers and FIDO Security Keys

With the rise of WebAuthn, I've had to figure out how expose my various FIDO security keys (YubiKey, Nitrokey, OnlyKey, SoloKeys, etc) to the LXD containers I use for web browsers.

The core of the solution is to expose the HIDRAW device that the security key is using to the LXD container — and to configure the device in the container to be owned by the user account who will use it. If you only have one such key plugged in, it's most likely using the /dev/hidraw0 device; and usually it's user 1000 who needs to use it. An LXD profile entry like the following allows such access:

config: {}
description: exposes FIDO devices
devices:
  hidraw0:
    required: false
    source: /dev/hidraw0
    type: unix-char
    uid: "1000"
name: fido
used_by: []

A profile like this can be created, configured, and applied to a container with the following commands:

$ lxc profile create fido
Profile fido created
$ lxc profile device add fido hidraw0 unix-char required=false source=/dev/hidraw0 uid=1000
$ lxc profile add mycontainer fido
Profile fido added to mycontainer

However, the exact HIDRAW device number that a particular security key uses is not stable, and may vary as you plug and unplug various keys (or other USB or Bluetooth devices). How do you tell which HIDRAW device is being used by a particular physical device? The simplest way is to print out the content of the uevent pseudo file in the sysfs filesystem corresponding to each HIDRAW device until you find the one you want. For example, this is what the entry for one of my SoloKeys looks like, at hidraw11:

$ cat /sys/class/hidraw/hidraw11/device/uevent
DRIVER=hid-generic
HID_ID=0003:00001209:0000BEEE
HID_NAME=SoloKeys Solo 2 Security Key
HID_PHYS=usb-0000:00:14.0-4/input1
HID_UNIQ=1234567890ABCDEF1234567890ABCDEF
MODALIAS=hid:b0003g0001v00001209p0000BEEE

You can also get similar information — without the specific device name, but with the general type of device, like FIDO_TOKEN — from the udevadm command:

$ udevadm info /dev/hidraw11
P: /devices/pci0000:00/0000:00:24.0/usb1/2-4/2-4:1.4/0003:1209:BEEE.0022/hidraw/hidraw11
N: hidraw11
L: 0
E: DEVPATH=/devices/pci0000:00/0000:00:24.0/usb1/2-4/2-4:1.4/0003:1209:BEEE.0022/hidraw/hidraw11
E: DEVNAME=/dev/hidraw11
E: MAJOR=232
E: MINOR=12
E: SUBSYSTEM=hidraw
E: USEC_INITIALIZED=123456789010
E: ID_FIDO_TOKEN=1
E: ID_SECURITY_TOKEN=1
E: ID_PATH=pci-0000:00:24.0-usb-0:4:1.4
E: ID_PATH_TAG=pci-0000_00_24_0-usb-0_4_1_4
E: ID_FOR_SEAT=hidraw-pci-0000_00_24_0-usb-0_4_1_4
E: TAGS=:uaccess:seat:snap_firefox_geckodriver:security-device:snap_firefox_firefox:
E: CURRENT_TAGS=:uaccess:seat:snap_firefox_geckodriver:security-device:snap_firefox_firefox:

Using the udevadm info and lxc profile device list and commands, you can write a simple script that checks each /dev/hidraw* device on your host system against the HIDRAW devices registered for a particular LXD profile, and add or remove HIDRAW devices dynamically to that profile to match the current FIDO devices you have plugged in. Here's such a script:

#!/bin/sh -eu
profile=${1:-fido}
existing=$(lxc profile device list $profile)

for dev_path in /dev/hidraw*; do
    dev_name=$(basename $dev_path)
    if udevadm info $dev_path | grep FIDO >/dev/null; then
        if ! echo "$existing" | egrep '^'$dev_name'$' >/dev/null; then
            lxc profile device add $profile $dev_name \
                unix-char required=false source=$dev_path uid=1000
        fi
    else
        if echo "$existing" | egrep '^'$dev_name'$' >/dev/null; then
            lxc profile device remove $profile $dev_name
        fi
    fi
done

echo done

You can run the script manually every time you plug in a new security key, to make sure the security key is registered at the right HIDRAW slot in your LXD profile — or you can add a custom udev rule file to run it automatically.

If you save the above script as /usr/local/bin/add-fido-hidraw-devices-to-lxc-profile.sh, you can then add the below file as /etc/udev/rules.d/75-fido.rules (replacing justin with the username of your daily user) to automatically run the script for several different brands of FIDO security keys:

# Nitrokey 3
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{idVendor}=="20a0", ATTRS{idProduct}=="42b2", RUN+="/bin/su justin -c /usr/local/bin/add-fido-hidraw-devices-to-lxc-profile.sh"
# OnlyKey
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="60fc", RUN+="/bin/su justin -c /usr/local/bin/add-fido-hidraw-devices-to-lxc-profile.sh"
# SoloKeys
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="5070|50b0|beee", RUN+="/bin/su justin -c /usr/local/bin/add-fido-hidraw-devices-to-lxc-profile.sh"
# Yubico YubiKey
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0113|0114|0115|0116|0120|0121|0200|0402|0403|0406|0407|0410", RUN+="/bin/su justin -c /usr/local/bin/add-fido-hidraw-devices-to-lxc-profile.sh"

Run the sudo udevadm control --reload-rules and sudo udevadm trigger commands to reload your udev rule files and trigger them for your currently plugged-in devices. If you use a different brand of security key, you can probably find its vendor and product IDs in the libfido2 udev rules file (or you can figure it out from the output of the udevadm info command).

Sunday, November 1, 2015

Sandboxing Firefox with Firejail

I typically run about five or six different instances of Firefox with different profiles for different tasks or groups of websites (like one for dev work, one for my "daily driver", one for financial accounts, one for each organization I work for, etc). I've started using Firejail to better isolate each instance — not only from each other, but from the rest of my system.

Separate Home Directories

To run each profile with a separate home directory, I first created a new ~/fj dir, with a separate directory in it for each Firefox profile (like ~/fj/ff-dev, ~/fj/ff-company-x, etc). Then I moved the existing profile for each into its own .mozilla/firefox sub-directory (like ~/fj/ff-dev/.mozilla/firefox/abc123.dev), and added a single-profile profiles.ini into the same dir, containing just the entry for the single profile:

[General]
StartWithLastProfile=1

[Profile0]
Name=dev
IsRelative=1
Path=abc123.dev

Then I adjusted my startup script for each profile to use Firejail with the separate home dir:

#!/bin/sh
firejail --private=~/fj/ff-dev firefox

Minimum Filesystem Access

I've also been experimenting with custom Firejail profiles to give Firefox just the minimum access to the filesystem it needs to work. I've found that the following profile (saved as ~/.config/firejail/firefox.profile) on Ubuntu 15.04 enables Firefox to use the system's fonts (private-etc fonts), timezone settings (private-etc localtime), and DNS (private-etc resolve.conf and noblacklist /run/resolveconf); my custom DNS overrides from etc/hosts (private-etc hosts,nsswitch.conf); and DRM flash videos from Hulu, Amazon, etc (private-etc alternatives and noblacklist /run/dbus,/var/cache/hald):

noblacklist /run/dbus
noblacklist /run/resolvconf
noblacklist /run/user
noblacklist /var/cache/hald
noblacklist /var/run
blacklist /boot
blacklist /cdrom
blacklist /lost+found
blacklist /media
blacklist /mnt
blacklist /opt
blacklist /proc
blacklist /run/*
blacklist /sbin
blacklist /srv
blacklist /sys
blacklist /usr/sbin
blacklist /var/*
private-dev
private-etc alternatives,firefox,fonts,hosts,localtime,nsswitch.conf,resolv.conf
read-only /bin
read-only /lib
read-only /lib64
read-only /usr
tmpfs /tmp
caps.drop all
seccomp
netfilter
noroot

I've also built a similar custom Firejail profile for Chrome (saved as ~/.config/firejail/google-chrome.profile) which allows for the same (needing a little less access to run its own built-in version of flash):

noblacklist /opt/google
noblacklist /run/resolvconf
noblacklist /run/user
blacklist /boot
blacklist /cdrom
blacklist /lost+found
blacklist /media
blacklist /mnt
blacklist /opt/*
blacklist /proc
blacklist /run/*
blacklist /sbin
blacklist /srv
blacklist /sys
blacklist /usr/sbin
blacklist /var
private-dev
private-etc alternatives,chromium-browser,fonts,hosts,localtime,nsswitch.conf,resolv.conf
read-only /bin
read-only /lib
read-only /lib64
read-only /usr
tmpfs /tmp
caps.drop all
seccomp
netfilter
noroot

Sunday, May 3, 2015

Blocking Attacks on Apache Behind a Load Balancer

OSSEC, a host-based intrusion detection system, comes with a lot of defenses set up and running right out of the box, including detecting malicious web requests and blocking them with firewall rules. This works really well for web servers not running behind load balancers, or when running OSSEC on the load balancers themselves, but doesn't work to actually block the attacks when your web servers are behind load balancers that aren't running OSSEC (for example, when using AWS ELB).

Fortunately, OSSEC has all the infrastructure needed to make it work — you just need to create a custom "active response" that updates your web server's configuration with the IP addresses to block. This is what we've done to make our Apache 2.4 servers block IP addresses as directed by OSSEC:

1. Log client IPs, not LB IPs

If you're running Apache 2.4 behind a load balancer, you should install mod_remoteip (there's also a backport of mod_remoteip for Apache 2.2 available that you can compile yourself). Make sure you register the header used by your load balancer to indicate the originating client IP address via the RemoteIPHeader directive, and the IP addresses (or blocks) used by your load balancer via the RemoteIPInternalProxy directive. Put these directives either in your base Apache config, or in your <VirtualHost> blocks. You'll also want to adjust any of the standard log formats you use (as well your own custom log formats) to replace %h with %a:

# name of the header from your load balancer
# that contains the originating client IP address
RemoteIPHeader X-Forwarded-For
# IP address or block of your load balancer
# (from the perspective of your back-end servers)
RemoteIPInternalProxy 10.0.0.1/24

# standard log file formats rewritten with %a in place of %h
LogFormat "%v:%p %a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%a %l %u %t \"%r\" %>s %O" common

After installing mod_remoteip and updating your Apache config, restart Apache and try accessing your server through the load balancer. You should now see your client IP address in the Apache access logs where your load balancer IP address used to be. This is a prerequisite for blocking web attacks with OSSEC — if your load balancer IP is in the logs instead of the originating client's IP, you'll end up blocking all access from your load balancer instead of just from the attacker!

2. Configure Apache to use a block list

Next, make sure you have mod_authz_core and mod_authz_host installed (which are usually installed by default) so that you can use them to block access to specific IP addresses in your Apache config. What we're going to do is have OSSEC manage a list of blocked IP addresses by writing them to a specific directory, one IP address to a file, in a format mod_authz_host interprets as denying access to the IP address.

If you've used earlier versions of Apache, you're probably used to access-control settings like this:

Order deny,allow
Deny from 1.2.3.4

In Apache 2.4, however, you instead use the Require directive for access control (optionally nested inside <RequireAll>, <RequireAny>, and <RequireNone> blocks to handle AND, OR, and NOT logic). So after upgrading Apache, you'd replace the above with this:

Require not ip 1.2.3.4

So for each IP address OSSEC wants to block, we'll have it write out Require not ip [the IP address] to a file, and keep all those block files in a specific directory. I chose /etc/apache2/authz for that directory, but it can be anywhere you want. We can source the files from that directory into our Apache config via the IncludeOptional directive.

So create that directory (as /etc/apache2/authz or your own preferred location), and then source it in beside your other auth directives in your Apache config. Since we're using negated auth directives in the sourced files (Require not ip 1.2.3.4), make sure you wrap all auth directives in your Apache config with a <RequireAll> block — outside of <RequireAll>, <RequireAny>, and <RequireNone> blocks, auth directives are combined using <RequireAny> semantics, rendering any negated auth directives moot.

Here is a simple example you might use in a <VirtualHost> block, allowing all GET, HEAD, and POST requests except those from the IP addresses included in our sourced directory:

DocumentRoot /srv/www
<Directory />
    # all included directives must pass
    <RequireAll>
        # allow only GET (and HEAD) and POST requests
        Require method GET POST
        # source additional directives from /etc/apache2/authz directory
        IncludeOptional authz/*.conf
    </RequireAll>
</Directory>

If you update your config with something like the above, and restart Apache, you should still be able to access your site as normal. But if you add a test.conf file to your /etc/apache2/authz directory with the following (replacing 1.2.3.4 with your own IP address), and restart Apache, Apache should now respond to you with a 403:

Require not ip 1.2.3.4

3. Create an OSSEC "active response" script

OSSEC comes with a few built-in active-response scripts, including the ability to add an offending IP address to the /etc/hosts.deny file, and to add the offending IP address to a firewall drop rule. It's easy to create custom active-response scripts, however; add this custom script as /var/ossec/active-response/bin/apache-deny.sh (or in the active-response/bin directory of wherever you installed OSSEC):

#!/bin/sh -e
# add/delete ip to/from apache authz deny list
# apache-deny.sh add - 1.2.3.4 1234567890.123456 1234

SCRIPT=$0
ACTION=$1
USER=$2
IP=$3
TIMESTAMP=$4
RULE=$5

# directory in which to add/delete authz entries
AUTHZ_DIR=/etc/apache2/authz
# assume /var/ossec/active-response/bin/apache-deny.sh
OSSEC_DIR=$(dirname $SCRIPT)/../..
# file to log activity
LOG_FILE=$OSSEC_DIR/logs/active-responses.log

log() {
    MSG="$(date) $SCRIPT $1"
    if [ "$ACTION" = "test" ]; then
        echo "$MSG"
    else
        echo "$MSG" >> $LOG_FILE
    fi
}

# log invocation of this script
log "$1 $2 $3 $4 $5"

# use python to validate legal ip address
IP_TYPE=$(cat <<EOF | python -
import socket
try:
    socket.inet_aton('$IP')
    print 'ipv4'
except socket.error:
    try:
        socket.inet_pton(socket.AF_INET6, '$IP')
        print 'ipv6'
    except socket.error:
        print ''
EOF
)

if [ "$IP_TYPE" = "" ]; then
    log "invalid ip $IP"
    exit 1
fi

# create safe name for authz deny file
AUTHZ_FILE="deny-$(echo -n "$IP" | tr -c '[:alnum:]' '-').conf"

case "$ACTION" in
    # add authz deny file
    add)
        echo "Require not ip $IP" > $AUTHZ_DIR/$AUTHZ_FILE
        service apache2 reload
        ;;
    # delete authz deny file
    delete)
        rm -f $AUTHZ_DIR/$AUTHZ_FILE
        service apache2 reload
        ;;
    # display test string
    test)
        echo "Require not ip $IP > $AUTHZ_DIR/$AUTHZ_FILE"
        ;;
    *)
        log "unknown action $ACTION"
        exit 1
esac

If you're using a directory other than /etc/apache2/authz for your block lists, replace the AUTHZ_DIR variable's value in the script with your custom directory path. Try it out by running the following command:

sudo /var/ossec/active-response/bin/apache-deny.sh add - 1.2.3.4

This should add a file named deny-1-2-3-4.conf to your /etc/apache2/authz directory with the following content:

Require not ip 1.2.3.4

The command also reloads your Apache config, so Apache will start acting on the newly added directive right way (you will need to adjust the script if your system has a different command for reloading Apache other than service apache2 reload). And if you run the command with the delete action, it will delete the same file:

sudo /var/ossec/active-response/bin/apache-deny.sh delete - 1.2.3.4

4. Configure OSSEC to invoke an active response

Now that Apache is configured to use the block list, and the apache-deny.sh script is in place to add IP addresses to and remove them from the list programmatically, we can configure OSSEC to trigger the apache-deny.sh script automatically whenever an existing OSSEC rule fires an alert at or above a certain level of importance. To do so, add two entries to your ossec.conf configuration file (typically located at /var/ossec/etc/ossec.conf).

The first entry registers our apache-deny.sh script as a command called apache-deny (and indicates it applies only to alerts that include a source IP). Insert it after the other <command> entries in your ossec.conf file:

  <command>
    <name>apache-deny</name>
    <executable>apache-deny.sh</executable>
    <expect>srcip</expect>
    <timeout_allowed>yes</timeout_allowed>
  </command>

The second entry configures OSSEC to execute the apache-deny command for all alerts at or above alert level 6. OSSEC has a bunch of built-in rules for general web attacks and general webapp security (as well as for common servers like Apache and Ngnix, and common platforms like WordPress, etc), so while you can tune the level setting to your own preferences, setting it to 6 will block anyone trying out common web-app vulnerabilities on your server. Insert it after the other <active-response> entries in your ossec.conf file:

  <active-response>
    <command>apache-deny</command>
    <location>local</location>
    <level>6</level>
    <timeout>600</timeout>
  </active-response>

The <timeout>600</timeout> setting above will direct OSSEC to use the same apache-deny command to remove an IP address from the block list after 600 seconds (10 minutes) has elapsed since adding the IP to the block list. You may also want to add a repeated_offenders entry to your ossec.conf, to extend the timeout for repeat offenders. Insert the following after the other <active-response> entries to extend the timeouts by 30 minutes, then 60 minutes, and finally 120 minutes:

<active-response><repeated_offenders>30,60,120</repeated_offenders></active-response>

One other thing you probably should configure in your ossec.conf, just to be safe, is to whitelist your load balancer IPs. Make sure you have a <white_list> entry in the <global> section of your ossec.conf that matches the RemoteIPInternalProxy setting you added to your Apache config in step 1:

<white_list>10.0.0.1/24</white_list>

Restart OSSEC, and then test it out by trying a URL for which OSSEC alerts automatically. Make sure you test from a box whose IP address is not in your OSSEC whitelist — otherwise you won't see OSSEC do anything. Here's an easy URL to test (replacing www.example.com with your load balancer's public-facing name):

curl -I 'https://www.example.com/?cmd.exe'

After trying that once (and giving OSSEC a second or two to do its thing), any further HTTP requests you make from that box for the next 10 minutes should result in a 403 error page from Apache. If that doesn't happen, look in OSSEC's main log file (/var/ossec/logs/ossec.log) and active-response log (/var/ossec/logs/active-responses.log) to check for errors executing the apache-deny active-response. If everything's working, there shouldn't be any messages about the apache-deny script in the main OSSEC log; but there should be an entry in the active-respone log that looks like the following:

Thu Jan  1 01:23:45 UTC 2015 /var/ossec/active-response/bin/apache-deny.sh - 1.2.3.4 1234567890.1234 31104

The last three fields in the log will be the IP address blocked, the unix timestamp, and the OSSEC rule that triggered the block.

5. Optionally add your own OSSEC rules

Simply using the out-of-the-box OSSEC rules is perfect for blocking all those run-of-the-mill bot drive-bys you see all the time from scanning your web site for common vulnerabilities. However, you can also add custom rules to direct OSSEC to detect potential attacks that are specific to your site, and either alert you, or block them automatically (or both).

One thing in particular you might do is add some rules that block simple/unintentional DOS attacks (especially if your site has certain pages that are especially vulnerable to DOS attacks, like with slow back-end queries, etc). Here is an example of a set of rules you might add to your /var/ossec/rules/local_rules.xml file to detect and block simple DOS attacks (in the form of an unusually large number of web requests from the same IP address in a short amount of time) site-wide, with more sensitive rules for more-sensitive URL paths:

  <rule id="900000" level="1">
    <if_sid>31100,31108,31101,31120</if_sid>
    <description>Any web request.</description>
  </rule>

  <rule id="900001" level="0">
    <if_sid>900000</if_sid>
    <url>^/css|^/img|^/js</url>
    <description>Static css/img/js request.</description>
  </rule>

  <rule id="900002" level="1">
    <if_sid>900000</if_sid>
    <url>^/foo/special|^/baz</url>
    <description>Special foo or baz request.</description>
  </rule>

  <rule id="900003" level="1">
    <if_sid>900000</if_sid>
    <url>^/foo|^/bar</url>
    <description>General foo or bar request.</description>
  </rule>

  <rule id="900010" level="6" frequency="10" timeframe="3">
    <if_matched_sid>900000</if_matched_sid>
    <same_source_ip />
    <options>alert_by_email</options>
    <description>Alert if more than 12 (10+2) requests from same IP in 3 seconds.</description>
  </rule>

  <rule id="900011" level="6" frequency="50" timeframe="30">
    <if_matched_sid>900000</if_matched_sid>
    <same_source_ip />
    <options>alert_by_email</options>
    <description>Alert if more than 52 (50+2) requests from same IP in 30 seconds.</description>
  </rule>

  <rule id="900012" level="6" frequency="5" timeframe="30">
    <if_matched_sid>900002</if_matched_sid>
    <same_source_ip />
    <options>alert_by_email</options>
    <description>Alert if more than 7 (5+2) special foo or baz requests from same IP in 30 seconds.</description>
  </rule>

  <rule id="900013" level="6" frequency="10" timeframe="10">
    <if_matched_sid>900003</if_matched_sid>
    <same_source_ip />
    <options>alert_by_email</options>
    <description>Alert if more than 12 (10+2) general foo or bar requests from same IP in 10 seconds.</description>
  </rule>

You'll have to customize the URL paths, frequencies, and timeframes (and add or substract rules as necessary) if you want to use something similar on your web site. Here's a rule-by-rule explanation of the above (keeping in mind that OSSEC rules work like a pipeline, where you have to connect later rules to earlier ones if you want the later ones to be able to operate on the events that earlier rules have already matched):

Rule 900000 identifies general web requests that haven't otherwise been handled by the default OSSEC rules (gathering the output of the 31100, 31108, 31101, and 31120 rules from the built-in /var/ossec/rules/web_rules.xml file that haven't been matched by any other built-in rules). The rules following 900000 will filter through all the requests that 900000 has collected:

  <rule id="900000" level="1">
    <if_sid>31100,31108,31101,31120</if_sid>
    <description>Any web request.</description>
  </rule>

Rule 900001 filters out any requests for URLs starting with /css, /js, and /img (note the <url> element does not handle full perl-style regexes — it instead uses a very limited string-matching syntax that recognizes only ^, $, and | as special characters). This will prevent any requests for our app's static CSS, JavaScript, or image files from being counted by the other rules that rely on rule 900000:

  <rule id="900001" level="0">
    <if_sid>900000</if_sid>
    <url>^/css|^/img|^/js</url>
    <description>Static css/img/js request.</description>
  </rule>

Rule 900002 identifies requests for our most-sensitive URLs (URLs starting with /foo/special or /baz). Rule 900012 (later on in the pipeline) will raise a higher-level alert if we get too many of these too quickly:

  <rule id="900002" level="1">
    <if_sid>900000</if_sid>
    <url>^/foo/special|^/baz</url>
    <description>Special foo or baz request.</description>
  </rule>

Rule 900003 identifies requests for our moderately-sensitive URLs (URLs starting with /foo or /bar). Since Rule 900002 was specified first, URLs starting with /foo/special will be filtered out by 900002; Rule 900003 won't match them, but will match all other URLs starting with /foo. Rule 900013 (later on in the pipeline) will raise a higher-level alert if we get too many of these too quickly:

  <rule id="900003" level="1">
    <if_sid>900000</if_sid>
    <url>^/foo|^/bar</url>
    <description>General foo or bar request.</description>
  </rule>

Rule 900010 raises a level-6 alert if we get 12 or more requests that have been matched by rule 900000 (not including those matched by rule 900001, 900002, or 900003) from the same IP address in under 3 seconds. Note that one of the eccentricities of OSSEC is that rules configured with the frequency attribute are fired only after the rule has been matched twice more than the configured value — so defining a rule with frequency="10" actually means that the rule must be matched 12 times before it fires.

Also, whenever you add new rules that will result in active responses (like this one will, assuming your apache-deny active response is also set at level 6), it's usually a good idea to include the alert_by_email option until your sure it's working smoothly — that way OSSEC will always send you an email whenever the rule fires, allowing you to check that it's firing only under the circumstances you want it to (and that it's triggered about as often as you'd expect). You can remove the alert_by_email option once your satisfied it's working as planned:

  <rule id="900010" level="6" frequency="10" timeframe="3">
    <if_matched_sid>900000</if_matched_sid>
    <same_source_ip />
    <options>alert_by_email</options>
    <description>Alert if more than 12 (10+2) requests from same IP in 3 seconds.</description>
  </rule>

Rule 900011 raises a level-6 alert if we get 52 or more requests that have been matched by rule 900000 from the same IP in under 30 seconds. So 900010 will result in blocking an IP address if we get a quick burst of requests from it (12 in 3 seconds), whereas 900011 will fire if we get a steady stream (52 in 30 seconds):

  <rule id="900011" level="6" frequency="50" timeframe="30">
    <if_matched_sid>900000</if_matched_sid>
    <same_source_ip />
    <options>alert_by_email</options>
    <description>Alert if more than 52 (50+2) requests from same IP in 30 seconds.</description>
  </rule>

Rule 900012 raises a level-6 alert if we get 7 or more requests matched by rule 900002 — our most sensitive URLs — from the same IP in under 30 seconds. So rule 900012 has a much lesser tolerance for requests than our general rules 900010 and 900011, blocking IPs after just a few requests:

  <rule id="900012" level="6" frequency="5" timeframe="30">
    <if_matched_sid>900002</if_matched_sid>
    <same_source_ip />
    <options>alert_by_email</options>
    <description>Alert if more than 7 (5+2) special foo or baz requests from same IP in 30 seconds.</description>
  </rule>

Rule 900013 raises a level-6 alert if we get 12 or more requests matched by rule 900003 — our moderately-sensitive URLs — from the same IP in under 10 seconds. It straddles the gap between the very-sensitive rule 900012, and the more tolerant rules 900010 and 900011:

  <rule id="900013" level="6" frequency="10" timeframe="10">
    <if_matched_sid>900003</if_matched_sid>
    <same_source_ip />
    <options>alert_by_email</options>
    <description>Alert if more than 12 (10+2) general foo or bar requests from same IP in 10 seconds.</description>
  </rule>

Keep in mind that whenever you do IP-based blocking like this, multiple users behind the same NAT (ie in the same office or using the same Internet connection) will appear to your servers as all having the same IP address — so be sure to allow for that when deciding for your site how many requests from the same IP should trigger alerts.

Sunday, June 6, 2010

Grails Passwords, Salted

Getting authentication with Spring Security (s2) set up on Grails is nice and easy; getting your s2 passwords salted with a unique value, not so much. There's a real nice grails s2 plugin that Burt Beckwith maintains. It actually is quite well documented, but there's an awful lot of places where the code is still a ways ahead of the documentation.

So here's a quick tutorial to get your grails passwords salted:

1 Install S2

First install the spring-security plugin:

$ grails install-plugin spring-security-core

Then create your "user" and "role" domain objects. You can call the "user" and "role" classes whatever you want, and put them in whatever package you choose. I called mine cq.User and cq.Role:

$ grails s2-quickstart cq User Role

2 Basic Configuration

The s2-quickstart script will automatically add the following to your grails-app/conf/Config.groovy config file:

// Added by the Spring Security Core plugin: grails.plugins.springsecurity.userLookup.userDomainClassName = 'cq.User' grails.plugins.springsecurity.userLookup.authorityJoinClassName = 'cq.UserRole' grails.plugins.springsecurity.authority.className = 'cq.Role'

If you know that your usernames won't change, you can use them to salt the password. While not ideal as salts (an attacker can still build out rainbow tables of common username/password combinations pretty easily), they're a lot better than no salt at all.

To use the username as a salt, all you need to do is add one config setting to your Config.groovy. Unfortunately, the s2 manual had the wrong name for this setting; this is the right setting to add to Config.groovy:

grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'username'

I'd also recommend turning on the setting that encodes the hashed passwords as base-64 strings (instead of strings of hex digits; it'll shave a dozen characters off the size of the hashed password):

grails.plugins.springsecurity.password.encodeHashAsBase64 = true

3 Basic Password Hashing

The other thing you need to do is make sure you hash the password with the salt whenever the password is saved. The s2 quickstart tutorial directs you to do this in your user controller. Don't; you should do this in the domain models, so you don't repeat yourself.

So update your "user" class to look like this:

package cq class User { def springSecurityService String username String password boolean enabled boolean accountExpired boolean accountLocked boolean passwordExpired static mapping = { // password is a keyword in some sql dialects, so quote with backticks // password is stored as 44-char base64 hashed value password column: '`password`', length: 44 } static constraints = { username blank: false, size: 1..50, unique: true password blank: false, size: 8..100 } def beforeInsert() { encodePassword() } def beforeUpdate() { if (isDirty('password')) encodePassword() } Set getAuthorities() { UserRole.findAllByUser(this).collect { it.role } as Set } protected encodePassword() { password = springSecurityService.encodePassword(password, username) } }

The main difference between the above and what s2-quickstart generates is the internal encodePassword() method. When a new user is saved, the beforeInsert() method will be called by the gorm framework, and our user class will hash the password, with the username as a salt. When an existing user is updated, the beforeUpdate() method will be called; it will check if the password has changed, and if it has, it will also hash the new password the same way.

This way you never have to hash a user's password in a controller or other code; just pass it on through to the domain model like any other property.

4 A Quick Test

At this point you've done enough to store passwords hashed with the username as a salt. Test it out by adding some test users in your bootstrap code, and a check for authenticated users on your home page.

In grails-app/conf/BootStrap.groovy, create and save a new test user:

class BootStrap { def init = { servletContext -> new cq.User(username: 'test', enabled: true, password: 'password').save(flush: true) } def destroy = { } }

And in grails-app/views/index.gsp, add this to the top of the body:

... <body> <sec:ifLoggedIn><h1>Hey, I know you; you're <sec:username/>!</h1></sec:ifLoggedIn> <sec:ifNotLoggedIn><h1>Who are you?</h1></sec:ifNotLoggedIn> ...

Now run your app (with clean, just to make sure everything gets rebuilt properly):

$ grails clean && grails run-app

Navigate to http://localhost:8080/cq/login (where cq is the name of your app), and login with a username of test and a password of password. Pretty sweet what you get (just about) out of the box, huh?

5 Adding a Unique Salt

Let's kick it up a notch. To create a unique salt for each user (making it impractical for an attacker to use rainbow tables to crack the hashed passwords), add a salt field to your "user" class, and override the getter for this field to initialize it with a unique salt:

package cq import java.security.SecureRandom; // add class User { def springSecurityService String username String password String salt // add boolean enabled boolean accountExpired boolean accountLocked boolean passwordExpired static mapping = { // password is a keyword in some sql dialects, so quote with backticks // password is stored as 44-char base64 hashed value password column: '`password`', length: 44 } static constraints = { username blank: false, size: 1..50, unique: true password blank: false, size: 8..100 // salt is stored as 64-char base64 value salt maxSize: 64 // add } def beforeInsert() { encodePassword() } def beforeUpdate() { if (isDirty('password')) encodePassword() } // add: String getSalt() { if (!this.salt) { def rnd = new byte[48]; new SecureRandom().nextBytes(rnd) this.salt = rnd.encodeBase64() } this.salt } Set getAuthorities() { UserRole.findAllByUser(this).collect { it.role } as Set } protected encodePassword() { password = springSecurityService.encodePassword(password, salt) // update } }

Don't forget to also update the encodePassword() method to hash the password with the salt field, instead of the username field.

6 Adding Custom UserDetails

Here's where it gets tricky. S2 maintains a user class for authentication called UserDetails that's completely separate from your "user" domain model. So you have to provide a custom UserDetailsService class that creates a custom UserDetails object given a username, as well as a custom SaltSource helper-class to extract the salt value from the custom UserDetails.

The good news is that you don't have to write a whole lot of code to do this. Create the following class as src/groovy/cq/MyUserDetailsService.groovy (or with whatever namespace and classname you like):

package cq import org.codehaus.groovy.grails.plugins.springsecurity.GormUserDetailsService import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.UserDetails class MyUserDetailsService extends GormUserDetailsService { protected UserDetails createUserDetails(user, Collection authorities) { new MyUserDetails((GrailsUser) super.createUserDetails(user, authorities), user.salt ) } }

And create the following as src/groovy/cq/MyUserDetails.groovy:

package cq import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser class MyUserDetails extends GrailsUser { public final String salt MyUserDetails(GrailsUser base, String salt) { super(base.username, base.password, base.enabled, base.accountNonExpired, base.credentialsNonExpired, base.accountNonLocked, base.authorities, base.id) this.salt = salt; } }

And create the following as src/groovy/cq/MySaltSource.groovy:

package cq import org.springframework.security.authentication.dao.ReflectionSaltSource import org.springframework.security.core.userdetails.UserDetails class MySaltSource extends ReflectionSaltSource { Object getSalt(UserDetails user) { user[userPropertyToUse] } }

Note that if you use a java UserDetails implementation, instead of a groovy implementation, you can just use ReflectionSaltSource directly — you need to customize it only to do groovy "reflection" (it does java reflection just fine).

7 Configuring UserDetails

Finally, you can configure s2 to use your custom UserDetails class by adding the following to your grails-app/conf/spring/resources.groovy:

import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH beans = { userDetailsService(cq.MyUserDetailsService) { sessionFactory = ref('sessionFactory') transactionManager = ref('transactionManager') } saltSource(cq.MySaltSource) { userPropertyToUse = CH.config.grails.plugins.springsecurity.dao.reflectionSaltSourceProperty } }

If you were to implement your UserDetails class in java you could omit the saltSource bean (since it comes configured out-of-the-box to do reflection on java classes). Otherwise, the one last piece of the puzzle is to go back and change the dao.reflectionSaltSourceProperty setting in your grails-app/conf/Config.groovy to your new salt field:

grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'salt'

8 A Real Test

Now to verify that all this stuff is working (and will still work when you mess around with your user domain model in the future), you need some integration tests. First, let's tackle the password-hashing scheme; create a test/integration/cq/UsersTests.groovy class, and dump this in it:

package cq class UserTests extends GroovyTestCase { def springSecurityService void testPasswordIsEncodedWhenUserIsCreated() { def user = new User(username: 'testuser1', password: 'password').save(flush: true) assertEquals springSecurityService.encodePassword('password', user.salt), user.password } void testPasswordIsReEncodedWhenUserIsUpdatedWithNewPassword() { def user = new User(username: 'testuser1', password: 'password').save(flush: true) // update password user.password = 'password1' user.save(flush: true) assertEquals springSecurityService.encodePassword('password1', user.salt), user.password } void testPasswordIsNotReEncodedWhenUserIsUpdatedWithoutNewPassword() { def user = new User(username: 'testuser1', password: 'password').save(flush: true) // update user, but not password user.enabled = true user.save(flush: true) assertEquals springSecurityService.encodePassword('password', user.salt), user.password } void testPasswordIsNotReEncodedWhenUserIsReloaded() { new User(username: 'testuser1', password: 'password').save(flush: true) // reload user def user = User.findByUsername('testuser1') assertNotNull user assertEquals springSecurityService.encodePassword('password', user.salt), user.password } }

Now the authentication part. This is a bit awkward, because what we're really testing is that your custom UserDetails is implemented and configured correctly, but let's pretend that it's testing your login controller and stick it in your LoginControllerTests anyway. Create a test/integration/cq/LoginControllerTests.groovy class, and put this in it:

package cq import java.security.Principal import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; class LoginControllerTests extends GroovyTestCase { def daoAuthenticationProvider void testAuthenticationFailsWithIncorrectPassword() { def user = new User( username: 'testuser1', password: 'password', enabled: true ).save(flush: true) def token = new UsernamePasswordAuthenticationToken( new TestPrincipal('testuser1'), 'password1' ) shouldFail(BadCredentialsException) { daoAuthenticationProvider.authenticate(token) } } void testAuthenticationSucceedsWithCorrectPassword() { def user = new User( username: 'testuser1', password: 'password', enabled: true ).save(flush: true) def token = new UsernamePasswordAuthenticationToken( new TestPrincipal('testuser1'), 'password' ) def result = daoAuthenticationProvider.authenticate(token) assertTrue result.authenticated } class TestPrincipal implements Principal { String name TestPrincipal(def name) { this.name = name } boolean equals(Object o) { if (name == null) return o == null return name.equals(o) } int hashCode() { return toString().hashCode() } String toString() { return String.valueOf(name) } } }

And now run your integration tests:

$ grails test-app integration:

The console outputs only a brief overview of the results. If something went wrong, you can find the details in the target/test-reports folder; enter the following in your browser address bar for the html version of the report (where $PROJECT_HOME is the full path to your project):

file://$PROJECT_HOME/target/test-reports/html/index.html

And hey presto, you've got salt.