Compare commits
	
		
			30 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						39c3fc0342
	
				 | 
					
					
						|||
| 
						
						
							
						
						221a7c2de2
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c8cd6c00f
	
				 | 
					
					
						|||
| 
						
						
							
						
						ae3c846a39
	
				 | 
					
					
						|||
| 
						
						
							
						
						5291140caf
	
				 | 
					
					
						|||
| 
						
						
							
						
						ee66b64cd8
	
				 | 
					
					
						|||
| 
						
						
							
						
						7f7d398e5c
	
				 | 
					
					
						|||
| 
						
						
							
						
						b539eac83d
	
				 | 
					
					
						|||
| 
						
						
							
						
						f7cc4d9d4e
	
				 | 
					
					
						|||
| 
						
						
							
						
						63c8e39b59
	
				 | 
					
					
						|||
| 
						
						
							
						
						0b1294b6e7
	
				 | 
					
					
						|||
| 
						
						
							
						
						dd68fd4ead
	
				 | 
					
					
						|||
| 
						
						
							
						
						a64d17b2e8
	
				 | 
					
					
						|||
| 
						
						
							
						
						3e64189f8f
	
				 | 
					
					
						|||
| 45dc173ea7 | |||
| 9b85bfabdb | |||
| 
						
						
							
						
						969ba0f64c
	
				 | 
					
					
						|||
| 
						
						
							
						
						d9b5d36835
	
				 | 
					
					
						|||
| 9f86e143fe | |||
| 
						
						
							
						
						a49da1f3ef
	
				 | 
					
					
						|||
| 
						
						
							
						
						36ff86c71e
	
				 | 
					
					
						|||
| 29f6e6093b | |||
| ccc7165d1b | |||
| 72f0e095ca | |||
| 
						
						
							
						
						61869049a0
	
				 | 
					
					
						|||
| 
						
						
							
						
						c868c63aa7
	
				 | 
					
					
						|||
| 
						
						
							
						
						9875dccec0
	
				 | 
					
					
						|||
| 
						
						
							
						
						359514e581
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ed6b65b6d
	
				 | 
					
					
						|||
| 
						
						
							
						
						9c6208f5c0
	
				 | 
					
					
						
							
								
								
									
										17
									
								
								.gitea/workflows/flake8.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								.gitea/workflows/flake8.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
---
 | 
			
		||||
name: Flake8
 | 
			
		||||
on: [push]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# XXX need to do stuff with uv
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: freebsd
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version: ["3.11"]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Analyse code with Flake8
 | 
			
		||||
        run: |
 | 
			
		||||
          flake8 $(git ls-files '*.py')
 | 
			
		||||
							
								
								
									
										17
									
								
								.gitea/workflows/mypy.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								.gitea/workflows/mypy.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
---
 | 
			
		||||
name: Mypy
 | 
			
		||||
on: [push]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# XXX need to do stuff with uv
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: freebsd
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version: ["3.11"]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Analyse code with Mypy
 | 
			
		||||
        run: |
 | 
			
		||||
          mypy --install-types --non-interactive $(git ls-files '*.py')
 | 
			
		||||
							
								
								
									
										17
									
								
								.gitea/workflows/pylint.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								.gitea/workflows/pylint.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
---
 | 
			
		||||
name: Pylint
 | 
			
		||||
on: [push]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# XXX need to do stuff with uv
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: freebsd
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version: ["3.11"]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Analyse code with Pylint
 | 
			
		||||
        run: |
 | 
			
		||||
          pylint $(git ls-files '*.py')
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -12,3 +12,6 @@ htmlcov/
 | 
			
		||||
dist/
 | 
			
		||||
build/
 | 
			
		||||
*.egg-info/
 | 
			
		||||
 | 
			
		||||
coverage.xml
 | 
			
		||||
report.xml
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,34 @@
 | 
			
		||||
---
 | 
			
		||||
