How to create a router from Centos 8 Stream

Today, I need to rebuild and create a new one linux router. It will work with many networks, there will be DHCP and DNS server as well.

So, at first, we will secure our server on service: ssh. I assume, that you already have enabled firewalld and public zone, only for sshd service. And internal network with dhcp or anything else.

SSHD deamon

At first, we configure our ssh service to be secure. Edit ssh configuration file and adjust there variables:

vim /etc/ssh/sshd_config
Port 22
Protocol 2
PermitRootLogin no
PubkeyAuthentication yes
PermitEmptyPasswords no
PasswordAuthentication no
AllowUsers bob

Now, we create on our client machine (in my place Ubuntu desktop PC) a new rsa-key pair for ssh authentication. You can set some passphrase, but it is you opinion. In quotas, there is a comment for there key pair.

ssh-keygen -t rsa -b 4096 -C ""
-b bits. Number of bits in the key to create
-t type. Specify type of key to create
-C comment

When you had these key pairs generated (under ~/.ssh/) like: id_rsa and id_rsa_pub, you can adjust your server, for accepting these key pair. Now, you must authorize to your server by password (last time).

ssh-copy-id bob@IP-OF-SERVER

If everything is OK, now you can restart your sshd service on server, to load new configurations:

systemctl restart sshd.service

And we can test, that other users is NOT allowed, and there is a need for key authentication:

ssh Alice@
  Alice@ Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

But Bob is allowed:

ssh bob@
   Last login: Tue Aug 31 14:47:13 2021 from...

Setting up public interface

We use nmcli (command-line tool for controlling NetworkManager) for watching out of our interfaces:

nmcli device status
ens1   ethernet  connected    ens1       
ens2    ethernet  unmanaged    --         
ens3    ethernet  unmanaged    --         
ens4   ethernet  unmanaged    --         
ens5   ethernet  unmanaged    --         
lo      loopback  unmanaged    -- 

Unmanaged interfaces are edited here:

vim /etc/NetworkManager/conf.d/99-unmanaged-devices.conf

And our ens1 is in config below. UUID we can generate with command:

uuidgen ens1
cat /etc/sysconfig/network-scripts/ifcfg-ens1 
#ens1 - Internal Subnet

Now, we can configure our public interface. In our example, it will be interface ens2. So, we can copy our first network-script for interface ens2 and edit it. First, generate uuid for this interface. Then adjust your public IP numbers.

uuidgen ens2
cp /etc/sysconfig/network-scripts/ifcfg-ens1 /etc/sysconfig/network-scripts/ifcfg-ens2
vim /etc/sysconfig/network-scripts/ifcfg-ens2


nmcli device status
ens1   ethernet  connected    ens1       
ens2    ethernet  connected    ens2  

Now, after some time, we can see many ssh attacks to our system. We can make more things. One is, change the port for ssh daemon. Or I use fail2ban services, for ban IP, that try to hack our server. Like:

Aug 31 13:50:45 r3 sshd[3551]: Invalid user support from port 17306
Aug 31 13:50:48 r3 sshd[3553]: Invalid user geral from port 17006
Aug 31 13:50:49 r3 sshd[3555]: Invalid user omega from port 57344
Aug 31 13:50:51 r3 sshd[3557]: Invalid user matt from port 49776
Aug 31 13:50:57 r3 sshd[3559]: Invalid user rebecca from port 38964
Aug 31 13:51:02 r3 sshd[3561]: Invalid user admin from port 32926
Aug 31 13:51:24 r3 sshd[3564]: Invalid user support from port 49868
Aug 31 13:55:38 r3 sshd[3603]: Invalid user vasilias from port 54654
Aug 31 13:55:42 r3 sshd[3605]: Invalid user admin from port 54656
Aug 31 13:59:53 r3 sshd[3734]: Invalid user user from port 45564

Or we can count it (after two hours). We had 644 ssh login attempts to our server:

cat /var/log/secure | grep Invalid | wc -l

