Fortigate DNS Filter - All You Need to Know (almost)


Intro

Few facts to remember:

  • The DNS query/response traffic HAS to cross the Fortigate for it to be inspected/filtered.

  • You do NOT need to set the Fortinet/FortiGuard DNS servers as DNS resolvers in Fortigate. Even better - do NOT use Fortinet DNS servers as regular DNS resolvers, you will have problems.

DNS Filtering allows us to control/filter DNS queries crossing the Fortigate from clients, this way preventing DNS resolving the names to IP addresses for malicious/unwanted domains.

As Web Filtering, DNS Filtering also gives us option to use Fortiguard-based category filtering, and Static Domain names filtering where we configure wildcard/regex domain names to match. In addition, DNS Filter has Botnet Command Center block, using dynamically updated IPs/domains list of known bad, this way blocking malware in LAN contacting them on the Internet.

Botnet C&C Block and Category-based filtering is a licensed feature - you need Web Filtering license for that. DNS Filtering uses the same FortiGuard servers and the same websites/domain ratings as the Web Filter does. The mechanism is different, though. DNS Filtering watches/controls DNS resolving queries only, not the following traffic.

THe DNS protocols supported are clear text port 53 TCP/UDP queries protocol, and DNS over TLS encrypted in TLS DNS queries on port 853 (DOT). For controlling DOT traffic the Deep SSL Inspeciton profile has to be used, and the FortiOS version should be 7.0.x or newer.

DNS Filter has 3 possible actions: Allow, Monitor, Redirect to Block Portal.

  • Redirect with Block - for bad/banned domains, DNS Filter replaces A record of the resolved bad domain with IP address of the FortiGuard server which holds the vanilla web page with the block message (in case the blocked DNS query was for the HTTP/HTTPS server so client can see this page). The non-HTTP traffic will be redirected to the same server, obvioulsy, but w/o any indication what happened - end user will see time out in her application.

  • Allow - passes the DNS query and answer unaltered, thus resolving the domain to its original IP address.

  • Monitor - well, monitors DNS traffic, logging DNS queries and responses, see below on Proxy vs Flow mode.

DNS Profile can be applied in 2 places - either in a specific Security Policy, or on the DNS server configuration for the interface, when enabling Fortigate to do recursive queries for clients on that interface.

Order of processing: Static Domains list → Remote Category → Category Fortiguard-based rating.

If the Static Domain filter action is set to Allow - Fortigate does NOT check Category-based filters for matched domains. If, on the other hand, Static Domain filter is set to Monitor, and domain is matched, then in Proxy mode it will be allowed, but in Flow mode will be sent for a check by Category-based filter.

Block for the page/domain using HTTPS will show browser error to the end user on net::ERR_CERT_COMMON_NAME_INVALID, as the resulting block page hosted by Fortiguard (and its SSL/TLS certificate) has nothing to do with the original website the user tried to enter.

Note
Whatever happens in DNS Filtering has no relation to further checks by Web Filter if it is enabled in the same security rule as well. The 2 features are independent of each other.
Note
All the examples below were done using Proxy mode security policy. When using Flow mode, IPS is mostly responsible for DNS filtering and the debug should also be dia ips debug enable dns.

Local/Static Domain Filter

Just as with Web Filtering, we can manually create list of domains to match. The domains can be in the form of Simple, Wildcard, and Regex.

The Local/Static Domain filter will look like that:

  • Static Domain filter table of domains:

config dnsfilter domain-filter
    edit 1
        set name "Auto-dnsfilter-domain-filter_uuctvw6su"
        config entries
            edit 1
                set domain "*.yurisk.info"
                set type wildcard
                set action allow
            next
            edit 2
                set domain "*"
                set type wildcard
            next
        end
    next
    edit 2
        set name "Auto-dnsfilter-domain-filter_a28yutjw3"
        config entries
            edit 1
                set domain "mail.ru"
            next
            edit 2
                set domain "*.fox.com"
                set type wildcard
            next
        end
    next
    edit 3
        set name "Auto-dnsfilter-domain-filter_965xz31fp"
        config entries
            edit 1
                set domain "*.cisco.com"
                set type wildcard
            next
            edit 2
                set domain ".*check.*\\.com"
                set type regex
            next
        end
    next
