Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Let's assume that the nethsecurity-controller instance is named `nethsecurity-co
Launch `configure-module`, by setting the following parameters:
- `host`: a fully qualified domain name for the controller
- `lets_encrypt`: enable or disable Let's Encrypt certificate
- `ovpn_network`: OpenVPN network
- `ovpn_netmask`: OpenVPN netmask
- `ovpn_network`: OpenVPN network, it must be the first address of the network
- `ovpn_netmask`: OpenVPN netmask, from `255.255.240.0` (/20) to `255.255.255.0` (/24)
- `ovpn_cn`: OpenVPN Certificate CN
- `api_user`: controller admin user
- `api_password`: controller admin password, change it after first login
Expand All @@ -49,7 +49,7 @@ Launch `configure-module`, by setting the following parameters:

Example:

api-cli run module/nethsecurity-controller1/configure-module --data '{"host": "mycontroller.nethsecurity.org", "lets_encrypt": false, "ovpn_network": "172.19.64.0", "ovpn_netmask": "255.255.255.0", "ovpn_cn": "nethsec", "api_user": "admin", "api_password": "password", "loki_retention": 180, "prometheus_retention": 15, "maxmind_license": "xxx"}'
api-cli run module/nethsecurity-controller1/configure-module --data '{"host": "mycontroller.nethsecurity.org", "lets_encrypt": false, "ovpn_network": "172.19.64.0", "ovpn_netmask": "255.255.240.0", "ovpn_cn": "nethsec", "api_user": "admin", "api_password": "password", "loki_retention": 180, "prometheus_retention": 15, "maxmind_license": "xxx"}'

The above command will:
- start and configure the nethsecurity-controller instance
Expand All @@ -65,6 +65,38 @@ The above command will:

Once the controller is configured, you access the controller URL, eg. `mycontroller.nethsecurity.org`, and manage NethSecurity units.

New controllers get a random `/20` VPN network, controllers installed before got a `/24`.
VPN network and netmask can't be changed after the first configuration.

### Change the VPN network

This is not supported by `configure-module`, follow these steps only if you really need it.
The steps below widen the netmask keeping the same network address, e.g. from `172.19.64.0/24` to `172.19.64.0/20`:
units keep their VPN IP and need no change. The network address must be aligned to the new netmask
(`172.19.64.0` is valid for a `/20`, `172.19.65.0` is not).

Moving to a different network address changes the controller VPN IP: units must be removed and added again.

1. Stop the controller and read the tun name and the node ID:

runagent -m nethsecurity-controller1 systemctl --user stop controller.service
runagent -m nethsecurity-controller1 grep OVPN_TUN network.env
runagent -m nethsecurity-controller1 printenv NODE_ID

2. Remove the tun, replace `tunnsc1` and `1` with the values from step 1:

api-cli run node/1/remove-tun --data '{"tun": "tunnsc1"}'

3. Set the new netmask inside the saved configuration:

runagent -m nethsecurity-controller1 python3 -c 'import json; c = json.load(open("config.json")); c["ovpn_netmask"] = "255.255.240.0"; json.dump(c, open("config.json", "w"))'

4. Run `configure-module` with the current configuration, it creates the tun and the firewall rules again and starts the controller:

api-cli run module/nethsecurity-controller1/get-configuration | jq -c 'del(.api_password)' | api-cli run module/nethsecurity-controller1/configure-module --data -

Units reconnect on their own.

## Module overview

The module is composed by the following systemd units:
Expand Down
1 change: 1 addition & 0 deletions controller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ The following environment variables can be used to configure the containers:
- `OVPN_TUN`: OpenVPN tun device name, default is `tunsec`
- `OVPN_TUN_MTU`: OpenVPN tun device MTU, default is `1500`
- `OVPN_MSSFIX`: OpenVPN mssfix value, default is `1450`
- `OVPN_MAX_CLIENTS`: OpenVPN max connected clients, default is `1024`
- `UI_PORT`: UI listening port, default is `3000`
- `UI_BIND_IP`: UI binding IP, default is `0.0.0.0`
- `API_PORT`: API server listening port, default is `5000`
Expand Down
2 changes: 2 additions & 0 deletions controller/api/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ func TestListIPsEdgeCases(t *testing.T) {
{"32", "192.168.1.1", "255.255.255.255", 1},
{"31", "192.168.1.0", "255.255.255.254", 0},
{"30", "192.168.1.0", "255.255.255.252", 2},
{"24", "172.20.16.0", "255.255.255.0", 254},
{"20", "172.20.16.0", "255.255.240.0", 4094},
}