So, find I will install fail2ban services, for ban these IPs…

dnf install -y fail2ban-firewalld.noarch

Now, we must create a new jail.local, where we define actions for those IPs and services. So, create this file and add some variables. If you wish, adjust by your needs:

vim /etc/fail2ban/jail.local

# Ban IP/hosts for 24 hour ( 24h*3600s = 86400s):
bantime = 86400
# An ip address/host is banned if it has generated "maxretry" during the last "findtime" seconds.
findtime = 900
maxretry = 2

# Enable sshd protection
enabled = true

Now, we can start/restart fail2ban services and view its status. And next, use fail2ban client to view detailed status:

systemctl restart fail2ban
systemctl status fail2ban

fail2ban-client status
fail2ban-client status sshd

Now, we must see some IPs, that are already banned by our fail2ban. If you wish to manually ban/unban IP, lets do this:

fail2ban-client unban
fail2ban-client set sshd banip

Or you can set your trusted IPs to whitelist:

vim /etc/fail2ban/jail.conf

ignoreip =

Time server – chronyd

Now, we install chronyd service, for obtain and distributing time from world to our subnets. So, install chronyd, and set localisation to you system

dnf install chrony -y
systemctl enable chronyd
timedatectl set-timezone Europe/Bratislava

Now, edit some variables in configurations file for chronyd. Add some servers from pool, and edit local subnets, where we delived time:

vim /etc/chrony.conf

pool iburst
pool iburst
pool iburst

Now start/restart our service, and check, if it is working:

systemctl restart chronyd
systemctl status chronyd.service
chronyc sources

IPV4 forwarding

As the first things, we must check, if our system has enabled IP forwarding in kernel. Check it out:

sysctl -a | grep ip_forward

net.ipv4.ip_forward = 1

If not, enable it:

sysctl -w net.ipv4.ip_forward=1

vim /etc/sysctl.d/99-sysctl.conf

Setting up nftables

In out environment, we use nftables. It is default backend firewall module in Centos 8/ RHEL 8. It is already managed by firewalld, but we gonna use native nft command. The nftables is able to collapse firewall management for IPv4, IPv6 and bridging into the single command line utility: nft.

So, at the beginning (or before run our later nft script), we disable firewalld utility:

systemctl disable --now firewalld
systemctl mask firewalld

Creating Tables and Chains

Rebooting the system after disabling firewalld will ensure that we have no remnants of the tables, chains and rules added by firewalld. We start up with nothing in-place. Unlike iptables, nftables does not have any tables or chains created by default. Managing nftables natively requires us to create these objects:

protocol family: At the top level of the configuration tree we have protocol families: ip, ip6, arp, bridge, netdev and inet. Using inet we can cater for both IPv4 and IPv6 rules in the one table.

table: Below the protocol family we define tables to group chains together. The default table was the filter table in iptables but we also had other tables such as the nat table.

chain: A chain presents a set of rules that are read from the top down. We stop reading once we match a rule. When creating a chain in nftables we can specify the type of chain – we use the filter type, the hook – we will be working with input hook and a priority. The lower the number the higher the priority and the highest priority chain takes precedence. We will also specify the default policy which defines the action if no rule is met.

To list existing tables, we shouldn’t have any, we use the following:

nft list tables

Now, we create s script file, in which will be all our rules. So, you have to do some learning about nft. Here you are the basic rules:

cat /root/

# the executable for nftables

# wan and lan ports. Home routers typically have two ports minimum.

# flush/reset rules
${nft} flush ruleset

#create tables called "filter" for ipv4 and ipv6
${nft} add table ip filter
${nft} add table ip6 filter

# one more table called 'nat' for our NAT/masquerading
${nft} add table nat

#create chains
${nft} add chain ip filter input { type filter hook input priority 0 ; }
${nft} add chain ip filter output {type filter hook output priority 0 ; }
${nft} add chain ip filter forward {type filter hook forward priority 0 ; }
${nft} add chain ip filter postrouting {type filter hook postrouting priority 0 ; }
${nft} add chain nat postrouting {type nat hook postrouting priority 100 ; }

