Firewall#

Use iptables to block IP addresses by country for inbound ports on a server. This is a whitelist: what is not explicitly allowed is blocked. This is useful, for example, to filter most SSH bruteforce attacks while leaving a webserver freely available. This is certainly not the most efficient approach at filtering because you can easily end with thousands of iptables rules that need to be scanned one by one.

See also

  • A collection of scripts I have written and/or adapted that I currently use on my systems as automated tasks 1

  • Simple shell script for GNU/Linux, built on iptables, which is able to filter incoming packets based on accepted port numbers and countries. It is aimed to SOHO users. 2

  • Linux Iptables Just Block By Country - nixCraft 3

  • Simple stateful firewall - ArchWiki 4

  • iptables - ArchWiki 5

  • 25 Most Frequently Used Linux IPTables Rules Examples 6

Setup#

  1. install the dependencies

    apt-get install iptables python3-yaml python3-requests iptables-persistent
    
  2. answer Yes to all questions

  3. install fpyutils. See reference

  4. create the jobs directories. See reference

    mkdir -p /home/jobs/{scripts,services}/by-user/root
    
  5. create the script

    /home/jobs/scripts/by-user/root/iptables_geoport.py#
      1#!/usr/bin/env python3
      2# -*- coding: utf-8 -*-
      3#
      4# iptables_geoport.py
      5#
      6# Copyright (C) 2020-2022 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
      7#
      8# This program is free software; you can redistribute it and/or modify
      9# it under the terms of the GNU General Public License as published by
     10# the Free Software Foundation; either version 2 of the License, or
     11# (at your option) any later version.
     12#
     13# This program is distributed in the hope that it will be useful,
     14# but WITHOUT ANY WARRANTY; without even the implied warranty of
     15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16# GNU General Public License for more details.
     17#
     18# You should have received a copy of the GNU General Public License along
     19# with this program; if not, write to the Free Software Foundation, Inc.,
     20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
     21
     22# OLD NOTICES
     23# ===========
     24# See url for more info - http://www.cyberciti.biz/faq/?p=3402
     25# Author: nixCraft <www.cyberciti.biz> under GPL v.2.0+
     26# Post Author: frnmst (Franco Masotti) (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
     27# New version heavily based on https://wiki.archlinux.org/index.php/Simple_stateful_firewall
     28#   https://wiki.archlinux.org/index.php/Iptables
     29#   and a little on http://www.thegeekstuff.com/2011/06/iptables-rules-examples/ as well as nixCraft for the bash stuff.
     30# ===========
     31r"""iptables_geoport.py."""
     32
     33import copy
     34import ipaddress
     35import os
     36import pathlib
     37import shlex
     38import sys
     39import urllib
     40
     41import fpyutils
     42import requests
     43import yaml
     44
     45
     46class UserNotRoot(Exception):
     47    r"""The user running the script is not root."""
     48
     49
     50##################
     51# Basic commands #
     52##################
     53
     54
     55def reset_rules():
     56    r"""Reset the chains and the tables."""
     57    # https://wiki.archlinux.org/index.php/Iptables#Resetting_rules
     58    #
     59    # Copyright (C) 2020 Arch Wiki contributors.
     60    # Permission is granted to copy, distribute and/or modify this document
     61    # under the terms of the GNU Free Documentation License, Version 1.3
     62    # or any later version published by the Free Software Foundation;
     63    # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
     64    # A copy of the license is included in the section entitled "GNU
     65    # Free Documentation License".
     66    commands = dict()
     67    commands['flush_tcp_chain'] = 'iptables --flush TCP'
     68    commands['flush_udp_chain'] = 'iptables --flush UDP'
     69    commands['flush_input_chain'] = 'iptables --flush INPUT'
     70    commands['flush_output_chain'] = 'iptables --flush OUTPUT'
     71    commands['flush_logging_chain'] = 'iptables --flush LOGGING'
     72
     73    commands['delete_tcp_chain'] = 'iptables --delete-chain TCP'
     74    commands['delete_udp_chain'] = 'iptables --delete-chain UDP'
     75    commands['delete_logging_chain'] = 'iptables --delete-chain LOGGING'
     76
     77    commands['flush_mangle_table'] = 'iptables --table mangle --flush'
     78    commands['delete_mangle_chain'] = 'iptables --table mangle --delete-chain'
     79    commands['flush_raw_table'] = 'iptables --table raw --flush'
     80    commands['delete_raw_chain'] = 'iptables --table raw --delete-chain'
     81    commands['flush_security_table'] = 'iptables --table security --flush'
     82    commands[
     83        'delete_security_chain'] = 'iptables --table security --delete-chain'
     84    commands['accept_input'] = 'iptables --policy INPUT ACCEPT'
     85    commands['accept_forward'] = 'iptables --policy FORWARD ACCEPT'
     86    commands['accept_output'] = 'iptables --policy OUTPUT ACCEPT'
     87
     88    # sys._getframe().f_code.co_name is the function name.
     89    fix_dict_keys(commands, sys._getframe().f_code.co_name, '__')
     90    return commands
     91
     92
     93def initialize_basic_chains() -> dict:
     94    r"""Apply some basic rules for a single machine."""
     95    # https://wiki.archlinux.org/index.php/Simple_stateful_firewall#Firewall_for_a_single_machine
     96    #
     97    # Copyright (C) 2020 Arch Wiki contributors.
     98    # Permission is granted to copy, distribute and/or modify this document
     99    # under the terms of the GNU Free Documentation License, Version 1.3
    100    # or any later version published by the Free Software Foundation;
    101    # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
    102    # A copy of the license is included in the section entitled "GNU
    103    # Free Documentation License".
    104    commands = dict()
    105
    106    # Output traffic is NOT filtered.
    107    commands['output_chain'] = 'iptables --policy OUTPUT ACCEPT'
    108
    109    # Create two user defined chains that will define tcp an udp protocol rules later.
    110    commands['tcp_chain'] = 'iptables --new-chain TCP'
    111    commands['udp_chain'] = 'iptables --new-chain UDP'
    112
    113    # For a single machine, however, we simply set the policy of the FORWARD chain to DROP and move on
    114    commands['drop_forward'] = 'iptables --policy FORWARD DROP'
    115
    116    # The first rule added to the INPUT chain will allow traffic that belongs
    117    # to established connections, or new valid traffic that is related to these
    118    # connections such as ICMP errors, or echo replies.
    119    commands[
    120        'allow_realted_established'] = 'iptables --append INPUT --match conntrack --ctstate RELATED,ESTABLISHED --jump ACCEPT'
    121
    122    # loopback interface INPUT traffic enabled for ping and debugging stuff.
    123    commands[
    124        'loopback'] = 'iptables --append INPUT --in-interface lo --jump ACCEPT'
    125
    126    # Drop all invalid INPUT (i.e. damaged) packets.
    127    # To do this connection must be tracked (conntrack)
    128    # and connection state (cstate) is set to INVALID.
    129    commands[
    130        'invalid_input'] = 'iptables --append INPUT --match conntrack --ctstate INVALID --jump DROP'
    131
    132    # Allow icmp type 8 (i.e. ping) to all interfaces.
    133    commands[
    134        'ping'] = 'iptables --append INPUT --protocol icmp --icmp-type 8 --match conntrack --ctstate NEW --jump ACCEPT'
    135
    136    # TCP snd UDP chains are connected to INPUT chains.
    137    # These two user-defined chains will manage the ports.
    138    # Remember that tcp uses SYN to initialize a connection, unlike UDP
    139    commands[
    140        'connect_tcp_chain'] = 'iptables --append INPUT --protocol tcp --syn -m conntrack --ctstate NEW --jump TCP'
    141    commands[
    142        'connect_udp_chain'] = 'iptables --append INPUT --protocol udp -m conntrack --ctstate NEW --jump UDP'
    143
    144    fix_dict_keys(commands, sys._getframe().f_code.co_name, '__')
    145    return commands
    146
    147
    148def initialize_logging_chain() -> dict:
    149    r"""Create the logging chain."""
    150    # https://wiki.archlinux.org/index.php/Iptables#Logging
    151    #
    152    # Copyright (C) 2020 Arch Wiki contributors.
    153    # Permission is granted to copy, distribute and/or modify this document
    154    # under the terms of the GNU Free Documentation License, Version 1.3
    155    # or any later version published by the Free Software Foundation;
    156    # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
    157    # A copy of the license is included in the section entitled "GNU
    158    # Free Documentation License".
    159    commands = dict()
    160
    161    commands['logging_chain'] = 'iptables --new-chain LOGGING'
    162    commands[
    163        'connect_logging_chain'] = 'iptables --append INPUT --jump LOGGING'
    164    commands[
    165        'logging_limit'] = 'iptables --append LOGGING --match limit --limit 2/hour --limit-burst 10 --jump LOG'
    166
    167    fix_dict_keys(commands, sys._getframe().f_code.co_name, '__')
    168    return commands
    169
    170
    171def initialize_blocking_rules(drop_packets: bool = True,
    172                              logging: bool = True) -> dict:
    173    r"""Initialize blocking rules."""
    174    # https://wiki.archlinux.org/index.php/Simple_stateful_firewall#Firewall_for_a_single_machine
    175    #
    176    # Copyright (C) 2020 Arch Wiki contributors.
    177    # Permission is granted to copy, distribute and/or modify this document
    178    # under the terms of the GNU Free Documentation License, Version 1.3
    179    # or any later version published by the Free Software Foundation;
    180    # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
    181    # A copy of the license is included in the section entitled "GNU
    182    # Free Documentation License".
    183    commands = dict()
    184
    185    if logging:
    186        chain = 'LOGGING'
    187    else:
    188        chain = 'INPUT'
    189    if drop_packets:
    190        # Drop everything.
    191        commands['drop'] = 'iptables --append ' + chain + ' --jump DROP'
    192    else:
    193        # RFC compilant.
    194        commands[
    195            'rfc_tcp'] = 'iptables --append ' + chain + ' --protocol tcp --jump REJECT --reject-with tcp-rst'
    196        commands[
    197            'rfc_udp'] = 'iptables --append ' + chain + ' --protocol udp --jump REJECT --reject-with icmp-port-unreachable'
    198        # Other protocols are usually not used, so REJECT those packets with icmp-proto-unreachable.
    199        commands[
    200            'proto_unreachable'] = 'iptables --append ' + chain + ' --jump REJECT --reject-with icmp-proto-unreachable'
    201
    202    fix_dict_keys(commands, sys._getframe().f_code.co_name, '__')
    203    return commands
    204
    205
    206def set_accepted_rules(ports: dict, accepted_ips: dict) -> dict:
    207    r"""Set accepted rules."""
    208    assert_input_ports_struct(ports)
    209    assert_accepted_ips_struct(accepted_ips)
    210
    211    commands = dict()
    212    # O(ports*chains*ips) <= O(ips^3) because of how this script works.
    213    i = 0
    214    for port in ports:
    215        source = ports[port]['source']
    216        ips = accepted_ips[source]
    217        chains, protocols = get_chains_and_protocols(ports[port]['protocol'])
    218        for w, chain in enumerate(chains):
    219            for ip in ips:
    220                commands[str(i)] = generate_accepted_rule_command(
    221                    chain, protocols[w], port, ip)
    222                i += 1
    223
    224    fix_dict_keys(commands, sys._getframe().f_code.co_name, '__')
    225    return commands
    226
    227
    228def set_patch_rules(rules: list) -> dict:
    229    r"""Pass raw commands directly."""
    230    for r in rules:
    231        if not isinstance(r, str):
    232            raise TypeError
    233
    234    commands = dict()
    235    for i, rule in enumerate(rules):
    236        commands[str(i)] = rule
    237
    238    fix_dict_keys(commands, sys._getframe().f_code.co_name, '__')
    239    return commands
    240
    241
    242def initialize_drop_rules() -> dict:
    243    r"""Initialize drop rules."""
    244    commands = dict()
    245    commands['drop'] = 'iptables --policy INPUT DROP'
    246
    247    fix_dict_keys(commands, sys._getframe().f_code.co_name, '__')
    248    return commands
    249
    250
    251#########
    252# Utils #
    253#########
    254
    255
    256def get_chains_and_protocols(protocol: str) -> tuple:
    257    r"""Compute the iptables chain and protocol values."""
    258    if protocol not in ['tcp', 'udp', 'all']:
    259        raise ValueError
    260
    261    chains = list()
    262    protocols = list()
    263    if protocol == 'tcp':
    264        chains.append('TCP')
    265        protocols.append('tcp')
    266    elif protocol == 'udp':
    267        chains.append('UDP')
    268        protocols.append('udp')
    269    elif protocol == 'all':
    270        chains.append('TCP')
    271        chains.append('UDP')
    272        protocols.append('tcp')
    273        protocols.append('udp')
    274
    275    return chains, protocols
    276
    277
    278def generate_accepted_rule_command(chain: str, protocol: str, port: str,
    279                                   remote_ip: str) -> str:
    280    r"""Generate a single command for the accepted rules."""
    281    check_port(port)
    282    check_ip_address(remote_ip)
    283
    284    return ('iptables --append ' + chain + ' --protocol ' + protocol +
    285            ' --dport ' + port + ' --source ' + remote_ip + ' --jump ACCEPT')
    286
    287
    288def fix_dict_keys(dictionary: dict, prefix: str, separator: str):
    289    r"""Fix the keys of a dictionary by adding a prefix and separator."""
    290    d = copy.deepcopy(dictionary)
    291    for key in d:
    292        dictionary[prefix + separator + key] = d[key]
    293        del dictionary[key]
    294
    295
    296def load_zone_file(zone_file: str) -> list:
    297    r"""Load zone file."""
    298    zones = list()
    299    with open(zone_file, 'r') as f:
    300        line = f.readline().strip()
    301        while line:
    302            check_ip_address(line)
    303            zones.append(line)
    304            line = f.readline().strip()
    305
    306    return zones
    307
    308
    309def load_zone_files(zone_files: list) -> list:
    310    r"""Load all the zone files content in a flat data structure."""
    311    for zf in zone_files:
    312        if not isinstance(zf, str):
    313            raise TypeError
    314
    315    zones = list()
    316    for zf in zone_files:
    317        zones += load_zone_file(zf)
    318
    319    return zones
    320
    321
    322def get_filename_from_url(url: str) -> str:
    323    r"""Use some tricks to get the filemame from a URL."""
    324    return pathlib.PurePath(urllib.parse.urlparse(url).path).name
    325
    326
    327def download_zone_file(url: str, dst_directory: str) -> str:
    328    r"""Save the zone file."""
    329    filename = get_filename_from_url(url)
    330    pathlib.Path(dst_directory).mkdir(mode=0o700, parents=True, exist_ok=True)
    331    full_path = str(pathlib.Path(dst_directory, filename))
    332    try:
    333        r = requests.get(url)
    334        with open(full_path, 'w') as f:
    335            f.write(r.text)
    336    except requests.ConnectionError:
    337        pass
    338
    339    return full_path
    340
    341
    342def download_zone_files(urls: list, cache_directory: str) -> list:
    343    r"""Download multiple zone files."""
    344    for u in urls:
    345        if not isinstance(u, str):
    346            raise TypeError
    347
    348    files = list()
    349    for u in urls:
    350        files.append(download_zone_file(u, cache_directory))
    351
    352    return files
    353
    354
    355def update_accepted_ips_structure(accepted_ips: dict, zones: list):
    356    r"""Update some data structures."""
    357    assert_accepted_ips_struct(accepted_ips)
    358    assert_zones_struct(zones)
    359
    360    accepted_ips['wan'] = zones
    361    accepted_ips['all'] = accepted_ips['lan'] + accepted_ips['wan']
    362
    363
    364def get_packet_policy(invalid_packet_policy: str) -> bool:
    365    r"""Update a variable."""
    366    if invalid_packet_policy not in ['polite', 'rude']:
    367        raise ValueError
    368
    369    drop = True
    370    if invalid_packet_policy == 'rude':
    371        drop = True
    372    elif invalid_packet_policy == 'polite':
    373        drop = False
    374    return drop
    375
    376
    377##############
    378# Assertions #
    379##############
    380
    381
    382def check_ip_address(ip: str):
    383    r"""Verify that we are dealing with a network address."""
    384    ipaddress.ip_network(ip, strict=True)
    385
    386
    387def check_port(port: str):
    388    r"""Check that the input port is a valid port number."""
    389    if not port.isdigit():
    390        raise TypeError
    391    if not (int(port) >= 0 and int(port) <= (2**16) - 1):
    392        raise ValueError
    393
    394
    395def assert_zones_struct(zones: list):
    396    r"""Check that the data structure is a list of ip addresses."""
    397    for z in zones:
    398        if not isinstance(z, str):
    399            raise TypeError
    400        check_ip_address(z)
    401
    402
    403def assert_accepted_ips_struct(accepted_ips: dict):
    404    r"""Check that the data structure is a list of ip addresses."""
    405    for ips in accepted_ips:
    406        if not isinstance(accepted_ips[ips], list):
    407            raise TypeError
    408        for j in accepted_ips[ips]:
    409            if not isinstance(j, str):
    410                raise TypeError
    411            check_ip_address(j)
    412
    413
    414def assert_input_ports_struct(ports: dict):
    415    r"""Check that the data structure is correct."""
    416    for port in ports:
    417        check_port(port)
    418        if not isinstance(ports[port], dict):
    419            raise TypeError
    420        if 'source' not in ports[port]:
    421            raise ValueError
    422        if 'protocol' not in ports[port]:
    423            raise ValueError
    424        if ports[port]['source'] not in ['lan', 'wan', 'all']:
    425            raise ValueError
    426
    427
    428############
    429# Pipeline #
    430############
    431
    432if __name__ == '__main__':
    433    if os.getuid() != 0:
    434        raise UserNotRoot
    435
    436    # Load the configuration.
    437    configuration_file = shlex.quote(sys.argv[1])
    438    config = yaml.load(open(configuration_file, 'r'), Loader=yaml.SafeLoader)
    439    dry_run = config['dry_run']
    440    cache_directory = config['cache_directory']
    441    zone_files = config['accepted_ips']['wan']
    442    accepted_ips = dict()
    443    accepted_ips['lan'] = config['accepted_ips']['lan']
    444    accepted_ips['wan'] = list()
    445    patch_rules = config['patch_rules']
    446    set_patch_rules_first = config['set_patch_rules_first']
    447    input_ports = config['input_ports']
    448    logging = config['logging_enabled']
    449    invalid_packet_policy = config['invalid_packet_policy']
    450    drop_packets = get_packet_policy(invalid_packet_policy)
    451
    452    # Get the data.
    453    zones = load_zone_files(download_zone_files(zone_files, cache_directory))
    454    update_accepted_ips_structure(accepted_ips, zones)
    455
    456    # Apply the rules.
    457    reset = reset_rules()
    458    basic_chains = initialize_basic_chains()
    459    logging_chain = initialize_logging_chain()
    460    blocking_rules = initialize_blocking_rules(drop_packets, logging)
    461    rules = set_accepted_rules(input_ports, accepted_ips)
    462    patch = set_patch_rules(patch_rules)
    463    drop_by_default = initialize_drop_rules()
    464
    465    # Merge the rules.
    466    if set_patch_rules_first:
    467        commands = {
    468            **reset,
    469            **basic_chains,
    470            **logging_chain,
    471            **blocking_rules,
    472            **patch,
    473            **rules,
    474            **drop_by_default
    475        }
    476    else:
    477        commands = {
    478            **reset,
    479            **basic_chains,
    480            **logging_chain,
    481            **blocking_rules,
    482            **rules,
    483            **patch,
    484            **drop_by_default
    485        }
    486
    487    # Apply the rules.
    488    for c in commands:
    489        fpyutils.shell.execute_command_live_output(commands[c],
    490                                                   dry_run=dry_run)
    
  6. create a configuration file

    /home/jobs/scripts/by-user/root/iptables_geoport.yaml#
     1#
     2# iptables_geoport.yaml
     3#
     4# Copyright (C) 2020-2022 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
     5#
     6# This program is free software; you can redistribute it and/or modify
     7# it under the terms of the GNU General Public License as published by
     8# the Free Software Foundation; either version 2 of the License, or
     9# (at your option) any later version.
    10#
    11# This program is distributed in the hope that it will be useful,
    12# but WITHOUT ANY WARRANTY; without even the implied warranty of
    13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14# GNU General Public License for more details.
    15#
    16# You should have received a copy of the GNU General Public License along
    17# with this program; if not, write to the Free Software Foundation, Inc.,
    18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
    19
    20# If set to true print the commands that would be executed.
    21dry_run: true
    22
    23logging_enabled: true
    24
    25# {rude,polite}
    26invalid_packet_policy: 'rude'
    27
    28# The path where the ip addresses list will be saved.
    29cache_directory: './.cache'
    30
    31# Have a look at https://www.ipdeny.com/ipblocks/
    32accepted_ips:
    33  wan:
    34    - 'https://www.ipdeny.com/ipblocks/data/countries/it.zone'
    35  lan:
    36    - '192.168.0.0/24'
    37
    38# If set to 'true' the "patch rules" will be applied before the "input ports" rules.
    39set_patch_rules_first: true
    40
    41# Raw rules that override the default ones.
    42# Use "[]" if you do not need patch rules.
    43patch_rules:
    44  # Web server.
    45  - 'iptables -A TCP -p tcp --dport 80 -j ACCEPT'
    46  - 'iptables -A TCP -p tcp --dport 443 -j ACCEPT'
    47
    48  # SSH LAN only.
    49  - 'iptables -A TCP -s 192.168.0.0/24 -p tcp -m tcp --dport 22 -j ACCEPT'
    50
    51  # SMTP.
    52  - 'iptables -A TCP -p tcp --dport 465 -j ACCEPT'
    53
    54  # IMAP.
    55  - 'iptables -A TCP -p tcp --dport 993 -j ACCEPT'
    56
    57  # Avahi
    58  - 'iptables -A UDP -p udp -m udp --dport 5353 -j ACCEPT'
    59  - 'iptables -A INPUT -p udp --dport 5353 -j ACCEPT'
    60  - 'iptables -A OUTPUT -p udp --dport 5353 -j ACCEPT'
    61
    62  # KDE Connect.
    63  - 'iptables -A UDP -p udp --dport 1714:1764 -j ACCEPT'
    64  - 'iptables -A TCP -p tcp --dport 1714:1764 -j ACCEPT'
    65
    66# source:       {lan,wan,all}
    67# protocol:     {tcp,udp,all}
    68input_ports:
    69  '2222':
    70    source:     'lan'
    71    protocol:   'tcp'
    72  '2223':
    73    source:     'lan'
    74    protocol:   'tcp'
    75  '5555':
    76    source:     'lan'
    77    protocol:   'tcp'
    78  '53':
    79    source:     'lan'
    80    protocol:   'all'
    81
    82  # CUPS.
    83  '631':
    84    source:     'lan'
    85    protocol:   'tcp'
    86
    87  '8100':
    88    source:     'lan'
    89    protocol:   'tcp'
    90
    91  # Required for SANE.
    92  '6566':
    93    source:     'lan'
    94    protocol:   'all'
    95
    96  # Move SSH rules here for performance reasons.
    97  '22':
    98    source:     'wan'
    99    protocol:   'tcp'
    

    Note

    Rules are scanned sequentially. Move frequently used rules upper in the file to improve performance.

  7. use this Systemd service unit file

    /home/jobs/services/by-user/root/iptables-geoport.service#
     1[Unit]
     2Description=Apply the iptables geoport rules
     3Wants=network-online.target
     4After=network-online.target
     5Requires=netfilter-persistent.service
     6After=netfilter-persistent.service
     7
     8[Service]
     9Type=simple
    10# Answer Yes (default value) to dpkg-reconfigure.
    11ExecStart=/usr/bin/bash -c '/home/jobs/scripts/by-user/root/iptables_geoport.py /home/jobs/scripts/by-user/root/iptables_geoport.yaml && [ -f /sbin/dpkg-reconfigure ] && dpkg-reconfigure --frontend noninteractive iptables-persistent'
    12
    13User=root
    14Group=root
    
  8. fix the permissions

    chmod 700 -R /home/jobs/scripts/by-user/iptables_geoport.*
    chmod 700 -R /home/jobs/services/by-user/root
    
  9. run the deploy script