for _, tc := range testCases {
Expand Down
3 changes: 3 additions & 0 deletions controller/vpn/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ovpn_port=${OVPN_UDP_PORT:-1194}
tun=${OVPN_TUN:-tunsec}
tun_mtu=${OVPN_TUN_MTU:-1500}
mssfix=${OVPN_MSSFIX:-1450}
max_clients=${OVPN_MAX_CLIENTS:-1024}

if [ ! -f /etc/openvpn/pki/ca.crt ]; then
cd /etc/openvpn
Expand Down Expand Up @@ -50,6 +51,8 @@ client-config-dir /etc/openvpn/ccd
ifconfig-pool-persist host-to-net.pool 0

port $ovpn_port
max-clients $max_clients
explicit-exit-notify 1
script-security 3
float
multihome
Expand Down
30 changes: 28 additions & 2 deletions imageroot/actions/configure-module/20configure
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,42 @@ import sys
import agent
import os
import uuid
import ipaddress

request = json.load(sys.stdin)

(start,end) = os.environ["TCP_PORTS_RANGE"].split('-')
ports = [*range(int(start), int(end)+1)]

def validation_failed(field, value, error):
agent.set_status('validation-failed')
json.dump([{'field': field, 'parameter': field, 'value': value, 'error': error}], fp=sys.stdout)
sys.exit(2)

try:
with open('config.json', 'r') as tmp:
config = json.load(tmp)
except:
config = request

ovpn_network = request['ovpn_network']
ovpn_netmask = request['ovpn_netmask']

# The tun address is set only once by add_tun, so the network can't change after the first configuration
if config['ovpn_network'] != ovpn_network:
validation_failed('network', ovpn_network, 'network_change_not_supported')
if config['ovpn_netmask'] != ovpn_netmask:
validation_failed('netmask', ovpn_netmask, 'network_change_not_supported')

try:
vpn_network = ipaddress.IPv4Network(f'{ovpn_network}/{ovpn_netmask}')
except ipaddress.NetmaskValueError:
validation_failed('netmask', ovpn_netmask, 'invalid_netmask')
except ValueError:
validation_failed('network', ovpn_network, 'invalid_network')
if not 20 <= vpn_network.prefixlen <= 24:
validation_failed('netmask', ovpn_netmask, 'netmask_out_of_range')

for path in ['loki_path', 'prometheus_path', 'webssh_path']:
if not config.get(path):
config[path] = f'/{uuid.uuid4()}'
Expand Down Expand Up @@ -120,6 +144,8 @@ with open('config.env', 'w') as env:
env.write(f'ADMIN_PASSWORD={request.get("api_password", config["api_password"])}\n')
env.write(f'OVPN_NETWORK={request["ovpn_network"]}\n')
env.write(f'OVPN_NETMASK={request["ovpn_netmask"]}\n')
# network, broadcast and server addresses excluded
env.write(f'OVPN_MAX_CLIENTS={vpn_network.num_addresses - 3}\n')
env.write(f'OVPN_CN={request["ovpn_cn"]}\n')
env.write(f'FQDN={request["host"]}\n')
env.write(f'ISSUER_2FA={request["host"]}\n')
Expand Down Expand Up @@ -183,8 +209,8 @@ agent.write_envfile('network.env', network)

# Setup firewall
tun = network.get('OVPN_TUN')
bits = sum(bin(int(x)).count('1') for x in request["ovpn_netmask"].split('.'))
cidr = f'{request["ovpn_network"]}/{bits}'
bits = vpn_network.prefixlen
cidr = vpn_network.with_prefixlen
# Allow access only on Promtail and API ports, reject everything else
rules = [
f'rule family=ipv4 priority=-100 source address={server_address} destination address={cidr} accept',
Expand Down
4 changes: 2 additions & 2 deletions imageroot/actions/configure-module/70platform_info
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import json
import agent
import os
import ipaddress

vpn_port = os.environ['TCP_PORTS_RANGE'].split('-')[0]
controller_version = os.environ.get('IMAGE_URL', '').split(':')[-1]

config = agent.read_envfile('config.env')
bits = sum(bin(int(x)).count('1') for x in config.get('OVPN_NETWORK', '').split('.'))
vpn_network = f"{config.get('OVPN_NETWORK', '')}/{bits}"
vpn_network = ipaddress.IPv4Network(f"{config['OVPN_NETWORK']}/{config['OVPN_NETMASK']}").with_prefixlen
metrics_retension_days = config.get('RETENTION_DAYS', '30')

loki = agent.read_envfile('loki.env')
Expand Down
8 changes: 4 additions & 4 deletions imageroot/actions/configure-module/validate-input.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"api_user": "admin",
"api_password": "admin",
"host": "controller.nethserver.org",
"ovpn_network": "127.2.10.0",
"ovpn_netmask": "255.255.0.0",
"ovpn_network": "172.20.16.0",
"ovpn_netmask": "255.255.240.0",
"ovpn_cn": "nethsec",
"loki_retention": 180,
"prometheus_retention": 15,
Expand Down Expand Up @@ -47,12 +47,12 @@
"ovpn_network": {
"type": "string",
"format": "ipv4",
"description": "VPN client network, like '127.2.1.0.0'"
"description": "VPN client network, like '172.20.16.0'. It can't change after the first configuration"
},
"ovpn_netmask": {
"type": "string",
"format": "ipv4",
"description": "VPN client netmask, like '255.255.0.0'"
"description": "VPN client netmask, from '255.255.240.0' (/20) to '255.255.255.0' (/24). It can't change after the first configuration"
},
"ovpn_cn": {
"type": "string",
Expand Down
8 changes: 4 additions & 4 deletions imageroot/actions/get-configuration/20read
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ if os.path.isfile('config.json'):
config["lets_encrypt"] = agent.get_route(os.environ['MODULE_ID']).get('lets_encrypt', False)
else:
# Prepare random values for first-configuration
# Pick a newtork inside 172.16.0.0/12 (range 172.16.0.0-172.31.255.255)
foctet=random.randrange(16,31)
soctet=random.randrange(1,254)
# Pick a /20 network inside 172.16.0.0/12 (range 172.16.0.0-172.31.255.255)
foctet=random.randrange(16,32)
soctet=16*random.randrange(0,16)

config = {'host': '', 'lets_encrypt': False, 'ovpn_network': f'172.{foctet}.{soctet}.0', 'ovpn_netmask': '255.255.255.0', 'ovpn_cn': 'nethsec', 'api_user': 'admin', 'api_password': '', 'loki_retention': 180, 'prometheus_retention': 15, 'vpn_port': '', 'allowed_ips': [], 'tun_mtu': 1500, 'mssfix': 1450}
config = {'host': '', 'lets_encrypt': False, 'ovpn_network': f'172.{foctet}.{soctet}.0', 'ovpn_netmask': '255.255.240.0', 'ovpn_cn': 'nethsec', 'api_user': 'admin', 'api_password': '', 'loki_retention': 180, 'prometheus_retention': 15, 'vpn_port': '', 'allowed_ips': [], 'tun_mtu': 1500, 'mssfix': 1450}

characters = list(string.ascii_letters + string.digits + "!@#%^+_")
random.shuffle(characters)
Expand Down
4 changes: 2 additions & 2 deletions imageroot/actions/get-configuration/validate-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"api_password": "admin",
"host": "controller.nethserver.org",
"lets_encrypt": true,
"ovpn_network": "127.2.10.0",
"ovpn_netmask": "255.255.0.0",
"ovpn_network": "172.20.16.0",
"ovpn_netmask": "255.255.240.0",
"ovpn_cn": "nethsec",
"loki_retention": 180,
"maxmind_license": "1234567890",
Expand Down
4 changes: 2 additions & 2 deletions imageroot/update-module.d/40firewall
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import json
import sys
import agent
import os
import ipaddress

(start,end) = os.environ["TCP_PORTS_RANGE"].split('-')
ports = [*range(int(start), int(end)+1)]
Expand All @@ -26,8 +27,7 @@ server_address = config["ovpn_network"].removesuffix('.0') + '.1'
network = agent.read_envfile('network.env')

tun = network.get('OVPN_TUN')
bits = sum(bin(int(x)).count('1') for x in config["ovpn_netmask"].split('.'))
cidr = f'{config["ovpn_network"]}/{bits}'
cidr = ipaddress.IPv4Network(f'{config["ovpn_network"]}/{config["ovpn_netmask"]}').with_prefixlen
rules = [
f'rule family=ipv4 priority=-100 source address={server_address} destination address={cidr} accept',
f'rule family=ipv4 priority=-99 source address={cidr} destination address={server_address} port port={ports[4]} protocol=tcp accept',
Expand Down
2 changes: 1 addition & 1 deletion tests/nextsec-controller.robot
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Check if nethsecurity-controller is installed correctly
Set Global Variable ${module_id} ${output.module_id}

Check if nethsecurity-controller can be configured
${out} ${err} ${rc} = Execute Command api-cli run module/${module_id}/configure-module --data '{"host": "controller.dom.test", "lets_encrypt": false, "api_user": "admin", "api_password": "Nethesis,1234", "ovpn_network": "172.19.64.0", "ovpn_netmask": "255.255.255.0", "ovpn_cn": "nethsec", "loki_retention": 180, "prometheus_retention": 15}'
${out} ${err} ${rc} = Execute Command api-cli run module/${module_id}/configure-module --data '{"host": "controller.dom.test", "lets_encrypt": false, "api_user": "admin", "api_password": "Nethesis,1234", "ovpn_network": "172.19.64.0", "ovpn_netmask": "255.255.240.0", "ovpn_cn": "nethsec", "loki_retention": 180, "prometheus_retention": 15}'
... return_rc=True return_stdout=True return_stderr=True
Should Be Equal As Integers ${rc} 0

Expand Down
10 changes: 7 additions & 3 deletions ui/public/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@
"cn_tooltip": "The field will be read-only after the first configuration",
"network_tooltip": "Make sure this network does not overlap with the units' networks. The field will be read-only after the first configuration",
"netmask_tooltip": "Do not change this value unless you know what you are doing. The field will be read-only after the first configuration",
"netmask_helper": "The netmask determines the size of the VPN network. Use only class C networks",
"netmask_helper": "The netmask determines the size of the VPN network, from 255.255.240.0 (/20) to 255.255.255.0 (/24)",
"invalid_network": "Invalid network, it must be the first address of the network, like 172.20.16.0 for a /20",
"invalid_netmask": "Invalid netmask",
"netmask_out_of_range": "Netmask must be between 255.255.240.0 (/20) and 255.255.255.0 (/24)",
"network_change_not_supported": "VPN network and netmask can't be changed after the first configuration",
"password_placeholder": "Password can be modified from the controller webapp",
"password_information_title": "Default password",
"password_information_description": "The default administrator password is displayed only once, please store it in a safe place",
Expand Down Expand Up @@ -105,8 +109,8 @@
"prometheus_rention_min": "Metrics retention must be greater than 1 day",
"loki_retention_max": "Logs retention must be less than 365 days",
"loki_retention_min": "Logs retention must be greater than 1 day",
"invalid_network": "Invalid network, it must be a class C network like 192.168.200.0",
"invalid_netmask": "Invalid netmask, it must be a valid netmask like 255.255.255.0",
"invalid_network": "Invalid network, it must end with .0, like 172.20.16.0",
"invalid_netmask": "Invalid netmask, it must be between 255.255.240.0 (/20) and 255.255.255.0 (/24)",
"invalid_allowed_ips": "Invalid allowed IPs, each entry must be a and IP or CIDR network.",
"tun_mtu_min": "TUN MTU must be at least 576",
"mssfix_min": "MSS Fix must be at least 0"
Expand Down
4 changes: 1 addition & 3 deletions ui/src/views/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -635,9 +635,7 @@ export default {
}

// validate netmask
const netmask_re = new RegExp(
/^(255|254|252|248|240|224|192|128|0)\.(255|254|252|248|240|224|192|128|0)\.(255|254|252|248|240|224|192|128|0)\.(0|128|192|224|240|248|252|254|255)$/
);
const netmask_re = new RegExp(/^255\.255\.(240|248|252|254|255)\.0$/);
if (!netmask_re.test(this.netmask)) {
this.error.netmask = this.$t("error.invalid_netmask");
this.focusElement("netmask");
Expand Down
Loading