# and for ipv6
${nft} add chain ip6 filter input { type filter hook input priority 0 ; }
${nft} add chain ip6 filter output {type filter hook output priority 0 ; }
${nft} add chain ip6 filter forward {type filter hook forward priority 0 ; }
${nft} add chain ip6 filter postrouting {type filter hook postrouting priority 0 ; }
${nft} add chain ip6 filter nat {type nat hook postrouting priority 100 ; }


#forward traffic from WAN to LAN if related to established context
${nft} add rule filter forward iif $wan oif $lan ct state { established, related } accept

#forward from LAN to WAN always
${nft} add rule filter forward iif $lan oif $wan accept

#drop everything else from WAN to LAN
${nft} add rule filter forward iif $wan oif $lan counter drop

#ipv6 just in case we have this in future.
${nft} add rule ip6 filter forward iif $wan6 oif $lan ct state { established,related } accept
${nft} add rule ip6 filter forward iif $wan6 oif $lan icmpv6 type echo-request accept

#forward ipv6 from LAN to WAN.
${nft} add rule ip6 filter forward iif $lan oif $wan6 counter accept

#drop any other ipv6 from WAN to LAN
${nft} add rule filter forward iif $wan6 oif $lan counter drop

${nft} add rule filter input ct state { established, related } accept

# always accept loopback
${nft} add rule filter input iif lo accept
# comment next rule to disallow ssh in
${nft} add rule filter input tcp dport ssh counter log accept

#accept DNS, SSH, from LAN
${nft} add rule filter input iif $lan tcp dport { 53, 22 } counter log accept
#accept DNS on LAN
${nft} add rule filter input iif $lan udp dport { 53 } accept

#accept ICMP on the LAN,WAN
${nft} add rule filter input iif $lan ip protocol icmp accept
${nft} add rule filter input iif $wan ip protocol icmp accept

${nft} add rule filter input counter drop

################# ipv6 ###################
${nft} add rule ip6 filter input ct state { established, related } accept
${nft} add rule ip6 filter input iif lo accept
#uncomment next rule to allow ssh in over ipv6
#${nft} add rule ip6 filter input tcp dport ssh counter log accept

${nft} add rule ip6 filter input icmpv6 type { nd-neighbor-solicit, echo-request, nd-router-advert, nd-neighbor-advert } accept

${nft} add rule ip6 filter input counter drop

# allow output from us for new, or existing connections.
${nft} add rule filter output ct state { established, related, new } accept

# Always allow loopback traffic
${nft} add rule filter output iif lo accept

${nft} add rule ip6 filter output ct state { established, related, new } accept
${nft} add rule ip6 filter output oif lo accept

${nft} add rule nat postrouting masquerade

So, this script can be run by:

sh /root/

And now, we can view our filter and rules:

nft list tables
nft list table filter

This is only for this session, and after reboot, there will be empty list. To ensure, that filter persist reboot, run following:

nft list ruleset > /etc/sysconfig/nftables.conf
systemctl enable nftables.service
nft list table ip filter

Install and create own DNS resolver

Now we set up a local DNS resolver on CentOS 8/RHEL 8, with the widely-used BIND9 DNS software.

Be aware that a DNS server can also called a name server. Examples of DNS resolver are (Google public DNS server) and (Cloudflare public DNS server).

Normally, your computer or router uses your ISP’s DNS resolver to query domain names in order to get an IP address. Running your own local DNS resolver can speed up DNS lookups, because

  1. The local DNS resolver only listens to your DNS requests and does not answer other people’s DNS requests, so you have a much higher chance of getting DNS responese directly from the cache on the resolver.
  2. The network latency between your computer and DNS resolver is eliminated (almost zero), so DNS queries can be sent to root DNS servers more quickly.