end
  • DNS FIlter Profile, combining Static and Fortiguard-based filters:

config dnsfilter profile
    edit "DNSProfile"
        config domain-filter
            set domain-filter-table 3 <-- STATIC DOMAIN FILTER TABLE ID
        end
        config ftgd-dns
            config filters
                edit 1
                    set category 26
                    set action block
                next
                edit 7
                    set category 46
                    set action block
                next
            end
        end
        set log-all-domain enable
        set block-botnet enable <-- ENABLE C&C BLOCK
    next
end

In logs, Security Events → DNS Query, the block (actually Action = redirect), will look:

fortigate dns filter1
fortigate dns filter2

The end user will see the default block page:

fortigate dns filter3

Remote Category

Fortiguard-based Categories

This one works exactly as with Web Filtering, only for DNS protocol queries. In debug output, the Fortiguard servers are called SDNS servers. By default, Fortigate uses DOH (encrypted) DNS protocol to send rating requests to SDNS server.

fortigate dns filter18

Domains Feed

This option enables us to specify external domain list, hosted on a web server and not on Fortiguard servers/Fortigate, for Fortigate to periodically download it and use in DNS Filter (Category Web Filtering should be enabled) to block/allow access to the domains in the list.

The list should be a text file in the format (the file name does not matter), only Simple/Wildcards names are supported:

threat.txt
cnn.com
facebook.com
*.reddit.com

After we created such threat list on external web server, we create an External Connector in the Fabric Connectors:

fortigate web filter remote threat

Where we set the URL to fetch the external threat feed, with optional authentication and configurable refresh time (default 5 mins):

fortigate web filter remote threat2
fortigate web filter remote threat3

After some refreshing the status turns green (connected) and we can actually see the list of domains in the feed and their status:

fortigate web filter remote threat4
fortigate web filter remote threat5

Now, we can see this Remote feed appear in the Categories and we can set its action to Block:

fortigate dns filter4

Config on CLI:

External connector
config system external-resource
    edit "Remote threat feed"
        set type domain
        set category 192
        set resource "https://yurisk.info/threat/threat.txt"
    next
end

Now we use this custom category (number 192, will differ per FortiOS version) with the action Block and Redirect in the Fortigaurd Category section of the DNS Filter profile:

    edit "DNSProfile"
        config domain-filter
            set domain-filter-table 3
        end
        config ftgd-dns
            config filters
                            edit 9
                    set category 192
                    set action block
                next
            end
        end
    end
end

IP addresses feed

This works as above but instead of the domains list, we provide to the Fortigate an externally hosted text file that lists IP addresses (specific, netwrok masked). And when Fortigate sees such IP address in DNS response, it replaces it with the default IP of Fortigaurd block page.

The configuration is the same as above Domain list, just choose in the External Connectors IP Address as a connector:

fortigate dns filter5

And configure with the URL of external feed to download:

fortigate dns filter7

The IP addresses list will look like:

ip-list.txt
192.0.64.0/18

And it will be located at https://yurisk.info/threat/ip-list.txt.

After the Fortigate successfully contacts and downloads the remote feed, we can use it in Block statement of the DNS Filter profile:

fortigate dns filter8

Now, we use this DNS Filter profile in a security rule, and it will block resolving to any IP in the range specified:

fortigate dns filter9

CLI Configuration:

External connector
config system external-resource
    edit "IP-block-list-feed"
        set type address
        set resource "https://yurisk.info/threat/ip-list.txt"
    next
end

And use it in the DNS Profile:

DNS FIlter Profile
config dnsfilter profile
    edit "DNSProfile"
    set log-all-domain enable
        set block-botnet enable
        set external-ip-blocklist "IP-block-list-feed"
    next
end

DNS Translation

