Cisco networking gear authentication using Radius and Ldap

June 26, 2012

This is a post to describe one way to use FreeRadius + OpenLDAP to authenticate, authorise and do some accounting on Cisco network gear. The problem we also wanted to solve is the one that we wanted to be able to provide different access level to different people on different group of switches/routers. The separation up to now is that we have access, distribution , backbone switches and routers. We have created two major groups, one for access switches (called Access) and one Core that includes everything else.

The cisco configuration is quite straight forward (depends a bit on the CatOS/IOS version but there is plenty of documentation), so we will deal with the other two components. A small side note, when cisco is using radius for authentication/authorization in order to define the priviledge level the user will have is using the Cisco-AVPair shell:priv-lvl=x, where x is the priviledge level (from 1 to 15).

On OpenLDAP I created a new objectclass that will be used to define the access level for each category :

###########################################################################
#
# -- UOA Network Admin schema --
# Used for radius authentication for the NoC-UoA admins and helpdesk
# In this version there is one attribute
# 1) UoAAccess-Lvl: defines the access level. The vocabulary is
#            <Switch-Class>=<level>
#     where, <level> takes values from 0 to 15, and <Switch-Class> is defined
# according to the radius HuntGroups. Core is defined as the default level.
# It is a multivalue attribute for each <Switch-Class>
#
###########################################################################
attributetype ( 1.3.6.1.4.1.16375.10.35.1
    NAME 'UoAAccess-Lvl'
    DESC 'Defines the access level on the network equipment'
    EQUALITY caseIgnoreMatch
    ORDERING caseIgnoreOrderingMatch
    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

#################
# OBJECTCLASSES #
#################

objectclass ( 1.3.6.1.4.1.16375.10.35.100
    NAME 'UoANetAdmin'
    DESC 'University of Athens Network Admin Access'
    SUP top AUXILIARY
    MUST ( uid $ userPassword $ UoACore-Lvl $ UoAAccess-Lvl )
)

the important attribute is UoAAccess-Lvl, which is multivalued and has the format <Device-class>=<level>, for example Access=1 or Core=2. Each entry defines the value for one class. This way it is easy to expand the access layer by defining more classes for network elements, and assigning them to the users.

On the FreeRadius part, the first think to do is to define the different classes the equipment will belong. This can be done using the huntgroups file and adding something like

Access         NAS-IP-Address == x.x.x.x
Access         NAS-IP-Address == y.y.y.y

this defines that in the group Access the devices with ip address x.x.x.x and y.y.y.y are defined.

The next step is to setup the dictionary files for the private radius pairs that will be mapped to ldap attributes.

VENDOR        PRI           666
BEGIN-VENDOR  PRI
# Switch auth internal attrs
ATTRIBUTE     UOA-NocAuth-Final-Lvl  110   integer
ATTRIBUTE     UOA-NocAuth-Access-Lvl 111   string
END-VENDOR    PRI

changing the PRI, and 666 to the corresponding vendor specific numbers. 110, and 111 have no special meaning and you can use the first available one. Having these we setup the file that maps the Radius Pairs to ldap attributes in ldap.attrmap file

checkItem     Stripped-User-Name       uid
checkItem     Password-With-Header     userPassword
checkItem     UOA-NocAuth-Access-Lvl   UoAAccess-Lvl&nbsp    +=

Note the += part in the last attribute, which is used to concatenate all the values of the multivalue attribute.

For the accounting part we are going to use a mysql database, and we crete the database together with the corresponding schema:

create database Accounting;

CREATE TABLE `Accounting` (
`Id` bigint(21) NOT NULL auto_increment,
`UniqueSessionId` varchar(50) NOT NULL default '',
`AcctSessionId` varchar(50) NOT NULL default '',
`UserName` varchar(100) NOT NULL default '',
`CallingStationId` varchar(100) NOT NULL default '',
`NASIPAddress` varchar(16) NOT NULL default '',
`NASPort` int(10) unsigned NOT NULL default '0',
`NASPortId` varchar(16) NOT NULL default '',
`NASPortType` varchar(50) NOT NULL default '',
`TimestampAccessStart` datetime default NULL,
`TimestampAccessStop` datetime default NULL,
`AcctSessionTime` int(10) unsigned NOT NULL default '0',
`AcctTerminateCause` varchar(100) NOT NULL default '',
PRIMARY KEY&nbsp; (`Id`),
KEY `UniqueSessionId` (`UniqueSessionId`,`AcctSessionId`,`UserName`),
KEY `UserName` (`UserName`,`TimestampAccessStart`),
KEY `NAS` (`NASIPAddress`,`NASPort`,`TimestampAccessStart`),
KEY `CallingStationId` (`CallingStationId`,`TimestampAccessStart`)
) ENGINE=MyISAM DEFAULT CHARSET=greek

And we create the file query.conf that includes the queries needed for the accounting:

accounting_start_query = " \
    INSERT INTO Accounting ( \
        UniqueSessionId, AcctSessionId, UserName, \
        CallingStationId, \
        NASIPAddress, NASPort, NASPortID, \
        NasPortType, TimestampAccessStart \
    ) VALUES ( \
        '%{Acct-Unique-Session-Id}', '%{Acct-Session-Id}', '%{User-Name}', \
        LOWER('%{Calling-Station-Id}'), \
        '%{NAS-IP-Address}', '%{NAS-Port}', '%{NAS-Port-Id}', \
        '%{NAS-Port-Type}', '%S' \
    ) \
"
accounting_stop_query = " \
    UPDATE Accounting SET \
        TimestampAccessStop = '%S', \
        AcctTerminateCause  = '%{Acct-Terminate-Cause}', \
        AcctSessionTime&nbsp = '%{Acct-Session-Time}' \
    WHERE UniqueSessionId = '%{Acct-Unique-Session-Id}' \
        AND AcctSessionId = '%{Acct-Session-Id}' \
        AND UserName      = '%{User-Name}' \
"

Finally we have the last two files that glue everything together. First is modules.conf that initializes all the modules needed in this configuration :

preprocess preprocess.radauth {
    huntgroups = /path/to/huntgroups
    hints = /dev/null
    with_cisco_vsa_hack = no
}
linelog postauthsyslog.radauth {
    filename = syslog
    format = "radauth[POSTAUTH]: %{reply:Packet-Type} U=%{User-Name}@%{%{Calling-Station-ID}:-UNKNOWN} Req=%{NAS-Port-Id}@%{Client-IP-Address}  Lvl=%{control:UOA-NocAuth-Final-Lvl}"
}

ldap ldap.radauth {
    server = "ldap.example.com"
    identity = "cn=Manager,dc=example,dc=com"
    password = 'password'
    basedn = "dc=example,dc=gr"
    filter = "(&(objectclass=UoANetAdmin)(uid=%{User-Name}))"
    ldap_connections_number = 5
    timeout = 5
    timelimit = 3
    net_timeout = 1

    dictionary_mapping = /path/to/ldap.attrmap

    access_attr_used_for_allow = no
    set_auth_type = no

    compare_check_items = no
    do_xlat = yes

    #ldap_debug = 0x0028
}

sql sql.radacct {
    driver = "rlm_sql_mysql"
    server = "sql.example.com"
    login = "accountuser"
    password = "password"
    radius_db = "Accounting"
    read_groups = no
    deletestalesessions = no
    sqltrace = no
    num_sql_socks = 5
    connect_failure_retry_delay = 60
    readclients = no
    #safe-characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /"
    $INCLUDE /path/to/query.conf
}

and then site.conf that includes the site configuration:

client x.x.x.x {
    shortname       = test-switch
    secret          = secret
    virtual_server  = radauth
}

client y.y.y.y {
    shortname       = switch2
    secret          = secret
    virtual_server  = radauth
}
server radauth {
    authorize {
        preprocess.radauth
        ldap.radauth
        pap
        update control {
            UoA-NocAuth-Final-Lvl := 0
        }
        if("%{control:UOA-NocAuth-Access-Lvl[*]}" =~ /%{%{Huntgroup-Name}:-Core}=\([0-9][0-9]*\)/ ) {
            update control {
                UoA-NocAuth-Final-Lvl := "%{1}"
            }
        }
        if("%{control:UOA-NocAuth-Final-Lvl}" == 0 ) {
            reject
        }
        update reply {
            Cisco-AVPair := "shell:priv-lvl=%{control:UOA-NocAuth-Final-Lvl}"
        }
    }
    authenticate {
         Auth-Type PAP {
             pap
         }
    }
    post-auth {
        postauthsyslog.radauth
        Post-Auth-Type REJECT {
            postauthsyslog.radauth
        }
    }

    preacct {
        acct_unique
    }
    accounting {
        sql.radacct
        attr_filter.accounting_response
    }

}

It is important to note that in this configuration we use the acct_unique module, so that there will not be problems in the database with the accountint section IDs, but this is something that can be easily found in the main radius configuration. Since we have a multivalue attribute we use the %{contorl:Attribute_name[*]} construct, so that it concatenates all the values separating them with new line, and we use the regexp to choose the wanted one. In FreeRadius 3.x a better way would be to use the foreach keyword tha permits iterations over the values. This hack however works.

We include these files in the main radius configuration, and with some needed modifications we have a working config. In the accounting section Cisco unfortunately (READ: WHY DEAR GOD, WHY??) doesn’t support sending each command a user types to RADIUS, but only for TACACS+, so the accounting part is at least handicapped.

Advertisements

One Response to “Cisco networking gear authentication using Radius and Ldap”


  1. […] this post what was left was my discomfort that cisco didn’t provide a way to log the commands send […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: