summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/openvpn/server.conf.tmpl11
-rw-r--r--interface-definitions/interfaces-openvpn.xml.in47
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py25
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):