"""Validated private-interface settings and a fixed-identity reverse proxy.""" import ipaddress import json PROXY_IMAGE = 'nginx:stable-alpine@sha256:0985e772fb9f729e6fa0980da05fca5d9c468e870eed43071545afa9d2e27d94' NETWORK = 'adamwen-test-console-net' PROXY = 'adamwen-test-console-proxy' def validate_settings(value): if not isinstance(value, dict) or set(value) != {'address', 'allowed_cidrs', 'port'}: raise ValueError('Exactly address, allowed_cidrs and port must be configured') address = ipaddress.ip_address(value['address']) if not isinstance(value['allowed_cidrs'],list) or not 1 <= len(value['allowed_cidrs']) <= 8: raise ValueError('Configure one to eight explicit private source networks') networks = [ipaddress.ip_network(n, strict=True) for n in value['allowed_cidrs']] private = [ipaddress.ip_network(n) for n in ('10.0.0.0/8','172.16.0.0/12','192.168.0.0/16')] if address.version != 4 or any(n.version != 4 or not any(n.subnet_of(p) for p in private) for n in networks): raise ValueError('An explicit RFC1918 IPv4 interface and network are required') if not any(address in n and address not in (n.network_address,n.broadcast_address) for n in networks): raise ValueError('The interface must belong to the allowed LAN subnet') if type(value['port']) is not int or not 1024 <= value['port'] <= 65535: raise ValueError('The port must be an integer between 1024 and 65535') if len({str(n) for n in networks}) != len(networks): raise ValueError('Duplicate source networks are not allowed') return {'address': str(address), 'allowed_cidrs': [str(n) for n in networks], 'port': value['port']} def load_settings(path): return validate_settings(json.loads(path.read_text(encoding='utf-8'))) def proxy_config(settings): value = validate_settings(settings) address, port = value['address'], value['port'] allows = '\n '.join(f'allow {cidr};' for cidr in value['allowed_cidrs']) origin = f'http://{address}:{port}' return f'''worker_processes 1; pid /tmp/nginx.pid; error_log /dev/stderr warn; events {{ worker_connections 256; }} http {{ client_body_temp_path /tmp/client_body; proxy_temp_path /tmp/proxy; fastcgi_temp_path /tmp/fastcgi; uwsgi_temp_path /tmp/uwsgi; scgi_temp_path /tmp/scgi; log_format limited '$remote_addr [$time_local] "$request_method $uri $server_protocol" $status $body_bytes_sent'; access_log /dev/stdout limited; map $http_origin $origin_allowed {{ default 0; "" 1; "{origin}" 1; }} map $http_sec_fetch_site $cross_site {{ default 0; cross-site 1; }} server {{ listen 8080; server_name {address}; {allows} deny all; if ($host != {address}) {{ return 403; }} if ($origin_allowed = 0) {{ return 403; }} if ($cross_site = 1) {{ return 403; }} if ($arg_authtoken != "") {{ return 403; }} client_max_body_size 1m; keepalive_timeout 30s; send_timeout 30s; location / {{ proxy_pass http://127.0.0.1:4440; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host {address}:{port}; proxy_set_header X-Forwarded-Host {address}:{port}; proxy_set_header X-Forwarded-Port {port}; proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Forwarded ""; proxy_set_header X-Forwarded-Uuid xas-intranet; proxy_set_header X-Forwarded-Roles xas-operator; proxy_set_header X-Forwarded-User-FirstName ""; proxy_set_header X-Forwarded-User-LastName ""; proxy_set_header X-Forwarded-User-Email ""; proxy_set_header Authorization ""; proxy_set_header X-Rundeck-Auth-Token ""; proxy_buffering off; proxy_connect_timeout 5s; proxy_read_timeout 60s; proxy_send_timeout 30s; }} }} }} '''