Other vendors also call it DNS Doctoring (Cisco PIX/ASA anyone?) - Fortigate checks every DNS response returning to the end client and passing the Fortigate and if the configured IP address is seen, it is replaced by another IP address we specify. It is not that much of a security measure, but is helpful when say LAN hosts try to access your corporate website resolving against external DNS servers on the Internet - the whole Internet will get as DNS response a legal IP address, while hosts behind Fortigate will see some Internal IP. Example is the best here.

Let’s say I have hosted somewhere a website named "vpn.yurisk.com" that resolves for the whole world to IP of 52.58.153.81, but for hosts behind Fortigate, I want them to connect to the internal copy of the website located at 10.100.102.17. To do so, inside the DNS Filter Profile, I do:

fortigate dns filter10

The end result will look:

fortigate dns filter11

End client before DNS Translation is applied:

C:\WINDOWS\system32>ping vpn.yurisk.com

Pinging vpn.yurisk.com [52.58.153.81] with 32 bytes of data:

And after:

C:\WINDOWS\system32>ping vpn.yurisk.com

Pinging vpn.yurisk.com [10.100.102.17] with 32 bytes of data:

Configuration on CLI:

config dnsfilter profile
    edit "DNSProfile"
            config dns-translation
            edit 1
                set src 52.58.153.81
                set dst 10.100.102.17
            next
        end
    next
end

Applying the DNS Filter Profile on the Fortigate Interface

Beyond security rules, we can apply the DNS Filter profile on the interface where (and if) we enabled DNS server to answer queries from the attached to the interface hosts. The feature functions the same, except it is less granular and is applied to ALL hosts behind the interface sending DNS queries.

Make sure to make this feature visible: System → Feature Visibility → DNS Database.

After that we go to Network → DNS Servers, and create a new one:

fortigate dns filter12

Choose interface we want to enable DNS server for, then enable DNS filter and pick the one:

fortigate dns filter13
fortigate dns filter14

Config on CLI:

config system dns-server
    edit "port2"
        set dnsfilter-profile "DNSProfile"
    next
end

Protecting Internal DNS Server

Fortigate can also provide some protection to the internal DNS available to queries from the Internet, if serving as an authority DNS server for your domain. As an example let’s protect our internal DNS server 10.100.104.15 available to the Internet via VIP, external IP being 10.100.100.227 (yeah, lots of NAT), and holding the DNS zone for "yurisk.info". We want to open this DNS server to queries from ANY IP on the Internet, but also protect it from bad actors. We can apply IPS Sensor to all incoming connections, this way blocking the known exploits by signature (not shown here), and we can limit incoming DNS queries to the domain and its subdomains "yurisk.info" only by using the wildcard in the DNS Filter profile of "*.yurisk.info".

First, the VIP (nothing special here):

config firewall vip
edit "VIP-DNS-Linux"
        set extip 10.100.100.227 <-- WAN IP OF FGT
        set mappedip "10.100.104.15" <-- LAN IP OF DNS
        set extintf "port1"
        set portforward enable
        set protocol udp
        set extport 53
        set mappedport 53
    next
end

As I know for sure my domain does not include large records, no TCP is needed.

Now, I create the Static DNS Filter to allow only queries for sub/domain "yurisk.info" and block anything else:

config dnsfilter domain-filter
    edit 1
        set name "Auto-dnsfilter-domain-filter_uuctvw6su"
        config entries
            edit 1
                set domain "*.yurisk.info" <-- ALLOW QUERY OF MY DOMAIN
                set type wildcard
                set action allow
            next
            edit 2
                set domain "*"  <-- BLOCK ANYTHING ELSE
                set type wildcard (1)
            next
        end
    next
end
  1. - the default action is "block".

Next, we create DNS Filter that uses this domains list:

config dnsfilter profile
    edit "ProtectDNS"
        config domain-filter
            set domain-filter-table 1
        end
        config ftgd-dns
            set options ftgd-disable
        end
        set log-all-domain enable
    next
end

Or in GUI:

fortigate dns filter17

One tweak is a good practice here - if left as is, any query for unrelated domain will be redirected to the default Fortigaurd-hosted block page, i.e. it will be "successful" DNS-wise. And that may encourage DDoS attackers to try and use our DNS in amplification attack, thinking we are accepting queries to any domain, not good. For this, we have the option to change the default Redirect to Block action to one of the NXDOMAIN (no domain found), or SERVFAIL (authoritative server did not answer). For our purpose the 1st option is better as the attackers will most probably see this error and will understand we are NOT an open DNS resolver and it would be useless to try any further.

FGT-Perimeter (ProtectDNS) # set block-action
block             Return NXDOMAIN for blocked domains.
redirect          Redirect blocked domains to SDNS portal. <-- THE DEFAULT
block-sevrfail    Return SERVFAIL for blocked domains.

set block-action block <-- SET TO NXDOMAIN

The final DNS Profile will look:

config dnsfilter profile
    edit "ProtectDNS"
        config domain-filter
            set domain-filter-table 1
        end
        config ftgd-dns
            set options ftgd-disable
        end
        set log-all-domain enable
        set block-action block
    next
end

And, finally, the security rule:

config firewall policy
    edit 8
        set name "DNS incoming"
        set srcintf "port1"
        set dstintf "port2"
        set action accept
        set srcaddr "all"
        set dstaddr "VIP-DNS-Linux"
        set schedule "always"
        set service "ALL"
        set utm-status enable
        set inspection-mode proxy (1)
        set dnsfilter-profile "ProtectDNS"
        set logtraffic all
    next
end
  1. - for this to work, we have to use Proxy mode policy, otherwise banned requests would not be stopped.

For a test, I will try to resolve yahoo.com against the internal DNS server:

Log of the block:

fortigate dns filter15

Trying to query for yahoo.com, the DNS client will get NXDOMAIN as expected:

fortigate dns filter16

Inspecting Encrypted DNS Traffic

Fortigate starting with FOrtiOS 7.0.x can inspect encrypted DNS traffic as well. You will, obviously, need Deep SSL Inspection to be enabled on such traffic. I haven’t tested this feature yet, so cannot comment - once I will have tested it, I will write a separate blog post.

Debug and Verification

  • Display DNS Filter related logs on CLI:

exe log filter category utm-dns or exe log filter category 15

Then exe log display:

date=2025-03-18 time=10:46:38 eventtime=1742319998319056364 tz="-0700"
logid="1501054802" type="utm" subtype="dns" eventtype="dns-response"
level="notice" vd="root" policyid=21 poluuid="06ac0942"
policytype="policy" sessionid=21428 user="localvpn1"
group="ipsecgrp" srcip=192.168.17.0 srcport=64537
srccountry="Reserved" srcintf="IKEv1" srcintfrole="undefined"
dstip=8.8.8.8 dstport=53 dstcountry="United States"
dstintf="port1" dstintfrole="undefined" proto=17 profile="DNSProfile"
xid=5287 qname="edge.microsoft.com" qtype="A" qtypeval=1 qclass="IN"
ipaddr="13.107.21.239, 204.79.197.239" msg="Domain is monitored"
action="pass" cat=52 catdesc="Information Technology"
  • diagnose test app dnsproxy 2 Show configs & statistics

FGT-Perimeter # diagnose test app dnsproxy 2
worker idx: 0
worker: count=1 idx=0
retry_interval=500 query_timeout=1495

DNS latency info:<-- LATENCY FOR DNS RESOLVING
vfid=0 server=10.100.0.2 latency=1 updated=1274
vfid=0 server=96.45.45.45 latency=1 updated=389863
vfid=0 server=96.45.46.46 latency=3 updated=389899

