diff options
-rw-r--r-- | data/templates/openvpn/server.conf.tmpl | 11 | ||||
-rw-r--r-- | interface-definitions/interfaces-openvpn.xml.in | 47 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 25 |
3 files changed, 80 insertions, 3 deletions
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index 0968a18ba..91f8d7515 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -127,6 +127,14 @@ push "dhcp-option DNS6 {{ nameserver }}" {% if server.domain_name is defined and server.domain_name is not none %} push "dhcp-option DOMAIN {{ server.domain_name }}" {% endif %} +{% if server['2fa']['totp'] is defined and server['2fa']['totp'] is not none %} +plugin "/usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-otp.so" "otp_secrets=/config/otp-secrets otp_slop= +{{- server['2fa']['totp']['slop']|default(180) }} totp_t0= +{{- server['2fa']['totp']['t0']|default(0) }} totp_step= +{{- server['2fa']['totp']['step']|default(30) }} totp_digits= +{{- server['2fa']['totp']['digits']|default(6)}} password_is_cr= +{%-if server['2fa']['totp']['challenge']|default('enabled') == 'enabled' %}1{% else %}0{% endif %}" +{% endif %} {% endif %} {% else %} # @@ -218,6 +226,9 @@ auth-user-pass {{ auth_user_pass_file }} auth-retry nointeract {% endif %} + +{% if openvpn_option is defined and openvpn_option is not none %} + {% if openvpn_option is defined and openvpn_option is not none %} # # Custom options added by user (not validated) diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 7ff08ac86..1a07e7d91 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -635,6 +635,53 @@ </properties> <defaultValue>net30</defaultValue> </leafNode> + <node name="2fa"> + <properties> + <help>2-factor authentication</help> + </properties> + <children> + <node name="totp"> + <properties> + <help>Time-based One-Time Passwords</help> + </properties> + <children> + <leafNode name="slop"> + <properties> + <help>Maximum allowed clock slop in seconds (default: 180)</help> + </properties> + <defaultValue>180</defaultValue> + </leafNode> + <leafNode name="t0"> + <properties> + <help>time drift in seconds (default: 0)</help> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="step"> + <properties> + <help>Step value for TOTP in seconds (default: 30)</help> + </properties> + <defaultValue>30</defaultValue> + </leafNode> + <leafNode name="digits"> + <properties> + <help>Number of digits to use from TOTP hash (default: 6)</help> + </properties> + <defaultValue>6</defaultValue> + </leafNode> + <leafNode name="challenge"> + <properties> + <help>expect password as result of a challenge response protocol (default: enabled)</help> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + <defaultValue>enable</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> </children> </node> <leafNode name="shared-secret-key"> diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 74e29ed82..f19804910 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -25,7 +25,9 @@ from ipaddress import IPv4Network from ipaddress import IPv6Address from ipaddress import IPv6Network from ipaddress import summarize_address_range +from pathlib import Path from netifaces import interfaces +from secrets import SystemRandom from shutil import rmtree from vyos.config import Config @@ -309,9 +311,9 @@ def verify(openvpn): raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode') - for client in (dict_search('client', openvpn) or []): - if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1: - raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP') + for client_k, client_v in (dict_search('server.client', openvpn).items() or []): + if (client_v.get('ip') and len(client_v['ip']) > 1) or (client_v.get('ipv6_ip') and len(client_v['ipv6_ip']) > 1): + raise ConfigError(f'Server client "{client_k}": cannot specify more than 1 IPv4 and 1 IPv6 IP') if dict_search('server.client_ip_pool', openvpn): if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)): @@ -359,6 +361,23 @@ def verify(openvpn): if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet: print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.') + if dict_search('server.2fa.totp', openvpn): + if not Path(otp_file).is_file(): + Path(otp_file).touch() + for client in (dict_search('server.client', openvpn) or []): + with open(otp_file, "r+") as f: + users = f.readlines() + exists = None + for user in users: + if re.search('^' + client + ' ', user): + exists = 'true' + + if not exists: + random = SystemRandom() + totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) + f.write("{0} otp totp:sha1:base32:{1}::xxx *\n".format(client, totp_secret)) + + else: # checks for both client and site-to-site go here if dict_search('server.reject_unconfigured_clients', openvpn): |