From 1796f6f4c06b11b138f0843a46e746b60ff35cf6 Mon Sep 17 00:00:00 2001 From: Arran Cudbard-Bell Date: Tue, 17 Jul 2012 21:43:16 +0100 Subject: [PATCH] Move policy.conf stanzas into individual configuration files --- .gitignore | 1 - raddb/Makefile | 5 +- raddb/policy.conf | 419 ---------------------------------------- raddb/policy.d/accounting | 63 ++++++ raddb/policy.d/canonicalization | 91 +++++++++ raddb/policy.d/control | 12 ++ raddb/policy.d/cui | 72 +++++++ raddb/policy.d/dhcp | 25 +++ raddb/policy.d/filter | 135 +++++++++++++ raddb/radiusd.conf.in | 17 +- 10 files changed, 414 insertions(+), 426 deletions(-) delete mode 100644 raddb/policy.conf create mode 100644 raddb/policy.d/accounting create mode 100644 raddb/policy.d/canonicalization create mode 100644 raddb/policy.d/control create mode 100644 raddb/policy.d/cui create mode 100644 raddb/policy.d/dhcp create mode 100644 raddb/policy.d/filter diff --git a/.gitignore b/.gitignore index 5df2284..3d2c460 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ *~ *.o *.a -*.d *.lo *.loT *.la diff --git a/raddb/Makefile b/raddb/Makefile index 6e81ce2..23ecf37 100644 --- a/raddb/Makefile +++ b/raddb/Makefile @@ -13,7 +13,7 @@ FILES = acct_users attrs attrs.access_reject attrs.accounting_response \ attrs.pre-proxy clients.conf dictionary templates.conf \ experimental.conf hints huntgroups ldap.attrmap \ policy.txt preproxy_users proxy.conf radiusd.conf trigger.conf \ - users policy.conf attrs.access_challenge README + users attrs.access_challenge README.rst DEFAULT_SITES = default inner-tunnel @@ -58,8 +58,9 @@ install: $(INSTALL) -d -m 750 $(R)$(raddbdir)/sites-enabled $(INSTALL) -d -m 750 $(R)$(raddbdir)/mods-available $(INSTALL) -d -m 750 $(R)$(raddbdir)/mods-enabled + $(INSTALL) -d -m 750 $(R)$(raddbdir)/policy.d @echo "Creating/updating files in $(R)$(raddbdir)"; \ - for i in $(FILES) `find sites-available/ mods-available/ -type f -print | sed 's/.*~//;s/.*#.*//' `; do \ + for i in $(FILES) `find sites-available/ mods-available/ policy.d/ -type f -print | sed 's/.*~//;s/.*#.*//' `; do \ [ ! -f $(R)$(raddbdir)/$$i ] && $(INSTALL) -m 640 $$i $(R)$(raddbdir)/$$i; \ if [ "`find $$i -newer $(R)$(raddbdir)/$$i`" ]; then \ echo "** $(R)$(raddbdir)/$$i"; \ diff --git a/raddb/policy.conf b/raddb/policy.conf deleted file mode 100644 index 7a935b5..0000000 --- a/raddb/policy.conf +++ /dev/null @@ -1,419 +0,0 @@ -# -*- text -*- -## -## policy.conf -- FreeRADIUS server configuration file. -## -## http://www.freeradius.org/ -## $Id$ -## - -# -# Policies are virtual modules, similar to those defined in the -# "instantate" section of radiusd.conf. -# -# Defining a policy here means that it can be referenced in multiple -# places as a *name*, rather than as a series of conditions to match, -# and actions to take. -# -# Policies are something like subroutines in a normal language, but -# they cannot be called recursively. They MUST be defined in order. -# If policy A calls policy B, then B MUST be defined before A. -# -policy { - # 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:' - - # - # Overload the default acct_unique module, it's not - # smart enough. - # - 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). - # - if("%{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-IP-Address}%{NAS-Port-ID}%{NAS-Port}}" - } - } - } - - # - # Insert a (hopefully unique) value into class - # - insert_acct_class { - update reply { - Class = "${policy.class_value_prefix}%{md5:%t%I%{Packet-Src-Port}%{Packet-Src-IP-Address}%{NAS-IP-Address}%{Calling-Station-ID}%{User-Name}}" - } - } - - # - # 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: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 - } - } - - # - # 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 =~ /${policy.nai_regexp}/){ - update request { - Stripped-User-Name := "%{1}" - 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 =~ /${policy.nai_regexp}/){ - update proxy-reply { - Stripped-User-Name := "%{1}" - Stripped-User-Domain = "%{3}" - } - updated - } - else { - noop - } - } - - # - # Example of forbidding all attempts to login via - # realms. - # - deny_realms { - if (User-Name =~ /@|\\/) { - reject - } - } - - # - # 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 - } - - # - # 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 { - # - # 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 reply { - Reply-Message += "Rejected: Username contains whitespace" - } - reject - } - - # - # reject Multiple @'s - # e.g. "user@site.com@site.com" - # - if(User-Name =~ /@(.+)?@/i ) { - update reply { - Reply-Message += "Rejected: Multiple @ in username" - } - reject - } - - # - # reject double dots - # e.g. "user@site..com" - # - if (User-Name =~ /\\.\\./ ) { - update reply { - Reply-Message += "Rejected: Username comtains ..s" - } - reject - } - - # - # must have at least 1 string-dot-string after @ - # e.g. "user@site.com" - # - if (User-Name !~ /@(.+)\\.(.+)$/) { - update reply { - Reply-Message += "Rejected: Realm does not have at least one dot seperator" - } - reject - } - - # - # Realm ends with a dot - # e.g. "user@site.com." - # - if (User-Name =~ /\\.$/) { - update reply { - Reply-Message += "Rejected: Realm ends with a dot" - } - reject - } - - # - # Realm begins with a dot - # e.g. "user@.site.com" - # - if (User-Name =~ /@\\./) { - update reply { - Reply-Message += "Rejected: Realm begins with a dot" - } - reject - } - } - - # - # 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. - # - - # - # The client indicates it can do CUI by sending a CUI attribute - # containing one zero byte - # - cui.authorize { - update request { - Chargeable-User-Identity:='\\000' - } - } - - # - # Add a CUI attribute based on the User-Name, and a secret key - # known only to this server. - # - cui.post-auth { - if (FreeRadius-Proxied-To == 127.0.0.1) { - if (outer.request:Chargeable-User-Identity) { - update outer.reply { - Chargeable-User-Identity:="%{md5:%{config:cui_hash_key}%{User-Name}}" - } - } - } - else { - if (Chargeable-User-Identity) { - update reply { - Chargeable-User-Identity="%{md5:%{config:cui_hash_key}%{User-Name}}" - } - } - } - } - - - # - # 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 := "%{cui: 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 != "")) { - cui - } - } - - # - # If there is a CUI attribute in the reply, add it to the DB. - # - cui_updatedb { - if (reply:Chargeable-User-Identity) { - cui - } - } - - # - # 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. - # - rewrite_called_station_id { - if(Called-Station-Id =~ /^${policy.mac-addr-regexp}(:(.+))?$/i) { - update request { - Called-Station-Id := "%{tolower:%{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. - # - rewrite_calling_station_id { - if(Calling-Station-Id =~ /^${policy.mac-addr-regexp}$/i) { - update request { - Calling-Station-Id := "%{tolower:%{1}-%{2}-%{3}-%{4}-%{5}-%{6}}" - } - updated - } - else { - noop - } - } - - # Assign compatibility data to request for sqlippool - dhcp_sqlippool.post-auth { - - - # Do some minor hacks to the request so that it looks - # like a RADIUS request to the SQL IP Pool module. - update request { - User-Name = "DHCP-%{DHCP-Client-Hardware-Address}" - Calling-Station-Id = "%{DHCP-Client-Hardware-Address}" - NAS-IP-Address = "%{%{DHCP-Gateway-IP-Address}:-127.0.0.1}" - Acct-Status-Type = Start - } - - # Call the actual module - dhcp_sqlippool - - # Convert Framed-IP-Address to DHCP, but only if we - # actually allocated an address. - if (ok) { - update reply { - DHCP-Your-IP-Address = "%{reply:Framed-IP-Address}" - } - } - } -} diff --git a/raddb/policy.d/accounting b/raddb/policy.d/accounting new file mode 100644 index 0000000..02f1cc5 --- /dev/null +++ b/raddb/policy.d/accounting @@ -0,0 +1,63 @@ +# 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:' + +# +# Overload the default acct_unique module, it's not +# smart enough. +# +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). + # + if("%{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-IP-Address}%{NAS-Port-ID}%{NAS-Port}}" + } + } +} + +# +# Insert a (hopefully unique) value into class +# +insert_acct_class { + update reply { + Class = "${policy.class_value_prefix}%{md5:%t%I%{Packet-Src-Port}%{Packet-Src-IP-Address}%{NAS-IP-Address}%{Calling-Station-ID}%{User-Name}}" + } +} + diff --git a/raddb/policy.d/canonicalization b/raddb/policy.d/canonicalization new file mode 100644 index 0000000..f9666c1 --- /dev/null +++ b/raddb/policy.d/canonicalization @@ -0,0 +1,91 @@ +# +# 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 =~ /${policy.nai_regexp}/){ + update request { + Stripped-User-Name := "%{1}" + 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 =~ /${policy.nai_regexp}/){ + update proxy-reply { + Stripped-User-Name := "%{1}" + 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. +# +rewrite_called_station_id { + if(Called-Station-Id =~ /^${policy.mac-addr-regexp}(:(.+))?$/i) { + update request { + Called-Station-Id := "%{tolower:%{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. +# +rewrite_calling_station_id { + if(Calling-Station-Id =~ /^${policy.mac-addr-regexp}$/i) { + update request { + Calling-Station-Id := "%{tolower:%{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..cbf23af --- /dev/null +++ b/raddb/policy.d/control @@ -0,0 +1,12 @@ +# +# 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 +} + diff --git a/raddb/policy.d/cui b/raddb/policy.d/cui new file mode 100644 index 0000000..52c4de6 --- /dev/null +++ b/raddb/policy.d/cui @@ -0,0 +1,72 @@ +# +# 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. +# + +# +# The client indicates it can do CUI by sending a CUI attribute +# containing one zero byte +# +cui.authorize { + update request { + Chargeable-User-Identity:='\\000' + } +} + +# +# Add a CUI attribute based on the User-Name, and a secret key +# known only to this server. +# +cui.post-auth { + if (FreeRadius-Proxied-To == 127.0.0.1) { + if (outer.request:Chargeable-User-Identity) { + update outer.reply { + Chargeable-User-Identity:="%{md5:%{config:cui_hash_key}%{User-Name}}" + } + } + } + else { + if (Chargeable-User-Identity) { + update reply { + Chargeable-User-Identity="%{md5:%{config:cui_hash_key}%{User-Name}}" + } + } + } +} + + +# +# 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 := "%{cui: 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 != "")) { + cui + } +} + +# +# If there is a CUI attribute in the reply, add it to the DB. +# +cui_updatedb { + if (reply:Chargeable-User-Identity) { + cui + } +} + diff --git a/raddb/policy.d/dhcp b/raddb/policy.d/dhcp new file mode 100644 index 0000000..4396f06 --- /dev/null +++ b/raddb/policy.d/dhcp @@ -0,0 +1,25 @@ +# Assign compatibility data to request for sqlippool +dhcp_sqlippool.post-auth { + + + # Do some minor hacks to the request so that it looks + # like a RADIUS request to the SQL IP Pool module. + update request { + User-Name = "DHCP-%{DHCP-Client-Hardware-Address}" + Calling-Station-Id = "%{DHCP-Client-Hardware-Address}" + NAS-IP-Address = "%{%{DHCP-Gateway-IP-Address}:-127.0.0.1}" + Acct-Status-Type = Start + } + + # Call the actual module + dhcp_sqlippool + + # Convert Framed-IP-Address to DHCP, but only if we + # actually allocated an address. + if (ok) { + update reply { + DHCP-Your-IP-Address = "%{reply:Framed-IP-Address}" + } + } +} + diff --git a/raddb/policy.d/filter b/raddb/policy.d/filter new file mode 100644 index 0000000..c3b48fa --- /dev/null +++ b/raddb/policy.d/filter @@ -0,0 +1,135 @@ +# +# 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: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 + } +} + +# +# Example of forbidding all attempts to login via +# realms. +# +deny_realms { + if (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 { + # + # 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 reply { + Reply-Message += "Rejected: Username contains whitespace" + } + reject + } + + # + # reject Multiple @'s + # e.g. "user@site.com@site.com" + # + if(User-Name =~ /@(.+)?@/i ) { + update reply { + Reply-Message += "Rejected: Multiple @ in username" + } + reject + } + + # + # reject double dots + # e.g. "user@site..com" + # + if (User-Name =~ /\\.\\./ ) { + update reply { + Reply-Message += "Rejected: Username comtains ..s" + } + reject + } + + # + # must have at least 1 string-dot-string after @ + # e.g. "user@site.com" + # + if (User-Name !~ /@(.+)\\.(.+)$/) { + update reply { + Reply-Message += "Rejected: Realm does not have at least one dot seperator" + } + reject + } + + # + # Realm ends with a dot + # e.g. "user@site.com." + # + if (User-Name =~ /\\.$/) { + update reply { + Reply-Message += "Rejected: Realm ends with a dot" + } + reject + } + + # + # Realm begins with a dot + # e.g. "user@.site.com" + # + if (User-Name =~ /@\\./) { + update reply { + Reply-Message += "Rejected: Realm begins with a dot" + } + reject + } +} + diff --git a/raddb/radiusd.conf.in b/raddb/radiusd.conf.in index 584fc34..6e68f3a 100644 --- a/raddb/radiusd.conf.in +++ b/raddb/radiusd.conf.in @@ -822,12 +822,21 @@ instantiate { ###################################################################### # -# Policies that can be applied in multiple places are listed -# globally. That way, they can be defined once, and referred -# to multiple times. +# Policies are virtual modules, similar to those defined in the +# "instantate" section above. +# +# Defining a policy in one of the policy.d files means that it can be +# referenced in multiple places as a *name*, rather than as a series of +# conditions to match, and actions to take. +# +# Policies are something like subroutines in a normal language, but +# they cannot be called recursively. They MUST be defined in order. +# If policy A calls policy B, then B MUST be defined before A. # ###################################################################### -$INCLUDE policy.conf +policy { + $INCLUDE policy.d/ +} ###################################################################### # -- 2.1.4