run pylint:
 | 
			
		||||
  stage: test
 | 
			
		||||
  image: python:3.11
 | 
			
		||||
  script:
 | 
			
		||||
    - pip install --upgrade pylint
 | 
			
		||||
    - pylint $(git ls-files '*.py')
 | 
			
		||||
  tags:
 | 
			
		||||
    - docker
 | 
			
		||||
 | 
			
		||||
run flake8:
 | 
			
		||||
  stage: test
 | 
			
		||||
  image: python:3.11
 | 
			
		||||
  script:
 | 
			
		||||
    - pip install --upgrade flake8
 | 
			
		||||
    - flake8 $(git ls-files '*.py')
 | 
			
		||||
  tags:
 | 
			
		||||
    - docker
 | 
			
		||||
 | 
			
		||||
run mypy:
 | 
			
		||||
  stage: test
 | 
			
		||||
  image: python:3.11
 | 
			
		||||
  script:
 | 
			
		||||
    - pip install --upgrade mypy
 | 
			
		||||
    - mypy --install-types --non-interactive $(git ls-files '*.py')
 | 
			
		||||
  tags:
 | 
			
		||||
    - docker
 | 
			
		||||
 | 
			
		||||
run tests:
 | 
			
		||||
  stage: test
 | 
			
		||||
  image: python:3.8
 | 
			
		||||
  image: python:3.11
 | 
			
		||||
  script:
 | 
			
		||||
    - pip install pytest pytest-cov pytest-mock pytest-flask
 | 
			
		||||
    - pip install Flask-HTTPAuth
 | 
			
		||||
@ -11,7 +39,9 @@ run tests:
 | 
			
		||||
  artifacts:
 | 
			
		||||
    when: always
 | 
			
		||||
    reports:
 | 
			
		||||
      cobertura: coverage.xml
 | 
			
		||||
      coverage_report:
 | 
			
		||||
        coverage_format: cobertura
 | 
			
		||||
        path: coverage.xml
 | 
			
		||||
      junit: report.xml
 | 
			
		||||
  tags:
 | 
			
		||||
    - docker
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								CHANGELOG
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								CHANGELOG
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
- 2023.1
 | 
			
		||||
 | 
			
		||||
* Implement #3, a /ping health check endpoint
 | 
			
		||||
							
								
								
									
										128
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								README.md
									
									
									
									
									
								
							@ -1,3 +1,131 @@
 | 
			
		||||
