QCSec Security Blog

IPTables vereen­voudigd

Een lokale firewall op Linux, inclusief IPv6, in 12 stappen. Of download het script!

geplaatst door Mark Koek op 29-Jan-2014

Elke client en server met beveili­gings­eisen hoort een lokale (host based) firewall te hebben. Niet alleen kan daarmee worden voorkomen dat onver­wachtse services aan het netwerk worden aangeboden, maar als ook uitgaand verkeer wordt gefilterd (doen!) kan de impact van een onver­hoopte inbraak beperkt worden door een goede host based firewall.

Linux-systemen beschikken met IPTables over een geavan­ceerd ingebouwd firewall-systeem. Zoals wel vaker met krachtige software biedt IPTables wel heel wat meer mogelijk­heden en complexi­teit dan een client of server nodig heeft. Als het systeem niet routeert valt al een hele klasse aan features af; ook is IPTables dermate generiek dat het een hele uitdaging kan zijn om een simpele set regels voor je Linux-server of -werk­station te configu­reren.

In dit artikel lopen we stap voor stap door een simpel shell-script waarin een aantal elemen­taire parameters kunnen worden ingesteld en dat daarna IPTables aanroept om deze elemen­taire regels aan te zetten.

Stap 1: een shell script

We starten met de standaard startregel voor een shell script, en het instellen van de locatie van de te gebruiken commando's:

#!/bin/sh
IPTABLES=/sbin/iptables
IP6TABLES=/sbin/ip6tables

Het commando iptables vinden we meestal in /sbin. Staat het daar niet, probeer het dan te vinden met which iptables of find / -name iptables.

IPv6

Firewall­regels voor IPv6 staan helemaal los van de 'normale' IPv4-firewall­regels. Ze moeten ook met een apart commando worden ingesteld: ip6tables, ook al werken ze verder in principe hetzelfde. Als het systeem dat je wilt beveiligen met een lokale firewall echt geen IPv6 onder­steunt kun je de stukken en commando's in dit artikel overslaan die daar betrekking op hebben. Maar de meeste systemen van tegen­woordig onder­steunen IPv6 'out of the box', en ook op veel netwerken functio­neert het (zelfs al heeft de beheerder daar niet speciaal iets voor gedaan... apparatuur die IPv6 onder­steunt zal met IPv6 onderling communi­ceren). Dus ik raad sterk aan om IPv6 mee te nemen in je lokale firewall tenzij je zeker weet dat het systeem het helemaal niet onder­steunt.

Hierna volgen de commando's en locaties die je aan het eind nodig hebt om de set firewall­regels op te slaan, zodat ze niet verdwenen zijn na een herstart (met IPTables ingestelde regels overleven standaard een herstart niet, een potentieel gevaar­lijke situatie die systemen open kan laten na een herstart).

IPTABLES_SAVE=/sbin/iptables-save
IP6TABLES_SAVE=/sbin/ip6tables-save
IPTABLES_RULES=/etc/iptables/rules.v4
IP6TABLES_RULES=/etc/iptables/rules.v6

Check dat deze commando's aanwezig zijn op het systeem. In Debian GNU/Linux en Ubuntu Linux is het bijvoor­beeld nodig dat het pakket iptables-persistent geïnstalleerd is. De file waarin de rules worden opgeslagen kan ver­schillen per distri­butie; er zijn ook systemen waar de locatie /etc/init.d/rules.v4, v6 is.

In Red Hat Enterprise Linux en distri­buties die erop gebaseerd zijn (zoals Fedora en CentOS) zijn boven­staande script­regels niet nodig en is het genoeg om in het confi­guratie­bestand /etc/sysconfig/iptables-config de volgende instelling te doen: IPTABLES_SAVE_ON_STOP="yes" en IPTABLES_SAVE_ON_RESTART="yes".

Het mooie van Linux is dat er zoveel keuze is, nietwaar. Check in elk geval altijd of de firewall­regels nog werken na een herstart, want het is gevaarlijk als het niet zo is en elke distro heeft het anders opgelost.

Stap 2: welke poorten zijn voor iedereen bereikbaar?

