diff options
Diffstat (limited to 'raddb/policy.d')
-rw-r--r-- | raddb/policy.d/abfab-tr | 106 | ||||
-rw-r--r-- | raddb/policy.d/accounting | 134 | ||||
-rw-r--r-- | raddb/policy.d/canonicalization | 113 | ||||
-rw-r--r-- | raddb/policy.d/control | 40 | ||||
-rw-r--r-- | raddb/policy.d/cui | 131 | ||||
-rw-r--r-- | raddb/policy.d/debug | 64 | ||||
-rw-r--r-- | raddb/policy.d/dhcp | 327 | ||||
-rw-r--r-- | raddb/policy.d/eap | 54 | ||||
-rw-r--r-- | raddb/policy.d/filter | 211 | ||||
-rw-r--r-- | raddb/policy.d/moonshot-targeted-ids | 249 | ||||
-rw-r--r-- | raddb/policy.d/operator-name | 46 | ||||
-rw-r--r-- | raddb/policy.d/rfc7542 | 46 |
12 files changed, 1521 insertions, 0 deletions
diff --git a/raddb/policy.d/abfab-tr b/raddb/policy.d/abfab-tr new file mode 100644 index 0000000..3a08853 --- /dev/null +++ b/raddb/policy.d/abfab-tr @@ -0,0 +1,106 @@ +# +# ABFAB Trust router policies. +# +# $Id$ +# + + +# +# Verify rp parameters +# +psk_authorize { + if (&TLS-PSK-Identity) { + # TODO: may need to check trust-router-apc as well + if ("%{psksql:select distinct keyid from authorizations_keys where keyid = '%{tls-psk-identity}' and '%{trust-router-coi}' like coi and '%{gss-acceptor-realm-name}' like acceptor_realm and '%{gss-acceptor-host-name}' like hostname;}") { + # do things here + } + else { + update reply { + Reply-Message = "RP not authorized for this ABFAB request" + } + reject + } + } +} + +abfab_client_check { + # check that GSS-Acceptor-Host-Name is correct + if ("%{client:gss_acceptor_host_name}") { + if (&request:GSS-Acceptor-Host-Name) { + if (&request:GSS-Acceptor-Host-Name != "%{client:gss_acceptor_host_name}") { + update reply { + Reply-Message = "GSS-Acceptor-Host-Name incorrect" + } + reject + } + } + else { + # set GSS-Acceptor-Host-Name if it is not set by the mechanism + # but it is defined in the client configuration + update request { + GSS-Acceptor-Host-Name = "%{client:gss_acceptor_host_name}" + } + } + } + + # set Trust-Router-COI attribute from the client configuration + if ("%{client:trust_router_coi}") { + update request { + Trust-Router-COI := "%{client:trust_router_coi}" + } + } + + # set GSS-Acceptor-Realm-Name attribute from the client configuration + if ("%{client:gss_acceptor_realm_name}") { + update request { + GSS-Acceptor-Realm-Name := "%{client:gss_acceptor_realm_name}" + } + } + + # set GSS-Acceptor-Service-Name attribute from the client configuration + if ("%{client:gss_acceptor_service_name}") { + update request { + GSS-Acceptor-Service-Name = "%{client:gss_acceptor_service_name}" + } + } + +} + +# A policy which is used to validate channel-bindings. +# +abfab_channel_bindings { + if (&GSS-Acceptor-Service-Name && (&outer.request:GSS-Acceptor-Service-Name != &GSS-Acceptor-Service-Name)) { + reject + } + + if (&GSS-Acceptor-Host-Name && &outer.request:GSS-Acceptor-Host-Name != &GSS-Acceptor-Host-Name ) { + reject + } + + if (&GSS-Acceptor-Realm-Name && &outer.request:GSS-Acceptor-Realm-Name != &GSS-Acceptor-Realm-Name ) { + reject + } + + if (&GSS-Acceptor-Service-Name || &GSS-Acceptor-Realm-Name || &GSS-Acceptor-Host-Name) { + update control { + &Chbind-Response-Code := success + } + + # + # ACK the attributes in the request. + # + # If any one of these attributes don't exist in the request, + # then they won't be copied to the reply. + # + update reply { + &GSS-Acceptor-Service-Name = &GSS-Acceptor-Service-Name + &GSS-Acceptor-Host-Name = &GSS-Acceptor-Host-Name + &GSS-Acceptor-Realm-Name = &GSS-Acceptor-Realm-Name + } + } + + # + # Return "handled" so that the "authenticate" section isn't used. + # + handled +} diff --git a/raddb/policy.d/accounting b/raddb/policy.d/accounting new file mode 100644 index 0000000..7c52637 --- /dev/null +++ b/raddb/policy.d/accounting @@ -0,0 +1,134 @@ +# We check for this prefix to determine whether the class value was +# generated by this server. It should be changed so that it is +# globally unique. +class_value_prefix = 'ai:' + +# +# Replacement for the old rlm_acct_unique module +# +acct_unique { + # + # If we have a class attribute in the format + # 'auth_id:[0-9a-f]{32}' it'll have a local value + # (defined by insert_acct_class), this ensures + # uniqueness and suitability. + # + # We could just use the Class attribute as + # Acct-Unique-Session-Id, but this may cause problems + # with NAS that carry Class values across between + # multiple linked sessions. So we rehash class with + # Acct-Session-ID to provide a truely unique session + # identifier. + # + # Using a Class/Session-ID combination is more robust + # than using elements in the Accounting-Request, + # which may be subject to change, such as + # NAS-IP-Address, Client-IP-Address and + # NAS-Port-ID/NAS-Port. + # + # This policy should ensure that session data is not + # affected if NAS IP addresses change, or the client + # roams to a different 'port' whilst maintaining its + # initial authentication session (Common in a + # wireless environment). + # + update request { + &Tmp-String-9 := "${policy.class_value_prefix}" + } + + if (("%{hex:&Class}" =~ /^%{hex:&Tmp-String-9}/) && \ + ("%{string:&Class}" =~ /^${policy.class_value_prefix}([0-9a-f]{32})/i)) { + update request { + &Acct-Unique-Session-Id := "%{md5:%{1},%{Acct-Session-ID}}" + } + } + + # + # Not All devices respect RFC 2865 when dealing with + # the class attribute, so be prepared to use the + # older style of hashing scheme if a class attribute + # is not included + # + else { + update request { + &Acct-Unique-Session-Id := "%{md5:%{User-Name},%{Acct-Session-ID},%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}},%{NAS-Identifier},%{NAS-Port-ID},%{NAS-Port}}" + } + } + + update request { + &Tmp-String-9 !* ANY + } +} + +# +# Insert a (hopefully unique) value into class +# +insert_acct_class { + update reply { + &Class = "${policy.class_value_prefix}%{md5:%t,%{Packet-Src-Port},%{%{Packet-Src-IP-Address}:-%{Packet-Src-IPv6-Address}},%{NAS-IP-Address},%{Calling-Station-ID},%{User-Name},%{session-state:User-Name} }" + } +} + +# +# Merges Acct-[Input|Output]-Octets and Acct-[Input|Output]-Gigawords into Acct-[Input|Output]-Octets64 +# +# If the &Attr-Foo doesn't exist, it's value is taken as zero. +# +acct_counters64.preacct { + update request { + &Acct-Input-Octets64 = "%{expr:(&Acct-Input-Gigawords << 32) | &Acct-Input-Octets}" + &Acct-Output-Octets64 = "%{expr:(&Acct-Output-Gigawords << 32) | &Acct-Output-Octets}" + } +} + +# +# There is a delay between sending the Access-Accept and receiving +# the corresponding Accounting-Request "start" packet. This delay +# can be leveraged by a user to bypass Simultaneous-Use checks. +# +# The user can start up multiple sessions at the same time. When +# that happens, both Simultaneous-Use checks are performed before any +# Accounting-Request packet is received. Both Simultaneous-Use +# checks will result in "no user session" in the radacct table, and +# both sessions will be allowed. At some point later in time, the +# Accounting-Request packets are received. But by then it's too +# late. +# +# The solution is to insert a temporary session into the "radacct" +# table, during the "post-auth" section. This is done by +# uncommenting the "sql_session_start" entry in +# sites-enabled/default. Then, reading +# raddb/mods-config/sql/main/*/queries.conf, and looking for the +# "sql_session_start" comments. Follow the instructions there to +# finalize the configuration. +# +# The server will then create a temporary entry in "radacct" before +# it returns the Access-Request. Any other Access-Request which is +# received at the same time will then have it's Simultaneous-Use +# check see that entry, and will be rejected. +# +# Subsequent Accounting-Request packets for the first session will +# then UPDATE (not INSERT) the data for the session. +# +# There is still a small race condition as the Simultaneous-Use +# checks are not done at the same time as updating radacct. But the +# window of opportunity is much smaller. i.e. milliseconds, instead +# of seconds. +# +# This policy can also be used to "bootstrap" accounting sessions. +# If there is data which is only available in the Access-Request, +# it can be placed in the accounting table. Then, when accounting +# packets are received, they will update the row which contains +# the session information. +# +sql_session_start.post-auth { + acct_unique + + # + # The SQL accounting queries need an Acct-Status-Type attribute + # + update request { + Acct-Status-Type := Start + } + sql.accounting +} diff --git a/raddb/policy.d/canonicalization b/raddb/policy.d/canonicalization new file mode 100644 index 0000000..6d90e37 --- /dev/null +++ b/raddb/policy.d/canonicalization @@ -0,0 +1,113 @@ +# +# Split User-Name in NAI format (RFC 4282) into components +# +# This policy writes the Username and Domain portions of the +# NAI into the Stripped-User-Name and Stripped-User-Domain +# attributes. +# +# The regular expression to do this is not strictly compliant +# with the standard, but it is not possible to write a +# compliant regexp without perl style regular expressions (or +# at least not a legible one). +# +nai_regexp = '^([^@]*)(@([-[:alnum:]]+\.[-[:alnum:].]+))?$' + +split_username_nai { + if (&User-Name && (&User-Name =~ /${policy.nai_regexp}/)) { + update request { + &Stripped-User-Name := "%{1}" + } + + # Only add the Stripped-User-Domain attribute if + # we have a domain. This means presence checks + # for Stripped-User-Domain work. + if ("%{3}" != '') { + update request { + &Stripped-User-Domain = "%{3}" + } + } + + # If any of the expansions result in a null + # string, the update section may return + # something other than updated... + updated + } + else { + noop + } +} + +# +# If called in post-proxy we modify the proxy-reply message +# +split_username_nai.post-proxy { + if (&proxy-reply:User-Name && (&proxy-reply:User-Name =~ /${policy.nai_regexp}/)) { + update proxy-reply { + &Stripped-User-Name := "%{1}" + } + + # Only add the Stripped-User-Domain attribute if + # we have a domain. This means presence checks + # for Stripped-User-Domain work. + if ("%{3}" != '') { + update proxy-reply { + &Stripped-User-Domain = "%{3}" + } + } + updated + } + else { + noop + } +} + +# +# Normalize the MAC Addresses in the Calling/Called-Station-Id +# +mac-addr-regexp = '([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})' + +# +# Add "rewrite_called_station_id" in the "authorize" and +# "preacct" sections. +# +# Makes Called-Station-ID conform to what RFC3580 says should +# be provided by 802.1X authenticators. +# +rewrite_called_station_id { + if (&Called-Station-Id && (&Called-Station-Id =~ /^${policy.mac-addr-regexp}([^0-9a-f](.+))?$/i)) { + update request { + &Called-Station-Id := "%{toupper:%{1}-%{2}-%{3}-%{4}-%{5}-%{6}}" + } + + # SSID component? + if ("%{8}") { + update request { + &Called-Station-SSID := "%{8}" + } + } + updated + } + else { + noop + } +} + +# +# Add "rewrite_calling_station_id" in the "authorize" and +# "preacct" sections. +# +# Makes Calling-Station-ID conform to what RFC3580 says should +# be provided by 802.1X authenticators. +# +rewrite_calling_station_id { + if (&Calling-Station-Id && (&Calling-Station-Id =~ /^${policy.mac-addr-regexp}$/i)) { + update request { + &Calling-Station-Id := "%{toupper:%{1}-%{2}-%{3}-%{4}-%{5}-%{6}}" + } + updated + } + else { + noop + } +} + diff --git a/raddb/policy.d/control b/raddb/policy.d/control new file mode 100644 index 0000000..b3f1e03 --- /dev/null +++ b/raddb/policy.d/control @@ -0,0 +1,40 @@ +# +# If you want the server to pretend that it is dead, +# then use the "do_not_respond" policy. +# +do_not_respond { + update control { + &Response-Packet-Type := Do-Not-Respond + } + handled +} + +# +# Send Access-Accept immediately +# +accept { + update control { + &Response-Packet-Type = Access-Accept + } + handled +} + +# +# Send Access-Challenge immediately +# +challenge { + update control { + &Response-Packet-Type = Access-Challenge + } + handled +} + +# +# Send an Accounting-Response immediately +# +acct_response { + update control { + &Response-Packet-Type = Accounting-Response + } + handled +} diff --git a/raddb/policy.d/cui b/raddb/policy.d/cui new file mode 100644 index 0000000..08b2c91 --- /dev/null +++ b/raddb/policy.d/cui @@ -0,0 +1,131 @@ +# +# The following policies are for the Chargeable-User-Identity +# (CUI) configuration. +# +# The policies below can be called as just 'cui' (not +# cui.authorize etc..) from the various config sections. +# + +# +# cui_hash_key definition +# This key serves the purpose of protecting CUI values against +# dictionary attacks, therefore should be chosen as a "random" +# string and kept secret. +# +cui_hash_key = "changeme" + +# +# cui_require_operator_name switch +# If this is set to nonzero value then CUI will only be added +# when a non-empty Operator-Name value is present in the request +# +cui_require_operator_name = "no" + +# +# The client indicates it can do CUI by sending a CUI attribute +# containing one zero byte. +# A non-empty value in Operator-Name can be an additional requirement. +# Normally CUI support is turned on only for such requests. +# CUI support can be used for local clients which do not +# supports CUI themselves, the server can simulate a CUI request +# adding the missing NUL CUI value and the Operator-Name attribute. +# Clients which are supposed to get this treatment should +# be marked by add_cui flag in clients.conf +# We assume that local clients are marked in the client.conf with +# add_cui flag, e.g. +# client xxxx { +# ... +# add_cui = yes +# } +# +cui.authorize { + if ("%{client:add_cui}" == 'yes') { + update request { + &Chargeable-User-Identity := 0x00 + } + } +} + +# +# Before proxing an Access-Request to a remote server, a NUL CUI +# attribute should be added, unless it is already present in the request. +# +cui.pre-proxy { + if (("%{request:Packet-Type}" == 'Access-Request') && ("%{client:add_cui}" == 'yes')) { + update proxy-request { + &Chargeable-User-Identity = 0x00 + } + } +} + + +# +# Add a CUI attribute based on the User-Name, and a secret key +# known only to this server. +# For EAP-TTLS and EAP-PEAP methods +# use_tunneled_reply parameter MUST be set to yes +# +cui.post-auth { + if (!&control:Proxy-To-Realm && &Chargeable-User-Identity && !&reply:Chargeable-User-Identity && \ + (&Operator-Name || ('${policy.cui_require_operator_name}' != 'yes')) ) { + update reply { + &Chargeable-User-Identity = "%{sha1:${policy.cui_hash_key}%{tolower:%{User-Name}%{%{Operator-Name}:-}}}" + } + } + + # + # The section below will store a CUI for the User in the DB and remove the + # User-Name attribute from the reply if a CUI is present. + # + # You need to configure the cuisql module and your database for this to work. + # If your NAS can do CUI based accounting themselves or you do not care about + # accounting, comment out the 'cuisql' line below. + # + if (&reply:Chargeable-User-Identity) { + # Force User-Name to be the User-Name from the request + update { + &reply:User-Name := &request:User-Name + } + cuisql + } +} + + +cui-inner.post-auth { + if (&outer.request:Chargeable-User-Identity && \ + (&outer.request:Operator-Name || ('${policy.cui_require_operator_name}' != 'yes'))) { + update reply { + &Chargeable-User-Identity := "%{sha1:${policy.cui_hash_key}%{tolower:%{User-Name}%{%{outer.request:Operator-Name}:-}}}" + } + } +} + +# +# If your NAS can do CUI based accounting or you do not care about +# accounting then just comment out the call to cui in ...... +# +# If we had stored a CUI for the User, add it to the request. +# +cui.accounting { + # + # If the CUI isn't in the packet, see if we can find it + # in the DB. + # + if (!&Chargeable-User-Identity) { + update request { + &Chargeable-User-Identity := "%{cuisql:\ + SELECT cui FROM cui \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}'}" + } + } + + # + # If it exists now, then write out when we last saw + # this CUI. + # + if (&Chargeable-User-Identity && (&Chargeable-User-Identity != '')) { + cuisql + } +} diff --git a/raddb/policy.d/debug b/raddb/policy.d/debug new file mode 100644 index 0000000..26583f1 --- /dev/null +++ b/raddb/policy.d/debug @@ -0,0 +1,64 @@ +# +# Outputs the contents of the control list in debugging (-X) mode +# +debug_control { + if("%{debug_attr:control:}" == '') { + noop + } +} + +# +# Outputs the contents of the request list in debugging (-X) mode +# +debug_request { + if("%{debug_attr:request:}" == '') { + noop + } +} + +# +# Outputs the contents of the coa list in debugging (-X) mode +# +debug_coa { + if("%{debug_attr:coa:}" == '') { + noop + } +} + +# +# Outputs the contents of the reply list in debugging (-X) mode +# +debug_reply { + if("%{debug_attr:reply:}" == '') { + noop + } +} + +# +# Outputs the contents of the session state list in debugging (-X) mode +# +debug_session_state { + if("%{debug_attr:session-state:}" == '') { + noop + } +} + +# +# Outputs the contents of the proxy-request state list in debugging (-X) mode +# +debug_proxy_request { + if("%{debug_attr:proxy-request:}" == '') { + noop + } +} + +# +# Outputs the contents of the main lists in debugging (-X) mode +# +debug_all { + debug_control + debug_request + debug_coa + debug_reply + debug_session_state +} diff --git a/raddb/policy.d/dhcp b/raddb/policy.d/dhcp new file mode 100644 index 0000000..1752acb --- /dev/null +++ b/raddb/policy.d/dhcp @@ -0,0 +1,327 @@ +# Assign common DHCP reply packet options +dhcp_common { + # The contents here are invented. Change them! + update reply { + &DHCP-Domain-Name-Server = 127.0.0.1 + &DHCP-Domain-Name-Server += 127.0.0.2 + &DHCP-Subnet-Mask = 255.255.255.0 + &DHCP-Router-Address = 192.0.2.1 + &DHCP-Broadcast-Address = 192.0.2.255 + &DHCP-IP-Address-Lease-Time = 7200 + &DHCP-DHCP-Server-Identifier = &control:DHCP-DHCP-Server-Identifier + } +} + +# Lookup DHCP group based options. This policy allows for membership +# of multiple groups so can cover the ISC concepts of "group" and "class" +# To use this enable the "dhcp_files" module +#dhcp_group_options { +# foreach &request:DHCP-Group-Name { +# dhcp_set_group_options +# } +#} + +# Policy to override DHCP-Network-Subnet +# +# Some networks have a "shared-network" or "multinet" configuration (as +# defined by some other DHCP servers) in which multiple IP subnets may +# co-exist in a single Layer 2 network (or VLAN). +# +# In enterprise environments this is often for the purpose of providing loose +# segregation between classes of devices such as local network-attached +# storage or IP telephony. There are valid reasons why each of the subnets is +# not seperately VLANed, such as to enable the use of ICMP redirects to avoid +# hairpinning of cross-subnet traffic via a gateway. +# +# In ISP environments this is a common configuration for edge networks whose +# access is provided by DOCSIS cable modems that share a VLAN with the devices +# they provide a service to but are seperately addressed. +# +# Where it is necessary to force the selection of a particular subnet for a +# device, multiple pools must be configured for each subnet and referenced +# with unique identifiers in the *network-specific* section of +# mods-config/files/dhcp. +# +# By default DHCP-Network-Subnet is populated such that it normally +# refers to the Layer 2 network from which the DHCP query originates - we +# cannot know the intended subnet for the device without additional input to +# the policy. +# +# Override DHCP-Network-Subnet to be an address within the desired +# network to force selection of a particular address pool and/or network +# parameters. +# +# Note: If each subnet within a network is equally valid for the DHCP requests +# originating from that network then you do not need to call this policy, +# rather look at the examples concerning dhcp_subnet in +# mods-config/files/dhcp instead, which use a single pool containing addresses +# from all subnets then set the correct subnet-specific options based on the +# randomly assigned IP address. +# +#dhcp_override_network { +# if (&DHCP-Vendor-Class-Identifier && &DHCP-Vendor-Class-Identifier == "SIP100") +# update request { +# DHCP-Network-Subnet := 10.10.0.0 +# } +# } +#} + + +# Policy that calls the files instance of the same name after first making +# DHCP-Network-Subnet specific to the allocated IP address of the client. +#dhcp_subnet { +# update { +# &DHCP-Network-Subnet := "%{%{reply:DHCP-Your-IP-Address}:-%{DHCP-Client-IP-Address}}" +# } +# +# # Call the dhcp_subnet instance of the files module +# dhcp_subnet +#} + +# Assign compatibility data to request for sqlippool for DHCP Request +dhcp_sqlippool_request { + + # + # During initial address selection (DORA) the REQUEST is broadcast and + # requested-ip must be provided. We revoke any active offers for addresses + # not matching the requested-ip, i.e. those made by other servers when + # processing the DISCOVER. + # + # If there is only a single server then this optimisation can be disabled. + # + if (&DHCP-Requested-IP-Address) { + update request { + &Acct-Status-Type := Start + } + dhcp_sqlippool.accounting + } + + # Extend an existing offer or active lease + update request { + &Acct-Status-Type := Alive + } + dhcp_sqlippool.accounting { + notfound = return + } + + update reply { + &DHCP-Your-IP-Address := "%{%{DHCP-Requested-IP-Address}:-%{DHCP-Client-IP-Address}}" + } + +} + +# Assign compatibility data to request for sqlippool for DHCP Release +dhcp_sqlippool_release { + + # Do some minor hacks to the request so that it looks + # like a RADIUS Accounting Stop request to the SQL IP Pool module. + update request { + &Acct-Status-Type = Stop + } + + # Call the actual module in accounting context + dhcp_sqlippool.accounting + +} + +# Assign compatibility data to request for sqlippool for DHCP Decline +dhcp_sqlippool_decline { + + # Do a minor hack to the request so that it looks + # like a RADIUS Accounting Off request to the SQL IP Pool module. + update request { + &Acct-Status-Type = Accounting-Off + } + + # Call the actual module in accounting context + dhcp_sqlippool.accounting + +} + +# Example policy for fetching option data from SQL +dhcp_policy_sql { + + # + # Network-specific options + # + + # + # We want to lookup the Layer 2 network specific DHCP options to + # include in the reply. For this we need a stable identifier for the + # network from which the request is originating (based on + # DHCP-Network-Subnet) which can be used as the lookup key + # (DHCP-SQL-Option-Identifier) for the network options. + # + # Here we fabricate an example for the purpose of placing all + # configuration elements into SQL. We use a PostgreSQL query that + # returns the network identifier in the row containing the smallest + # enclosing CIDR, which assumes a schema such as the following: + # + # CREATE TABLE fr_network_to_identifier (network CIDR, network_id TEXT) + # + # Note: An rlm_files based lookup of the network_identifier (as per + # the examples in the dhcp virtual server) may be preferable to an ad + # hoc SQL query assuming that the network topology does not change + # frequently. + # +# update control { +# &control:Tmp-String-0 := "%{dhcp_sql:SELECT network_id \ +# FROM fr_network_to_identifier \ +# WHERE '%{DHCP-Network-Subnet}'::inet << network \ +# ORDER BY MASKLEN(network) DESC LIMIT 1;}" +# } + + # + # Use the network identifer to lookup the options specific to the + # originating network, using "network" context. Common network + # settings can be placed into a group and shared, with individual + # networks mapped to one or more option groups. + # + # - Place network-specific options in the dhcpreply table with + # "context = 'network'". + # - Add "Fall-Through := Yes" to the network options in the dhcpreply + # table to trigger group lookups for the network, which are + # disabled by default. + # - Place "identifier = <network_id>, groupname = <group>, + # priority = <priority>, context = 'network'" in the dhcpgroup + # table to map a network to a shared set of network options. + # - Place group-specific options in the dhcpgroupreply table with + # "context = 'network'". + # + # Note: In "shared-network" or "multinet" topologies you can instead + # just set all of the network options once in the subnet-specific + # options (after obtaining an IP address), below. + # +# update control { +# &DHCP-SQL-Option-Context := "network" +# &DHCP-SQL-Option-Identifier := &control:Tmp-String-0 +# } +# dhcp_sql.authorize + + + # + # Allocate IPs from the DHCP pool in SQL. + # + # Here we simply reuse the network_id (obtained previously) as the + # Pool-Name. + # +# update control { +# &Pool-Name := &control:Tmp-String-0 +# } +# dhcp_sqlippool + + + # + # Subnet-specific options + # + + # + # In "shared-network" or "multinet" topologies (in which a Layer 2 + # network has a single pool that contains addresses from multiple + # subnets) it is necessary to set subnet-specific options based on the + # address that has just been allocated. + # + # Again, for this we need to derive a stable identifier for the subnet + # to which the IP address we are issuing belongs that will serve as a + # lookup key for the network options. + # + # Continuing our previous example, we can use a PostgreSQL query to + # find the subnet identifer in the row with the closest enclosing + # CIDR, which assumes a schema such as the following: + # + # CREATE TABLE fr_subnet_to_identifier (subnet CIDR, subnet_id TEXT) + # + # Note: An rlm_files based lookup of the subnet_identifier (as per the + # examples in the dhcp virtual server) is preferable to an ad hoc SQL + # query assuming that the network topology does not change frequently. + # +# update control { +# &control:Tmp-String-0 := "%{dhcp_sql:SELECT subnet_id \ +# FROM fr_subnet_to_identifier \ +# WHERE '%{reply:DHCP-Your-IP-Address}'::inet << subnet \ +# ORDER BY MASKLEN(subnet) DESC LIMIT 1;}" +# } + + # + # Use the subnet identifer to lookup the options specific to the + # subnet for the IP we are allocating, using "subnet" context. Common + # subnet settings can be placed into a group and shared, with + # individual subnets mapped to one or more option groups. + # + # - Place subnet-specific options in the dhcpreply table with + # "context = 'subnet'". + # - Add "Fall-Through := Yes" to the subnet options in the dhcpreply + # table to trigger group lookups for the subnet, which are + # disabled by default. + # - Place "identifier = <subnet_id>, groupname = <group>, + # priority = <priority>, context = 'subnet'" in the dhcpgroup + # table to map a subnet to a shared set of subnet options. + # - Place group-specific options in the dhcpgroupreply table with + # "context = 'subnet'". + # +# update control { +# &DHCP-SQL-Option-Context := "subnet" +# &DHCP-SQL-Option-Identifier := &control:Tmp-String-0 +# } +# dhcp_sql.authorize + + + # + # Host-specific and group-specific options + # + + # "Groups" conventionally differentiate devices based on manual + # groupings using a device-specific identifier such as the MAC + # address. + # + # - Place host-specific options in the dhcpreply table with + # "context = 'group'". + # - Add "Fall-Through := Yes" to the device options in the dhcpreply + # table to trigger group lookups, which are disabled by default. + # - Place "identifier = <MAC-Address>, groupname = <group>, + # priority = <priority>, context='group'" in the dhcpgroup table + # to map a device to its groups. + # - Place group-specific options in the dhcpgroupreply table with + # "context = 'group'". + # +# update control { +# &DHCP-SQL-Option-Context := "group" +# &DHCP-SQL-Option-Identifier := &request:DHCP-Client-Hardware-Address +# } +# dhcp_sql.authorize + + + # + # Class/subclass-specific options + # + + # + # "Classes" conventionally differentiate devices based on all or part + # of one or more DHCP request options, or any combination of + # information that is available in the request or has already looked + # up from some datastore. + # + # Create multiple instances of the following block, one for each + # class. Differentiate between classes by setting + # DHCP-SQL-Option-Context uniquely. + # + # - Place "subclass"-specific options (i.e. each member of a class) + # in the dhcpreply table with "context = <class-name>". + # - For class-level options common to every member of a class, + # either: + # - Duplicate the options for each member of the subclass. + # or: + # - Add "Fall-Through := Yes" to each members options to trigger + # group lookups, which are disabled by default. + # - Map each member of the class to a group in the dhcpgroup + # table with context = '<class-name>'; + # - Create the corresponding class in the dhcpgroupreply table + # with "context = '<class-name>'". + # +# update control { +# &DHCP-SQL-Option-Context := "class-vci-substring" +# &DHCP-SQL-Option-Identifier := "%{substring %{request:DHCP-Vendor-Class-Identifier} 5 4}" +# } +# dhcp_sql.authorize + +} diff --git a/raddb/policy.d/eap b/raddb/policy.d/eap new file mode 100644 index 0000000..c8dac22 --- /dev/null +++ b/raddb/policy.d/eap @@ -0,0 +1,54 @@ +# +# Forbid all EAP types. Enable this by putting "forbid_eap" +# into the "authorize" section. +# +forbid_eap { + if (&EAP-Message) { + reject + } +} + +# +# Forbid all non-EAP types outside of an EAP tunnel. +# +permit_only_eap { + if (!&EAP-Message) { + # We MAY be inside of a TTLS tunnel. + # PEAP and EAP-FAST require EAP inside of + # the tunnel, so this check is OK. + # If so, then there MUST be an outer EAP message. + if (!&outer.request || !&outer.request:EAP-Message) { + reject + } + } +} + +# +# Remove Reply-Message from response if were doing EAP +# +# Be RFC 3579 2.6.5 compliant - EAP-Message and Reply-Message should +# not be present in the same response. +# +remove_reply_message_if_eap { + if (&reply:EAP-Message && &reply:Reply-Message) { + update reply { + &Reply-Message !* ANY + } + } + else { + noop + } +} + +verify_tls_client_common_name { + # + # If the User-Name is anonymized, then don't check it. + # + # But if User-Name is realm AND there's a certificate name, then check + # if they match. This is not always the case, but it is the case + # often enough that it matters. + # + if ((&User-Name !~ /^@/) && &TLS-Client-Cert-Common-Name && (&TLS-Client-Cert-Common-Name != &User-Name)) { + reject + } +} diff --git a/raddb/policy.d/filter b/raddb/policy.d/filter new file mode 100644 index 0000000..ff8f531 --- /dev/null +++ b/raddb/policy.d/filter @@ -0,0 +1,211 @@ +# +# Example of forbidding all attempts to login via +# realms. +# +deny_realms { + if (&User-Name && (&User-Name =~ /@|\\/)) { + reject + } +} + +# +# Filter the username +# +# Force some sanity on User-Name. This helps to avoid issues +# issues where the back-end database is "forgiving" about +# what constitutes a user name. +# +filter_username { + if (&User-Name) { + # + # reject mixed case e.g. "UseRNaMe" + # + #if (&User-Name != "%{tolower:%{User-Name}}") { + # reject + #} + + # + # reject all whitespace + # e.g. "user@ site.com", or "us er", or " user", or "user " + # + if (&User-Name =~ / /) { + update request { + &Module-Failure-Message += 'Rejected: User-Name contains whitespace' + } + reject + } + + # + # reject Multiple @'s + # e.g. "user@site.com@site.com" + # + if (&User-Name =~ /@[^@]*@/ ) { + update request { + &Module-Failure-Message += 'Rejected: Multiple @ in User-Name' + } + reject + } + + # + # reject double dots + # e.g. "user@site..com" + # + if (&User-Name =~ /\.\./ ) { + update request { + &Module-Failure-Message += 'Rejected: User-Name contains multiple ..s' + } + reject + } + + # + # must have at least 1 string-dot-string after @ + # e.g. "user@site.com" + # + if ((&User-Name =~ /@/) && (&User-Name !~ /@(.+)\.(.+)$/)) { + update request { + &Module-Failure-Message += 'Rejected: Realm does not have at least one dot separator' + } + reject + } + + # + # Realm ends with a dot + # e.g. "user@site.com." + # + if (&User-Name =~ /\.$/) { + update request { + &Module-Failure-Message += 'Rejected: Realm ends with a dot' + } + reject + } + + # + # Realm begins with a dot + # e.g. "user@.site.com" + # + if (&User-Name =~ /@\./) { + update request { + &Module-Failure-Message += 'Rejected: Realm begins with a dot' + } + reject + } + } +} + +# +# Filter the User-Password +# +# Some equipment sends passwords with embedded zeros. +# This policy filters them out. +# +filter_password { + if (&User-Password && \ + (&User-Password != "%{string:User-Password}")) { + update request { + &Tmp-String-0 := "%{string:User-Password}" + &User-Password := "%{string:Tmp-String-0}" + &Tmp-String-0 !* "" + } + } +} + +filter_inner_identity { + # + # No names, reject. + # + if (!&outer.request:User-Name || !&User-Name) { + update request { + Module-Failure-Message = "User-Name is required for tunneled authentication" + } + reject + } + + # + # Do detailed checks only if the inner and outer + # NAIs are different. + # + # If the NAIs are the same, it violates user privacy, + # but is allowed. + # + if (&outer.request:User-Name != &User-Name) { + # + # Get the outer realm. + # + if (&outer.request:User-Name =~ /@([^@]+)$/) { + update request { + Outer-Realm-Name = "%{1}" + } + + # + # When we have an outer realm name, the user portion + # MUST either be empty, or begin with "anon". + # + # We don't check for the full "anonymous", because + # some vendors don't follow the standards. + # + if (&outer.request:User-Name !~ /^(anon|@)/) { + update request { + Module-Failure-Message = "User-Name is not anonymized" + } + reject + } + } + + # + # There's no outer realm. The outer NAI is different from the + # inner NAI. The User-Name MUST be anonymized. + # + # Otherwise, you could log in as outer "bob", and inner "doug", + # and we'd have no idea which one was correct. + # + elsif (&outer.request:User-Name !~ /^anon/) { + update request { + Module-Failure-Message = "User-Name is not anonymized" + } + reject + } + + # + # Get the inner realm. + # + if (&User-Name =~ /@([^@]+)$/) { + update request { + Inner-Realm-Name = "%{1}" + } + + # + # Note that we do EQUALITY checks for realm names. + # There is no simple way to do case insensitive checks + # on internationalized domain names. There is no reason + # to allow outer "anonymous@EXAMPLE.COM" and inner + # "user@example.com". The user should enter the same + # realm for both identities. + # + # If the inner realm isn't the same as the outer realm, + # the inner realm MUST be a subdomain of the outer realm. + # + if (&Outer-Realm-Name && \ + (&Inner-Realm-Name != &Outer-Realm-Name) && \ + (&Inner-Realm-Name !~ /\.%{Outer-Realm-Name}$/)) { + update request { + Module-Failure-Message = "Inner realm '%{Inner-Realm-Name}' and outer realm '%{Outer-Realm-Name}' are not from the same domain." + } + reject + } + + # + # It's OK to have an inner realm and no outer realm. + # + # That won't work for roaming, but the local RADIUS server + # can still authenticate the user. + # + } + + # + # It's OK to have an outer realm and no inner realm. + # + # It will work for roaming, and the local RADIUS server + # can authenticate the user without the realm. + # + } +} diff --git a/raddb/policy.d/moonshot-targeted-ids b/raddb/policy.d/moonshot-targeted-ids new file mode 100644 index 0000000..98ae4a1 --- /dev/null +++ b/raddb/policy.d/moonshot-targeted-ids @@ -0,0 +1,249 @@ +# +# The following policies generate targeted IDs for ABFAB (Moonshot) +# +# This policy requires that the UUID package is installed on your platform +# and that this is called from the inner-tunnel +# +# The following string attributes need to exist in the UKERNA dictionary +# Moonshot-Host-TargetedId (138) +# Moonshot-Realm-TargetedId (139) +# Moonshot-TR-COI-TargetedId (140) +# Moonshot-MSTID-GSS-Acceptor (141) +# Moonshot-MSTID-Namespace (142) +# Moonshot-MSTID-TargetedId (143) +# +# These attributes should also be listed in the attr_filter policies +# post-proxy and pre-proxy when you use attribute filtering: +# Moonshot-Host-TargetedId =* ANY, +# Moonshot-Realm-TargetedId =* ANY, +# Moonshot-TR-COI-TargetedId =* ANY, +# + +# +# targeted_id_salt definition +# This salt serves the purpose of protecting targeted IDs against +# dictionary attacks, therefore should be chosen as a "random" +# string and kept secret. +# +# If you use special characters %, { and }, escape them with a \ first +# +targeted_id_salt = 'changeme' + +# +# Moonshot namespaces +# These namespaces are used for UUID generation. +# They should not be changed by implementors +# +moonshot_host_namespace = 'a574a04e-b7ff-4850-aa24-a8599c7de1c6' +moonshot_realm_namespace = 'dea5f26d-a013-4444-977d-d09fc990d2e6' +moonshot_coi_namespace = '145d7e7e-7d54-43ee-bbcb-3c6ad9428247' + + +# This policy generates a host-specific TargetedId +# +moonshot_host_tid.post-auth { + # retrieve or generate a UUID for Moonshot-Host-TargetedId + if (&outer.request:GSS-Acceptor-Host-Name) { + # prep some variables (used regardless of SQL backing or not!) + update control { + Moonshot-MSTID-GSS-Acceptor := "%{tolower:%{outer.request:GSS-Acceptor-Host-Name}}" + Moonshot-MSTID-Namespace := "${policy.moonshot_host_namespace}" + } + + # if you want to use SQL-based backing, remove the comment from + # this line. You also have to configure and enable the + # moonshot-targeted-ids sql module in mods-enabled. + # +# moonshot_get_targeted_id + + # generate a UUID for Moonshot-Host-TargetedId + if (!&control:Moonshot-MSTID-TargetedId) { + # generate the TID + moonshot_make_targeted_id + + # if you want to store your TargetedId in SQL-based backing, + # remove the comment from this line. You also have to configure + # and enable the moonshot-targeted-ids sql module in mods-enabled. + # +# moonshot_tid_sql + } + + # set the actual TargetedId in the session-state list + if (&control:Moonshot-MSTID-TargetedId) { + update outer.session-state { + Moonshot-Host-TargetedId := &control:Moonshot-MSTID-TargetedId + } + update control { + Moonshot-MSTID-TargetedId !* ANY + } + } + + # Sanitise the control list to remove the internal attributes + update control { + Moonshot-MSTID-GSS-Acceptor !* ANY + Moonshot-MSTID-Namespace !* ANY + } + } +} + +# This policy generates a realm-specific TargetedId +# +moonshot_realm_tid.post-auth { + # retrieve or generate a UUID for Moonshot-Realm-TargetedId + if (&outer.request:GSS-Acceptor-Realm-Name) { + # prep some variables (used regardless of SQL backing or not!) + update control { + Moonshot-MSTID-GSS-Acceptor := "%{tolower:%{outer.request:GSS-Acceptor-Realm-Name}}" + Moonshot-MSTID-Namespace := "${policy.moonshot_realm_namespace}" + } + + # if you want to use SQL-based backing, remove the comment from + # this line. You also have to configure and enable the + # moonshot-targeted-ids sql module in mods-enabled. + # +# moonshot_get_targeted_id + + # generate a UUID for Moonshot-Realm-TargetedId + if (!&control:Moonshot-MSTID-TargetedId) { + # generate the TID + moonshot_make_targeted_id + + # if you want to store your TargetedId in SQL-based backing, + # remove the comment from this line. You also have to configure + # and enable the moonshot-targeted-ids sql module in mods-enabled. + # +# moonshot_tid_sql + } + + # set the actual TargetedId in the session-state list + if (&control:Moonshot-MSTID-TargetedId) { + update outer.session-state { + Moonshot-Realm-TargetedId := &control:Moonshot-MSTID-TargetedId + } + update control { + Moonshot-MSTID-TargetedId !* ANY + } + } + + # Sanitise the control list to remove the internal attributes + update control { + Moonshot-MSTID-GSS-Acceptor !* ANY + Moonshot-MSTID-Namespace !* ANY + } + } +} + +# This policy generates a COI-specific targeted ID +# +moonshot_coi_tid.post-auth { + # retrieve or generate a UUID for Moonshot-TR-COI-TargetedId + if (&outer.request:Trust-Router-COI) { + # prep some variables (used regardless of SQL backing or not!) + update control { + Moonshot-MSTID-GSS-Acceptor := "%{tolower:%{outer.request:Trust-Router-COI}}" + Moonshot-MSTID-Namespace := "${policy.moonshot_coi_namespace}" + } + + # if you want to use SQL-based backing, remove the comment from + # this line. You also have to configure and enable the + # moonshot-targeted-ids sql module in mods-enabled. + # +# moonshot_get_targeted_id + + # generate a UUID for Moonshot-TR-COI-TargetedId + if (!&control:Moonshot-MSTID-TargetedId) { + # generate the TID + moonshot_make_targeted_id + + # if you want to store your TargetedId in SQL-based backing, + # remove the comment from this line. You also have to configure + # and enable the moonshot-targeted-ids sql module in mods-enabled. + # +# moonshot_tid_sql + } + + # set the actual TargetedId in the session-state list + if (&control:Moonshot-MSTID-TargetedId) { + update outer.session-state { + Moonshot-TR-COI-TargetedId := &control:Moonshot-MSTID-TargetedId + } + update control { + Moonshot-MSTID-TargetedId !* ANY + } + } + + # Sanitise the control list to remove the internal attributes + update control { + Moonshot-MSTID-GSS-Acceptor !* ANY + Moonshot-MSTID-Namespace !* ANY + } + } +} + +# This is the generic generation policy. It requires moonshot_host_tid, moonshot_realm_tid, or moonshot_coi_tid to set variables +# +moonshot_make_targeted_id.post-auth { + # uses variables set in the control list + # + if (&control:Moonshot-MSTID-Namespace && &control:Moonshot-MSTID-GSS-Acceptor) { + # targeted id = (uuid -v 5 [namespace] [username][salt][GSS acceptor value])@[IdP realm name] + # + if ("%{echo:/usr/bin/uuid -v 5 %{control:Moonshot-MSTID-Namespace} %{tolower:%{User-Name}}${policy.targeted_id_salt}%{control:Moonshot-MSTID-GSS-Acceptor}}" =~ /^([^ ]+)([ ]*)$/) { + update control { + Moonshot-MSTID-TargetedId := "%{1}@%{tolower:%{request:Realm}}" + } + if (&control:Moonshot-MSTID-TargetedId =~ /([\%\{\}]+)/) { + update control { + Moonshot-MSTID-TargetedId !* ANY + } + update outer.session-state { + Module-Failure-Message = 'Invalid TargetedId generated, check your targeted_id_salt!' + } + reject + } + } + else { + # we simply return the 'echo' error message as the Module-Failure-Message, usually a lack of 'uuid' + reject + } + } + else { + # Our variables were not set, so we'll throw an error because there's no point in continuing! + update outer.session-state { + Module-Failure-Message = 'Required variables for moonshot_make_targeted_id not set!' + } + reject + } +} + +# This is the generic retrieval policy. It requires moonshot_host_tid, moonshot_realm_tid, or moonshot_coi_tid to set variables +# +moonshot_get_targeted_id.post-auth { + # uses variables set in the control list + # + if (&control:Moonshot-MSTID-Namespace && &control:Moonshot-MSTID-GSS-Acceptor) { + # retrieve the TargetedId + # + update control { + Moonshot-MSTID-TargetedId := "%{moonshot_tid_sql:\ + SELECT targeted_id FROM moonshot_targeted_ids \ + WHERE gss_acceptor = '%{control:Moonshot-MSTID-GSS-Acceptor}' \ + AND namespace = '%{control:Moonshot-MSTID-Namespace}' \ + AND username = '%{tolower:%{User-Name}}'}" + } + + # if the value is empty, there's no point in setting it and delete it from the control list! + if (&control:Moonshot-MSTID-TargetedId == '') { + update control { + Moonshot-MSTID-TargetedId !* ANY + } + } + } + else { + # Our variables were not set, so we'll throw an error because there's no point in continuing! + update outer.session-state { + Module-Failure-Message = 'Required variables for moonshot_get_targeted_id not set!' + } + reject + } +} diff --git a/raddb/policy.d/operator-name b/raddb/policy.d/operator-name new file mode 100644 index 0000000..6d042d4 --- /dev/null +++ b/raddb/policy.d/operator-name @@ -0,0 +1,46 @@ +# +# The following policies are for the Operator-Name +# configuration. +# +# The policies below can be called as just 'operator-name' (not +# operator-name.authorize etc..) from the various config sections. +# + +# If you require that the Operator-Name be set +# for local clients then call the 'operator-name' policy +# in the authorize section of the virtual-server for your clients in clients.conf + +# To inject an Operator-Name whilst proxying, call the +# 'operator-name' policy in the pre-proxy section of the virtual server +# No need to call this if you have already enabled this in +# the authorize section. + +# +# We assume that clients can have the operator-name definition +# in the client.conf, e.g. +# client xxxx { +# ... +# Operator-Name = 1your.domain +# } +# If this parameter is found for a client, then we add +# an Operator-Name attribute +# +operator-name.authorize { + if ("%{client:Operator-Name}") { + update request { + &Operator-Name = "%{client:Operator-Name}" + } + } +} + +# +# Before proxing the client add an Operator-Name +# attribute identifying this site if the operator-name is found for this client +# +operator-name.pre-proxy { + if (("%{request:Packet-Type}" == 'Access-Request') && "%{client:Operator-Name}") { + update proxy-request { + &Operator-Name := "%{client:Operator-Name}" + } + } +} diff --git a/raddb/policy.d/rfc7542 b/raddb/policy.d/rfc7542 new file mode 100644 index 0000000..84a5c17 --- /dev/null +++ b/raddb/policy.d/rfc7542 @@ -0,0 +1,46 @@ +# +# The following policy is for RFC7542-style bang path +# management. +# +# It hands control from the standard 'suffix' realm +# processor to the 'bangpath' processer, allowing the +# definition of specific routing information in the +# decoration of the User-Name. +# +# Use this with caution. In particular, read the following +# RFC document sections for reasons why you shouldn't use +# this, and also why this is used: +# +# 1. https://tools.ietf.org/html/rfc4282#section-2.7 +# 2. https://tools.ietf.org/html/rfc7542#section-3.3.1 +# +# $Id$ +# + +# This is a |-separated list of realms this specific service +# is responsible for. We cannot read this from the proxy.conf +# file, so we turn this into an 'or list' regex. +# Examples: rfc7542_realms = 'example.com' +# rfc7542_realms = 'example.com|another.net|this.org' +# +rfc7542_realms = 'changeme' + +# This policy checks the User-Name attribute whether it is in +# RFC7542 bang-path format. If it is, it lets the bangpath realm +# processor handle it, otherwise it leaves it for suffix to handle +# +rfc7542.authorize { + # Format: not_local_realm!...@local_realm: Handle with bangpath + if ( (&request:User-Name =~ /(.+)!(.*)\@(${policy.rfc7542_realms})/) && \ + !(&request:User-Name =~ /(${policy.rfc7542_realms})!(.*)\@(.+)/) ) { + bangpath + updated + } + + # Format: local_realm!...@not_local_realm: Handle with bangpath + elsif ( (&request:User-Name =~ /(${policy.rfc7542_realms})!(.*)\@(.+)/) && \ + !(&request:User-Name =~ /(.+)!(.*)\@(${policy.rfc7542_realms})/) ) { + bangpath + updated + } +} |