If you own a website and want your own DNS server to handle name resolution for your domain name instead of using your domain registrar’s DNS server, then you will need to set up an authoritative DNS server, which is different than a DNS resolver. BIND can act as an authoritative DNS server and a DNS resolver at the same time

BIND (Berkeley Internet Name Domain) is an open-source DNS server software widely used on Unix/Linux due to it’s stability and high quality.

So, install it first, check version and start it. The BIND daemon is called named.

dnf update
dnf install bind bind-utils

named -v
    BIND 9.11.26-RedHat-9.11.26-6.el8 (Extended Support Version) <id:3ff8620>

systemctl start named
systemctl enable named

Usually, DNS queries are sent to the UDP port 53. The TCP port 53 is for responses size larger than 512 bytes. You must adjust your firewall to allow tcp and udp packets on port 53.

We can check the status of the BIND name server.:

rndc status

Configurations for a Local DNS Resolver

Out of the box, the BIND9 server on CentOS/RHEL provides recursive service for localhost only. Outside queries will be denied. Edit the BIND main configuration file:

vim /etc/named.conf

In the options clause, you can find the following two lines:

listen-on port 53 {; };
listen-on-v6 port 53 { ::1; };

This makes named listen on localhost only. If you want to allow clients in the same network to query domain names, then comment out these two lines. (add double slashes at the beginning of each line)

// listen-on port 53 {; };
// listen-on-v6 port 53 { ::1; };

Find and adjust these variables:

allow-query { localhost;; };
recursion yes;
 // hide version number from clients for security reasons.
 version "not currently available";
 // enable the query log
 querylog yes;

Now, check our config (silent output if is OK), and restart named:

systemctl restart named

Now, you have enabled DNS resolver.

Create own DNS zone, dnssec signed

If you want your own DNS server for your domain, accessible from world and you don’t want you domain provider, to be a DNS server for this domain, let read next.

First, create a new database file for this domain:

vim /var/named/

; BIND data file for
$TTL 604800
@       IN      SOA        (
        2021090608      ; Serial
        3600            ; Refresh
        86400           ; Retry
        2419200         ; Expire
        3600    )       ; Default TTL

        IN      NS

@	IN	A
ns1     IN      A
www	IN	CNAME	ns1