Meestal is er één poort waarop een server echt voor het hele netwerk (of zelfs het hele internet) bereikbaar moet zijn. In het geval van een webserver is dat bijvoor­beeld TCP-poort 80 (HTTP), en bij een mail gateway is dat TCP-poort 25 (SMTP). Voor servers in een intern netwerk kan het een groter aantal zijn. Om het voorbeeld niet te simpel te laten nemen we hier een intern mail­systeem, dat SMTP biedt op 25/TCP, 587/TCP, IMAP en IMAPS (143 en 993), en POP en POPS (110 en 995). Daarnaast functio­neert de server als DNS-resolver dus moet bereikbaar zijn op UDP-poort 53. We zetten deze poorten in een variabele:

PUBLIC_PORTS_TCP="25 587 143 993 110 995"
PUBLIC_PORTS_UDP=53

Omwille van het voorbeeld stellen we hier een heel aantal poorten open voor de buiten­wereld; in de praktijk is het aantal poorten hier hopelijk een stuk kleiner. Bij een werk­station laten we deze variabelen zelfs gewoon leeg, als het kan, want werk­stations bieden in het algemeen geen services aan in het netwerk.

Stap 3: wat mag het systeem zelf aan connecties initiëren?

Weinig systeem­beheerders doen dit, en toch is het erg nuttig. Houd controle over wat een systeem aan uitgaand verkeer kan genereren.

Als een infectie of hack van je server onverhoopt toch slaagt dan kan het de aanvaller ernstig hinderen als je uitgaande firewall­regels hebt ingesteld. De eerste stap van veel exploits is om een stuk kwaad­aardige code te downloaden en uit te voeren. Als die download niet slaagt doordat de uitgaande connectie geblok­keerd wordt door de lokale firewall, dan heb je een inbraak op je server of werk­station voorkomen!

Daarom onder­steunt ons script het instellen van uitgaande regels. De poorten waarop het systeem naar buiten mag configu­reren we in de volgende variabelen:

OUTBOUND_PORTS_TCP="25 53"
OUTBOUND_PORTS_UDP="53 123"

Het gaat hier om een mailserver, dus die moet ook mail kunnen uitsturen via SMTP (TCP-poort 25). Daarnaast moet de server zelf DNS-verzoeken kunnen resolven (53/TCP en 53/UDP) en we willen graag dat de server een correct ingestelde klok heeft en dus uitgaand NTP-verzoeken kan doen (123/UDP).

Stap 4: Beheerders mogen meer

Het systeem heeft meestal ook services die alleen voor beheerdoeleinden bedoeld zijn. Of anderszins voor een beperkte groep toegankelijk moet zijn. In een ideale situatie is er een aparte netwerkinterface voor beheer beschikbaar, maar in de praktijk is dat lang niet altijd het geval. Daarom definiëren we in ons eenvoudige firewallscript een aantal IP-adressen die wat meer netwerkdiensten mogen benaderen (denk aan SSH):

SEMIPUBLIC_REMOTE_IPS="213.138.110.240 83.86.121.144"
SEMIPUBLIC_REMOTE_IPS_6="2001:41c8:51:5f0:feff:ff:fe00:2f26"

Ook moeten we nog instellen op welke extra poorten deze IP-adressen toegang krijgen. In dit voorbeeld mogen alleen de bovenstaande adressen poort 22 (SSH) benaderen. Daarnaast geven we aan dat UDP-poort 161 (SNMP) alleen vanaf de beheerplekken toegankelijk moet zijn.

SEMIPUBLIC_PORTS_TCP=22
SEMIPUBLIC_PORTS_UDP=161

Stap 5: we willen wel security updates

Het systeem moet wel kunnen worden bijgewerkt met de laatste security updates. Daarom maakt het script een uitzon­dering voor uitgaande HTTP-connecties naar de update servers. In dit voorbeeld stellen we de IP-adressen in van de host security.debian.org:

UPDATE_SERVERS="212.211.132.32 212.211.132.250 195.20.242.89"
UPDATE_SERVERS_6="2001:8d8:580:400:6564:a62:0:2 2001:a78:5:0:216:35ff:fe7f:be4f 2001:a78:5:1:216:35ff:fe7f:6ceb"

