Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement UFW_TO port validation #8

Merged
merged 13 commits into from
Feb 6, 2021
Merged
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ services:
- UFW_MANAGED=true
- UFW_FROM=192.168.0.0/24
- UFW_DENY_OUTGOING=true
- UFW_TO=192.168.1.2;192.168.2.0/24
- UFW_TO=192.168.1.0/24;192.168.2.2:9000;192.168.2.3:8000/udp;192.168.3.0/24:tcp
ports:
- 80:80
```
Expand All @@ -207,8 +207,10 @@ To Action From
172.17.0.2 80/tcp ALLOW FWD 192.168.0.0/24 <= this entry allows only 192.168.1.0/24 to access nginx server
192.168.0.0/24 ALLOW FWD 172.17.0.2 80/tcp <= this entry enables nginx server to reply back

192.168.1.2 ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.1.2 ip for all tcp and udp ports
192.168.2.0/24 ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.2.0/24 subnet for all tcp and udp ports
192.168.1.0/24 ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.1.0/24 subnet for all tcp and udp ports
192.168.2.2 8000/udp ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.2.2 ip on 8000 port for udp protocol
192.168.2.3 9000 ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.2.3 ip on 9000 port for tcp and udp protocols
192.168.3.0/24/tcp ALLOW FWD 172.17.0.2/tcp <= this entry allow outgoing traffic to 192.168.3.0/24 subnet for all tcp ports

Anywhere DENY FWD 172.17.0.2 <= this entry block any other outgoing requests
```
54 changes: 50 additions & 4 deletions src/ufw-docker-automated.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,52 @@
#!/usr/bin/env python
import subprocess
import re
import docker
import subprocess
from ipaddress import ip_network

client = docker.from_env()

# implementation of a get method ontop __builtins__.list class
class _list(__builtins__.list):
def get(self, index, default=None):
try:
return self[index] if self[index] else default
except IndexError:
return default

def to_string_port(port):
if port.get(0) and port.get(1):
return f"on port {int(port.get(0))}/{port.get(1)}"
elif port.get(0):
return f"on port {int(port.get(0))}"
elif port.get(1):
return f"on proto {port.get(1)}"
else:
return ""

def validate_port(port):
if not port:
return {}
r = re.compile(r'^(\d+)?((/|^)(tcp|udp))?$')
if r.match(port) is None:
raise ValueError(f"'{port}' does not appear to be a valid port and protocol (examples: '80/tcp' or 'udp')")
if port in ['tcp', 'udp']:
return {'protocol': port, 'to_string_port': to_string_port(_list([None, port]))}
port_and_protocol_split = _list(port.split('/'))
if not (1 <= int(port_and_protocol_split.get(0)) <= 65535):
raise ValueError(f"'{port}' does not appear to be a valid port number")
return {'port': int(port_and_protocol_split.get(0)), 'protocol': port_and_protocol_split.get(1), 'to_string_port': to_string_port(port_and_protocol_split)}

def parse_ufw_to(label):
output = []
for item in label.split(';'):
item_list = _list(item.split(':'))
if len(item_list) == 2 or len(item_list) == 1:
output += [{
**{'ipnet': ip_network(item_list.get(0))},
**validate_port(port=item_list.get(1))
}]
return output

def manage_ufw():
for event in client.events(decode=True):
Expand Down Expand Up @@ -51,7 +93,7 @@ def manage_ufw():

if ufw_deny_outgoing == 'True' and 'UFW_TO' in container.labels:
try:
ufw_to = [ip_network(ipnet) for ipnet in container.labels.get('UFW_TO').split(';') if ipnet]
ufw_to = parse_ufw_to(container.labels.get('UFW_TO'))
except ValueError as e:
print(f"ufw-docker-automated: Invalid UFW label: UFW_TO={container.labels.get('UFW_TO')} exception={e}")
ufw_to = None
Expand Down Expand Up @@ -83,8 +125,12 @@ def manage_ufw():
if ufw_to:
for destination in ufw_to:
# Allow outgoing requests from the container to whitelisted IPs or Subnets
print(f"ufw-docker-automated: Adding UFW rule: allow outgoing from container {container.name} to {destination}")
subprocess.run([f"ufw route allow from {container_ip} to {destination}"],
print(f"ufw-docker-automated: Adding UFW rule: allow outgoing from container {container.name} to {destination.get('ipnet')} {destination.get('to_string_port', '')}")
destination_port = f"port {destination.get('port')}" if destination.get('port') else ""
destination_protocol = f"proto {destination.get('protocol')}" if destination.get('protocol') else ""
subprocess.run([f"ufw route allow {destination_protocol} \
from {container_ip} \
to {destination.get('ipnet')} {destination_port}"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,
shell=True)
# Deny any other outgoing requests
Expand Down