SDNS latency info:<-- LATENCY OF FORTIGUARD (SDNS) QUERIES
vfid=0 server=173.243.140.53 latency=2 updated=8867
vfid=0 server=139.138.105.53 latency=3 updated=8877
DNS_CACHE: alloc=90, hit=125
RATING_CACHE: alloc=165, hit=414
DNS query: alloc=0
DNS UDP: req=1762 res=1202 fwd=1112 cmp=29 retrans=25 to=18
         cur=92 switched=330103 num_switched=5
         v6_cur=0 v6_switched=0 num_v6_switched=0
DNS FTGD: ftg_fwd=166, ftg_res=166, ftg_retrans=0
DNS TCP: req=2, res=2, fwd=2, retrans=0, to=0

DNS TCP connections:

DNS UNIX streams: cfd=36
FQDN: alloc=1 nl_write_cnt=1 nl_send_cnt=1 nl_cur_cnt=0
Botnet: searched=288 hit=0 filtered=288 false_positive=0

DNS domain filter tables: <-- STATIC DOMAIN FILTERS PRESENT AND USED
vfid=0 id=1 name=Auto-dnsfilter-domain-filter_uuctvw searched=0 hit=0
vfid=0 id=3 name=Auto-dnsfilter-domain-filter_965xz3 searched=260 hit=2

DNS external domain filter tables:<-- EXTERNAL DOMAINS FEED
name=Remote threat feed uuid_idx=15845 searched=307 hit=17
  • diagnose test app dnsproxy 3 Even more stats.

worker idx: 0
VDOM: root, index=0, is primary, vdom dns is enabled, pip-0.0.0.0 dns_log=1
VDOM FROM WHICH FORTIDUARD (SDNS) QUERIES ARE SENT
dns64 is disabled
DNS servers: DNS SERVERS FOR RESOLVING, ENCRYPTED - DNS OVER TLS by FORTINET
10.100.0.2:53 vrf=0 tz=0 encrypt=none req=628 to=0 res=628 rt=8 ready=1 timer=0 probe=0 failure=0 last_failed=0
96.45.45.45:853 vrf=0 tz=0 encrypt=dot req=2 to=2 res=2 rt=1 ready=1 timer=0 probe=0 failure=0 last_failed=0
96.45.46.46:853 vrf=0 tz=0 encrypt=dot req=1 to=1 res=1 rt=3 ready=1 timer=0 probe=0 failure=0 last_failed=0


SDNS servers: <-- SDNS SERVERS
139.138.105.53:853 vrf=0 tz=-420 encrypt=dot req=68 to=0 res=68 rt=3 ready=1 timer=0 probe=0 failure=0 last_failed=0
173.243.140.53:853 vrf=0 tz=-420 encrypt=dot req=98 to=0 res=98 rt=2 ready=1 timer=0 probe=0 failure=0 last_failed=0
ALT servers:
Interface selecting method: auto
Specified interface:
FortiGuard interface selecting method: auto
FortiGuard specified interface:

DNS_CACHE: hash-size=2048, ttl=1800, min-ttl=60, max-num=5000
DNS FD: udp_s=12 udp_c=18:19 ha_c=23 unix_s=6, unix_nb_s=24, unix_nc_s=7
        v6_udp_s=13, v6_udp_c=21:22, snmp=25, redir=14, v6_redir=15
DNS FD: tcp_s=27, tcp_s6=28, redir=29 v6_redir=30
DNS UNIX FD: dnsproxy_un=31
FQDN: min_refresh=60 max_refresh=3600

FGD_DNS_SERVICE_LICENSE: <-- WEB FILTERING LICENSE STATUS
server=139.138.105.53:853, expiry=2038-01-02, expired=0, type=2
server=173.243.140.53:853, expiry=2038-01-02, expired=0, type=2
FGD_CATEGORY_VERSION:10
SERVER_LDB: gid=da0b, tz=-420, error_allow=0

DEFAULT REPLACEMENT IP TO REDIRECT TO BLOCK PAGE
FGD_REDIR_V4:208.91.112.55 FGD_REDIR_V6:[2620:101:9000:53::55]
  • dia test app dnsproxy 15 Show the cached category responses from Fortiguard, doe snot show Remote4 feeds or Local categories, only Fortiguard-based ratings.