Vul hier zelf de IP-adressen in van de update servers die je zelf gebruikt. In Debian en verwante systemen kun je ze vinden in /etc/apt/sources.list.

Inderdaad, als (in dit voorbeeld) Debian de IP-adressen verandert, dan moeten ook de firewall­regels worden aangepast. Het is niet anders - IPTables staat wel toe dat er een hostname wordt gebruikt bij het instellen van een regel, maar ook dan wordt die hostname vertaald naar een IP-adres op het moment dat de regel wordt ingevoerd; het is dus geen dynamische regel die voor elk pakketje een DNS-verzoek doet. Dat zou ook niet werken.

Let op als je lokale firewall uitgaand verkeer blokkeert. Dit is ofwel een indicatie van een bevei­ligings­probleem, of er is iets anders aan de hand dat je over het hoofd hebt gezien. Hoe dan ook, als een uitgaande connectie wordt geblok­keerd is het de moeite waard om dat even nader te onder­zoeken.

We komen verderop nog even terug op logging.

Stap 6: IPTables!

Eigenlijk hebben we alleen nog maar parameters in variabelen gezet; er is nog niks gebeurd op het systeem. Daar komt nu veran­dering in. Het script weet nu wat er moet gebeuren. Eerst wissen we een eventuele al bestaande configu­ratie:

$IPTABLES -F
$IP6TABLES -F

Met $IPTABLES roepen we de waarde van de variabele IPTABLES aan, in dit geval dus /sbin/iptables. De parameter -F (flush) wil niks anders zeggen dan: gooi alle regels weg. We beginnen dus met een schone lei.

Let op: terwijl het script draait heeft het systeem dus heel even geen firewall. Doe dit dus niet in een productie­omgeving! Het lijkt misschien wat paranoïde, maar een inbraak kan in een fractie van een seconde gebeuren.

Stap 7: generieke dingen die wél mogen

We beginnen met verkeer toe te staan op de loopback interface. Dat zijn netwerk­connecties die zich binnen het systeem bevinden en waar ook de lokale firewall niet in de weg moet zitten:

$IPTABLES -A INPUT -i lo -j ACCEPT
$IPTABLES -A OUTPUT -o lo -j ACCEPT
$IP6TABLES -A INPUT -i lo -j ACCEPT
$IP6TABLES -A OUTPUT -o lo -j ACCEPT

Zowel voor de input chain als de output chain en zowel voor IPv4 als IPv6 staan we al het verkeer toe van c.q. naar de loopback interface.

Het volgende type verkeer dat we toelaten is antwoordverkeer. IPTables is een volledige stateful firewall, wat betekent dat connecties worden bijgehouden zodat voor een binnenkomend netwerkpakket kan worden getoetst of het behoort bij een bestaande netwerkconnectie. Zelfs voor UDP-protocollen werkt dit; IPTables weet of een binnenkomend UDP-pakket op poort 53 het antwoord is op een eerder uitgegaan DNS-verzoek, of niet. Dit geven we aan door IPTables in de modus -m state aan te roepen, en als state ESTABLISHED op te geven. Met andere woorden, laat het pakket door als het bij een established connection hoort.

$IPTABLES -A INPUT -m state --state ESTABLISHED -j ACCEPT
$IPTABLES -A OUTPUT -m state --state ESTABLISHED -j ACCEPT
$IP6TABLES -A INPUT -m state --state ESTABLISHED -j ACCEPT
$IP6TABLES -A OUTPUT -m state --state ESTABLISHED -j ACCEPT

Ook hier doen we de instelling 4 keer: inkomend en uitgaand, voor IPv4 en voor IPv6.

Een ander voorbeeld van netwerkverkeer dat we toelaten is ICMP. Er is veel zinvols te zeggen over het wel of niet filteren van bepaalde typen ICMP-pakketjes, maar dat voert te ver voor deze blogpost. Iets voor een volgende keer misschien.