SANE#

To be able to use a remote scanner with SANE using this setup you need to follow these steps

See also

  • SANE - ArchWiki 8

  • Secure use of iptables and connection tracking helpers 7

  1. open TCP and UDP ports 6566

    /home/jobs/scripts/by-user/root/iptables_geoport.yaml (extract)#
    65# source:       {lan,wan,all}
    66# protocol:     {tcp,udp,all}
    67input_ports:
    68  '2222':
    69    source:     'lan'
    70    protocol:   'tcp'
    71  '2223':
    72    source:     'lan'
    73    protocol:   'tcp'
    74  '5555':
    75    source:     'lan'
    76    protocol:   'tcp'
    77  '53':
    78    source:     'lan'
    79    protocol:   'all'
    80
    81  # CUPS.
    82  '631':
    83    source:     'lan'
    84    protocol:   'tcp'
    85
    86  '8100':
    87    source:     'lan'
    88    protocol:   'tcp'
    89
    90  # Required for SANE.
    91  '6566':
    92    source:     'lan'
    93    protocol:   'all'
    94
    95  # Move SSH rules here for performance reasons.
    96  '22':
    97    source:     'wan'
    98    protocol:   'tcp'
    
  2. add this file to load the kernel module

    /etc/modprobe.d/nf_conntrack.conf#
    1options nf_conntrack nf_conntrack_helper=1
    
  3. add this file to load the kernel module

    /etc/modules-load.d/nf_conntrack_sane.conf#
    1nf_conntrack_sane
    
  4. reboot

  5. check if the rules are active: 1 should be returned by the cat command

    cat /proc/sys/net/netfilter/nf_conntrack_helper
    1
    

Footnotes

1

https://software.franco.net.eu.org/frnmst/automated-tasks GNU GPLv3+, copyright (c) 2019-2022, Franco Masotti

2

https://software.franco.net.eu.org/frnmst-archives/iptables-geoport-directives GNU GPLv3+ and GNU GPLv2+, copyright (c) 2015 Franco Masotti

3

http://www.cyberciti.biz/faq/?p=3402 GPL v.2.0+, copyright (c) Author: nixCraft <www.cyberciti.biz>

4

https://wiki.archlinux.org/index.php/Simple_stateful_firewall GNU Free Documentation License 1.3 or late, copyright (c) ArchWiki contributors

5

https://wiki.archlinux.org/index.php/Iptables GNU Free Documentation License 1.3 or late, copyright (c) ArchWiki contributors

6

http://www.thegeekstuff.com/2011/06/iptables-rules-examples/ unknown license

7

https://wiki.archlinux.org/title/SANE#Firewall GNU Free Documentation License 1.3 or late, copyright (c) ArchWiki contributors

8

https://home.regit.org/wp-content/uploads/2011/11/secure-conntrack-helpers.html unknown license