[](https://gitlab.niet.verweg.com/ruben/jail2ban-pf/-/commits/main)
 | 
			
		||||
[](https://gitlab.niet.verweg.com/ruben/jail2ban-pf/-/commits/main)
 | 
			
		||||
 | 
			
		||||
An API to remotely control a pf based fail2ban
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
* Install uwsgi 
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo pkg install www/uwsgi
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* Clone this repository
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
 | 
			
		||||
### rc.conf
 | 
			
		||||
 | 
			
		||||
* Use the following for configuring uwsgi in rc.conf
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo sysrc uwsgi\_enable="YES"
 | 
			
		||||
sudo sysrc uwsgi\_profiles="jail2ban\_pf"
 | 
			
		||||
sudo sysrc uwsgi\_jail2ban\_pf\_flags="-L -M --uid \_jail2ban --python-path /opt/jail2ban-pf --wsgi-file /opt/jail2ban-pf/wsgi.py --stats 127.0.0.1:9191 --socket 127.0.0.1:3031 --chdir /var/empty --callable app --manage-script-name"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### jail2ban
 | 
			
		||||
 | 
			
		||||
* Configure <installation root>/instance/config.py
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
SECRET\_KEY = os.urandom(32).hex()
 | 
			
		||||
AUTHFILE = '/usr/local/etc/jail2ban-pf-users.txt'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### nginx
 | 
			
		||||
 | 
			
		||||
* Configure a nginx upstream and vhost
 | 
			
		||||
 | 
			
		||||
_Of course you can listen on ipv4/ipv6 but you want to protect these addresses from inadvertent or malicious probes_
 | 
			
		||||
 | 
			
		||||
    upstream uwsgi_pf_jail2ban {
 | 
			
		||||
        server 127.0.0.1:3031;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    server {
 | 
			
		||||
        listen       unix:/path/to/jail_1/var/run/pf2ban/pf_jail2ban.sock;
 | 
			
		||||
        listen       unix:/path/to/jail_2/var/run/pf2ban/pf_jail2ban.sock;
 | 
			
		||||
        listen       unix:/path/to/jail_3/var/run/pf2ban/pf_jail2ban.sock;
 | 
			
		||||
        server_name  _;
 | 
			
		||||
 | 
			
		||||
        location / {
 | 
			
		||||
            index     index.html index.htm index.php;
 | 
			
		||||
            allow all;
 | 
			
		||||
            include /usr/local/etc/nginx/uwsgi_params-dist;
 | 
			
		||||
            uwsgi_pass uwsgi_pf_jail2ban;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
### /etc/pf.conf
 | 
			
		||||
 | 
			
		||||
* Place anchors in pf for jail2ban to use. You probably want to place the early in your existing pf configuration
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
anchor "f2b/*"
 | 
			
		||||
anchor f2b-jail {
 | 
			
		||||
    anchor "jail1_fqdn" to { <addr_jail1>, <addr_extra_jail1>, <addr_extra6_jail1> }
 | 
			
		||||
    anchor "jail2_fqdn" to { <addr_jail2>, <addr_extra_jail2>, <addr_extra6_jail2> }
 | 
			
		||||
    anchor "jail3_fqdn" to { <addr_jail3>, <addr_extra_jail3>, <addr_extra6_jail3> }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Having seperate anchors per jail makes it possible to have fine grained
 | 
			
		||||
blocking: Something that is harmful to jail2 might be perfectly legit for jail2.
 | 
			
		||||
 | 
			
		||||
#### Checking rules/tables made with fail2ban/jail2ban
 | 
			
		||||
Fail2ban will (re)create the per anchor rules on startup, and populate the designated address tables with offenders, e.g.:
 | 
			
		||||
 | 
			
		||||
    sudo pfctl -a f2b-jail/jail1\_fqdn -T show -t f2b-recidive
 | 
			
		||||
    192.0.2.66
 | 
			
		||||
    2001:db8:abad:cafe:0bad:f00d
 | 
			
		||||
 | 
			
		||||
And the rules referencing these tables
 | 
			
		||||
 | 
			
		||||
    sudo pfctl -a 'f2b-jail/jail1\_fqdn' -s rules
 | 
			
		||||
    block drop quick proto tcp from <f2b-dovecot> to any port = pop3
 | 
			
		||||
    block drop quick proto tcp from <f2b-dovecot> to any port = pop3s
 | 
			
		||||
    block drop quick proto tcp from <f2b-dovecot> to any port = imap
 | 
			
		||||
    block drop quick proto tcp from <f2b-dovecot> to any port = imaps
 | 
			
		||||
    block drop quick proto tcp from <f2b-dovecot> to any port = submission
 | 
			
		||||
    block drop quick proto tcp from <f2b-dovecot> to any port = smtps
 | 
			
		||||
    block drop quick proto tcp from <f2b-dovecot> to any port = sieve
 | 
			
		||||
    block drop quick proto tcp from <f2b-sendmail-auth> to any port = submission
 | 
			
		||||
    block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtps
 | 
			
		||||
    block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtp
 | 
			
		||||
    block drop quick proto tcp from <f2b-sshd> to any port = ssh
 | 
			
		||||
    block drop quick proto tcp from <f2b-recidive> to any
 | 
			
		||||
    
 | 
			
		||||
### fail2ban
 | 
			
		||||
 | 
			
		||||
* Create the following action plugin for fail2ban on the jail desiring to use fail2ban/jail2ban
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
cat <<'EOT' | tee /usr/local/etc/fail2ban/action.d/jail2ban-pf.conf > /dev/null
 | 
			
		||||
Definition]
 | 
			
		||||
actionstart = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -XPUT -H 'Content-Type: application/json' -d '{"port":"<actiontype>","name":"<name>","protocol":"<protocol>"}' http://localhost/register
 | 
			
		||||
actionstart_on_demand = false
 | 
			
		||||
actionstop = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -XDELETE -H 'Content-Type: application/json' -d '{"port":"<actiontype>","name":"<name>","protocol":"<protocol>"}' http://localhost/register
 | 
			
		||||
actionflush = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -X GET http://localhost/flush/<name>
 | 
			
		||||
actioncheck = 
 | 
			
		||||
actionban = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -X PUT -H 'Content-Type: application/json' -d '{"name":"<name>","ip":"<ip>"}' http://localhost/ban
 | 
			
		||||
actionunban = curl --unix-socket <jail2ban_sock> --basic -u '<jail2ban_user>:<jail2ban_pass>' -X DELETE -H 'Content-Type: application/json' -d '{"name":"<name>","ip":"<ip>"}' http://localhost/ban
 | 
			
		||||
[Init]
 | 
			
		||||
protocol = tcp
 | 
			
		||||
actiontype = <multiport>
 | 
			
		||||
allports = any
 | 
			
		||||
multiport = any port {<port>}
 | 
			
		||||
jail2ban_sock = /var/run/pf2ban/jail2ban.sock
 | 
			
		||||
jail2ban_user = login as set in password file for jail2ban
 | 
			
		||||
jail2ban_pass = password as set in password file for jail2ban
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* Configure jail.local
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
cat <<'EOT' | tee /usr/local/etc/fail2ban/jail.local > /dev/null
 | 
			
		||||
[DEFAULT]
 | 
			
		||||
banaction = jail2ban-pf
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,18 @@
 | 
			
		||||
from flask import Flask, request, jsonify, current_app
 | 
			
		||||
from flask_httpauth import HTTPBasicAuth
 | 
			
		||||
from werkzeug.security import check_password_hash
 | 
			
		||||
from ipaddress import ip_address
 | 
			
		||||
'''
 | 
			
		||||
An API to remotely control a pf based fail2ban
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from jail2ban.pfctl import pfctl_table_op, pfctl_cfg_read, pfctl_cfg_write
 | 
			
		||||
from jail2ban.auth import get_users
 | 
			
		||||
from ipaddress import ip_address
 | 
			
		||||
from subprocess import CalledProcessError
 | 
			
		||||
 | 
			
		||||
from flask import Flask, current_app, jsonify, request
 | 
			
		||||
from flask_httpauth import HTTPBasicAuth  # type: ignore
 | 
			
		||||
from werkzeug.security import check_password_hash
 | 
			
		||||
 | 
			
		||||
from jail2ban.auth import get_users
 | 
			
		||||
from jail2ban.pfctl import pfctl_cfg_read, pfctl_cfg_write, pfctl_table_op
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
auth = HTTPBasicAuth()
 | 
			
		||||
 | 
			
		||||
@ -16,39 +22,52 @@ PAT_PROT = r'^(?:tcp|udp)$'
 | 
			
		||||
PAT_NAME = r'^[\w\-]+$'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def untaint(pattern, string):
 | 
			
		||||
def untaint(pattern: str, string: str) -> str:
 | 
			
		||||
    '''
 | 
			
		||||
    untaint string (as perl does)
 | 
			
		||||
    '''
 | 
			
		||||
    match = re.match(pattern, string)
 | 
			
		||||
    if match:
 | 
			
		||||
        return match.string
 | 
			
		||||
    else:
 | 
			
		||||
    raise ValueError(f'"{string}" is tainted')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_app():
 | 
			
		||||
    '''
 | 
			
		||||
    Create the flask application
 | 
			
		||||
    '''
 | 
			
		||||
    app = Flask(__name__, instance_relative_config=True)
 | 
			
		||||
 | 
			
		||||
    # load the instance config, if it exists, when not testing
 | 
			
		||||
    app.config.from_pyfile('config.py', silent=True)
 | 
			
		||||
 | 
			
		||||
    @auth.verify_password
 | 
			
		||||
    def verify_password(username, password):
 | 
			
		||||
    def verify_password(username: str, password: str) -> str | None:
 | 
			
		||||
        users = get_users()
 | 
			
		||||
        current_app.logger.debug(users)
 | 
			
		||||
        current_app.logger.debug('Checking password of %s', username)
 | 
			
		||||
        if username in users and \
 | 
			
		||||
                check_password_hash(users.get(username), password):
 | 
			
		||||
            return username
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @app.route("/ping", methods=['GET'])
 | 
			
		||||
    @auth.login_required
 | 
			
		||||
    def ping():
 | 
			
		||||
        remote_user = auth.username()
 | 
			
		||||
        app.logger.info('Received ping for'
 | 
			
		||||
                        ' anchor f2b-jail/%s', remote_user)
 | 
			
		||||
        return jsonify({'anchor': f'f2b-jail/{remote_user}',
 | 
			
		||||
                        'operation': 'ping',
 | 
			
		||||
                        'result': 'pong'})
 | 
			
		||||
 | 
			
		||||
    @app.route("/flush/<name>", methods=['GET'])
 | 
			
		||||
    @auth.login_required
 | 
			
		||||
    def flush(name):
 | 
			
		||||
        remote_user = auth.username()
 | 
			
		||||
        name = untaint(PAT_NAME, name)
 | 
			
		||||
        app.logger.info(f'Flushing table f2b-{name}'
 | 
			
		||||
                        f' in anchor f2b-jail/{remote_user}')
 | 
			
		||||
        app.logger.info('Flushing table f2b-%s'
 | 
			
		||||
                        ' in anchor f2b-jail/%s', name, remote_user)
 | 
			
		||||
        res = pfctl_table_op('f2b-jail/{remote_user}',
 | 
			
		||||
                             table='f2b-{name}',
 | 
			
		||||
                             operation='flush')
 | 
			
		||||
@ -88,7 +107,7 @@ def create_app():
 | 
			
		||||
            pfctl_table_op(f'f2b-jail/{remote_user}',
 | 
			
		||||
                           table=f'f2b-{name}',
 | 
			
		||||
                           operation='kill')
 | 
			
		||||
        app.logger.info(f'pfctl -a f2b-jail/{remote_user} -f-')
 | 
			
		||||
        app.logger.info('pfctl -a f2b-jail/%s -f-', remote_user)
 | 
			
		||||
        return jsonify({'anchor': f'f2b-jail/{remote_user}',
 | 
			
		||||
                        'table': f'f2b-{name}',
 | 
			
		||||
                        'action': 'start' if request.method == 'PUT'
 | 
			
		||||
@ -104,15 +123,15 @@ def create_app():
 | 
			
		||||
        name = untaint(PAT_NAME, data['name'])
 | 
			
		||||
        ip = ip_address(data['ip'])
 | 
			
		||||
        if request.method == 'PUT':
 | 
			
		||||
            app.logger.info(f'Add {ip} to f2b-{name}'
 | 
			
		||||
                            f' in anchor f2b-jail/{remote_user}')
 | 
			
		||||
            app.logger.info('Add %s to f2b-%s'
 | 
			
		||||
                            ' in anchor f2b-jail/%s', ip, name, remote_user)
 | 
			
		||||
            res = pfctl_table_op(f'f2b-jail/{remote_user}',
 | 
			
		||||
                                 table=f'f2b-{name}',
 | 
			
		||||
                                 operation='add',
 | 
			
		||||
                                 value=str(ip))
 | 
			
		||||
        else:  # 'DELETE':
 | 
			
		||||
            app.logger.info(f'Remove {ip} from f2b-{name}'
 | 
			
		||||
                            f' in anchor f2b-jail/{remote_user}')
 | 
			
		||||
            app.logger.info('Remove %s from f2b-%s'
 | 
			
		||||
                            ' in anchor f2b-jail/%s', ip, name, remote_user)
 | 
			
		||||
            res = pfctl_table_op(f'f2b-jail/{remote_user}',
 | 
			
		||||
                                 table=f'f2b-{name}',
 | 
			
		||||
                                 operation='delete',
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,13 @@
 | 
			
		||||
'''
 | 
			
		||||
Authentication backend
 | 
			
		||||
'''
 | 
			
		||||
from flask import current_app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_users():
 | 
			
		||||
    '''
 | 
			
		||||
    Load users from password file (AUTHFILE)
 | 
			
		||||
    '''
 | 
			
		||||
    users = {}
 | 
			
		||||
    authfile = current_app.config['AUTHFILE']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,6 @@
 | 
			
		||||
'''
 | 
			
		||||
pf table/anchor operations
 | 
			
		||||
'''
 | 
			
		||||
import logging
 | 
			
		||||
from subprocess import run
 | 
			
		||||
 | 
			
		||||
@ -6,6 +9,9 @@ _PFCTL = '/sbin/pfctl'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pfctl_cfg_read(anchor):
 | 
			
		||||
    '''
 | 
			
		||||
    Read from pf anchor
 | 
			
		||||
    '''
 | 
			
		||||
    cmd = [_SUDO, _PFCTL, '-a', anchor, '-sr']
 | 
			
		||||
    logging.info('Running %s', cmd)
 | 
			
		||||
 | 
			
		||||
@ -16,6 +22,9 @@ def pfctl_cfg_read(anchor):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pfctl_cfg_write(anchor, cfg):
 | 
			
		||||
    '''
 | 
			
		||||
    Apply configuration to pf anchor
 | 
			
		||||
    '''
 | 
			
		||||
    cmd = [_SUDO, _PFCTL, '-a', anchor, '-f-']
 | 
			
		||||
    logging.info('Running %s', cmd)
 | 
			
		||||
    logging.info('Config %s', cfg)
 | 
			
		||||
@ -30,6 +39,13 @@ def pfctl_cfg_write(anchor, cfg):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pfctl_table_op(anchor, **kwargs):
 | 
			
		||||
    '''
 | 
			
		||||
    pf table operation
 | 
			
		||||
    Parameters:
 | 
			
		||||
    * table: which table to work on
 | 
			
		||||
    * operation: operation used on the table
 | 
			
		||||
    * value (optional): value used for the operation
 | 
			
		||||
    '''
 | 
			
		||||
    table = kwargs['table']
 | 
			
		||||
    operation = kwargs['operation']
 | 
			
		||||
    value = kwargs['value'] if 'value' in kwargs else None
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,10 @@
 | 
			
		||||
import pytest
 | 
			
		||||
'''
 | 
			
		||||
Test fixtures
 | 
			
		||||
'''
 | 
			
		||||
import base64
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from jail2ban import create_app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,14 +26,23 @@ def app():
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def client(app):
 | 
			
		||||
    '''
 | 
			
		||||
    Create a synthetic client
 | 
			
		||||
    '''
 | 
			
		||||
    return app.test_client()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def runner(app):
 | 
			
		||||
    '''
 | 
			
		||||
    Create a synthetic runner
 | 
			
		||||
    '''
 | 
			
		||||
    return app.test_cli_runner()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def valid_credentials():
 | 
			
		||||
    '''
 | 
			
		||||
    Mock authentication for the test
 | 
			
		||||
    '''
 | 
			
		||||
    return base64.b64encode(b"test.example.com:testpassword").decode("utf-8")
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,13 @@
 | 
			
		||||
'''
 | 
			
		||||
Test banning
 | 
			
		||||
'''
 | 
			
		||||
from types import SimpleNamespace
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_ban_ipv6(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test ban an IPv6 address
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = SimpleNamespace()
 | 
			
		||||
@ -22,6 +28,9 @@ def test_ban_ipv6(client, mocker, valid_credentials):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_ban_ipv4(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test ban an IPv4 address
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = SimpleNamespace()
 | 
			
		||||
@ -42,6 +51,9 @@ def test_ban_ipv4(client, mocker, valid_credentials):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_ban_invalid(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test ban an invalid address
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = SimpleNamespace()
 | 
			
		||||
@ -63,6 +75,9 @@ def test_ban_invalid(client, mocker, valid_credentials):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_unban_ipv6(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test unbanning an IPv6 address
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = SimpleNamespace()
 | 
			
		||||
@ -83,6 +98,9 @@ def test_unban_ipv6(client, mocker, valid_credentials):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_unban_ipv4(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test unbanning an IPv4 address
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = SimpleNamespace()
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,14 @@
 | 
			
		||||
'''
 | 
			
		||||
Test flushing pf tables
 | 
			
		||||
'''
 | 
			
		||||
from types import SimpleNamespace
 | 
			
		||||
from subprocess import CalledProcessError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_flush(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test flushing existing entry
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = SimpleNamespace()
 | 
			
		||||
@ -22,6 +28,9 @@ def test_flush(client, mocker, valid_credentials):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_flush_nonexistent(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test flushing non existing entry
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    cmd = ['/usr/local/bin/sudo',
 | 
			
		||||
           '/sbin/pfctl', '-a', 'some/anchor',
 | 
			
		||||
@ -42,6 +51,9 @@ def test_flush_nonexistent(client, mocker, valid_credentials):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wrong_method(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test invalid method
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    cmd = ['/usr/local/bin/sudo',
 | 
			
		||||
           '/sbin/pfctl', '-a', 'some/anchor',
 | 
			
		||||
@ -61,7 +73,10 @@ def test_wrong_method(client, mocker, valid_credentials):
 | 
			
		||||
    assert response.status_code == 405
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_filenotfound(app, mocker, valid_credentials):
 | 
			
		||||
def test_filenotfound(app, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test for when AUTHFILE cannot be found
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    app.config.update({
 | 
			
		||||
        "AUTHFILE": '../tests/nonexistent-users-test.txt'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								tests/test_ping.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/test_ping.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
'''
 | 
			
		||||
Test application health check
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_ping(client, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test application health check
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    response = client.get("/ping",
 | 
			
		||||
                          headers={"Authorization":
 | 
			
		||||
                                   "Basic " + valid_credentials})
 | 
			
		||||
 | 
			
		||||
    assert response.json['operation'] == 'ping'
 | 
			
		||||
@ -1,6 +1,9 @@
 | 
			
		||||
'''
 | 
			
		||||
Test various registration scenarios
 | 
			
		||||
'''
 | 
			
		||||
from subprocess import CompletedProcess
 | 
			
		||||
 | 
			
		||||
pfctl_stdout_lines = b'''
 | 
			
		||||
PFCTL_STDOUT_LINES = b'''
 | 
			
		||||
block drop quick proto tcp from <f2b-sendmail-auth> to any port = submission
 | 
			
		||||
block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtps
 | 
			
		||||
block drop quick proto tcp from <f2b-sendmail-auth> to any port = smtp
 | 
			
		||||
@ -8,13 +11,16 @@ block drop quick proto tcp from <f2b-sshd> to any port = ssh
 | 
			
		||||
block drop quick proto tcp from <f2b-recidive> to any
 | 
			
		||||
'''.strip() + b'\n'
 | 
			
		||||
 | 
			
		||||
pfctl_stdout_lines_scratch = b'table <f2b-dovecot> persist counters\n' \
 | 
			
		||||
PFCTL_STDOUT_LINES_SCRATCH = b'table <f2b-dovecot> persist counters\n' \
 | 
			
		||||
                             b'block quick proto tcp from <f2b-dovecot>' \
 | 
			
		||||
                             b' to any port ' \
 | 
			
		||||
                             b'{pop3,pop3s,imap,imaps,submission,465,sieve}\n'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_register_unauth(client):
 | 
			
		||||
    '''
 | 
			
		||||
    Test a registration without being authorized
 | 
			
		||||
    '''
 | 
			
		||||
    json_payload = {"port":
 | 
			
		||||
                    "any port {pop3,pop3s,imap,imaps,submission,465,sieve}",
 | 
			
		||||
                    "name": "dovecot", "protocol": "tcp"}
 | 
			
		||||
@ -24,10 +30,13 @@ def test_register_unauth(client):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_unregister_valid(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test unregistration
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = CompletedProcess(args=['true'], returncode=0)
 | 
			
		||||
    run_res.stdout = pfctl_stdout_lines
 | 
			
		||||
    run_res.stdout = PFCTL_STDOUT_LINES
 | 
			
		||||
    run_res.check_returncode = noop
 | 
			
		||||
 | 
			
		||||
    mocker.patch('jail2ban.pfctl.run', return_value=run_res)
 | 
			
		||||
@ -45,10 +54,13 @@ def test_unregister_valid(client, mocker, valid_credentials):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_register_valid(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test a registration of a rule
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = CompletedProcess(args=['true'], returncode=0)
 | 
			
		||||
    run_res.stdout = pfctl_stdout_lines
 | 
			
		||||
    run_res.stdout = PFCTL_STDOUT_LINES
 | 
			
		||||
    run_res.check_returncode = noop
 | 
			
		||||
 | 
			
		||||
    pfctl_run = mocker.patch('jail2ban.pfctl.run', return_value=run_res)
 | 
			
		||||
@ -63,13 +75,16 @@ def test_register_valid(client, mocker, valid_credentials):
 | 
			
		||||
                                   "Basic " + valid_credentials})
 | 
			
		||||
 | 
			
		||||
    pfctl_run_input_arg = pfctl_run.call_args_list[1][1]['input']
 | 
			
		||||
    for existing_line in pfctl_stdout_lines.splitlines():
 | 
			
		||||
    for existing_line in PFCTL_STDOUT_LINES.splitlines():
 | 
			
		||||
        assert existing_line in pfctl_run_input_arg.splitlines()
 | 
			
		||||
 | 
			
		||||
    assert response.json['action'] == 'start'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_register_valid_from_scratch(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test from scratch point of view
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = CompletedProcess(args=['true'], returncode=0)
 | 
			
		||||
@ -88,15 +103,18 @@ def test_register_valid_from_scratch(client, mocker, valid_credentials):
 | 
			
		||||
                                   "Basic " + valid_credentials})
 | 
			
		||||
 | 
			
		||||
    pfctl_run_input_arg = pfctl_run.call_args_list[1][1]['input']
 | 
			
		||||
    assert pfctl_run_input_arg == pfctl_stdout_lines_scratch
 | 
			
		||||
    assert pfctl_run_input_arg == PFCTL_STDOUT_LINES_SCRATCH
 | 
			
		||||
    assert response.json['action'] == 'start'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_register_invalid(client, mocker, valid_credentials):
 | 
			
		||||
    '''
 | 
			
		||||
    Test a bogus pf command
 | 
			
		||||
    '''
 | 
			
		||||
    def noop():
 | 
			
		||||
        pass
 | 
			
		||||
    run_res = CompletedProcess(args=['true'], returncode=0)
 | 
			
		||||
    run_res.stdout = pfctl_stdout_lines
 | 
			
		||||
    run_res.stdout = PFCTL_STDOUT_LINES
 | 
			
		||||
    run_res.check_returncode = noop
 | 
			
		||||
 | 
			
		||||
    mocker.patch('jail2ban.pfctl.run', return_value=run_res)
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user