$IPTABLES -A INPUT -p icmp -j ACCEPT
$IPTABLES -A OUTPUT -p icmp -j ACCEPT
$IP6TABLES -A INPUT -p ipv6-icmp -j ACCEPT
$IP6TABLES -A OUTPUT -p ipv6-icmp -j ACCEPT

Stap 8: poorten openzetten

Tijd om iets te gaan doen met de variabelen die we in het begin gedefinieerd hebben. PUBLIC_PORTS_TCP en PUBLIC_PORTS_UDP hebben we gedefinieerd als de poorten waarop nieuwe connecties mogen binnenkomen. Packets die onderdeel uitmaken van een bestaande connectie worden al doorgelaten door bovenstaande regels die alles dat onderdeel is van een ESTABLISHED-connectie doorlaat. Op de connectiestatus hoeven we hier dus niet meer te checken. De scriptcode ziet er zo uit:

for port in $PUBLIC_PORTS_TCP; do
 $IPTABLES -A INPUT -p tcp --dport $port -j ACCEPT
 $IP6TABLES -A INPUT -p tcp --dport $port -j ACCEPT
done

En nog een keer hetzelfde, maar dan voor de UDP-poorten:

for port in $PUBLIC_PORTS_UDP; do
 $IPTABLES -A INPUT -p udp --dport $port -j ACCEPT
 $IP6TABLES -A INPUT -p udp --dport $port -j ACCEPT
done

Daarna komt het meest complexe stuk scriptcode. Voor elke 'semi-publieke' poort (zowel TCP als UDP) moet per toegestaan IP-adres een regel worden uitgevoerd. Dat levert twee dubbele lussen op in het script:

for port in $SEMIPUBLIC_PORTS_TCP; do
 for ip in $SEMIPUBLIC_REMOTE_IPS; do
  $IPTABLES -A INPUT -p tcp -s $ip --dport $port -j ACCEPT
 done
 for ip in $SEMIPUBLIC_REMOTE_IPS_6; do
  $IP6TABLES -A INPUT -p tcp -s $ip --dport $port -j ACCEPT
 done
done
for port in $SEMIPUBLIC_PORTS_UDP; do
 for ip in $SEMIPUBLIC_REMOTE_IPS; do
  $IPTABLES -A INPUT -p udp -s $ip --dport $port -j ACCEPT
 done
 for ip in $SEMIPUBLIC_REMOTE_IPS_6; do
  $IP6TABLES -A INPUT -p udp -s $ip --dport $port -j ACCEPT
 done
done

De laatste poortjes die open moeten is HTTP uitgaand naar de update-servers. Gemakshalve ga ik ervan uit dat de updateservers via HTTP werken, mocht er nog via FTP gewerkt worden dan moet ook TCP-poort 21 nog worden opengezet. Dat is een leuke oefening voor de lezer.

for ip in $UPDATE_SERVERS; do
 $IPTABLES -A OUTPUT -p tcp -d $ip --dport 80 -j ACCEPT
done
for ip in $UPDATE_SERVERS_6; do
 $IP6TABLES -A OUTPUT -p tcp -d $ip --dport 80 -j ACCEPT
done

Stap 9: Logging

Een firewall is niet compleet zonder logging. Over nuttige firewall logs valt veel te zeggen, en mensen die beveiligingsincidenten moeten oplossen zullen altijd aanbevelen om elke connectie te loggen (de netflow data van alles dat een netwerk in- of uitgaat is heel nuttig bij onderzoek naar wat er precies gebeurd is). Maar voor een lokale firewall voert dat misschien wat ver; het voert in elk geval te ver voor de scope van dit artikel. Wel gaan we onze lokale firewall een simpele logfaciliteit geven, zodat we in elk geval kunnen zien wat er geblokkeerd wordt.

Om te kunnen loggen maken we een nieuwe 'chain' aan met de naam LOGGING. Door de manier waarop IPv6 geïmplenteerd wordt door IPTables moeten we dat weer met een dubbele aanroep doen om het ook voor IPv6 te laten werken:

$IPTABLES -N LOGGING
$IP6TABLES -N LOGGING