FGT-Perimeter # diagnose test app dnsproxy 15
worker idx: 0
SDNS rating cache:
name=srtb.msn.com, category=41, ttl=10732
name=img-s-msn-com.akamaized.net, category=82, ttl=10732
name=ntp.msn.com, category=41, ttl=10731
name=sb.scorecardresearch.com, category=52, ttl=10730
name=c.msn.com, category=41, ttl=10730
name=assets.msn.com, category=41, ttl=10730
name=browser.events.data.msn.com, category=41, ttl=10730
name=array613.prod.do.dsp.mp.microsoft.com, category=52, ttl=10721
name=1742309134-2243019459389133638-1048.cu.dzen.ru, category=36, ttl=10698
name=telemetry.dzen.ru, category=36, ttl=10643
name=api.vk.com, category=37, ttl=10639
name=stacks.vk-portal.net, category=37, ttl=10639
name=vk.com, category=37, ttl=10638
name=log.strm.yandex.ru, category=25, ttl=10638
name=static.vk.com, category=37, ttl=10638
name=rs.mail.ru, category=23, ttl=10638
name=csp.yandex.net, category=41, ttl=10638
  • Real-time debug dia deb application dnsproxy -1, will show ongoing DNS Filter decisions, may show partial output if Flow mode policy used as IPS sensor is handling large part of the feature.

E.g. trying to query for A record of yahoo.com the internal DNS server:

dia deb application  dnsproxy -1
dia deb enable

dns_secure_log_request()-1123: id:0x0773 pktlen=50
profile=ProtectDNS ifindex=3
dns_secure_log_request()-1179:
write to log: qname=yahoo.com qtype=1
dns_profile_do_url_rating()-1992:
vfid=0 profile=ProtectDNS category=255 domain=yahoo.com
dns_url_table_search()-1941: search domain yahoo.com
in Auto-dnsfilter-domain-filter_uuctvw
dns_profile_do_url_rating()-2021: found static domain filter for yahoo.com
(table=Auto-dnsfilter-dom uuctvw action=2)
dns_profile_do_url_rating()-2088:
request filter result for yahoo.com (type=1 action=2)
dns_secure_apply_action()-2229: action=2 category=0 log=0
error_allow=0 profile=ProtectDNS
dns_send_error_response()-1821: domain=yahoo.com err=3
dns_send_response()-1645: domain=yahoo.com reslen=50
dns_secure_log_response()-1256: id:0x7307 domain=yahoo.com
profile=ProtectDNS action=2 log=0
dns_policy_find_by_idx()-2924: vfid=0 idx=8
dns_secure_log_response()-1506: write to log: logid=54400
qname=yahoo.com

In this case I try to browse to www.reddit.com from LAN, when reddit.com is blocked in External feed:

dns_secure_log_request()-1179: write to log: qname=www.reddit.com qtype=1
dns_profile_do_url_rating()-1992: vfid=0 profile=DNSProfile category=255
domain=www.reddit.com
dns_url_table_search()-1941:
search domain www.reddit.com in Auto-dnsfilter-domain-filter_jv2g7v <-- SEARCH
STATIC DOMAINS LIST

NOW CHECKS BOTNET LIST:
botnet_domain_search()-2291: domain=www.reddit.com passed botnet check

FINALLY, SEARCHES CATEGORY 192 - EXTERNAL FEED:
dns_profile_do_url_rating()-2042: search domain www.reddit.com in category 192
dns_profile_do_url_rating()-2047: found external domain filter for www.reddit.com
(table=Remote threat feed category=192) <-- NAME OF EXTERNAL FEED CONENCTOR
dns_profile_do_url_rating()-2088: request filter result for www.reddit.com (type=7 action=10)
dns_secure_apply_action()-2229: action=10 category=192 log=1 error_allow=0 profile=DNSProfile

I also write cheat sheets/scripts/guides to help in daily work, so make sure to check out my Github at https://github.com/yuriskinfo and https://www.linkedin.com/in/yurislobodyanyuk/