We have three dns names, that direct to our domain (www, ns1 and the domain itself: Now, we must edit the main configuration file for named, and add point to this zone/domain/file. And add som variables for dnssec. Now we comment out previously commented two lines and edit some variables:

vim /etc/named.conf

        listen-on port 53 { any; };
        listen-on-v6 port 53 { any; };
        allow-query { any; };
        dnssec-enable yes;
        dnssec-validation yes;
        recursion no;

zone    "" IN {
        type master;
        file "";
        allow-transfer { none; };

Now, check the config and restart named:

named-checkzone /var/named/
systemctl restart named

Now, we can reach our domain from world, if domain registrator set NameServer ( to point to our public IP and let us for managing our dns zone.

Now, we can continue for dnssec with some theory:

DNS by itself is not secure. DNS was designed in the 1980s when the Internet was much smaller, and security was not a primary consideration in its design. As a result, when a recursive resolver sends a query to an authoritative name server, the resolver has no way to verify the authenticity of the response. The resolver can only check that a response appears to come from the same IP address where the resolver sent the original query. But relying on the source IP address of a response is not a strong authentication mechanism, since the source IP address of a DNS response packet can be easily forged, or spoofed.

DNSSEC strengthens authentication in DNS using digital signatures based on public key cryptography. With DNSSEC, it’s not DNS queries and responses themselves that are cryptographically signed, but rather DNS data itself is signed by the owner of the data…. More about this:

So, lets create a key-pairs:

1 – Create a Zone Signing Key (ZSK)
2 – Create a Key Signing Key (KSK)

cd /var/named
dnssec-keygen -a RSASHA256 -b 2048 -r /dev/urandom
dnssec-keygen -a RSASHA256 -b 4096 -r /dev/urandom -f KSK

eg: Generating key pair......................++ .............................................................................................................................................................................................................++

The directory will now have 4 keys – private/public pairs of ZSK and KSK. We have to add the public keys which contain the DNSKEY record to the zone file. For example:

cat >> /var/named/
cat >> /var/named/

Now, edit owner permisions of these keys to named:

chown named*

And now, check our zone and if is everything ok, sign the zone:

named-checkzone /var/named/

zone loaded serial 2021090901

dnssec-signzone -A -3 $(head -c 1000 /dev/urandom | sha1sum | cut -b 1-16) -N INCREMENT -o -t

Verifying the zone using the following algorithms: RSASHA256.
Zone fully signed:
Algorithm: RSASHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                      ZSKs: 1 active, 0 stand-by, 0 revoked
Signatures generated:                       13
Signatures retained:                         0
Signatures dropped:                          0
Signatures successfully verified:            0
Signatures unsuccessfully verified:          0
Signing time in seconds:                 0.020
Signatures per second:                 649.707
Runtime in seconds:                      0.025

Above command created a signed zone file for zone:

We are now required to edit zone configuration to use new file instead of old file, and add som variables:

vim /etc/named.conf

zone    "" IN {
        type master;
        file "";
        allow-transfer { none; };
   # DNSSEC keys Location
   key-directory "/var/named/"; 

   # Publish and Activate DNSSEC keys
   auto-dnssec maintain;

   # Use Inline Signing
   inline-signing yes;


Now, check named config, and our zone:

named-checkzone /var/named/

zone loaded serial 2021090902 (DNSSEC signed)

systemctl restart named

And now, we can try our zone, if dnssec is working:

dig DNSKEY +multiline

; <<>> DiG 9.11.26-RedHat-9.11.26-6.el8 <<>> DNSKEY +multiline
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19926
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

; EDNS: version: 0, flags:; udp: 4096

;; ANSWER SECTION:		604795 IN DNSKEY 256 3 8 (
				) ; ZSK; alg = RSASHA256 ; key id = 56241		604795 IN DNSKEY 257 3 8 (
				) ; KSK; alg = RSASHA256 ; key id = 18544

We have configured DNSSEC on our master DNS server.

DHCP server on lan

Now, we install dhcp server on our centos server. I have interface for this server: ens22, and our gateway will be and clients will have IPs from range: We must have a static IP from this subnet on this interface.

dnf makecache
dnf install dhcp-server

Now, create own configuration file for dhcp server, or copy example and adjust:

cp /usr/share/doc/dhcp-server/dhcpd.conf.example /etc/dhcp/dhcpd.conf
vim /etc/dhcp/dhcpd.conf

And edit this variables as you fit:

option domain-name "";
option domain-name-servers;

default-lease-time 600;
max-lease-time 7200;

ddns-update-style none;
log-facility local7;

subnet netmask {
subnet netmask {
  option routers;
  option domain-name-servers;

Now, we can start dhcp server and view our logs. I have 8 interfaces, but only on this is dhcp enabled:

systemctl start dhcpd.service
systemctl enable dhcpd.service
tail /var/log/messages -f

No subnet declaration for ens18 (no IPv4 addresses).
 ** Ignoring requests on ens18.  If this is not what
you want, please write a subnet declaration
in your dhcpd.conf file for the network segment
to which interface ens18 is attached. **

Now, enable UDP port 67 to access dhcp server. Look below for this port. So edit your script and apply it and view:

netstat -tulpen | grep dhcp
udp        0      0

iif "ens22" udp dport { 67 } accept

And start some PC on this network and we will see at logs:

tail -n 10 /var/log/messages
dhcpd[]: DHCPREQUEST for from ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPACK on to ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPREQUEST for from ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPACK on to ac:5a:32:98:0c:xx (pc1) via ens22
Total Page Visits: 160934 - Today Page Visits: 18