Nu gaan we ervoor zorgen dat pakketjes die geen match hebben gevonden in de rules die we hebben gebouwd worden doorgeleid naar de LOGGING chain.

$IPTABLES -A INPUT -j LOGGING
$IP6TABLES -A INPUT -j LOGGING
$IPTABLES -A OUTPUT -j LOGGING
$IP6TABLES -A OUTPUT -j LOGGING
$IPTABLES -A FORWARD -j LOGGING
$IP6TABLES -A FORWARD -j LOGGING

Voor alle zekerheid stellen we dit ook in voor de chain met de naam FORWARD. Dit is de chain die wordt gebruikt voor routering, en als het goed is wordt die niet gebruikt in een lokaal systeem (dat wil zeggen, een apparaat dat niet bedoeld is als router). Maar voor alle zekerheid willen we het toch even loggen als er iets in die chain terechtkomt.

Dan gaan we nu daadwerkelijk logregels genereren. Waar deze in het systeem terechtkomen is een ander onderwerp; idealiter gaan ze met syslog naar een aparte logserver. In de praktijk zal op de meeste Linux-systemen een logregel worden aangemaakt in /var/log/messages.

$IPTABLES -A LOGGING -m limit --limit 2/sec -j LOG --log-prefix "IPTables Dropped Packet: " --log-level 4
$IP6TABLES -A LOGGING -m limit --limit 2/sec -j LOG --log-prefix "IPTables Dropped Packet: " --log-level 4

Opnieuw is te zien hoe voor IPv4 en IPv6 twee keer dezelfde aanroep wordt gedaan maar dan in het geval van IPv6 aan ip6tables.

De oplettende lezer ziet ook dat we de modus limit hier gebruiken. Er is bij logging namelijk wel een risico dat het systeem overbelast raakt, of misschien zelfs de disk opvult, door het loggen van heel veel tegengehouden netwerkpakketjes. Daarom gebruiken we een mooie feature van IPTables om het loggen tot maximaal 2 regels per seconde te beperken. Dat moet voldoende zijn om een probleem te kunnen debuggen, en zal het systeem niet overbelasten.

Stap 10: pakketjes droppen!

Nadat een geweigerd pakketje de LOGGING chain doorlopen heeft moet het worden gedropt. Doen we dat niet, dan zal de policy die bij de betreffende chain hoort worden toegepast.

$IPTABLES -A LOGGING -j DROP
$IP6TABLES -A LOGGING -j DROP

En voor de zekerheid (maar het zou dus niet nodig moeten zijn, maar dubbel zeker is nog beter dan enkel zeker) stellen we voor de standaard chains de policy ook nog even in op het droppen van pakketjes zonder matchende regel:

$IPTABLES -P INPUT DROP
$IPTABLES -P OUTPUT DROP
$IPTABLES -P FORWARD DROP
$IP6TABLES -P INPUT DROP
$IP6TABLES -P OUTPUT DROP
$IP6TABLES -P FORWARD DROP

Stap 11: opslaan van de rules

We hebben het in het bovenstaande al even opgemerkt: niet alle Linux-distributies hebben standaard een voorziening die de firewall rules netjes opslaat bij het afsluiten, en weer toepast bij het opstarten. Daarom slaan we de instellingen die we net gedaan hebben even netjes op. Het kan zijn dat onderstaande regels op jouw systeem kunnen worden weggelaten, maar ook hier passen we weer het uitgangspunt toe: voor de zekerheid toch maar even doen.

$IPTABLES_SAVE > $IPTABLES_RULES
$IP6TABLES_SAVE > $IP6TABLES_RULES

Stap 12: voer het script uit!

Je kunt natuurlijk stap voor stap de bovenstaande commando's uitvoeren en daar leer je ongetwijfeld veel van. Mensen met minder geduld kunnen het hier ook downloaden.

Download het script

Geef het script de juiste permissies met chmod a+x qcsec-eenvoudige-firewall.sh en voer het uit als root: sudo ./qcsec-eenvoudige-firewall.sh. Ik ben benieuwd naar je ervaringen en hoor graag suggesties voor verbetering!

Feedback welkom!